77require "support/servers/runner"
88
99class ProxyServer
10+ Target = Struct . new ( :host , :port , :path , :query , keyword_init : true )
11+
1012 def initialize
1113 @tcp_server = TCPServer . new ( "127.0.0.1" , 0 )
1214 @port = @tcp_server . addr [ 1 ]
@@ -40,16 +42,20 @@ def shutdown
4042 private
4143
4244 def handle_request ( client )
43- method , uri , version , headers , body = read_proxy_request ( client )
44- return unless method
45+ method , target , version , headers , body = read_proxy_request ( client )
46+ return unless method && target
4547
4648 if ( response = authenticate ( headers ) )
4749 client . write ( response )
4850 return
4951 end
5052
51- forward_and_respond ( client , method , uri , body , version )
52- rescue IOError , Errno ::ECONNRESET , Errno ::EPIPE
53+ if method == "CONNECT"
54+ tunnel_connection ( client , target )
55+ else
56+ forward_and_respond ( client , method , target , body , version )
57+ end
58+ rescue IOError , Errno ::ECONNRESET , Errno ::EPIPE , URI ::InvalidURIError
5359 # Connection closed
5460 ensure
5561 client . close rescue nil # rubocop:disable Style/RescueModifier
@@ -59,11 +65,27 @@ def read_proxy_request(client)
5965 line = client . gets
6066 return unless line
6167
62- method , url , version = line . strip . split ( " " , 3 )
68+ method , target , version = line . strip . split ( " " , 3 )
6369 headers = read_headers ( client )
6470 body = headers [ "Content-Length" ] ? client . read ( headers [ "Content-Length" ] . to_i ) : nil
6571
66- [ method , URI . parse ( url ) , version , headers , body ]
72+ [ method , parse_target ( method , target ) , version , headers , body ]
73+ end
74+
75+ def parse_target ( method , target )
76+ return parse_connect_target ( target ) if method == "CONNECT"
77+
78+ uri = URI . parse ( target )
79+ Target . new ( host : uri . host , port : uri . port , path : uri . path , query : uri . query )
80+ end
81+
82+ def parse_connect_target ( target )
83+ host , port = target . split ( ":" , 2 )
84+ return unless host && port
85+
86+ Target . new ( host : host , port : Integer ( port ) )
87+ rescue ArgumentError
88+ nil
6789 end
6890
6991 def read_headers ( client )
@@ -81,24 +103,24 @@ def authenticate(_headers)
81103 nil
82104 end
83105
84- def forward_and_respond ( client , method , uri , body , version )
85- target = send_to_target ( method , uri , body , version )
86- relay_response ( client , target )
106+ def forward_and_respond ( client , method , target , body , version )
107+ target_socket = send_to_target ( method , target , body , version )
108+ relay_response ( client , target_socket )
87109 ensure
88- target &.close rescue nil # rubocop:disable Style/RescueModifier
110+ target_socket &.close rescue nil # rubocop:disable Style/RescueModifier
89111 end
90112
91- def send_to_target ( method , uri , body , version )
92- target = TCPSocket . new ( uri . host , uri . port )
93- path = uri . path . empty? ? "/" : uri . path
94- path = "#{ path } ?#{ uri . query } " if uri . query
95-
96- target . write ( "#{ method } #{ path } #{ version } \r \n " )
97- target . write ( "Host: #{ uri . host } :#{ uri . port } \r \n " )
98- target . write ( "Content-Length: #{ body . bytesize } \r \n " ) if body
99- target . write ( "\r \n " )
100- target . write ( body ) if body
101- target
113+ def send_to_target ( method , target , body , version )
114+ socket = TCPSocket . new ( target . host , target . port )
115+ path = target . path . to_s . empty? ? "/" : target . path
116+ path = "#{ path } ?#{ target . query } " if target . query
117+
118+ socket . write ( "#{ method } #{ path } #{ version } \r \n " )
119+ socket . write ( "Host: #{ target . host } :#{ target . port } \r \n " )
120+ socket . write ( "Content-Length: #{ body . bytesize } \r \n " ) if body
121+ socket . write ( "\r \n " )
122+ socket . write ( body ) if body
123+ socket
102124 end
103125
104126 def relay_response ( client , target )
@@ -113,6 +135,30 @@ def relay_response(client, target)
113135 # Target connection error
114136 end
115137
138+ def tunnel_connection ( client , target )
139+ target_socket = TCPSocket . new ( target . host , target . port )
140+
141+ client . write ( "HTTP/1.1 200 Connection established\r \n \r \n " )
142+ relay_tunnel ( client , target_socket )
143+ ensure
144+ target_socket &.close rescue nil # rubocop:disable Style/RescueModifier
145+ end
146+
147+ def relay_tunnel ( client , target )
148+ [
149+ Thread . new { copy_stream ( client , target ) } ,
150+ Thread . new { copy_stream ( target , client ) }
151+ ] . each ( &:join )
152+ end
153+
154+ def copy_stream ( source , destination )
155+ loop do
156+ destination . write ( source . readpartial ( 1024 ) )
157+ end
158+ rescue EOFError , IOError , Errno ::ECONNRESET , Errno ::EPIPE
159+ nil
160+ end
161+
116162 def read_response_headers ( target )
117163 headers = +""
118164 content_length = nil
0 commit comments