Skip to content

Commit 5db5ba6

Browse files
committed
Added mTLS support for http client.
1 parent d4e40cb commit 5db5ba6

File tree

7 files changed

+217
-26
lines changed

7 files changed

+217
-26
lines changed

http/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@ basic_auth_user = "",
1212
basic_auth_password = "",
1313
headers = {"key"="value"},
1414
debug = false,
15+
16+
# When set, the client will present the client cert for "mTLS"
17+
client_public_cert_pem_file = nil,
18+
client_private_key_pem_file = nil,
19+
20+
# When set, this will be used to verify the server certificate (useful for private enterprise certificate authorities).
21+
# Prefer this over insecure_ssl when possible
22+
root_cas_pem_file = "",
1523
```
1624
- `request(method, url, [data])` - make request userdata.
1725

http/api_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,19 @@ package http_test
22

33
import (
44
"crypto/subtle"
5+
"crypto/tls"
6+
"crypto/x509"
57
"fmt"
68
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
"github.com/vadv/gopher-lua-libs/tests"
711
"golang.org/x/sync/errgroup"
12+
"io"
813
"io/ioutil"
914
"log"
1015
"net/http"
16+
"net/http/httptest"
17+
"os"
1118
"strings"
1219
"testing"
1320
"time"
@@ -218,3 +225,34 @@ func TestApi(t *testing.T) {
218225
assert.NoError(t, state.DoFile("./test/test_serve_static.lua"))
219226
})
220227
}
228+
229+
func TestMTLS(t *testing.T) {
230+
s := httptest.NewUnstartedServer(http.HandlerFunc(func(writer http.ResponseWriter, r *http.Request) {
231+
_, _ = io.WriteString(writer, "OK\n")
232+
}))
233+
defer s.Close()
234+
serverCert, err := tls.LoadX509KeyPair("test/data/test.cert.pem", "test/data/test.key.pem")
235+
require.NoError(t, err)
236+
caData, err := os.ReadFile("test/data/test.cert.pem")
237+
require.NoError(t, err)
238+
cas := x509.NewCertPool()
239+
cas.AppendCertsFromPEM(caData)
240+
s.TLS = &tls.Config{
241+
Certificates: []tls.Certificate{serverCert},
242+
ClientCAs: cas,
243+
ClientAuth: tls.RequireAndVerifyClientCert,
244+
}
245+
s.StartTLS()
246+
247+
preload := tests.SeveralPreloadFuncs(
248+
lua_http.Preload,
249+
lua_time.Preload,
250+
inspect.Preload,
251+
plugin.Preload,
252+
func(L *lua.LState) {
253+
// Attach the server URL to the testing object
254+
L.SetGlobal("tURL", lua.LString(s.URL))
255+
},
256+
)
257+
assert.NotZero(t, tests.RunLuaTestFile(t, preload, "test/test_api.lua"))
258+
}

http/client/client.go

Lines changed: 55 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package http
22

33
import (
44
"crypto/tls"
5+
"crypto/x509"
56
"encoding/json"
67
"log"
78
"net/http"
@@ -81,22 +82,24 @@ func checkClient(L *lua.LState) *LuaClient {
8182

8283
// http.client(config) returns (user data, error)
8384
// config table:
84-
// {
85-
// proxy="http(s)://<user>:<password>@host:<port>",
86-
// timeout= 10,
87-
// insecure_ssl=false,
88-
// user_agent = "gopher-lua",
89-
// basic_auth_user = "",
90-
// basic_auth_password = "",
91-
// headers = {"key"="value"},
92-
// debug = false,
93-
// }
85+
//
86+
// {
87+
// proxy="http(s)://<user>:<password>@host:<port>",
88+
// timeout= 10,
89+
// insecure_ssl=false,
90+
// user_agent = "gopher-lua",
91+
// basic_auth_user = "",
92+
// basic_auth_password = "",
93+
// headers = {"key"="value"},
94+
// debug = false,
95+
// }
9496
func New(L *lua.LState) int {
9597
var config *lua.LTable
9698
if L.GetTop() > 0 {
9799
config = L.CheckTable(1)
98100
}
99101
client := &LuaClient{Client: &http.Client{Timeout: DefaultTimeout}, userAgent: DefaultUserAgent}
102+
tlsConfig := &tls.Config{}
100103
transport := &http.Transport{}
101104
// parse env
102105
if proxyEnv := os.Getenv(`HTTP_PROXY`); proxyEnv != `` {
@@ -110,17 +113,35 @@ func New(L *lua.LState) int {
110113
transport.IdleConnTimeout = DefaultTimeout
111114
// parse config
112115
if config != nil {
116+
// Client Cert and Key go together and handling in loop is challenging - just pull them out here
117+
clientPublicCertPEMFile := L.GetField(config, `client_public_cert_pem_file`)
118+
clientPrivateKeyPemFile := L.GetField(config, `client_private_key_pem_file`)
119+
if clientPublicCertPEMFile != lua.LNil && clientPrivateKeyPemFile != lua.LNil {
120+
if _, ok := clientPublicCertPEMFile.(lua.LString); !ok {
121+
L.ArgError(1, "client_public_cert_pem_file must be string")
122+
}
123+
if _, ok := clientPrivateKeyPemFile.(lua.LString); !ok {
124+
L.ArgError(1, "client_private_key_pem_file must be string")
125+
}
126+
clientCert, err := tls.LoadX509KeyPair(clientPublicCertPEMFile.String(), clientPrivateKeyPemFile.String())
127+
if err != nil {
128+
L.RaiseError("error loading client certificate from %s and %s: %v",
129+
clientPublicCertPEMFile, clientPrivateKeyPemFile, err)
130+
}
131+
tlsConfig.Certificates = []tls.Certificate{clientCert}
132+
transport.TLSClientConfig = tlsConfig
133+
}
113134
config.ForEach(func(k lua.LValue, v lua.LValue) {
135+
switch k.String() {
114136
// parse timeout
115-
if k.String() == `timeout` {
137+
case `timeout`:
116138
if value, ok := v.(lua.LNumber); ok {
117139
client.Timeout = time.Duration(value) * time.Second
118140
} else {
119141
L.ArgError(1, "timeout must be number")
120142
}
121-
}
122143
// parse proxy
123-
if k.String() == `proxy` {
144+
case `proxy`:
124145
if value, ok := v.(lua.LString); ok {
125146
proxyUrl, err := url.Parse(value.String())
126147
if err == nil {
@@ -131,51 +152,59 @@ func New(L *lua.LState) int {
131152
} else {
132153
L.ArgError(1, "http_proxy must be string")
133154
}
134-
}
135155
// parse insecure_ssl
136-
if k.String() == `insecure_ssl` {
156+
case `insecure_ssl`:
137157
if value, ok := v.(lua.LBool); ok {
138-
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: bool(value)}
158+
tlsConfig.InsecureSkipVerify = bool(value)
159+
transport.TLSClientConfig = tlsConfig
139160
} else {
140161
L.ArgError(1, "insecure_ssl must be bool")
141162
}
142-
}
163+
// parse root_cas
164+
case `root_cas_pem_file`:
165+
if value, ok := v.(lua.LString); ok {
166+
pemData, err := os.ReadFile(string(value))
167+
if err != nil {
168+
L.RaiseError("error loading root_cas_pem_file from %s: %v", value, err)
169+
}
170+
tlsConfig.RootCAs = x509.NewCertPool()
171+
tlsConfig.RootCAs.AppendCertsFromPEM(pemData)
172+
transport.TLSClientConfig = tlsConfig
173+
} else {
174+
L.ArgError(1, "root_cas_pem_file must be string")
175+
}
143176
// parse user_agent
144-
if k.String() == `user_agent` {
177+
case `user_agent`:
145178
if _, ok := v.(lua.LString); ok {
146179
client.userAgent = v.String()
147180
} else {
148181
L.ArgError(1, "user_agent must be string")
149182
}
150-
}
151183
// parse basic_auth_user
152-
if k.String() == `basic_auth_user` {
184+
case `basic_auth_user`:
153185
if _, ok := v.(lua.LString); ok {
154186
user := v.String()
155187
client.basicAuthUser = &user
156188
} else {
157189
L.ArgError(1, "basic_auth_user must be string")
158190
}
159-
}
160191
// parse basic_auth_password
161-
if k.String() == `basic_auth_password` {
192+
case `basic_auth_password`:
162193
if _, ok := v.(lua.LString); ok {
163194
password := v.String()
164195
client.basicAuthPasswd = &password
165196
} else {
166197
L.ArgError(1, "basic_auth_password must be string")
167198
}
168-
}
169199
// parse debug
170-
if k.String() == `debug` {
200+
case `debug`:
171201
if value, ok := v.(lua.LBool); ok {
172202
client.debug = bool(value)
173203
} else {
174204
L.ArgError(1, "debug must be bool")
175205
}
176-
}
177206
// parse headers
178-
if k.String() == `headers` {
207+
case `headers`:
179208
if tbl, ok := v.(*lua.LTable); ok {
180209
headers := make(map[string]string, 0)
181210
data, err := lua_json.ValueEncode(tbl)

http/test/data/mkcert.sh

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/usr/bin/env bash
2+
#
3+
# Make a test cert and key
4+
5+
readonly MY_DIR="$(dirname "${BASH_SOURCE[0]}")"
6+
7+
openssl req -nodes -x509 -newkey rsa:4096 -keyout "${MY_DIR}/test.key.pem" -out "${MY_DIR}/test.cert.pem" -sha256 \
8+
-days 365000 -addext 'subjectAltName = IP:127.0.0.1'

http/test/data/test.cert.pem

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIFEjCCAvqgAwIBAgIUJ3t8SQ3EvM+tOP+FKeiYTu9kOlAwDQYJKoZIhvcNAQEL
3+
BQAwDzENMAsGA1UEAwwEdGVzdDAgFw0yMjExMDIyMDQ3MjdaGA8zMDIyMDMwNTIw
4+
NDcyN1owDzENMAsGA1UEAwwEdGVzdDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC
5+
AgoCggIBALg2f7EsfQoZWX0P7T2laOmOKMP+ciwVpV82yVC6CqlP7f85gyWjCqp/
6+
+KcMAfxYWGWA64tz1F7IOVBNW85hR7aeQA9dG3x8541c31uJxaZuBz9+vMZ67+YZ
7+
ToSsDez1LYxRlW2+afTsAMBD8YGgsishCbCUA29UxO/JiMsSnZa+39QKQ1ent1gX
8+
VHQMblQTqyKpnetAnU3al/YWCqOe2kAxW+u2JMfycWxX3szkpzEMADzrXYvd1Gf4
9+
0wiYaxSRbmv2g2nM5czJjkAfJNRNVCbtL8Ih+8Qfv7JiMVfrbz6qkOOdaHkP1H13
10+
Hvv0ts0bPmNKDtxrobmRvV0bTcCnuQ2w9zBWNFRd3eP7Mj5ptwkQ8THtfBKgCTdl
11+
r0UvgIJWK2/t/riSWc7YdZ9Os0t+bwGGfgJiwy5YHl9ZOn0qzbNvlRv/m0Trzyl8
12+
z07APY7CgoqnS2wOhT0nT4ihVYQPch41xYKM9Y/el7M2P1IN5dHoZTqp/zP5xygV
13+
/xdCPMW4eFJucS1fiKAwQSd0RdxDM8wz6uq0AEZcc7Slxb24s6TMjTUPA2uDVw+r
14+
uXcW7ApxByasDNHHm9pfPuwUYAeKMhYDLhueImSiyU6R0xnfBKPM3DDqME2Dp941
15+
TyIU9RAG/uxIdpCygQcyJhCcXJIn9sp9/qapUwlNVMu1a2h6pHSxAgMBAAGjZDBi
16+
MB0GA1UdDgQWBBTWF7oiC4wRJCLlw/OW9Vi+/HHlQTAfBgNVHSMEGDAWgBTWF7oi
17+
C4wRJCLlw/OW9Vi+/HHlQTAPBgNVHRMBAf8EBTADAQH/MA8GA1UdEQQIMAaHBH8A
18+
AAEwDQYJKoZIhvcNAQELBQADggIBADWzGv8QptQc0HQ6yeCcYaV1QdHqmBAJuShQ
19+
QoX2+Bn7nMyhEiRCbG087HcTOz90AKn9h+SiEe8M2F6KF/k/y/0D28e9OHC35NIr
20+
alKqm/BPrcXcIqw6ZjdQt8fJV3SJecz0qZykBuyZX1yLviHoGTd3Yu9wi2qNat4O
21+
HzcNJDlXWoUgULKmeJFgaLqByI7XUCnPLVFJEyyPFhSGdkg0LmpTmSO06sU/o6Ru
22+
zEVE6HjuVCTXcI3cME56/658dEKOiPN0zpFSHrTe2StoQDcXPr5CnPr6j7W7dPt4
23+
ihr7ivLyRQLrhsWknSJgzs39YvXpz/Cif1UeI6AZY5HVAxiAIr4+wdeFvG1aA6fs
24+
Dv41gU9Xkv8mULVjqyQ8fF77KpHlOG+qz/9GsyttH1V/HhvuBSRr8QYaCW4ljmoO
25+
QG603kjsdWRAyOolsTh1Y7wxJ3kxsGu4NTTPRyrKo6VnIsEk06feDo6vanmTd6Qz
26+
B/kkQZ+CPFcFUMibfvtWv/o+2/XVFvaT+/Z7U9K2Kl0AZ5eP5GEyDvAPfsE7MQeq
27+
orBCNS9GOcQRJfgAOCOafn9hYmCz+aTUE6HJ8tawN6ObCo9sFdl1Dd8LY2y4+5nm
28+
BNU3892vjGhERmbaQluqUsTCGfCNsszw8aiyQSrSrf1GsSJD8BslHprojCMk8Oi2
29+
6lzZcLrN
30+
-----END CERTIFICATE-----

http/test/data/test.key.pem

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
-----BEGIN PRIVATE KEY-----
2+
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
51+
WixrUGdUTlPU9nWE0s9Eyf+G5gwezYg=
52+
-----END PRIVATE KEY-----

http/test/test_api.lua

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
local http = require 'http_client'
2+
3+
function TestMTLS(t)
4+
assert(tURL, 'tURL global is not set')
5+
6+
t:Run('no-client-cert fails', function(t)
7+
local client = http.client()
8+
local req, err = http.request("GET", tURL)
9+
assert(not err, tostring(err))
10+
local resp, err = client:do_request(req)
11+
assert(err, tostring(err))
12+
end)
13+
14+
t:Run('client-cert passes', function(t)
15+
local client = http.client {
16+
root_cas_pem_file = 'test/data/test.cert.pem',
17+
client_public_cert_pem_file = 'test/data/test.cert.pem',
18+
client_private_key_pem_file = 'test/data/test.key.pem',
19+
}
20+
local req, err = http.request("GET", tURL)
21+
assert(not err, tostring(err))
22+
local resp, err = client:do_request(req)
23+
assert(not err, tostring(err))
24+
assert(resp.code == 200, tostring(resp.code))
25+
end)
26+
end

0 commit comments

Comments
 (0)