-
Notifications
You must be signed in to change notification settings - Fork 170
Expand file tree
/
Copy pathproxy.lua
More file actions
224 lines (166 loc) · 6.44 KB
/
Copy pathproxy.lua
File metadata and controls
224 lines (166 loc) · 6.44 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
require('resty.core')
local http = require('resty.http')
local ngx_re_match = ngx.re.match
local str_lower = string.lower
local tonumber = tonumber
local format = string.format
local ceil = math.ceil
local inclusive = { inclusive = true }
local cr_lf = "\r\n"
local http_ok = 'HTTP/1.1 200 OK' .. cr_lf
ngx.log(ngx.INFO, 'proxy starting')
local function send(socket, data)
if not data or data == '' then
ngx.log(ngx.DEBUG, 'skipping sending nil')
return
end
return socket:send(data)
end
local function re_match(data, regex)
return ngx_re_match(data, regex, 'oj')
end
local function read_stream(input, output)
local data, err, partial
local timeout = 1
local exp = 1.3
input:settimeouts(nil, nil, timeout)
while true do
data, err, partial = input:receive(4 * 1028) -- read any data
if data or partial then
send(output, data or partial)
-- We need to print the "CONNECT host:port" so Test::APIcast can verify it in the logs.
-- After the tunnel is established and client is using keep-alive it would not be printed at all.
ngx.log(ngx.DEBUG, 'sending data for proxy request: ', ngx.ctx.request)
end
if err == 'closed' then
ngx.log(ngx.DEBUG, 'closed socket')
break
elseif err == 'timeout' and partial == '' then
timeout = ceil(timeout * exp)
input:settimeouts(nil, nil, timeout)
ngx.log(ngx.DEBUG, 'timeout reached when reading socket, next timeout: ', timeout)
elseif not err then
timeout = 1
end
end
if input.close then input:close() end
end
local function pipe_stream(sock, upstream)
local read = ngx.thread.spawn(read_stream, sock, upstream)
local write = ngx.thread.spawn(read_stream, upstream, sock)
ngx.thread.wait(read, write)
end
local function _forward_chunked_body(sock, upstream)
local length
repeat
-- Receive the chunk size
local str, err = sock:receive("*l")
if err then return nil, err end
length = tonumber(str, 16)
if not length then
return nil, 'unable to read chunksize'
end
send(upstream, str .. cr_lf)
if length > 0 then
send(upstream, sock:receive(length) .. cr_lf)
end
sock:receive(2) -- last cr_lf
until length == 0
send(upstream, cr_lf)
end
local function _forward_body(sock, upstream, length)
ngx.log(ngx.DEBUG, 'reading ', length, ' bytes')
send(upstream, sock:receive(length))
end
local noop = function() end
local function forward_http_stream(sock, upstream)
local read_header = sock:receiveuntil(cr_lf, inclusive)
local body_length, header_line
local body_reader = noop
repeat
header_line = read_header()
send(upstream, header_line)
local header = re_match(header_line, [[(?<name>[^:\s]+):\s*(?<value>.+)\r\n$]])
ngx.log(ngx.DEBUG, 'proxy http request - got header line: ', header_line)
if header and str_lower(header.name) == 'content-length' then
body_length = tonumber(header.value)
body_reader = _forward_body
end
if header and str_lower(header.name) == 'transfer-encoding' and str_lower(header.value) == 'chunked' then
body_reader = _forward_chunked_body
end
until header_line == cr_lf
body_reader(sock, upstream, body_length)
end
local function proxy_http_request(sock, method, url, http_version, upstream)
local uri = assert(http.parse_uri(nil, url))
local port = uri[3]
local host = uri[2]
local ok, err = upstream:connect(host, port)
if ok then
ngx.log(ngx.DEBUG, 'connected to upstream ', host, ':', port)
else
ngx.log(ngx.ERR, 'failed to connect to ', host, ':', port, ' err: ', err)
return nil, err
end
local req_line = format('%s %s %s%s', method, uri[4] or '/', http_version, cr_lf)
send(upstream, req_line)
forward_http_stream(sock, upstream)
forward_http_stream(upstream, sock)
ngx.log(ngx.DEBUG, 'finished forwarding http stream')
upstream:setkeepalive()
end
local function proxy_connect_request(sock, connect, upstream)
local ok, err = upstream:connect(connect.host, connect.port)
if ok then
ngx.log(ngx.DEBUG, 'connected to upstream ', connect.host, ':', connect.port)
else
ngx.log(ngx.ERR, 'failed to connect to ', connect.host, ':', connect.port, ' err: ', err)
return nil, err
end
local header_line
local read_request_header = sock:receiveuntil(cr_lf, inclusive)
repeat
header_line = read_request_header()
ngx.log(ngx.DEBUG, 'got header line: ', header_line)
until header_line == cr_lf
send(sock, http_ok)
send(sock, cr_lf)
return pipe_stream(sock, upstream)
end
--- HTTP(S) proxy implementation
-- Takes control of the incoming connection and implements http/s proxy.
-- Supports all HTTP methods that nginx supports + CONNECT for HTTPS tunneling.
-- Uses keep-alive to reuse both incoming and upstream connections.
-- Client can send several HTTP requests pointing to different upstreams reusing the same connection.
-- After sending CONNECT request the connection becomes a tunnel and all traffic is transparently proxied between sockets.
return function()
local client, err = ngx.req.socket(true)
if not client then return nil, err end
local readline = client:receiveuntil(cr_lf)
while true do
local data, partial
data, err, partial = readline()
if data then
ngx.log(ngx.DEBUG, 'got line: ', data)
else
ngx.log(ngx.WARN, 'got partial: ', partial, ' err: ', err)
ngx.exit(ngx.OK)
end
local match = re_match(data, [[^(?<method>[A-Z]+)\s(?<uri>\S+)\s(?<http>\S+)$]])
if not match then
ngx.log(ngx.WARN, 'got invalid request: ', data)
return client:close()
end
local upstream = ngx.socket.tcp()
ngx.log(ngx.DEBUG, 'proxy request: ', data)
-- store the header so we can print it again when using keep-alive https tunnel
ngx.ctx.request = data
if match.method == 'CONNECT' then
local connect = re_match(match.uri, [[^(?<host>[^:]+):(?<port>\d+)$]])
proxy_connect_request(client, connect, upstream)
else
proxy_http_request(client, match.method, match.uri, match.http, upstream)
end
end
end