505 lines
15 KiB
Plaintext
505 lines
15 KiB
Plaintext
#import "Basic";
|
|
#import "File";
|
|
#import "Thread";
|
|
#import "Windows";
|
|
#import "String";
|
|
|
|
|
|
Updater_Thread_Data :: struct {
|
|
hwnd: HWND;
|
|
update_interval_seconds: s32 = 3600; // 1 hour
|
|
stop_event: HANDLE;
|
|
is_test_mode: bool;
|
|
}
|
|
|
|
get_remote_file_size :: (url: string) -> u64, bool, string {
|
|
hInternet := InternetOpenW(
|
|
utf8_to_wide("Sing-boxTrayDownloader"),
|
|
INTERNET_OPEN_TYPE_PRECONFIG,
|
|
null,
|
|
null,
|
|
0
|
|
);
|
|
if !hInternet {
|
|
return 0, false, "Failed to initialize WinINet session";
|
|
}
|
|
defer InternetCloseHandle(hInternet);
|
|
|
|
flags: DWORD = INTERNET_FLAG_RELOAD | INTERNET_FLAG_NO_CACHE_WRITE;
|
|
if starts_with(url, "https") {
|
|
flags |= INTERNET_FLAG_SECURE;
|
|
}
|
|
|
|
headers := "User-Agent: Sing-boxTray\r\n";
|
|
hUrl := InternetOpenUrlW(
|
|
hInternet,
|
|
utf8_to_wide(url),
|
|
utf8_to_wide(headers),
|
|
cast(u32) headers.count,
|
|
flags,
|
|
0
|
|
);
|
|
if !hUrl {
|
|
err := GetLastError();
|
|
return 0, false, tprint("Failed to open URL (Win32 Error: %)", err);
|
|
}
|
|
defer InternetCloseHandle(hUrl);
|
|
|
|
response_code: DWORD;
|
|
response_code_size: DWORD = size_of(type_of(response_code));
|
|
if !HttpQueryInfoW(hUrl, HTTP_QUERY_STATUS_CODE | HTTP_QUERY_FLAG_NUMBER, *response_code, *response_code_size, null) {
|
|
err := GetLastError();
|
|
return 0, false, tprint("Failed to query HTTP status code (Win32 Error: %)", err);
|
|
}
|
|
if response_code != 200 {
|
|
return 0, false, tprint("HTTP request failed with status code %", response_code);
|
|
}
|
|
|
|
content_length: DWORD;
|
|
content_length_size: DWORD = size_of(type_of(content_length));
|
|
if !HttpQueryInfoW(hUrl, HTTP_QUERY_CONTENT_LENGTH | HTTP_QUERY_FLAG_NUMBER, *content_length, *content_length_size, null) {
|
|
err := GetLastError();
|
|
return 0, false, tprint("Failed to query download size (Win32 Error: %)", err);
|
|
}
|
|
|
|
return cast(u64) content_length, true, "";
|
|
}
|
|
|
|
format_file_size :: (bytes: u64) -> string {
|
|
mib: u64 = 1024 * 1024;
|
|
kib: u64 = 1024;
|
|
|
|
if bytes >= mib {
|
|
whole := bytes / mib;
|
|
tenths := (bytes % mib) * 10 / mib;
|
|
return tprint("%.% MiB", whole, tenths);
|
|
}
|
|
if bytes >= kib {
|
|
whole := bytes / kib;
|
|
tenths := (bytes % kib) * 10 / kib;
|
|
return tprint("%.% KiB", whole, tenths);
|
|
}
|
|
return tprint("% bytes", bytes);
|
|
}
|
|
|
|
download_url :: (url: string) -> string, bool, string {
|
|
log_info("Downloading config from URL: %\n", url);
|
|
|
|
if file_exists(url) {
|
|
content, ok := read_entire_file(url);
|
|
if ok return content, true, "";
|
|
return "", false, tprint("Failed to read local file: %", url);
|
|
}
|
|
|
|
hInternet := InternetOpenW(
|
|
utf8_to_wide("Sing-boxTrayUpdater"),
|
|
INTERNET_OPEN_TYPE_PRECONFIG,
|
|
null,
|
|
null,
|
|
0
|
|
);
|
|
if !hInternet {
|
|
return "", false, "Failed to initialize WinINet session";
|
|
}
|
|
defer InternetCloseHandle(hInternet);
|
|
|
|
flags: DWORD = INTERNET_FLAG_RELOAD | INTERNET_FLAG_NO_CACHE_WRITE;
|
|
if starts_with(url, "https") {
|
|
flags |= INTERNET_FLAG_SECURE;
|
|
}
|
|
|
|
headers := "User-Agent: Sing-boxTray\r\n";
|
|
hUrl := InternetOpenUrlW(
|
|
hInternet,
|
|
utf8_to_wide(url),
|
|
utf8_to_wide(headers),
|
|
cast(u32) headers.count,
|
|
flags,
|
|
0
|
|
);
|
|
if !hUrl {
|
|
err := GetLastError();
|
|
return "", false, tprint("Failed to open URL (Win32 Error: %)", err);
|
|
}
|
|
defer InternetCloseHandle(hUrl);
|
|
|
|
// Query response code
|
|
response_code: DWORD;
|
|
response_code_size: DWORD = size_of(type_of(response_code));
|
|
if HttpQueryInfoW(hUrl, HTTP_QUERY_STATUS_CODE | HTTP_QUERY_FLAG_NUMBER, *response_code, *response_code_size, null) {
|
|
if response_code != 200 {
|
|
return "", false, tprint("HTTP request failed with status code %", response_code);
|
|
}
|
|
} else {
|
|
err := GetLastError();
|
|
return "", false, tprint("Failed to query HTTP status code (Win32 Error: %)", err);
|
|
}
|
|
|
|
builder: String_Builder;
|
|
defer free_buffers(*builder);
|
|
|
|
buffer: [4096] u8;
|
|
bytes_read: DWORD;
|
|
while true {
|
|
ok := InternetReadFile(hUrl, buffer.data, xx buffer.count, *bytes_read);
|
|
if !ok {
|
|
err := GetLastError();
|
|
return "", false, tprint("Error reading from URL (Win32 Error: %)", err);
|
|
}
|
|
if bytes_read == 0 {
|
|
break;
|
|
}
|
|
append(*builder, buffer.data, xx bytes_read);
|
|
}
|
|
|
|
result_str := builder_to_string(*builder);
|
|
log_info("Configuration downloaded successfully (% bytes).\n", result_str.count);
|
|
return result_str, true, "";
|
|
}
|
|
|
|
perform_update :: (is_test_mode := false) -> changed: bool, success: bool, error_msg: string {
|
|
config_filename := global_config_path;
|
|
|
|
config_data, read_ok := read_entire_file(config_filename);
|
|
if !read_ok {
|
|
err := tprint("% does not exist. Please configure the Config URL first.", config_filename);
|
|
return false, false, err;
|
|
}
|
|
defer free(config_data);
|
|
|
|
url := get_json_string_field(config_data, "\"_url\"");
|
|
if !url {
|
|
err := tprint("No Config URL configured in %. Please configure the Config URL first.", config_filename);
|
|
return false, false, err;
|
|
}
|
|
|
|
mode := get_json_string_field(config_data, "\"_mode\"");
|
|
if !mode mode = "system_proxy";
|
|
|
|
port_str := get_json_val_field(config_data, "\"_port\"");
|
|
port := 10801;
|
|
if is_test_mode {
|
|
port = 10899; // force test port
|
|
} else if port_str {
|
|
val, parse_ok, remainder := to_integer(port_str);
|
|
if parse_ok {
|
|
port = xx val;
|
|
}
|
|
}
|
|
|
|
update_interval := 3600;
|
|
interval_str := get_json_val_field(config_data, "\"_update_interval\"");
|
|
if interval_str {
|
|
val, parse_ok, remainder := to_integer(interval_str);
|
|
if parse_ok {
|
|
update_interval = xx val;
|
|
}
|
|
}
|
|
|
|
downloaded, success, err_msg := download_url(url);
|
|
if !success return false, false, err_msg;
|
|
defer free(downloaded);
|
|
|
|
modified := modify_config_inbounds(downloaded, port);
|
|
defer free(modified);
|
|
|
|
final_config := set_json_metadata(modified, url, mode, port, update_interval);
|
|
defer free(final_config);
|
|
|
|
if config_data == final_config {
|
|
return false, true, "";
|
|
}
|
|
|
|
write_ok := write_entire_file(config_filename, final_config);
|
|
if !write_ok {
|
|
err := tprint("Failed to write updated content to %", config_filename);
|
|
return false, false, err;
|
|
}
|
|
|
|
return true, true, "";
|
|
}
|
|
|
|
updater_thread_proc :: (thread: *Thread) -> s64 {
|
|
data := cast(*Updater_Thread_Data) thread.data;
|
|
if !data return 1;
|
|
|
|
elapsed_seconds := 0;
|
|
while true {
|
|
status := WaitForSingleObject(data.stop_event, 1000);
|
|
if status == WAIT_OBJECT_0 {
|
|
break;
|
|
}
|
|
|
|
interval := data.update_interval_seconds;
|
|
if interval <= 0 {
|
|
elapsed_seconds = 0;
|
|
continue;
|
|
}
|
|
|
|
elapsed_seconds += 1;
|
|
if elapsed_seconds >= interval {
|
|
elapsed_seconds = 0;
|
|
|
|
log_debug("Background update: Starting automatic config check...\n");
|
|
changed, success, err_msg := perform_update(data.is_test_mode);
|
|
if success {
|
|
if changed {
|
|
log_info("Background update: Configuration updated successfully. Triggering sing-box restart...\n");
|
|
if data.hwnd {
|
|
PostMessageW(data.hwnd, WM_RESTART_SINGBOX, 0, 0);
|
|
}
|
|
} else {
|
|
log_debug("Background update: Configuration is already up-to-date.\n");
|
|
}
|
|
} else {
|
|
log_error("Background update failed: %\n", err_msg);
|
|
}
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
modify_config_inbounds :: (json_content: string, port: int) -> string {
|
|
// Find "inbounds" key
|
|
idx := find_index_from_left(json_content, "\"inbounds\"");
|
|
if idx == -1 {
|
|
first_brace := find_index_from_left(json_content, #char "{");
|
|
if first_brace == -1 return copy_string(json_content);
|
|
|
|
builder: String_Builder;
|
|
append(*builder, slice(json_content, 0, first_brace + 1));
|
|
append(*builder, tprint("\n \"inbounds\": [\n {\n \"type\": \"mixed\",\n \"listen\": \"127.0.0.1\",\n \"listen_port\": %\n }\n ],", port));
|
|
append(*builder, slice(json_content, first_brace + 1, json_content.count - (first_brace + 1)));
|
|
return builder_to_string(*builder);
|
|
}
|
|
|
|
// Find the opening bracket '[' after "inbounds"
|
|
search_start := idx + 10;
|
|
open_bracket_idx := -1;
|
|
for i: search_start..json_content.count-1 {
|
|
if json_content[i] == #char "[" {
|
|
open_bracket_idx = i;
|
|
break;
|
|
} else if json_content[i] == #char ":" || is_space(json_content[i]) {
|
|
continue;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if open_bracket_idx == -1 return copy_string(json_content);
|
|
|
|
// Find the matching closing bracket ']'
|
|
close_bracket_idx := -1;
|
|
bracket_depth := 0;
|
|
for i: open_bracket_idx..json_content.count-1 {
|
|
if json_content[i] == #char "[" {
|
|
bracket_depth += 1;
|
|
} else if json_content[i] == #char "]" {
|
|
bracket_depth -= 1;
|
|
if bracket_depth == 0 {
|
|
close_bracket_idx = i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if close_bracket_idx == -1 return copy_string(json_content);
|
|
|
|
builder: String_Builder;
|
|
append(*builder, slice(json_content, 0, open_bracket_idx));
|
|
append(*builder, tprint("[\n {\n \"type\": \"mixed\",\n \"listen\": \"127.0.0.1\",\n \"listen_port\": %\n }\n ]", port));
|
|
append(*builder, slice(json_content, close_bracket_idx + 1, json_content.count - (close_bracket_idx + 1)));
|
|
|
|
return builder_to_string(*builder);
|
|
}
|
|
|
|
get_json_string_field :: (json: string, field: string) -> string {
|
|
idx := find_index_from_left(json, field);
|
|
if idx == -1 return "";
|
|
|
|
search_start := idx + field.count;
|
|
open_quote := -1;
|
|
for i: search_start..json.count-1 {
|
|
if json[i] == #char "\"" {
|
|
open_quote = i;
|
|
break;
|
|
} else if json[i] == #char ":" || is_space(json[i]) {
|
|
continue;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
if open_quote == -1 return "";
|
|
|
|
close_quote := -1;
|
|
for i: open_quote+1..json.count-1 {
|
|
if json[i] == #char "\"" && json[i-1] != #char "\\" {
|
|
close_quote = i;
|
|
break;
|
|
}
|
|
}
|
|
if close_quote == -1 return "";
|
|
|
|
return slice(json, open_quote + 1, close_quote - open_quote - 1);
|
|
}
|
|
|
|
get_json_val_field :: (json: string, field: string) -> string {
|
|
idx := find_index_from_left(json, field);
|
|
if idx == -1 return "";
|
|
|
|
search_start := idx + field.count;
|
|
val_start := -1;
|
|
for i: search_start..json.count-1 {
|
|
if json[i] == #char ":" || is_space(json[i]) {
|
|
continue;
|
|
} else {
|
|
val_start = i;
|
|
break;
|
|
}
|
|
}
|
|
if val_start == -1 return "";
|
|
|
|
val_end := val_start;
|
|
while val_end < json.count - 1 {
|
|
next_char := json[val_end + 1];
|
|
if next_char == #char "," || next_char == #char "\n" || next_char == #char "\r" || next_char == #char "}" || next_char == #char "]" {
|
|
break;
|
|
}
|
|
val_end += 1;
|
|
}
|
|
|
|
return trim(slice(json, val_start, val_end - val_start + 1));
|
|
}
|
|
|
|
set_json_metadata :: (json: string, url: string, mode: string, port: int, update_interval: int) -> string {
|
|
lines := split(json, "\n");
|
|
defer array_free(lines);
|
|
|
|
builder: String_Builder;
|
|
|
|
inserted := false;
|
|
for lines {
|
|
trimmed := trim(it);
|
|
if find_index_from_left(trimmed, "\"_url\"") != -1 continue;
|
|
if find_index_from_left(trimmed, "\"_mode\"") != -1 continue;
|
|
if find_index_from_left(trimmed, "\"_port\"") != -1 continue;
|
|
if find_index_from_left(trimmed, "\"_update_interval\"") != -1 continue;
|
|
|
|
append(*builder, it);
|
|
append(*builder, "\n");
|
|
|
|
if !inserted && find_index_from_left(trimmed, "{") != -1 {
|
|
append(*builder, tprint(" \"_url\": \"%\",\n \"_mode\": \"%\",\n \"_port\": %,\n \"_update_interval\": %,\n", url, mode, port, update_interval));
|
|
inserted = true;
|
|
}
|
|
}
|
|
|
|
return builder_to_string(*builder);
|
|
}
|
|
|
|
strip_json_metadata :: (json: string) -> string {
|
|
lines := split(json, "\n");
|
|
defer array_free(lines);
|
|
|
|
builder: String_Builder;
|
|
for lines {
|
|
trimmed := trim(it);
|
|
if find_index_from_left(trimmed, "\"_url\"") != -1 continue;
|
|
if find_index_from_left(trimmed, "\"_mode\"") != -1 continue;
|
|
if find_index_from_left(trimmed, "\"_port\"") != -1 continue;
|
|
if find_index_from_left(trimmed, "\"_update_interval\"") != -1 continue;
|
|
|
|
append(*builder, it);
|
|
append(*builder, "\n");
|
|
}
|
|
|
|
return builder_to_string(*builder);
|
|
}
|
|
|
|
download_file :: (url: string, dest_path: string) -> bool, string {
|
|
log_info("Downloading file from URL: % to %\n", url, dest_path);
|
|
|
|
hInternet := InternetOpenW(
|
|
utf8_to_wide("Sing-boxTrayDownloader"),
|
|
INTERNET_OPEN_TYPE_PRECONFIG,
|
|
null,
|
|
null,
|
|
0
|
|
);
|
|
if !hInternet {
|
|
return false, "Failed to initialize WinINet session";
|
|
}
|
|
defer InternetCloseHandle(hInternet);
|
|
|
|
flags: DWORD = INTERNET_FLAG_RELOAD | INTERNET_FLAG_NO_CACHE_WRITE;
|
|
if starts_with(url, "https") {
|
|
flags |= INTERNET_FLAG_SECURE;
|
|
}
|
|
|
|
headers := "User-Agent: Sing-boxTray\r\n";
|
|
hUrl := InternetOpenUrlW(
|
|
hInternet,
|
|
utf8_to_wide(url),
|
|
utf8_to_wide(headers),
|
|
cast(u32) headers.count,
|
|
flags,
|
|
0
|
|
);
|
|
if !hUrl {
|
|
err := GetLastError();
|
|
return false, tprint("Failed to open URL (Win32 Error: %)", err);
|
|
}
|
|
defer InternetCloseHandle(hUrl);
|
|
|
|
// Query response code
|
|
response_code: DWORD;
|
|
response_code_size: DWORD = size_of(type_of(response_code));
|
|
if HttpQueryInfoW(hUrl, HTTP_QUERY_STATUS_CODE | HTTP_QUERY_FLAG_NUMBER, *response_code, *response_code_size, null) {
|
|
if response_code != 200 {
|
|
return false, tprint("HTTP request failed with status code %", response_code);
|
|
}
|
|
} else {
|
|
err := GetLastError();
|
|
return false, tprint("Failed to query HTTP status code (Win32 Error: %)", err);
|
|
}
|
|
|
|
// Open destination file for writing
|
|
hFile := CreateFileW(
|
|
utf8_to_wide(dest_path),
|
|
GENERIC_WRITE,
|
|
0,
|
|
null,
|
|
2, // CREATE_ALWAYS
|
|
0x80, // FILE_ATTRIBUTE_NORMAL
|
|
null
|
|
);
|
|
if hFile == INVALID_HANDLE_VALUE {
|
|
err := GetLastError();
|
|
return false, tprint("Failed to create local file (Win32 Error: %)", err);
|
|
}
|
|
defer CloseHandle(hFile);
|
|
|
|
buffer: [8192] u8;
|
|
bytes_read: DWORD;
|
|
bytes_written: DWORD;
|
|
while true {
|
|
ok := InternetReadFile(hUrl, buffer.data, xx buffer.count, *bytes_read);
|
|
if !ok {
|
|
err := GetLastError();
|
|
return false, tprint("Error reading from URL (Win32 Error: %)", err);
|
|
}
|
|
if bytes_read == 0 {
|
|
break;
|
|
}
|
|
write_ok := WriteFile(hFile, buffer.data, bytes_read, *bytes_written, null);
|
|
if !write_ok || bytes_written != bytes_read {
|
|
err := GetLastError();
|
|
return false, tprint("Error writing to local file (Win32 Error: %)", err);
|
|
}
|
|
}
|
|
|
|
return true, "";
|
|
}
|