| 1 | module http |
| 2 | |
| 3 | import encoding.base64 |
| 4 | import net |
| 5 | import net.urllib |
| 6 | import net.ssl |
| 7 | import net.socks |
| 8 | |
| 9 | @[heap] |
| 10 | struct HttpProxy { |
| 11 | mut: |
| 12 | scheme string |
| 13 | username string |
| 14 | password string |
| 15 | host string |
| 16 | hostname string |
| 17 | port int |
| 18 | url string |
| 19 | } |
| 20 | |
| 21 | // dial_tcp_via_proxy connects to `host` through the proxy specified by `proxy_url`. |
| 22 | // `host` should be in `host:port` form. |
| 23 | pub fn dial_tcp_via_proxy(proxy_url string, host string) !&net.TcpConn { |
| 24 | proxy := new_http_proxy(proxy_url)! |
| 25 | return proxy.dial(host)! |
| 26 | } |
| 27 | |
| 28 | // new_http_proxy creates a new HttpProxy instance, from the given http proxy url in `raw_url` |
| 29 | pub fn new_http_proxy(raw_url string) !&HttpProxy { |
| 30 | mut url := urllib.parse(raw_url) or { return error('malformed proxy url') } |
| 31 | scheme := url.scheme |
| 32 | |
| 33 | if scheme !in ['http', 'https', 'socks5'] { |
| 34 | return error('invalid scheme') |
| 35 | } |
| 36 | |
| 37 | url.path = '' |
| 38 | url.raw_path = '' |
| 39 | url.raw_query = '' |
| 40 | url.fragment = '' |
| 41 | mut username := '' |
| 42 | mut password := '' |
| 43 | |
| 44 | str_url := url.str() |
| 45 | |
| 46 | mut host := url.host |
| 47 | mut port := url.port().int() |
| 48 | |
| 49 | if port == 0 { |
| 50 | if scheme == 'https' { |
| 51 | port = 443 |
| 52 | host += ':' + port.str() |
| 53 | } else if scheme == 'http' { |
| 54 | port = 80 |
| 55 | host += ':' + port.str() |
| 56 | } |
| 57 | } |
| 58 | if port == 0 { |
| 59 | return error('Unknown port') |
| 60 | } |
| 61 | |
| 62 | if u := url.user { |
| 63 | username = u.username |
| 64 | password = u.password |
| 65 | } |
| 66 | |
| 67 | return &HttpProxy{ |
| 68 | scheme: scheme |
| 69 | username: username |
| 70 | password: password |
| 71 | host: host |
| 72 | hostname: url.hostname() |
| 73 | port: port |
| 74 | url: str_url |
| 75 | } |
| 76 | } |
| 77 | |
| 78 | // str returns the configured proxy URL for logging and debugging. |
| 79 | pub fn (pr &HttpProxy) str() string { |
| 80 | if isnil(pr) { |
| 81 | return 'nil' |
| 82 | } |
| 83 | return pr.url |
| 84 | } |
| 85 | |
| 86 | // host format - ip:port |
| 87 | fn (pr &HttpProxy) build_proxy_headers(host string) string { |
| 88 | mut uheaders := []string{} |
| 89 | address := host.all_before_last(':') |
| 90 | uheaders << 'Proxy-Connection: Keep-Alive\r\n' |
| 91 | if pr.username != '' { |
| 92 | mut authinfo := '' |
| 93 | |
| 94 | authinfo += pr.username |
| 95 | if pr.password != '' { |
| 96 | authinfo += ':${pr.password}' |
| 97 | } |
| 98 | |
| 99 | encoded_authinfo := base64.encode(authinfo.bytes()) |
| 100 | |
| 101 | uheaders << 'Proxy-Authorization: Basic ${encoded_authinfo}\r\n' |
| 102 | } |
| 103 | |
| 104 | version := Version.v1_1 |
| 105 | |
| 106 | return 'CONNECT ${host} ${version}\r\nHost: ${address}\r\n' + uheaders.join('') + '\r\n' |
| 107 | } |
| 108 | |
| 109 | fn read_proxy_connect_response(mut tcp net.TcpConn) !string { |
| 110 | mut total_bytes_read := 0 |
| 111 | mut msg := [4096]u8{} |
| 112 | mut buffer := [1]u8{} |
| 113 | for total_bytes_read < msg.len { |
| 114 | bytes_read := tcp.read_ptr(&buffer[0], 1)! |
| 115 | if bytes_read == 0 { |
| 116 | return error('proxy closed the connection while establishing a tunnel') |
| 117 | } |
| 118 | msg[total_bytes_read] = buffer[0] |
| 119 | total_bytes_read++ |
| 120 | if total_bytes_read > 3 && msg[total_bytes_read - 1] == `\n` |
| 121 | && msg[total_bytes_read - 2] == `\r` && msg[total_bytes_read - 3] == `\n` |
| 122 | && msg[total_bytes_read - 4] == `\r` { |
| 123 | return msg[..total_bytes_read].bytestr() |
| 124 | } |
| 125 | } |
| 126 | return error('proxy response headers exceeded 4096 bytes') |
| 127 | } |
| 128 | |
| 129 | fn validate_proxy_connect_response(response string) ! { |
| 130 | status_line := response.all_before('\r\n') |
| 131 | if !status_line.starts_with('HTTP/1.1 200') && !status_line.starts_with('HTTP/1.0 200') { |
| 132 | return error('proxy tunnel error: ${status_line}') |
| 133 | } |
| 134 | } |
| 135 | |
| 136 | fn (pr &HttpProxy) connect_tcp(host string) !&net.TcpConn { |
| 137 | if pr.scheme in ['http', 'https'] { |
| 138 | mut tcp := net.dial_tcp(pr.host)! |
| 139 | tcp.write(pr.build_proxy_headers(host).bytes())! |
| 140 | response := read_proxy_connect_response(mut tcp)! |
| 141 | validate_proxy_connect_response(response)! |
| 142 | return tcp |
| 143 | } else if pr.scheme == 'socks5' { |
| 144 | return socks.socks5_dial(pr.host, host, pr.username, pr.password)! |
| 145 | } else { |
| 146 | return error('http_proxy connect_tcp: invalid proxy scheme') |
| 147 | } |
| 148 | } |
| 149 | |
| 150 | fn (pr &HttpProxy) http_do(host urllib.URL, method Method, path string, req &Request, data string, header Header) !Response { |
| 151 | host_name := host.hostname() |
| 152 | mut port := host.port().int() |
| 153 | if port == 0 { |
| 154 | port = if host.scheme == 'https' { 443 } else { 80 } |
| 155 | } |
| 156 | port_part := if (host.scheme == 'http' && port == 80) || (host.scheme == 'https' && port == 443) { |
| 157 | '' |
| 158 | } else { |
| 159 | ':${port}' |
| 160 | } |
| 161 | |
| 162 | s := req.build_request_headers_with(method, host_name, port, |
| 163 | '${host.scheme}://${host_name}${port_part}${path}', data, header) |
| 164 | if host.scheme == 'https' { |
| 165 | mut client := pr.ssl_dial('${host_name}:${port}')! |
| 166 | |
| 167 | $if windows { |
| 168 | return error('Windows Not SUPPORTED') // TODO: windows ssl |
| 169 | // response_text := req.do_request(req.build_request_headers(req.method, host_name, |
| 170 | // path))! |
| 171 | // client.shutdown()! |
| 172 | // return response_text |
| 173 | } $else { |
| 174 | return req.do_request(req.build_request_headers_with(method, host_name, port, path, |
| 175 | data, header), mut client)! |
| 176 | } |
| 177 | } else if host.scheme == 'http' { |
| 178 | mut client := pr.dial('${host_name}:${port}')! |
| 179 | client.set_read_timeout(req.read_timeout) |
| 180 | client.set_write_timeout(req.write_timeout) |
| 181 | client.write_string(s)! |
| 182 | $if trace_http_request ? { |
| 183 | eprintln('> ${s}') |
| 184 | } |
| 185 | response_data := req.read_all_from_client_connection(client)! |
| 186 | client.close()! |
| 187 | response_text := response_data.data.bytestr() |
| 188 | $if trace_http_response ? { |
| 189 | eprintln('< ${response_text}') |
| 190 | } |
| 191 | if req.on_finish != unsafe { nil } { |
| 192 | req.on_finish(req, u64(response_text.len))! |
| 193 | } |
| 194 | return parse_received_response(response_text, response_data.info) |
| 195 | } |
| 196 | return error('Invalid Scheme') |
| 197 | } |
| 198 | |
| 199 | fn (pr &HttpProxy) dial(host string) !&net.TcpConn { |
| 200 | return pr.connect_tcp(host)! |
| 201 | } |
| 202 | |
| 203 | fn (pr &HttpProxy) ssl_dial(host string) !&ssl.SSLConn { |
| 204 | if pr.scheme in ['http', 'https'] { |
| 205 | mut tcp := pr.connect_tcp(host)! |
| 206 | mut ssl_conn := ssl.new_ssl_conn( |
| 207 | verify: '' |
| 208 | cert: '' |
| 209 | cert_key: '' |
| 210 | validate: false |
| 211 | in_memory_verification: false |
| 212 | )! |
| 213 | ssl_conn.connect(mut tcp, host.all_before_last(':')) or { |
| 214 | tcp.close() or {} |
| 215 | return err |
| 216 | } |
| 217 | ssl_conn.owns_socket = true |
| 218 | return ssl_conn |
| 219 | } else if pr.scheme == 'socks5' { |
| 220 | return socks.socks5_ssl_dial(pr.host, host, pr.username, pr.password)! |
| 221 | } else { |
| 222 | return error('http_proxy ssl_dial: invalid proxy scheme') |
| 223 | } |
| 224 | } |
| 225 | |