vschannel: fix proxy CONNECT hostname encoding (#28862)
* vschannel: fix proxy hostname encoding * vschannel: encode proxy domains as IDNA
This commit is contained in:
1 parent
748d6c64f0
commit
65f52d46db
2 files changed
+216
-16
No files matched your search
Vendored
+117
-16
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user