#include #include #include #include #include #include #include #include #include #include #include #ifdef _WIN32 #include #include #include #include #else #include #endif #define HTPDATE_VERSION "1.0.4rc1" #define DEFAULT_COUNT 5 #define DEFAULT_INTERVAL 500 #define DEFAULT_THRESHOLD 1500 #define DEFAULT_TIMEOUT 100000L #define DEFAULT_TRANSFER_TIMEOUT 60000L #define DEFAULT_DNS_TIMEOUT 30000L #define DEFAULT_USER_AGENT "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36" #define DEFAULT_METHOD "HEAD" #define DEFAULT_HTTP_VERSION "auto" #define DEFAULT_RETRY 0 #define DEFAULT_REDIRECT false #define DEFAULT_INSECURE false #define DEFAULT_ADJUST false #define DEFAULT_VERBOSE false typedef long long TIME_T; struct ResponseData { char *date; TIME_T duration; } data = {NULL, 0.0}; struct HeaderData { char *headers; size_t headers_len; } header_data = {NULL, 0}; struct CmdOptions { int count; int interval; long timeout; long transfer_timeout; long dns_timeout; char *user_agent; char *method; int retry; bool insecure; bool help; bool version; bool adjust; int threshold; } cmd_options = { .count = DEFAULT_COUNT, .interval = DEFAULT_INTERVAL, .timeout = DEFAULT_TIMEOUT, .transfer_timeout = DEFAULT_TRANSFER_TIMEOUT, .dns_timeout = DEFAULT_DNS_TIMEOUT, .user_agent = DEFAULT_USER_AGENT, .method = DEFAULT_METHOD, .retry = DEFAULT_RETRY, .insecure = DEFAULT_INSECURE, .help = false, .version = false, .adjust = DEFAULT_ADJUST, .threshold = DEFAULT_THRESHOLD }; size_t HeaderCallback(void *ptr, size_t size, size_t nmemb, void *userdata); size_t WriteCallback(void *ptr, size_t size, size_t nmemb, void *userdata); TIME_T get_current_time(); char *strtolower(const char *str); int parse_month(const char *month); TIME_T parse_date(const char *date_str); TIME_T parse_date_header(const char *headers, size_t headers_len, const char *url); void print_formatted(const char *format, const char **args, size_t arg_count); void print_help(); void parse_options(int argc, char *argv[]); char *add_protocol_if_needed(const char *url); TIME_T resolve_and_cache_dns(CURL *curl, const char *url); void process_url(CURL *curl, const char *url, TIME_T *deltas, size_t *delta_count, int *max_width); TIME_T calculate_median(TIME_T *deltas, size_t count); int qsort_compare(const void *a, const void *b); size_t HeaderCallback(void *ptr, size_t size, size_t nmemb, void *userdata) { size_t realsize = size * nmemb; struct HeaderData *header_data = (struct HeaderData *)userdata; char *new_headers = realloc(header_data->headers, header_data->headers_len + realsize + 1); if (new_headers) { header_data->headers = new_headers; memcpy(header_data->headers + header_data->headers_len, ptr, realsize); header_data->headers_len += realsize; header_data->headers[header_data->headers_len] = '\0'; } else { fprintf(stderr, "Memory allocation failed in HeaderCallback\n"); } return realsize; } size_t WriteCallback(void *ptr, size_t size, size_t nmemb, void *userdata) { // Do nothing, just to prevent curl from printing the response body return size * nmemb; } TIME_T get_current_time() { #ifdef _WIN32 FILETIME ft; ULARGE_INTEGER ul; GetSystemTimeAsFileTime(&ft); ul.LowPart = ft.dwLowDateTime; ul.HighPart = ft.dwHighDateTime; // FILETIME is in 100-nanosecond intervals since January 1, 1601 (UTC) // Convert to milliseconds since January 1, 1970 (UTC) return (ul.QuadPart / 10000LL - 11644473600000LL); #else struct timeval tv; gettimeofday(&tv, NULL); return tv.tv_sec * 1000 + tv.tv_usec / 1000; #endif } char *strtolower(const char *str) { char *lower = malloc(strlen(str) + 1); if (lower) { for (size_t i = 0; str[i]; i++) { lower[i] = tolower(str[i]); } lower[strlen(str)] = '\0'; } return lower; } int parse_month(const char *month) { static const char *months[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; for (int i = 0; i < 12; i++) { if (strcasecmp(month, months[i]) == 0) { return i; } } return -1; } TIME_T get_timezone_offset() { time_t now = time(NULL); struct tm *local_tm = localtime(&now); long timezone_offset = 0; #ifdef _WIN32 TIME_ZONE_INFORMATION tz_info; DWORD result = GetTimeZoneInformation(&tz_info); if (result == TIME_ZONE_ID_INVALID) { fprintf(stderr, "Failed to get timezone information\n"); exit(EXIT_FAILURE); } timezone_offset = 0 - tz_info.Bias * 60 * 1000; #else timezone_offset = local_tm->tm_gmtoff * 1000; #endif return timezone_offset; } TIME_T parse_date(const char *date_str) { struct tm tm = {0}; char day[4], month[4], year[5], time[9], zone[4]; if ((sscanf(date_str, "%3s, %2d %3s %4s %2d:%2d:%2d %3s", day, &tm.tm_mday, month, year, &tm.tm_hour, &tm.tm_min, &tm.tm_sec, zone) == 8) || (sscanf(date_str, "%3s, %2d-%3s-%4s %2d:%2d:%2d %3s", day, &tm.tm_mday, month, year, &tm.tm_hour, &tm.tm_min, &tm.tm_sec, zone) == 8)) { tm.tm_year = atoi(year) - 1900; tm.tm_mon = parse_month(month); if (tm.tm_mon != -1) { return mktime(&tm) * 1000 + get_timezone_offset() + 500; } } else { printf("sscanf failed: %s", date_str); } return 0; } TIME_T parse_date_header(const char *headers, size_t headers_len, const char *url) { char *lower_headers = strtolower(headers); const char *date_header = NULL; const char *last_date_header = NULL; char *temp_headers = lower_headers; while ((temp_headers = strstr(temp_headers, "date:"))) { last_date_header = temp_headers; temp_headers += 5; // Move past "Date:" } free(lower_headers); if (last_date_header) { size_t offset = last_date_header - lower_headers; last_date_header = headers + offset + 5; // Skip "Date:" while (*last_date_header == ' ') last_date_header++; // Skip spaces TIME_T server_timestamp = parse_date(last_date_header); if (server_timestamp < 1) fprintf(stderr, "Failed to parse Date header for URL %s - '%s'\n", url, last_date_header); return server_timestamp; } return 0.0; } void print_formatted(const char *format, const char **args, size_t arg_count) { char buffer[1024]; // Assume the formatted string will not exceed 1024 bytes size_t offset = 0; for (size_t i = 0; i < arg_count; i++) { const char *arg = args[i]; while (*format) { if (*format == '%') { format++; if (*format == 'd') { offset += sprintf(buffer + offset, "%d", (int)(intptr_t)arg); } else if (*format == 'l' && *(format + 1) == 'd') { offset += sprintf(buffer + offset, "%ld", (long)(intptr_t)arg); format++; } else if (*format == 's') { offset += sprintf(buffer + offset, "%s", arg); } format++; } else { buffer[offset++] = *format++; } } } buffer[offset] = '\0'; printf("%s", buffer); } void print_help() { struct HelpOption { const char *option; const char *description; const char **args; // Dynamic array to store arguments size_t arg_count; // Number of arguments }; struct HelpOption help_options[] = { {"-c, --count", "The number of requests for each URL (default: %d)", (const char *[]){(char*)(intptr_t)DEFAULT_COUNT, NULL}, 1}, {"-i, --interval", "The minimum milliseconds between requests (default: %d)", (const char *[]){(char*)(intptr_t)DEFAULT_INTERVAL, NULL}, 1}, {"-T, --timeout", "Total timeout value, milliseconds (default: %ld)", (const char *[]){(char*)(intptr_t)DEFAULT_TIMEOUT, NULL}, 1}, {"-R, --transfer-timeout", "Transfer timeout value, milliseconds (default: %ld)", (const char *[]){(char*)(intptr_t)DEFAULT_TRANSFER_TIMEOUT, NULL}, 1}, {"-D, --dns-timeout", "The timeout value of the domain name resolution, milliseconds (default: %ld)", (const char *[]){(char*)(intptr_t)DEFAULT_DNS_TIMEOUT, NULL}, 1}, {"-u, --user-agent", "Browser user agent name (default: '%s')", (const char *[]){DEFAULT_USER_AGENT, NULL}, 1}, {"-m, --method", "HTTP method (default: '%s')", (const char *[]){DEFAULT_METHOD, NULL}, 1}, {"-r, --retry", "Number of retries (default: %d)", (const char *[]){(char*)(intptr_t)DEFAULT_RETRY, NULL}, 1}, {"-k, --insecure", "Allow insecure server connections when using https or wss (default: %s)", (const char *[]){DEFAULT_INSECURE ? "true" : "false", NULL}, 1}, {"-a, --adjust", "Adjust system time if necessary (default: %s)", (const char *[]){DEFAULT_ADJUST ? "true" : "false", NULL}, 1}, {"-t, --threshold", "At least how many milliseconds are considered to adjust system time (default: %d)", (const char *[]){(char*)(intptr_t)DEFAULT_THRESHOLD, NULL}, 1}, {"-h, --help", "Display this help text", (const char *[]){NULL}, 0}, {"-V, --version", "Display the version of %s-%s and exit", (const char *[]){NULL}, 0} }; printf("Usage: htpdate [options...] URLs...\n"); printf("Options:\n"); for (size_t i = 0; i < sizeof(help_options) / sizeof(help_options[0]); i++) { printf(" %s", help_options[i].option); print_formatted(help_options[i].description, help_options[i].args, help_options[i].arg_count); printf("\n"); } } void parse_options(int argc, char *argv[]) { static struct option long_options[] = { {"count", required_argument, 0, 'c'}, {"interval", required_argument, 0, 'i'}, {"timeout", required_argument, 0, 'T'}, {"transfer-timeout", required_argument, 0, 'R'}, {"dns-timeout", required_argument, 0, 'D'}, {"user-agent", required_argument, 0, 'u'}, {"method", required_argument, 0, 'm'}, {"retry", required_argument, 0, 'r'}, {"insecure", no_argument, 0, 'k'}, {"adjust", no_argument, 0, 'a'}, {"threshold", required_argument, 0, 't'}, {"help", no_argument, 0, 'h'}, {"version", no_argument, 0, 'V'}, {0, 0, 0, 0} }; int option_index = 0; int c; while ((c = getopt_long(argc, argv, "c:i:T:R:D:u:m:r:kat:hV", long_options, &option_index)) != -1) { switch (c) { case 'c': cmd_options.count = atoi(optarg); break; case 'i': cmd_options.interval = atoi(optarg); break; case 'T': cmd_options.timeout = atol(optarg); break; case 'R': cmd_options.transfer_timeout = atol(optarg); break; case 'D': cmd_options.dns_timeout = atol(optarg); break; case 'u': cmd_options.user_agent = optarg; break; case 'm': cmd_options.method = optarg; break; case 'r': cmd_options.retry = atoi(optarg); break; case 'k': cmd_options.insecure = true; break; case 'a': cmd_options.adjust = true; break; case 't': cmd_options.threshold = atoi(optarg); break; case 'h': cmd_options.help = true; break; case 'V': cmd_options.version = true; break; default: fprintf(stderr, "Usage: htpdate [options...] URLs...\n"); exit(EXIT_FAILURE); } } } // Helper function to add protocol prefix if needed char *add_protocol_if_needed(const char *url) { const char *protocol_prefix = "https://"; size_t protocol_prefix_len = strlen(protocol_prefix); // Check if the URL already contains a protocol prefix const char *colon_slash_slash = strstr(url, "://"); if (colon_slash_slash) { // URL already contains a protocol prefix return strdup(url); } else { // URL does not have a protocol prefix, add https:// prefix char *new_url = malloc(protocol_prefix_len + strlen(url) + 1); if (new_url) { strcpy(new_url, protocol_prefix); strcat(new_url, url); } return new_url; } } // Function to perform DNS resolution and cache the result TIME_T resolve_and_cache_dns(CURL *curl, const char *url) { curl_easy_setopt(curl, CURLOPT_URL, url); curl_easy_setopt(curl, CURLOPT_CONNECT_ONLY, 1L); curl_easy_setopt(curl, CURLOPT_DNS_CACHE_TIMEOUT, cmd_options.dns_timeout / 1000); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, (long)(cmd_options.insecure ? 0L : 1L)); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, (long)(cmd_options.insecure ? 0L : 2L)); TIME_T start_time = get_current_time(); CURLcode res = CURLE_OK; int retry_count = 0; while (retry_count <= cmd_options.retry && res != CURLE_OK) { res = curl_easy_perform(curl); if (res != CURLE_OK) { fprintf(stderr, "DNS resolution failed for URL %s: %s\n", url, curl_easy_strerror(res)); retry_count++; } } TIME_T end_time = get_current_time(); return end_time - start_time; } void process_url(CURL *curl, const char *url, TIME_T *deltas, size_t *delta_count, int *max_width) { struct { CURLoption option; const void *parameter; } curl_options[] = { {CURLOPT_CONNECT_ONLY, (const void *)(intptr_t)0L}, {CURLOPT_USERAGENT, (const void *)cmd_options.user_agent}, {CURLOPT_TIMEOUT_MS, (const void *)(intptr_t)cmd_options.timeout}, {CURLOPT_CONNECTTIMEOUT_MS, (const void *)(intptr_t)cmd_options.transfer_timeout}, {CURLOPT_DNS_CACHE_TIMEOUT, (const void *)(intptr_t)cmd_options.dns_timeout}, {CURLOPT_SSL_VERIFYPEER, (const void *)(intptr_t)(cmd_options.insecure ? 0L : 1L)}, {CURLOPT_SSL_VERIFYHOST, (const void *)(intptr_t)(cmd_options.insecure ? 0L : 2L)}, {CURLOPT_FOLLOWLOCATION, (const void *)(intptr_t)(0L)}, {CURLOPT_URL, url}, {CURLOPT_CUSTOMREQUEST, cmd_options.method}, {CURLOPT_HEADERFUNCTION, HeaderCallback}, {CURLOPT_HEADERDATA, (const void *)&header_data}, {CURLOPT_FAILONERROR, (const void *)0L}, {CURLOPT_ERRORBUFFER, (const void *)0L}, {CURLOPT_NOBODY, (const void *)1L}, {CURLOPT_WRITEFUNCTION, WriteCallback}, {CURLOPT_VERBOSE, (const void *)0L}, // Enable verbose mode for debugging }; for (size_t i = 0; i < sizeof(curl_options) / sizeof(curl_options[0]); i++) { curl_easy_setopt(curl, curl_options[i].option, curl_options[i].parameter); } for (int i = 0; i < cmd_options.count; i++) { int retry_count = 0; bool success = false; printf("\n#%d\t", i + 1); while (retry_count <= cmd_options.retry && !success) { header_data.headers = NULL; header_data.headers_len = 0; TIME_T sent_at = get_current_time(); CURLcode res = curl_easy_perform(curl); TIME_T received_at = get_current_time(); TIME_T io_time = received_at - sent_at; if (res != CURLE_OK) { fprintf(stderr, "curl_easy_perform() failed for URL %s: %s\n", url, curl_easy_strerror(res)); } else { TIME_T server_timestamp = parse_date_header(header_data.headers, header_data.headers_len, url); if (server_timestamp > 1) { success = true; TIME_T server_time = server_timestamp + io_time / 2.0; TIME_T local_time = get_current_time(); TIME_T delta = server_time - local_time; char delta_str[50]; sprintf(delta_str, "%s%lld ms", delta >= 0 ? "+" : "", delta); *max_width = strlen(delta_str) > *max_width ? strlen(delta_str) : *max_width; printf("%*s", *max_width, delta_str); deltas[(*delta_count)++] = delta; continue; } else { curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); } } retry_count += 1; } if (header_data.headers) { free(header_data.headers); header_data.headers = NULL; header_data.headers_len = 0; } if (i < cmd_options.count - 1) { usleep(cmd_options.interval * 1000); // Sleep for the interval in milliseconds } } printf("\n"); } // Function to calculate the median of an array of deltas TIME_T calculate_median(TIME_T *deltas, size_t count) { if (count == 0) return 0.0; qsort(deltas, count, sizeof(TIME_T), qsort_compare); if (count % 2 == 0) { return (deltas[count / 2 - 1] + deltas[count / 2]) / 2; } else { return deltas[count / 2]; } } // Comparison function for qsort int qsort_compare(const void *a, const void *b) { TIME_T diff = (*(TIME_T *)a - *(TIME_T *)b); return (diff > 0.0) - (diff < 0.0); } void adjust_system_time(TIME_T delta) { #ifdef _WIN32 // Windows specific code char date_format[64]; // Get the short date format from the registry FILE *fp = popen("reg query \"HKCU\\Control Panel\\International\" /v sShortDate", "r"); if (fp) { while (fgets(date_format, sizeof(date_format), fp)) { if (strstr(date_format, "REG_SZ")) { break; } } pclose(fp); } // Extract the date format string char *format_start = strstr(date_format, "REG_SZ"); if (format_start) { format_start += strlen("REG_SZ"); while (*format_start == ' ') { format_start++; } char *format_end = strpbrk(format_start, "\r\n"); if (format_end) { while (*format_end == ' ' && format_end > format_start) { format_end--; } *format_end = '\0'; } } // Convert the date format to the desired format struct { const char *pattern; const char *format; } date_patterns[] = { {"yyyyy", "%Y-"}, {"yyyy", "%Y-"}, {"yy", "%Y-"}, {"Y", "%Y-"}, {"MM", "%m-"}, {"M", "%m-"}, {"mmm", "%m-"}, {"mm", "%m-"}, {"m", "%m-"}, {"DD", "%d-"}, {"D", "%d-"}, {"ddd", "%d-"}, {"dd", "%d-"}, {"d", "%d-"}, }; char command_format[128] = "date "; char *p = format_start; while (*p) { for (size_t i = 0; i < sizeof(date_patterns) / sizeof(date_patterns[0]); i++) { size_t pattern_len = strlen(date_patterns[i].pattern); if (strncmp(p, date_patterns[i].pattern, pattern_len) == 0) { strcat(command_format, date_patterns[i].format); p += pattern_len - 1; break; } } p++; } // remove the trailing '-' if (command_format[strlen(command_format) - 1] == '-') { command_format[strlen(command_format) - 1] = '\0'; } // add time command strcat(command_format, " && time %H:%M:%S"); #else char *command_format = "date -s '%Y-%m-%dT%H:%M:%S"; #endif // get target time time_t moment = time(NULL) + (time_t)(delta / 1000); int milliseconds = delta % 1000; char milliseconds_str[10]; char adjust_cmd[128]; #ifdef _WIN32 struct tm *tm_info = localtime(&moment); snprintf(milliseconds_str, sizeof(milliseconds_str), ".%02d", milliseconds / 10); #else struct tm *tm_info = gmtime(&moment); snprintf(milliseconds_str, sizeof(milliseconds_str), ".%03d'", milliseconds); #endif strftime(adjust_cmd, sizeof(adjust_cmd), command_format, tm_info); strcat(adjust_cmd, milliseconds_str); // printf("run %s\n", adjust_cmd); system(adjust_cmd); } int main(int argc, char *argv[]) { parse_options(argc, argv); if (cmd_options.help) { print_help(); return 0; } if (cmd_options.version) { printf("htpdate version %s\n", HTPDATE_VERSION); return 0; } curl_global_init(CURL_GLOBAL_DEFAULT); CURL *curl = curl_easy_init(); if (!curl) { fprintf(stderr, "curl init failed\n"); return -1; } TIME_T *deltas = malloc(cmd_options.count * (argc - optind) * sizeof(TIME_T)); if (!deltas) { fprintf(stderr, "Memory allocation failed for deltas\n"); curl_easy_cleanup(curl); curl_global_cleanup(); return -1; } size_t delta_count = 0; int max_width = 0; // Process each URL provided in the command line arguments for (int i = optind; i < argc; i++) { char *full_url = add_protocol_if_needed(argv[i]); if (!full_url) { fprintf(stderr, "Memory allocation failed for URL: %s\n", argv[i]); continue; } printf("%s %s", cmd_options.method, full_url); resolve_and_cache_dns(curl, full_url); process_url(curl, full_url, deltas, &delta_count, &max_width); free(full_url); } // Calculate and print the median delta if (delta_count > 0) { TIME_T median_delta = calculate_median(deltas, delta_count); char median_str[50]; sprintf(median_str, "%s%lld ms", median_delta >= 0 ? "+" : "", median_delta); printf("median:\t%*s\n", max_width, median_str); if (cmd_options.adjust && abs(median_delta) >= cmd_options.threshold) { adjust_system_time(median_delta); printf("System time adjusted by %s ms\n", median_str); } else { printf("System time adjustment not necessary\n"); } } free(deltas); curl_easy_cleanup(curl); curl_global_cleanup(); return 0; }