From 126d6e3e5fff2e8620e9fd11ae127487303e9191 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Wed, 15 Apr 2026 16:04:16 +0300 Subject: [PATCH] net.http: fix long-running HTTP polling program encountering socket error (fixes #24302) --- vlib/net/http/http_proxy.v | 23 +++-- vlib/net/http/http_proxy_test.v | 168 ++++++++++++++++++++++++++++++++ vlib/net/socks/socks5.v | 6 +- 3 files changed, 190 insertions(+), 7 deletions(-) diff --git a/vlib/net/http/http_proxy.v b/vlib/net/http/http_proxy.v index f3387185e..4c4b6d8ea 100644 --- a/vlib/net/http/http_proxy.v +++ b/vlib/net/http/http_proxy.v @@ -140,14 +140,21 @@ fn (pr &HttpProxy) connect_tcp(host string) !&net.TcpConn { } fn (pr &HttpProxy) http_do(host urllib.URL, _method Method, path string, req &Request) !Response { - host_name, port := net.split_address(host.hostname())! - - port_part := if port == 80 || port == 0 { '' } else { ':${port}' } + host_name := host.hostname() + mut port := host.port().int() + if port == 0 { + port = if host.scheme == 'https' { 443 } else { 80 } + } + port_part := if (host.scheme == 'http' && port == 80) || (host.scheme == 'https' && port == 443) { + '' + } else { + ':${port}' + } s := req.build_request_headers(req.method, host_name, port, '${host.scheme}://${host_name}${port_part}${path}') if host.scheme == 'https' { - mut client := pr.ssl_dial('${host.host}:443')! + mut client := pr.ssl_dial('${host_name}:${port}')! $if windows { return error('Windows Not SUPPORTED') // TODO: windows ssl @@ -160,7 +167,7 @@ fn (pr &HttpProxy) http_do(host urllib.URL, _method Method, path string, req &Re client)! } } else if host.scheme == 'http' { - mut client := pr.dial('${host.host}:80')! + mut client := pr.dial('${host_name}:${port}')! client.set_read_timeout(req.read_timeout) client.set_write_timeout(req.write_timeout) client.write_string(s)! @@ -195,7 +202,11 @@ fn (pr &HttpProxy) ssl_dial(host string) !&ssl.SSLConn { validate: false in_memory_verification: false )! - ssl_conn.connect(mut tcp, host.all_before_last(':'))! + ssl_conn.connect(mut tcp, host.all_before_last(':')) or { + tcp.close() or {} + return err + } + ssl_conn.owns_socket = true return ssl_conn } else if pr.scheme == 'socks5' { return socks.socks5_ssl_dial(pr.host, host, pr.username, pr.password)! diff --git a/vlib/net/http/http_proxy_test.v b/vlib/net/http/http_proxy_test.v index 37451518a..5b55c3c6e 100644 --- a/vlib/net/http/http_proxy_test.v +++ b/vlib/net/http/http_proxy_test.v @@ -1,8 +1,11 @@ module http import encoding.base64 +import net +import net.mbedtls import net.urllib import os +import time const sample_proxy_url = 'https://localhost' const sample_auth_proxy_url = 'http://user:pass@localhost:8888' @@ -12,6 +15,11 @@ const sample_request = &Request{ url: 'http://${sample_host}' } const sample_path = '/' +const proxy_https_request_count = 12 +const proxy_https_test_cert_path = @VEXEROOT + + '/vlib/net/websocket/tests/autobahn/fuzzing_server_wss/config/server.crt' +const proxy_https_test_key_path = @VEXEROOT + + '/vlib/net/websocket/tests/autobahn/fuzzing_server_wss/config/server.key' fn test_proxy_fields() ? { sample_proxy := new_http_proxy(sample_proxy_url)! @@ -49,6 +57,166 @@ fn test_proxy_headers_authenticated() ? { 'Proxy-Connection: Keep-Alive\r\nProxy-Authorization: Basic ${auth_token}\r\n\r\n' } +enum ProxyTunnelCopyResult { + data + timeout + closed +} + +fn count_open_file_descriptors() int { + $if windows { + return 0 + } $else { + fds := os.ls('/dev/fd') or { return 0 } + return fds.len + } +} + +fn start_https_proxy_test_target_server() !(int, chan bool) { + ready := chan int{cap: 1} + done := chan bool{cap: 1} + spawn fn [ready, done] () { + mut port_listener := net.listen_tcp(.ip, '127.0.0.1:0') or { panic(err) } + port := int((port_listener.addr() or { panic(err) }).port() or { panic(err) }) + port_listener.close() or {} + mut listener := mbedtls.new_ssl_listener('127.0.0.1:${port}', mbedtls.SSLConnectConfig{ + cert: proxy_https_test_cert_path + cert_key: proxy_https_test_key_path + validate: false + in_memory_verification: false + }) or { panic(err) } + ready <- port + defer { + listener.shutdown() or {} + done <- true + } + for _ in 0 .. proxy_https_request_count { + mut conn := listener.accept() or { panic(err) } + handle_https_proxy_test_target_connection(mut conn) + } + }() + return <-ready, done +} + +fn handle_https_proxy_test_target_connection(mut conn mbedtls.SSLConn) { + defer { + conn.shutdown() or {} + } + mut request_buf := []u8{len: 2048} + _ = conn.read(mut request_buf) or { return } + conn.write_string('HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\nok') or { + return + } +} + +fn start_https_proxy_test_server(target_port int) !(int, chan bool) { + ready := chan int{cap: 1} + done := chan bool{cap: 1} + spawn fn [ready, done, target_port] () { + mut listener := net.listen_tcp(.ip, '127.0.0.1:0') or { panic(err) } + port := int((listener.addr() or { panic(err) }).port() or { panic(err) }) + ready <- port + mut workers := []thread{cap: proxy_https_request_count} + for _ in 0 .. proxy_https_request_count { + mut client := listener.accept() or { panic(err) } + workers << spawn handle_https_proxy_test_tunnel(mut client, target_port) + } + listener.close() or {} + workers.wait() + done <- true + }() + return <-ready, done +} + +fn handle_https_proxy_test_tunnel(mut client net.TcpConn, target_port int) { + defer { + client.close() or {} + } + request := read_proxy_request(mut client) or { return } + if !request.starts_with('CONNECT 127.0.0.1:${target_port} HTTP/1.1\r\n') { + client.write_string('HTTP/1.1 400 Bad Request\r\n\r\n') or {} + return + } + mut upstream := net.dial_tcp('127.0.0.1:${target_port}') or { + client.write_string('HTTP/1.1 502 Bad Gateway\r\n\r\n') or {} + return + } + defer { + upstream.close() or {} + } + client.write_string('HTTP/1.1 200 Connection Established\r\n\r\n') or { return } + client.set_read_timeout(50 * time.millisecond) + upstream.set_read_timeout(50 * time.millisecond) + deadline := time.now().add(2 * time.second) + for time.now() < deadline { + client_state := copy_https_proxy_test_tunnel_data(mut client, mut upstream) + upstream_state := copy_https_proxy_test_tunnel_data(mut upstream, mut client) + if client_state == .closed || upstream_state == .closed { + return + } + } +} + +fn read_proxy_request(mut conn net.TcpConn) !string { + mut total_bytes_read := 0 + mut msg := [4096]u8{} + mut buffer := [1]u8{} + for total_bytes_read < msg.len { + bytes_read := conn.read_ptr(&buffer[0], 1)! + if bytes_read == 0 { + return error('unexpected EOF while reading proxy request') + } + msg[total_bytes_read] = buffer[0] + total_bytes_read++ + if total_bytes_read > 3 && msg[total_bytes_read - 1] == `\n` + && msg[total_bytes_read - 2] == `\r` && msg[total_bytes_read - 3] == `\n` + && msg[total_bytes_read - 4] == `\r` { + return msg[..total_bytes_read].bytestr() + } + } + return error('proxy request headers exceeded 4096 bytes') +} + +fn copy_https_proxy_test_tunnel_data(mut src net.TcpConn, mut dst net.TcpConn) ProxyTunnelCopyResult { + mut buf := []u8{len: 1024} + bytes_read := src.read(mut buf) or { + if err.code() == net.err_timed_out_code { + return .timeout + } + return .closed + } + if bytes_read <= 0 { + return .closed + } + dst.write(buf[..bytes_read]) or { return .closed } + return .data +} + +fn test_https_proxy_requests_do_not_leak_sockets() ! { + $if windows { + return + } + target_port, target_done := start_https_proxy_test_target_server()! + proxy_port, proxy_done := start_https_proxy_test_server(target_port)! + baseline_fds := count_open_file_descriptors() + proxy := new_http_proxy('http://127.0.0.1:${proxy_port}')! + for _ in 0 .. proxy_https_request_count { + resp := fetch( + method: .get + url: 'https://127.0.0.1:${target_port}/' + proxy: proxy + validate: false + )! + assert resp.status_code == 200 + assert resp.body == 'ok' + } + _ = <-target_done + _ = <-proxy_done + time.sleep(100 * time.millisecond) + final_fds := count_open_file_descriptors() + assert final_fds <= baseline_fds + 3 +} + fn test_http_proxy_do() { env := os.environ() mut env_proxy := '' diff --git a/vlib/net/socks/socks5.v b/vlib/net/socks/socks5.v index d5926a980..eb0de44b5 100644 --- a/vlib/net/socks/socks5.v +++ b/vlib/net/socks/socks5.v @@ -33,7 +33,11 @@ pub fn socks5_ssl_dial(proxy_url string, host string, username string, password in_memory_verification: false )! mut con := socks5_dial(proxy_url, host, username, password)! - ssl_conn.connect(mut con, host.all_before_last(':')) or { panic(err) } + ssl_conn.connect(mut con, host.all_before_last(':')) or { + con.close() or {} + return err + } + ssl_conn.owns_socket = true return ssl_conn } -- 2.39.5