vschannel: fix proxy CONNECT hostname encoding (#28862)

* vschannel: fix proxy hostname encoding

* vschannel: encode proxy domains as IDNA
This commit is contained in:
Maokaman1 authored and GitHub committed 2026-09-30 17:00:08 +03:00
1 parent 748d6c64f0
commit 65f52d46db
2 files changed
+216 -16

No files matched your search

+117 -16
View File
@@ -1,5 +1,14 @@
#include <vschannel.h>
#include <sspi.h>
#include <limits.h>
#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;
}
@@ -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
}
}