diff --git a/thirdparty/vschannel/vschannel.c b/thirdparty/vschannel/vschannel.c index 34fcde3374..84ea35bce0 100644 --- a/thirdparty/vschannel/vschannel.c +++ b/thirdparty/vschannel/vschannel.c @@ -1,5 +1,14 @@ #include #include +#include + +#ifndef WC_ERR_INVALID_CHARS +#define WC_ERR_INVALID_CHARS 0x00000080 +#endif + +#ifndef IDN_USE_STD3_ASCII_RULES +#define IDN_USE_STD3_ASCII_RULES 0x00000002 +#endif #ifndef SCHANNEL_NAME #ifdef UNICODE @@ -931,6 +940,90 @@ cleanup: } +static INT vschannel_idn_to_ascii(LPCWSTR host, LPWSTR ascii_host, INT capacity) { + // TCC does not ship a normaliz import library. Use an absolute system path + // so loading the API also works on Windows 7 without newer loader flags. + const WCHAR dll_name[] = L"\\normaliz.dll"; + WCHAR dll_path[MAX_PATH]; + UINT dir_length = GetSystemDirectoryW(dll_path, MAX_PATH); + if(dir_length == 0) { + return (INT)GetLastError(); + } + if(dir_length > MAX_PATH - sizeof(dll_name) / sizeof(WCHAR)) { + return ERROR_INSUFFICIENT_BUFFER; + } + memcpy(dll_path + dir_length, dll_name, sizeof(dll_name)); + HMODULE normaliz = LoadLibraryExW(dll_path, NULL, LOAD_WITH_ALTERED_SEARCH_PATH); + if(normaliz == NULL) { + return (INT)GetLastError(); + } + typedef INT (WINAPI *IdnToAsciiFn)(DWORD, LPCWSTR, INT, LPWSTR, INT); + IdnToAsciiFn idn_to_ascii = (IdnToAsciiFn)GetProcAddress(normaliz, "IdnToAscii"); + INT err_code = ERROR_SUCCESS; + if(idn_to_ascii == NULL) { + err_code = (INT)GetLastError(); + } else if(idn_to_ascii(IDN_USE_STD3_ASCII_RULES, host, -1, ascii_host, capacity) == 0) { + err_code = (INT)GetLastError(); + } + FreeLibrary(normaliz); + return err_code; +} + +// The caller owns the returned ASCII request and must release it with LocalFree. +static INT vschannel_build_proxy_request(LPCWSTR host, INT port_number, CHAR **request, INT *length) { + *request = NULL; + *length = 0; + if(host == NULL || host[0] == L'\0' || port_number < 1 || port_number > 65535) { + return ERROR_INVALID_PARAMETER; + } + BOOL needs_idna = FALSE; + for(LPCWSTR p = host; *p; ++p) { + if(*p <= L' ' || *p == 0x7f) { + return ERROR_INVALID_PARAMETER; + } + if(*p > 0x7f) { + needs_idna = TRUE; + } + } + // DNS names fit in 255 ASCII characters, including a trailing dot. + WCHAR ascii_host[256]; + if(needs_idna) { + INT err_code = vschannel_idn_to_ascii(host, ascii_host, sizeof(ascii_host) / sizeof(WCHAR)); + if(err_code != ERROR_SUCCESS) { + return err_code; + } + host = ascii_host; + } + + INT host_size = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, host, -1, NULL, 0, NULL, NULL); + if(host_size == 0) { + return (INT)GetLastError(); + } + const CHAR prefix[] = "CONNECT "; + const INT prefix_len = sizeof(prefix) - 1; + CHAR suffix[64]; + INT suffix_len = snprintf(suffix, sizeof(suffix), ":%d HTTP/1.0\r\nUser-Agent: webclient\r\n\r\n", port_number); + if(suffix_len < 0 || suffix_len >= (INT)sizeof(suffix) || host_size > INT_MAX - prefix_len - suffix_len) { + return ERROR_INSUFFICIENT_BUFFER; + } + CHAR *message = (CHAR *)LocalAlloc(LMEM_FIXED, (SIZE_T)prefix_len + host_size + suffix_len); + if(message == NULL) { + return ERROR_NOT_ENOUGH_MEMORY; + } + INT converted = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, host, -1, + message + prefix_len, host_size, NULL, NULL); + if(converted == 0) { + INT err_code = (INT)GetLastError(); + LocalFree(message); + return err_code; + } + memcpy(message, prefix, prefix_len); + memcpy(message + prefix_len + converted - 1, suffix, suffix_len + 1); + *request = message; + *length = prefix_len + converted - 1 + suffix_len; + return ERROR_SUCCESS; +} + static INT connect_to_server(TlsContext *tls_ctx, LPWSTR host, INT port_number) { SOCKET Socket; @@ -965,29 +1058,37 @@ static INT connect_to_server(TlsContext *tls_ctx, LPWSTR host, INT port_number) } if(use_proxy) { - BYTE pbMessage[200]; - DWORD cbMessage; - - // Build message for proxy server - strcpy(pbMessage, "CONNECT "); - strcat(pbMessage, host); - strcat(pbMessage, ":"); - _itoa(port_number, pbMessage + strlen(pbMessage), 10); - strcat(pbMessage, " HTTP/1.0\r\nUser-Agent: webclient\r\n\r\n"); - cbMessage = (DWORD)strlen(pbMessage); - - // Send message to proxy server - if(send(Socket, pbMessage, cbMessage, 0) == SOCKET_ERROR) { - INT err_code = WSAGetLastError(); + CHAR *message = NULL; + INT message_length = 0; + INT err_code = vschannel_build_proxy_request(host, port_number, &message, &message_length); + if(err_code != ERROR_SUCCESS) { vschannel_set_last_error(tls_ctx, err_code); + closesocket(Socket); return err_code; } + // Send message to proxy server + INT sent = 0; + while(sent < message_length) { + INT n = send(Socket, message + sent, message_length - sent, 0); + if(n <= 0) { + err_code = n == SOCKET_ERROR ? WSAGetLastError() : WSAECONNRESET; + LocalFree(message); + vschannel_set_last_error(tls_ctx, err_code); + closesocket(Socket); + return err_code; + } + sent += n; + } + LocalFree(message); + // Receive message from proxy server - cbMessage = recv(Socket, pbMessage, 200, 0); + CHAR response[200]; + INT cbMessage = recv(Socket, response, sizeof(response), 0); if(cbMessage == SOCKET_ERROR) { - INT err_code = WSAGetLastError(); + err_code = WSAGetLastError(); vschannel_set_last_error(tls_ctx, err_code); + closesocket(Socket); return err_code; } diff --git a/vlib/net/http/vschannel_proxy_windows_test.c.v b/vlib/net/http/vschannel_proxy_windows_test.c.v new file mode 100644 index 0000000000..e81a27b16b --- /dev/null +++ b/vlib/net/http/vschannel_proxy_windows_test.c.v @@ -0,0 +1,99 @@ +// vtest build: windows +module http + +fn C.vschannel_build_proxy_request(&u16, int, &&char, &int) int + +fn schannel_proxy_request_for_test(host &u16, port int) (int, string) { + $if no_vschannel ? { + return 0, '' + } $else { + mut buffer := unsafe { &char(nil) } + mut length := -1 + status := C.vschannel_build_proxy_request(host, port, &buffer, &length) + if status != 0 { + assert buffer == unsafe { nil } + assert length == 0 + return status, '' + } + defer { + C.LocalFree(buffer) + } + assert length > 0 + assert unsafe { buffer[length] } == 0 + return status, unsafe { (&u8(buffer)).vstring_with_len(length).clone() } + } +} + +fn test_schannel_proxy_request_preserves_hostname_and_port() { + $if no_vschannel ? { + return + } + for host in ['example.com', 'Example.COM', '127.0.0.1', '[2001:db8::1]', 'xn--mnich-kva.example', + 'example.com.'] { + for port in [1, 443, 8443, 65535] { + status, request := schannel_proxy_request_for_test(host.to_wide(), port) + assert status == 0 + assert request == 'CONNECT ${host}:${port} HTTP/1.0\r\nUser-Agent: webclient\r\n\r\n' + } + } +} + +fn test_schannel_proxy_request_encodes_internationalized_hostname() { + $if no_vschannel ? { + return + } + hosts := { + 'münich.example': 'xn--mnich-kva.example' + 'mu\u0308nich.example': 'xn--mnich-kva.example' + 'münich.example.': 'xn--mnich-kva.example.' + '例え.テスト': 'xn--r8jz45g.xn--zckzah' + 'münich。example': 'xn--mnich-kva.example' + } + for host, ascii_host in hosts { + for port in [1, 443, 8443, 65535] { + status, request := schannel_proxy_request_for_test(host.to_wide(), port) + assert status == 0 + assert request == 'CONNECT ${ascii_host}:${port} HTTP/1.0\r\nUser-Agent: webclient\r\n\r\n' + } + } +} + +fn test_schannel_proxy_request_accepts_long_hostname() { + $if no_vschannel ? { + return + } + host := 'a'.repeat(63) + '.' + 'b'.repeat(63) + '.' + 'c'.repeat(63) + '.' + 'd'.repeat(61) + assert host.len == 253 + status, request := schannel_proxy_request_for_test(host.to_wide(), 443) + assert status == 0 + assert request == 'CONNECT ${host}:443 HTTP/1.0\r\nUser-Agent: webclient\r\n\r\n' + unicode_host := ('ü'.repeat(57) + '.').repeat(3) + 'ü'.repeat(55) + ascii_host := ('xn--td' + 'a'.repeat(57) + '.').repeat(3) + 'xn--td' + 'a'.repeat(55) + assert ascii_host.len == 253 + idn_status, idn_request := schannel_proxy_request_for_test(unicode_host.to_wide(), 443) + assert idn_status == 0 + assert idn_request == 'CONNECT ${ascii_host}:443 HTTP/1.0\r\nUser-Agent: webclient\r\n\r\n' +} + +fn test_schannel_proxy_request_rejects_invalid_input() { + $if no_vschannel ? { + return + } + for host in ['', 'bad host', 'host\tname', 'example.com\r\nInjected: value'] { + status, _ := schannel_proxy_request_for_test(host.to_wide(), 443) + assert status == 87 // ERROR_INVALID_PARAMETER + } + for port in [-1, 0, 65536] { + status, _ := schannel_proxy_request_for_test('example.com'.to_wide(), port) + assert status == 87 + } + null_status, _ := schannel_proxy_request_for_test(unsafe { nil }, 443) + assert null_status == 87 + invalid_utf16 := [u16(0xd800), 0]! + status, _ := schannel_proxy_request_for_test(&invalid_utf16[0], 443) + assert status != 0 + for host in ['münich/.example', 'münich/example', 'ü'.repeat(58) + '.example'] { + idn_status, _ := schannel_proxy_request_for_test(host.to_wide(), 443) + assert idn_status == 123 // ERROR_INVALID_NAME + } +}