-
Notifications
You must be signed in to change notification settings - Fork 152
/
Copy pathproxy
executable file
·456 lines (393 loc) · 12.6 KB
/
proxy
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
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
#!/usr/bin/env ruby
# Usage: proxy [<port=8000>]
#
# A debugging HTTP proxy that handles both HTTP and HTTPS traffic and outputs
# request/response information to STDOUT.
#
# Author: Mislav Marohnić
#
# TODO:
# - pretty query strings
# - pretty application/x-www-form-urlencoded data
# - present HTTP headers in a more logical order, e.g.
# content-type & content-length first
# - possible to simplify CONNECT?
# https://github.com/23tux/vcr_proxy/blob/master/vcr_proxy.rb
require 'openssl'
require 'webrick'
require 'webrick/https'
require 'webrick/httpproxy'
require 'zlib'
require 'stringio'
require 'net/https'
begin
require 'json'
rescue LoadError
warn "Warning: #{$!.message}. Will not be able to prettify JSON responses."
end
class DebuggingProxy
attr_reader :proxy_port, :proxy_server, :ssl_server, :ssl_thread
attr_reader :io
attr_accessor :cert_info
def initialize(port)
@io = $stdout
@proxy_port = port
@proxy_server = nil
end
COLORS = {
:clear => "\e[0m",
:bold => "\e[1m",
:black => "\e[30m",
:red => "\e[31m",
:green => "\e[32m",
:yellow => "\e[33m",
:blue => "\e[34m",
:magenta => "\e[35m",
:cyan => "\e[36m",
:white => "\e[37m",
}
PYGMENTIZE = `which pygmentize 2>/dev/null`.chomp
def c(color)
Array(color).map { |colo|
COLORS.fetch(colo.to_sym)
}.join('')
end
def print(str, color = nil)
if color && io.tty?
io.print("#{c(color)}#{str}#{c(:clear)}")
else
io.print(str)
end
end
def flush
io.flush
end
def silence_header?(key, value = nil)
key =~ %r{
^(?:
host |
(?:proxy-)? connection |
date |
status | # already shown by response.status_line
server |
accept-ranges |
vary |
via
)$ | ^x-
}ix
end
def dump_headers(hash)
hash.each { |key, value|
value = value.join(', ') if value.respond_to?(:join)
next if silence_header?(key, value)
print(key, :yellow)
print(': ')
if 'content-type' == key
value, extra = value.split(';', 2)
print(value, :green)
print(";#{extra}") if extra
else
print(value)
end
print("\n")
}
flush
end
def dump_body(req)
if req.body && !req.body.empty? && !html?(req)
body = req.body
body = unzip(body) if gzipped?(req)
body = pretty_json(body) if json?(req)
body = body + "\n" if body[-1,1] != "\n"
print(body)
flush
end
end
def html?(req)
req['content-type'] =~ %r!/(html|xhtml\+xml)($|;)!
end
def json?(req)
req['content-type'] =~ %r![/+]json($|;)!
end
def gzipped?(req)
req['content-encoding'] == 'gzip'
end
def unzip(body)
Zlib::GzipReader.new(StringIO.new(body), :encoding => 'ASCII-8BIT').read
end
def pretty_json(body)
body = JSON.pretty_generate(JSON.parse(body)) if defined?(JSON)
body = syntax_highlight(body, 'json') if io.tty?
body
end
def syntax_highlight(body, type)
if !PYGMENTIZE.empty?
formatter = 'terminal'
formatter += '256' if ENV['TERM'].include?('256color')
style = 'monokai'
File.popen("#{PYGMENTIZE} -l #{type} -f #{formatter} -O style=#{style}", 'r+') do |py|
py.write(body)
py.close_write
body = py.read
end
end
body
end
def record_interaction(req, res)
return if req.request_method == 'CONNECT'
print("#{req.request_method} ", :blue)
print("#{req.request_uri} ", :red)
print("HTTP/#{req.http_version}\n")
dump_headers(req.header)
dump_body(req)
print(res.status_line, :blue)
dump_headers(res.header)
dump_body(res)
print("\n")
end
class HTTPProxy < WEBrick::HTTPProxyServer
def service(req, res)
if req.request_method == "CONNECT"
do_CONNECT(req, res)
elsif req.request_uri.to_s =~ %r!^https?:/!i
proxy_service(req, res)
else
super(req, res)
end
end
# Ruby 1.8 compatibility; copied verbatim from Ruby 2.0
def proxy_service(req, res)
# Proxy Authentication
proxy_auth(req, res)
begin
self.send("do_#{req.request_method}", req, res)
rescue NoMethodError
raise WEBrick::HTTPStatus::MethodNotAllowed,
"unsupported method `#{req.request_method}'."
rescue => err
logger.debug("#{err.class}: #{err.message}")
raise WEBrick::HTTPStatus::ServiceUnavailable, err.message
end
# Process contents
if handler = @config[:ProxyContentHandler]
handler.call(req, res)
end
end
# Ruby 1.8 compatibility; copied verbatim from Ruby 2.0
def setup_proxy_header(req, res)
# Choose header fields to transfer
header = Hash.new
choose_header(req, header)
set_via(header)
return header
end
attr_accessor :local_ssl_host, :local_ssl_port
# copied almost verbatim from WEBrick::HTTPProxyServer to monkeypatch
def do_CONNECT(req, res)
proxy_auth(req, res)
ua = Thread.current[:WEBrickSocket] # User-Agent
raise WEBrick::HTTPStatus::InternalServerError, "[BUG] cannot get socket" unless ua
# HERE is what I override: Instead to the target host; point the traffic
# to the local ssl_server that acts as a man-in-the-middle.
# host, port = req.unparsed_uri.split(":", 2)
host = local_ssl_host
port = local_ssl_port
begin
@logger.debug("CONNECT: upstream proxy is `#{host}:#{port}'.")
os = TCPSocket.new(host, port) # origin server
@logger.debug("CONNECT #{host}:#{port}: succeeded")
res.status = WEBrick::HTTPStatus::RC_OK
rescue => ex
@logger.debug("CONNECT #{host}:#{port}: failed `#{ex.message}'")
res.set_error(ex)
raise WEBrick::HTTPStatus::EOFError
ensure
if handler = @config[:ProxyContentHandler]
handler.call(req, res)
end
res.send_response(ua)
access_log(@config, req, res)
# Should clear request-line not to send the response twice.
# see: HTTPServer#run
req.parse(WEBrick::NullReader) rescue nil
end
begin
while fds = IO::select([ua, os])
if fds[0].member?(ua)
buf = ua.sysread(1024);
@logger.debug("CONNECT: #{buf.bytesize} byte from User-Agent")
os.syswrite(buf)
elsif fds[0].member?(os)
buf = os.sysread(1024);
@logger.debug("CONNECT: #{buf.bytesize} byte from #{host}:#{port}")
ua.syswrite(buf)
end
end
rescue => ex
os.close
@logger.debug("CONNECT #{host}:#{port}: closed")
end
raise WEBrick::HTTPStatus::EOFError
end
SUPPORTED_METHODS = %w[GET HEAD POST PUT PATCH DELETE]
SUPPORTED_METHODS.each do |http_method|
class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
def do_#{http_method}(req, res)
perform_proxy_request(req, res) do |http, path, header|
request = Net::HTTPGenericRequest.new("#{http_method}", !!req.body, #{http_method != 'HEAD'}, path, header)
request.body = req.body.to_s
http.request(request)
end
end
RUBY
end
def do_OPTIONS(req, res)
res['allow'] = (SUPPORTED_METHODS + %w[OPTIONS CONNECT]).join(',')
end
# copied almost verbatim from WEBrick::HTTPProxyServer to monkeypatch
def perform_proxy_request(req, res)
uri = req.request_uri
header = setup_proxy_header(req, res)
# upstream = setup_upstream_proxy_authentication(req, res, header)
response = nil
http = Net::HTTP.new(uri.host, uri.port) # upstream.host, upstream.port)
# HERE is what I add: SSL support
if http.use_ssl = (uri.scheme == 'https' || uri.port == 443)
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
http.cert_store = ssl_cert_store
end
http.start do
if @config[:ProxyTimeout]
################################## these issues are
http.open_timeout = 30 # secs # necessary (maybe bacause
http.read_timeout = 60 # secs # Ruby's bug, but why?)
##################################
end
response = yield(http, uri.request_uri, header)
end
# Persistent connection requirements are mysterious for me.
# So I will close the connection in every response.
res['proxy-connection'] = "close"
res['connection'] = "close"
# Convert Net::HTTP::HTTPResponse to WEBrick::HTTPResponse
res.status = response.code.to_i
choose_header(response, res)
set_cookie(response, res)
set_via(res)
res.body = response.body
end
def ssl_cert_store
cert_store = OpenSSL::X509::Store.new
cert_store.set_default_paths
cert_store
end
end
class ProcHandler < WEBrick::HTTPServlet::ProcHandler
alias do_PUT do_GET
alias do_PATCH do_GET
alias do_DELETE do_GET
end
class HTTPServer < WEBrick::HTTPServer
def do_OPTIONS(req, res)
res['allow'] = (HTTPProxy::SUPPORTED_METHODS + %w[OPTIONS CONNECT]).join(',')
end
def mount_proc(dir)
mount(dir, ProcHandler.new(Proc.new))
end
end
def logger
logio = $stderr
WEBrick::BasicLog.new(logio, WEBrick::BasicLog::WARN)
end
def proxy_server_options
{ :ProxyContentHandler => method(:record_interaction),
:Port => proxy_port,
:Logger => logger,
:AccessLog => [],
}
end
def ssl_server_options
{ :Port => 0,
:BindAddress => '127.0.0.1',
:ServerType => Thread,
:Logger => logger,
:AccessLog => [],
:SSLEnable => true,
# :SSLCertName => [['CN', 'localhost']],
# :SSLCertComment => 'DebuggingProxy CA',
:SSLPrivateKey => OpenSSL::PKey::RSA.new(cert_info),
:SSLCertificate => OpenSSL::X509::Certificate.new(cert_info),
:SSLVerifyClient => OpenSSL::SSL::VERIFY_NONE
}
end
def start
proxy = HTTPProxy.new(proxy_server_options)
@ssl_thread = start_ssl_server do |server, host, port|
@ssl_server = server
proxy.local_ssl_host = host
proxy.local_ssl_port = port
# have the proxy handle the decoded SSL traffic
server.mount_proc("/", &proxy.method(:service))
end
@proxy_server = proxy
proxy.start # blocking
end
def start_ssl_server
server = HTTPServer.new(ssl_server_options)
thread = server.start
addr = server.listeners[0].addr
yield server, addr[3], addr[1]
return thread
end
def shutdown
ssl_server.shutdown
ssl_thread.join
proxy_server.shutdown
end
end
WEBrick::HTTPRequest.class_eval do
alias parse_uri_without_path_fix parse_uri
def parse_uri(str, scheme = 'https')
uri = parse_uri_without_path_fix(str, scheme)
# URIs with blank path are valid but WEBrick chokes on them nevertheless
# in `normalize_path`.
uri.path = '/' if uri.path.empty?
return uri
end
end
if $0 == __FILE__
port = ARGV[0] || 8000
proxy = DebuggingProxy.new(port)
proxy.cert_info = DATA.read
trap('INT') { proxy.shutdown }
trap('TERM') { proxy.shutdown }
warn "proxy server starting on localhost:#{proxy.proxy_port}"
proxy.start
end
__END__
-----BEGIN CERTIFICATE-----
MIIBnDCCAQWgAwIBAgIBATANBgkqhkiG9w0BAQUFADAUMRIwEAYDVQQDDAlsb2Nh
bGhvc3QwHhcNMTMwNzA1MTQ0MzAzWhcNMTQwNzA1MTQ0MzAzWjAUMRIwEAYDVQQD
DAlsb2NhbGhvc3QwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMkuQhCEljVI
wlTmZvKeOudYkBTiIxxVB6VgE7WHYnrz+WMztHggNSYyqPiNXqy+7uDdQKxNTG7L
K7XEcNfdC/+0EWFVuDnC6XS/kkeHd1OyfcdkZKjIupo+H4e/fYEFzKz+MMiJGJgC
3nk4Pdwq2DUb2a6SZ9ZEj65zZbP8KvaNAgMBAAEwDQYJKoZIhvcNAQEFBQADgYEA
RnhLr+EhO4+WXfG/aHWWKpla0k+Id+AsosRVixq/Q1ODeaLTQxXFPPAXcWsskK8S
y/75soozScKTs3Df5QOtuf9wX8+1devv8bkhNnGR5zObw/jF8LjgOg9Un8MnHwWX
zWqomAOBzpXZSuASihc8OGjKUCCLwPogDg/63VfEYeo=
-----END CERTIFICATE-----
-----BEGIN RSA PRIVATE KEY-----
MIICXgIBAAKBgQDJLkIQhJY1SMJU5mbynjrnWJAU4iMcVQelYBO1h2J68/ljM7R4
IDUmMqj4jV6svu7g3UCsTUxuyyu1xHDX3Qv/tBFhVbg5wul0v5JHh3dTsn3HZGSo
yLqaPh+Hv32BBcys/jDIiRiYAt55OD3cKtg1G9mukmfWRI+uc2Wz/Cr2jQIDAQAB
AoGAQvwI+TD8Rn+UXOpeKrguiqr9RkbJQ/y30AN+bHnIe4HSbopfs4Odzrsdcay4
cjIcnXhtuTD/mwBA7IOcwvMRtBjz6/hVxAcIi2sNR1SjUk+zvI7zutPSbavWOl83
pPbOczVtgRfcgIa7GUQf7UAFX8aFuuLhofph2uTTygdDsKECQQD9o1idKLSSQ5dy
yBphnqygQv8rVLGwB3qz/8wd7P4nNWazpW3z3RbNlMSFpdphGvPy3mZhCcH7hO+u
6R6GNMeJAkEAyw3bhucnF2r0JYPi9PDAGEteK8FOU+s+EAg4TMdI8bh8t25DuZ4Y
KKI48qKwtcawdge8hLRJShyNBEC30plx5QJBANAo8R76O0gXBQKEy3H2ocJdecH8
HmBToxZ7BvBAgk13fDRPvq93cbGDOM5e0Z0EW9WlROy672MXNQad+Bk409ECQQDE
JeLGckzk5kBqbPi0vEwUK9oQUm+jyT7upcIdCPWB60EuwKlTiVC1D+ALIvWo4wJb
QiXt22pg2SuvzATGv8uxAkEApjb8pXjHmiMdu2H4tz97askDfEKyAko44TBK65kc
8RzFYjHklbM3lTk+5kJfSM9uQU5XoP0APZAybWWCk5igLg==
-----END RSA PRIVATE KEY-----