#include "ConnectivityHTTPTransport.h" #include #include #include #include #include #include #include #include #include #include struct ConnectivityHTTPCancellation { atomic_bool cancelled; }; uint64_t ConnectivityHTTPNow(void) { struct timespec time; clock_gettime(CLOCK_MONOTONIC, &time); return (uint64_t)time.tv_sec * 1000000000ULL + (uint64_t)time.tv_nsec; } ConnectivityHTTPCancellation *ConnectivityHTTPCreateCancellation(void) { ConnectivityHTTPCancellation *token = malloc(sizeof(*token)); if (token) atomic_init(&token->cancelled, 0); return token; } void ConnectivityHTTPCancel(ConnectivityHTTPCancellation *token) { if (token) atomic_store(&token->cancelled, 1); } void ConnectivityHTTPDestroyCancellation(ConnectivityHTTPCancellation *token) { free(token); } static double milliseconds(uint64_t end, uint64_t start) { return (double)(end - start) / 1000000.0; } /* Every wait shares one monotonic deadline. Cancellation is checked at most 25 ms apart, including peers that accept but never read or send anything. */ static int ready(int fd, short events, uint64_t deadline, ConnectivityHTTPCancellation *token) { while (!atomic_load(&token->cancelled)) { uint64_t now = ConnectivityHTTPNow(); if (now >= deadline) return 0; uint64_t remaining = (deadline - now + 999999ULL) / 1000000ULL; int timeout = remaining > 25 ? 25 : (int)remaining; struct pollfd descriptor = { .fd = fd, .events = events }; int result = poll(&descriptor, 1, timeout); if (result > 0) return 1; /* recv / SO_ERROR also handle HUP and ERR. */ if (result < 0 && errno != EINTR) return 0; } return 0; } /* Only HTTP/1.x final headers are accepted. The cumulative header budget also bounds arbitrarily many informational responses; bodies are never awaited. */ static int response_status(const char *bytes, size_t length) { if (length < 16 || (memcmp(bytes, "HTTP/1.1 ", 9) && memcmp(bytes, "HTTP/1.0 ", 9))) return -1; for (int i = 9; i < 12; i++) if (bytes[i] < '0' || bytes[i] > '9') return -1; if (bytes[12] != ' ' && bytes[12] != '\r') return -1; int status = (bytes[9] - '0') * 100 + (bytes[10] - '0') * 10 + bytes[11] - '0'; if (status < 100 || status > 599) return -1; const char *end = bytes + length; const char *line = memmem(bytes, length, "\r\n", 2); if (!line) return -1; line += 2; while (line < end - 2) { const char *next = memmem(line, (size_t)(end - line), "\r\n", 2); if (!next || next == line || !memchr(line, ':', (size_t)(next - line))) return -1; for (const char *p = line; p < next; p++) { if ((unsigned char)*p < 32 && *p != '\t') return -1; } line = next + 2; } return status; } ConnectivityHTTPResult ConnectivityHTTPProbe(const char *request, int proxy_port, int timeout_ms, ConnectivityHTTPCancellation *token) { return ConnectivityHTTPProbeWithReader(request, proxy_port, timeout_ms, token, NULL, NULL); } ConnectivityHTTPResult ConnectivityHTTPProbeWithReader(const char *request, int proxy_port, int timeout_ms, ConnectivityHTTPCancellation *token, ConnectivityHTTPReader reader, void *context) { ConnectivityHTTPResult result = { .stage = 1, .local_tcp_ms = -1, .send_ms = -1, .first_byte_ms = -1, .headers_ms = -1 }; int fd = -1; uint64_t started = ConnectivityHTTPNow(); uint64_t deadline = started + (uint64_t)(timeout_ms > 0 ? timeout_ms : 0) * 1000000ULL; if (!token || !request || proxy_port < 1 || proxy_port > 65535) goto cleanup; fd = socket(AF_INET, SOCK_STREAM, 0); if (fd < 0) goto cleanup; int one = 1; setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &one, sizeof(one)); if (fcntl(fd, F_SETFL, O_NONBLOCK) < 0) goto cleanup; struct sockaddr_in address = { .sin_family = AF_INET, .sin_port = htons((uint16_t)proxy_port), .sin_addr.s_addr = htonl(INADDR_LOOPBACK) }; if (connect(fd, (struct sockaddr *)&address, sizeof(address)) < 0 && errno != EINPROGRESS) goto cleanup; if (!ready(fd, POLLOUT, deadline, token)) goto cleanup; int error = 0; socklen_t size = sizeof(error); if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &error, &size) || error) goto cleanup; uint64_t connected = ConnectivityHTTPNow(); result.local_tcp_ms = milliseconds(connected, started); result.stage = 2; size_t length = strlen(request), offset = 0; while (offset < length) { if (!ready(fd, POLLOUT, deadline, token)) goto cleanup; ssize_t sent = send(fd, request + offset, length - offset, 0); if (sent > 0) offset += (size_t)sent; else if (sent == 0 || (errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR)) goto cleanup; } uint64_t sent = ConnectivityHTTPNow(), first = 0; result.send_ms = milliseconds(sent, connected); result.stage = 3; char bytes[65536]; size_t used = 0, consumed = 0, cumulative = 0; while (used < sizeof(bytes)) { if (!ready(fd, POLLIN, deadline, token)) goto cleanup; ssize_t count = reader ? reader(bytes + used, sizeof(bytes) - used, context) : recv(fd, bytes + used, sizeof(bytes) - used, 0); if (count <= 0) { if (count < 0 && (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR)) continue; goto cleanup; } if (!first) { first = ConnectivityHTTPNow(); result.first_byte_ms = milliseconds(first, sent); result.stage = 4; } used += (size_t)count; const char *end; while ((end = memmem(bytes + consumed, used - consumed, "\r\n\r\n", 4))) { size_t header_length = (size_t)(end - (bytes + consumed)) + 4; if (cumulative + consumed + header_length > sizeof(bytes)) goto cleanup; int status = response_status(bytes + consumed, header_length); if (status < 0) goto cleanup; consumed += header_length; if (status >= 200 || status == 101) { uint64_t finished = ConnectivityHTTPNow(); result.status = status; result.headers_ms = milliseconds(finished, first); result.received_headers = finished < deadline && !atomic_load(&token->cancelled); if (result.received_headers) result.stage = 5; goto cleanup; } } cumulative += consumed; if (cumulative + used - consumed >= sizeof(bytes)) goto cleanup; if (consumed) { memmove(bytes, bytes + consumed, used - consumed); used -= consumed; consumed = 0; } } cleanup: result.milliseconds = milliseconds(ConnectivityHTTPNow(), started); result.timed_out = !result.received_headers && ConnectivityHTTPNow() >= deadline; if (fd >= 0) close(fd); return result; }