Download src/ds4_web.c from julianmb/ds4fa: direct link, hf CLI and curl.
- Browser
- Download file 49.1 kB
-
https://huggingface.co/julianmb/ds4fa/resolve/main/src/ds4_web.c
- Command line
-
hf download hf://julianmb/ds4fa/src/ds4_web.c
-
curl -L -o ds4_web.c https://huggingface.co/julianmb/ds4fa/resolve/main/src/ds4_web.c
49.1 kB
| typedef struct { | |
| char *ptr; | |
| size_t len; | |
| size_t cap; | |
| } web_buf; | |
| struct ds4_web { | |
| char home[PATH_MAX]; | |
| char profile_dir[PATH_MAX]; | |
| int port; | |
| pid_t chrome_pid; | |
| bool browser_allowed; | |
| ds4_web_confirm_fn confirm; | |
| void *confirm_privdata; | |
| ds4_web_log_fn log; | |
| void *log_privdata; | |
| ds4_web_cancel_fn cancel; | |
| void *cancel_privdata; | |
| int next_cdp_id; | |
| }; | |
| typedef struct { | |
| int fd; | |
| int next_id; | |
| ds4_web *web; | |
| } cdp_ws; | |
| typedef struct { | |
| char *id; | |
| char *ws_url; | |
| } web_tab; | |
| static void *web_xmalloc(size_t n) { | |
| void *p = malloc(n ? n : 1); | |
| if (!p) { | |
| perror("ds4_web: malloc"); | |
| exit(1); | |
| } | |
| return p; | |
| } | |
| static char *web_xstrdup(const char *s) { | |
| if (!s) s = ""; | |
| size_t n = strlen(s); | |
| char *p = web_xmalloc(n + 1); | |
| memcpy(p, s, n + 1); | |
| return p; | |
| } | |
| static void web_buf_append(web_buf *b, const char *s, size_t n) { | |
| if (!n) return; | |
| if (b->len + n + 1 > b->cap) { | |
| size_t cap = b->cap ? b->cap * 2 : 4096; | |
| while (cap < b->len + n + 1) cap *= 2; | |
| char *p = realloc(b->ptr, cap); | |
| if (!p) { | |
| perror("ds4_web: realloc"); | |
| exit(1); | |
| } | |
| b->ptr = p; | |
| b->cap = cap; | |
| } | |
| memcpy(b->ptr + b->len, s, n); | |
| b->len += n; | |
| b->ptr[b->len] = '\0'; | |
| } | |
| static void web_buf_puts(web_buf *b, const char *s) { | |
| web_buf_append(b, s, strlen(s)); | |
| } | |
| static char *web_buf_take(web_buf *b) { | |
| if (!b->ptr) return web_xstrdup(""); | |
| char *p = b->ptr; | |
| b->ptr = NULL; | |
| b->len = b->cap = 0; | |
| return p; | |
| } | |
| static void web_set_err(char *err, size_t err_len, const char *fmt, ...) { | |
| if (!err || err_len == 0) return; | |
| va_list ap; | |
| va_start(ap, fmt); | |
| vsnprintf(err, err_len, fmt, ap); | |
| va_end(ap); | |
| } | |
| static void web_log(ds4_web *web, const char *msg) { | |
| if (web && web->log) web->log(web->log_privdata, msg); | |
| } | |
| static double web_now_sec(void) { | |
| struct timespec ts; | |
| clock_gettime(CLOCK_MONOTONIC, &ts); | |
| return (double)ts.tv_sec + (double)ts.tv_nsec / 1000000000.0; | |
| } | |
| static bool web_cancelled(ds4_web *web) { | |
| return web && web->cancel && web->cancel(web->cancel_privdata); | |
| } | |
| static bool web_set_cancel_err(ds4_web *web, char *err, size_t err_len) { | |
| if (!web_cancelled(web)) return false; | |
| web_set_err(err, err_len, "interrupted"); | |
| return true; | |
| } | |
| static bool web_sleep_ms(ds4_web *web, int ms) { | |
| int left = ms; | |
| while (left > 0) { | |
| if (web_cancelled(web)) return false; | |
| int step = left < 50 ? left : 50; | |
| usleep((useconds_t)step * 1000u); | |
| left -= step; | |
| } | |
| return !web_cancelled(web); | |
| } | |
| static bool web_err_is_interrupted(const char *err) { | |
| return err && !strcmp(err, "interrupted"); | |
| } | |
| static bool web_mkdir_p(const char *path) { | |
| if (!path || !path[0]) return false; | |
| char tmp[PATH_MAX]; | |
| snprintf(tmp, sizeof(tmp), "%s", path); | |
| for (char *p = tmp + 1; *p; p++) { | |
| if (*p != '/') continue; | |
| *p = '\0'; | |
| if (mkdir(tmp, 0700) != 0 && errno != EEXIST) return false; | |
| *p = '/'; | |
| } | |
| return mkdir(tmp, 0700) == 0 || errno == EEXIST; | |
| } | |
| static int web_tcp_connect(const char *host, int port, int timeout_ms, | |
| char *err, size_t err_len) { | |
| char service[32]; | |
| snprintf(service, sizeof(service), "%d", port); | |
| struct addrinfo hints; | |
| memset(&hints, 0, sizeof(hints)); | |
| hints.ai_socktype = SOCK_STREAM; | |
| hints.ai_family = AF_UNSPEC; | |
| struct addrinfo *res = NULL; | |
| int gai = getaddrinfo(host, service, &hints, &res); | |
| if (gai != 0) { | |
| web_set_err(err, err_len, "getaddrinfo %s: %s", host, gai_strerror(gai)); | |
| return -1; | |
| } | |
| int fd = -1; | |
| for (struct addrinfo *ai = res; ai; ai = ai->ai_next) { | |
| fd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol); | |
| if (fd < 0) continue; | |
| int flags = fcntl(fd, F_GETFL, 0); | |
| if (flags >= 0) fcntl(fd, F_SETFL, flags | O_NONBLOCK); | |
| int rc = connect(fd, ai->ai_addr, ai->ai_addrlen); | |
| if (rc == 0) { | |
| if (flags >= 0) fcntl(fd, F_SETFL, flags); | |
| break; | |
| } | |
| if (errno == EINPROGRESS) { | |
| struct pollfd pfd = {.fd = fd, .events = POLLOUT}; | |
| rc = poll(&pfd, 1, timeout_ms); | |
| if (rc > 0) { | |
| int soerr = 0; | |
| socklen_t slen = sizeof(soerr); | |
| getsockopt(fd, SOL_SOCKET, SO_ERROR, &soerr, &slen); | |
| if (soerr == 0) { | |
| if (flags >= 0) fcntl(fd, F_SETFL, flags); | |
| break; | |
| } | |
| errno = soerr; | |
| } | |
| } | |
| close(fd); | |
| fd = -1; | |
| } | |
| freeaddrinfo(res); | |
| if (fd < 0) web_set_err(err, err_len, "connect %s:%d failed: %s", | |
| host, port, strerror(errno)); | |
| return fd; | |
| } | |
| static int web_write_all(int fd, const void *buf, size_t len) { | |
| const char *p = buf; | |
| while (len) { | |
| ssize_t n = send(fd, p, len, MSG_NOSIGNAL); | |
| ssize_t n = write(fd, p, len); | |
| if (n < 0 && errno == EINTR) continue; | |
| if (n <= 0) return -1; | |
| p += n; | |
| len -= (size_t)n; | |
| } | |
| return 0; | |
| } | |
| static ssize_t web_read_some(int fd, char *buf, size_t len, int timeout_ms) { | |
| struct pollfd pfd = {.fd = fd, .events = POLLIN}; | |
| int rc = poll(&pfd, 1, timeout_ms); | |
| if (rc <= 0) return rc == 0 ? 0 : -1; | |
| for (;;) { | |
| ssize_t n = read(fd, buf, len); | |
| if (n < 0 && errno == EINTR) continue; | |
| return n; | |
| } | |
| } | |
| static char *web_http_request(const char *method, int port, const char *path, | |
| char *err, size_t err_len) { | |
| int fd = web_tcp_connect("127.0.0.1", port, DS4_WEB_CONNECT_TIMEOUT_MS, | |
| err, err_len); | |
| if (fd < 0) return NULL; | |
| web_buf req = {0}; | |
| char line[512]; | |
| snprintf(line, sizeof(line), | |
| "%s %s HTTP/1.1\r\nHost: 127.0.0.1:%d\r\nConnection: close\r\n\r\n", | |
| method, path, port); | |
| web_buf_puts(&req, line); | |
| if (web_write_all(fd, req.ptr, req.len) != 0) { | |
| web_set_err(err, err_len, "write HTTP request failed: %s", strerror(errno)); | |
| close(fd); | |
| free(req.ptr); | |
| return NULL; | |
| } | |
| free(req.ptr); | |
| web_buf resp = {0}; | |
| char tmp[4096]; | |
| for (;;) { | |
| ssize_t n = web_read_some(fd, tmp, sizeof(tmp), DS4_WEB_CONNECT_TIMEOUT_MS); | |
| if (n < 0) { | |
| web_set_err(err, err_len, "read HTTP response failed: %s", strerror(errno)); | |
| close(fd); | |
| free(resp.ptr); | |
| return NULL; | |
| } | |
| if (n == 0) break; | |
| web_buf_append(&resp, tmp, (size_t)n); | |
| } | |
| close(fd); | |
| if (!resp.ptr) { | |
| web_set_err(err, err_len, "empty HTTP response"); | |
| return NULL; | |
| } | |
| char *body = strstr(resp.ptr, "\r\n\r\n"); | |
| if (!body) { | |
| web_set_err(err, err_len, "malformed HTTP response"); | |
| free(resp.ptr); | |
| return NULL; | |
| } | |
| body += 4; | |
| char *out = web_xstrdup(body); | |
| free(resp.ptr); | |
| return out; | |
| } | |
| static bool web_cdp_alive(ds4_web *web) { | |
| char err[160] = {0}; | |
| char *body = web_http_request("GET", web->port, "/json/version", err, sizeof(err)); | |
| if (!body) return false; | |
| bool ok = strstr(body, "webSocketDebuggerUrl") != NULL; | |
| free(body); | |
| return ok; | |
| } | |
| static char *web_json_get_string(const char *json, const char *key); | |
| static char *web_url_encode(const char *s) { | |
| static const char hex[] = "0123456789ABCDEF"; | |
| web_buf b = {0}; | |
| for (const unsigned char *p = (const unsigned char *)s; p && *p; p++) { | |
| unsigned char c = *p; | |
| if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') { | |
| web_buf_append(&b, (const char *)&c, 1); | |
| } else { | |
| char e[3] = {'%', hex[c >> 4], hex[c & 15]}; | |
| web_buf_append(&b, e, 3); | |
| } | |
| } | |
| return web_buf_take(&b); | |
| } | |
| static void web_random_bytes(unsigned char *buf, size_t len) { | |
| int fd = open("/dev/urandom", O_RDONLY); | |
| if (fd >= 0) { | |
| size_t off = 0; | |
| while (off < len) { | |
| ssize_t n = read(fd, buf + off, len - off); | |
| if (n < 0 && errno == EINTR) continue; | |
| if (n <= 0) break; | |
| off += (size_t)n; | |
| } | |
| close(fd); | |
| if (off == len) return; | |
| } | |
| uint64_t x = (uint64_t)time(NULL) ^ ((uint64_t)getpid() << 32); | |
| for (size_t i = 0; i < len; i++) { | |
| x = x * 6364136223846793005ULL + 1; | |
| buf[i] = (unsigned char)(x >> 32); | |
| } | |
| } | |
| static char *web_base64(const unsigned char *data, size_t len) { | |
| static const char tab[] = | |
| "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; | |
| size_t outlen = ((len + 2) / 3) * 4; | |
| char *out = web_xmalloc(outlen + 1); | |
| size_t j = 0; | |
| for (size_t i = 0; i < len; i += 3) { | |
| uint32_t v = (uint32_t)data[i] << 16; | |
| if (i + 1 < len) v |= (uint32_t)data[i + 1] << 8; | |
| if (i + 2 < len) v |= data[i + 2]; | |
| out[j++] = tab[(v >> 18) & 63]; | |
| out[j++] = tab[(v >> 12) & 63]; | |
| out[j++] = (i + 1 < len) ? tab[(v >> 6) & 63] : '='; | |
| out[j++] = (i + 2 < len) ? tab[v & 63] : '='; | |
| } | |
| out[j] = '\0'; | |
| return out; | |
| } | |
| static char *web_json_quote(const char *s) { | |
| web_buf b = {0}; | |
| web_buf_puts(&b, "\""); | |
| for (const unsigned char *p = (const unsigned char *)s; p && *p; p++) { | |
| unsigned char c = *p; | |
| switch (c) { | |
| case '\\': web_buf_puts(&b, "\\\\"); break; | |
| case '"': web_buf_puts(&b, "\\\""); break; | |
| case '\n': web_buf_puts(&b, "\\n"); break; | |
| case '\r': web_buf_puts(&b, "\\r"); break; | |
| case '\t': web_buf_puts(&b, "\\t"); break; | |
| default: | |
| if (c < 0x20) { | |
| char tmp[8]; | |
| snprintf(tmp, sizeof(tmp), "\\u%04x", c); | |
| web_buf_puts(&b, tmp); | |
| } else { | |
| web_buf_append(&b, (const char *)&c, 1); | |
| } | |
| break; | |
| } | |
| } | |
| web_buf_puts(&b, "\""); | |
| return web_buf_take(&b); | |
| } | |
| static int web_ws_connect(const char *ws_url, cdp_ws *ws, | |
| char *err, size_t err_len) { | |
| const char *p = ws_url; | |
| if (strncmp(p, "ws://", 5) != 0) { | |
| web_set_err(err, err_len, "unsupported websocket URL: %s", ws_url); | |
| return -1; | |
| } | |
| p += 5; | |
| const char *slash = strchr(p, '/'); | |
| if (!slash) { | |
| web_set_err(err, err_len, "malformed websocket URL"); | |
| return -1; | |
| } | |
| char hostport[256]; | |
| size_t hp_len = (size_t)(slash - p); | |
| if (hp_len >= sizeof(hostport)) hp_len = sizeof(hostport) - 1; | |
| memcpy(hostport, p, hp_len); | |
| hostport[hp_len] = '\0'; | |
| char *colon = strrchr(hostport, ':'); | |
| int port = 80; | |
| if (colon) { | |
| *colon = '\0'; | |
| port = atoi(colon + 1); | |
| } | |
| const char *host = hostport; | |
| int fd = web_tcp_connect(host, port, DS4_WEB_CONNECT_TIMEOUT_MS, err, err_len); | |
| if (fd < 0) return -1; | |
| unsigned char rnd[16]; | |
| web_random_bytes(rnd, sizeof(rnd)); | |
| char *key = web_base64(rnd, sizeof(rnd)); | |
| web_buf req = {0}; | |
| char line[512]; | |
| snprintf(line, sizeof(line), | |
| "GET %s HTTP/1.1\r\n" | |
| "Host: %s:%d\r\n" | |
| "Upgrade: websocket\r\n" | |
| "Connection: Upgrade\r\n" | |
| "Sec-WebSocket-Key: %s\r\n" | |
| "Sec-WebSocket-Version: 13\r\n\r\n", | |
| slash, host, port, key); | |
| web_buf_puts(&req, line); | |
| free(key); | |
| if (web_write_all(fd, req.ptr, req.len) != 0) { | |
| web_set_err(err, err_len, "websocket handshake write failed"); | |
| close(fd); | |
| free(req.ptr); | |
| return -1; | |
| } | |
| free(req.ptr); | |
| web_buf resp = {0}; | |
| char tmp[1024]; | |
| double deadline = web_now_sec() + (double)DS4_WEB_CONNECT_TIMEOUT_MS / 1000.0; | |
| while (!strstr(resp.ptr ? resp.ptr : "", "\r\n\r\n")) { | |
| if (web_set_cancel_err(ws ? ws->web : NULL, err, err_len)) { | |
| close(fd); | |
| free(resp.ptr); | |
| return -1; | |
| } | |
| double now = web_now_sec(); | |
| if (now >= deadline) { | |
| web_set_err(err, err_len, "websocket handshake read failed"); | |
| close(fd); | |
| free(resp.ptr); | |
| return -1; | |
| } | |
| int slice = 100; | |
| int remaining = (int)((deadline - now) * 1000.0); | |
| if (remaining < slice) slice = remaining > 0 ? remaining : 1; | |
| ssize_t n = web_read_some(fd, tmp, sizeof(tmp), slice); | |
| if (n < 0) { | |
| web_set_err(err, err_len, "websocket handshake read failed"); | |
| close(fd); | |
| free(resp.ptr); | |
| return -1; | |
| } | |
| if (n == 0) continue; | |
| web_buf_append(&resp, tmp, (size_t)n); | |
| if (resp.len > 8192) break; | |
| } | |
| bool ok = resp.ptr && strstr(resp.ptr, " 101 ") != NULL; | |
| free(resp.ptr); | |
| if (!ok) { | |
| web_set_err(err, err_len, "websocket handshake rejected"); | |
| close(fd); | |
| return -1; | |
| } | |
| ws->fd = fd; | |
| ws->next_id = 1; | |
| return 0; | |
| } | |
| static void web_ws_close(cdp_ws *ws) { | |
| if (ws && ws->fd >= 0) { | |
| close(ws->fd); | |
| ws->fd = -1; | |
| } | |
| } | |
| static int web_read_exact(cdp_ws *ws, unsigned char *buf, size_t len, | |
| int timeout_ms, char *err, size_t err_len) { | |
| size_t off = 0; | |
| double deadline = web_now_sec() + (double)timeout_ms / 1000.0; | |
| while (off < len) { | |
| if (web_set_cancel_err(ws ? ws->web : NULL, err, err_len)) return -1; | |
| double now = web_now_sec(); | |
| if (now >= deadline) { | |
| web_set_err(err, err_len, "websocket read timeout"); | |
| return -1; | |
| } | |
| int slice = 100; | |
| int remaining = (int)((deadline - now) * 1000.0); | |
| if (remaining < slice) slice = remaining > 0 ? remaining : 1; | |
| ssize_t n = web_read_some(ws->fd, (char *)buf + off, len - off, slice); | |
| if (n < 0) { | |
| web_set_err(err, err_len, "websocket frame read failed"); | |
| return -1; | |
| } | |
| if (n == 0) continue; | |
| off += (size_t)n; | |
| } | |
| return 0; | |
| } | |
| static int web_ws_send_text(cdp_ws *ws, const char *text, | |
| char *err, size_t err_len) { | |
| size_t len = strlen(text); | |
| web_buf frame = {0}; | |
| unsigned char hdr[14]; | |
| size_t h = 0; | |
| hdr[h++] = 0x81; | |
| if (len < 126) { | |
| hdr[h++] = 0x80 | (unsigned char)len; | |
| } else if (len <= 0xffff) { | |
| hdr[h++] = 0x80 | 126; | |
| hdr[h++] = (unsigned char)(len >> 8); | |
| hdr[h++] = (unsigned char)len; | |
| } else { | |
| hdr[h++] = 0x80 | 127; | |
| for (int i = 7; i >= 0; i--) hdr[h++] = (unsigned char)((uint64_t)len >> (i * 8)); | |
| } | |
| unsigned char mask[4]; | |
| web_random_bytes(mask, sizeof(mask)); | |
| for (int i = 0; i < 4; i++) hdr[h++] = mask[i]; | |
| web_buf_append(&frame, (const char *)hdr, h); | |
| for (size_t i = 0; i < len; i++) { | |
| char c = text[i] ^ mask[i & 3]; | |
| web_buf_append(&frame, &c, 1); | |
| } | |
| int rc = web_write_all(ws->fd, frame.ptr, frame.len); | |
| free(frame.ptr); | |
| if (rc != 0) { | |
| web_set_err(err, err_len, "websocket write failed: %s", strerror(errno)); | |
| return -1; | |
| } | |
| return 0; | |
| } | |
| static int web_ws_send_pong(cdp_ws *ws, const unsigned char *payload, size_t len) { | |
| if (len > 125) len = 125; | |
| unsigned char hdr[2 + 4 + 125]; | |
| hdr[0] = 0x8a; | |
| hdr[1] = 0x80 | (unsigned char)len; | |
| unsigned char mask[4]; | |
| web_random_bytes(mask, sizeof(mask)); | |
| memcpy(hdr + 2, mask, 4); | |
| for (size_t i = 0; i < len; i++) hdr[6 + i] = payload[i] ^ mask[i & 3]; | |
| return web_write_all(ws->fd, hdr, 6 + len); | |
| } | |
| static char *web_ws_read_message(cdp_ws *ws, char *err, size_t err_len) { | |
| web_buf msg = {0}; | |
| for (;;) { | |
| unsigned char h[2]; | |
| if (web_read_exact(ws, h, 2, DS4_WEB_CDP_TIMEOUT_MS, err, err_len) != 0) { | |
| free(msg.ptr); | |
| return NULL; | |
| } | |
| bool fin = (h[0] & 0x80) != 0; | |
| int opcode = h[0] & 0x0f; | |
| bool masked = (h[1] & 0x80) != 0; | |
| uint64_t len = h[1] & 0x7f; | |
| if (len == 126) { | |
| unsigned char x[2]; | |
| if (web_read_exact(ws, x, 2, DS4_WEB_CDP_TIMEOUT_MS, err, err_len) != 0) goto fail; | |
| len = ((uint64_t)x[0] << 8) | x[1]; | |
| } else if (len == 127) { | |
| unsigned char x[8]; | |
| if (web_read_exact(ws, x, 8, DS4_WEB_CDP_TIMEOUT_MS, err, err_len) != 0) goto fail; | |
| len = 0; | |
| for (int i = 0; i < 8; i++) len = (len << 8) | x[i]; | |
| } | |
| unsigned char mask[4] = {0}; | |
| if (masked && web_read_exact(ws, mask, 4, DS4_WEB_CDP_TIMEOUT_MS, err, err_len) != 0) | |
| goto fail; | |
| if (len > DS4_WEB_MAX_RESULT_BYTES * 4ULL) { | |
| web_set_err(err, err_len, "websocket message too large"); | |
| free(msg.ptr); | |
| return NULL; | |
| } | |
| unsigned char *payload = web_xmalloc((size_t)len + 1); | |
| if (len && web_read_exact(ws, payload, (size_t)len, | |
| DS4_WEB_CDP_TIMEOUT_MS, err, err_len) != 0) { | |
| free(payload); | |
| goto fail; | |
| } | |
| for (uint64_t i = 0; masked && i < len; i++) payload[i] ^= mask[i & 3]; | |
| payload[len] = '\0'; | |
| if (opcode == 0x8) { | |
| free(payload); | |
| web_set_err(err, err_len, "websocket closed"); | |
| free(msg.ptr); | |
| return NULL; | |
| } else if (opcode == 0x9) { | |
| web_ws_send_pong(ws, payload, (size_t)len); | |
| free(payload); | |
| continue; | |
| } else if (opcode == 0x1 || opcode == 0x0) { | |
| web_buf_append(&msg, (const char *)payload, (size_t)len); | |
| free(payload); | |
| if (fin) return web_buf_take(&msg); | |
| } else { | |
| free(payload); | |
| } | |
| } | |
| fail: | |
| if (err && err_len && !err[0]) web_set_err(err, err_len, "websocket frame read failed"); | |
| free(msg.ptr); | |
| return NULL; | |
| } | |
| static bool web_json_id_matches(const char *json, int id) { | |
| const char *p = strstr(json, "\"id\""); | |
| if (!p) return false; | |
| p = strchr(p, ':'); | |
| if (!p) return false; | |
| p++; | |
| while (*p == ' ' || *p == '\t') p++; | |
| return atoi(p) == id; | |
| } | |
| static char *web_cdp_call(cdp_ws *ws, const char *method, const char *params, | |
| char *err, size_t err_len) { | |
| if (web_set_cancel_err(ws ? ws->web : NULL, err, err_len)) return NULL; | |
| int id = ws->next_id++; | |
| web_buf req = {0}; | |
| char head[256]; | |
| snprintf(head, sizeof(head), "{\"id\":%d,\"method\":", id); | |
| web_buf_puts(&req, head); | |
| char *qmethod = web_json_quote(method); | |
| web_buf_puts(&req, qmethod); | |
| free(qmethod); | |
| if (params && params[0]) { | |
| web_buf_puts(&req, ",\"params\":"); | |
| web_buf_puts(&req, params); | |
| } | |
| web_buf_puts(&req, "}"); | |
| char *wire = web_buf_take(&req); | |
| if (web_ws_send_text(ws, wire, err, err_len) != 0) { | |
| free(wire); | |
| return NULL; | |
| } | |
| free(wire); | |
| for (;;) { | |
| if (web_set_cancel_err(ws ? ws->web : NULL, err, err_len)) return NULL; | |
| char *msg = web_ws_read_message(ws, err, err_len); | |
| if (!msg) return NULL; | |
| if (web_json_id_matches(msg, id)) return msg; | |
| free(msg); | |
| } | |
| } | |
| static void web_cdp_call_optional(cdp_ws *ws, const char *method, const char *params) { | |
| char err[160] = {0}; | |
| char *resp = web_cdp_call(ws, method, params, err, sizeof(err)); | |
| free(resp); | |
| } | |
| static int web_hex4(const char *p) { | |
| int v = 0; | |
| for (int i = 0; i < 4; i++) { | |
| char c = p[i]; | |
| int x; | |
| if (c >= '0' && c <= '9') x = c - '0'; | |
| else if (c >= 'a' && c <= 'f') x = c - 'a' + 10; | |
| else if (c >= 'A' && c <= 'F') x = c - 'A' + 10; | |
| else return -1; | |
| v = (v << 4) | x; | |
| } | |
| return v; | |
| } | |
| static void web_utf8_append(web_buf *b, unsigned code) { | |
| char out[4]; | |
| if (code <= 0x7f) { | |
| out[0] = (char)code; | |
| web_buf_append(b, out, 1); | |
| } else if (code <= 0x7ff) { | |
| out[0] = (char)(0xc0 | (code >> 6)); | |
| out[1] = (char)(0x80 | (code & 0x3f)); | |
| web_buf_append(b, out, 2); | |
| } else if (code <= 0xffff) { | |
| out[0] = (char)(0xe0 | (code >> 12)); | |
| out[1] = (char)(0x80 | ((code >> 6) & 0x3f)); | |
| out[2] = (char)(0x80 | (code & 0x3f)); | |
| web_buf_append(b, out, 3); | |
| } else { | |
| out[0] = (char)(0xf0 | (code >> 18)); | |
| out[1] = (char)(0x80 | ((code >> 12) & 0x3f)); | |
| out[2] = (char)(0x80 | ((code >> 6) & 0x3f)); | |
| out[3] = (char)(0x80 | (code & 0x3f)); | |
| web_buf_append(b, out, 4); | |
| } | |
| } | |
| static char *web_json_parse_string_at(const char *q, const char **endp) { | |
| if (*q != '"') return NULL; | |
| q++; | |
| web_buf b = {0}; | |
| while (*q && *q != '"') { | |
| if (*q != '\\') { | |
| web_buf_append(&b, q++, 1); | |
| continue; | |
| } | |
| q++; | |
| switch (*q) { | |
| case '"': web_buf_append(&b, "\"", 1); q++; break; | |
| case '\\': web_buf_append(&b, "\\", 1); q++; break; | |
| case '/': web_buf_append(&b, "/", 1); q++; break; | |
| case 'b': web_buf_append(&b, "\b", 1); q++; break; | |
| case 'f': web_buf_append(&b, "\f", 1); q++; break; | |
| case 'n': web_buf_append(&b, "\n", 1); q++; break; | |
| case 'r': web_buf_append(&b, "\r", 1); q++; break; | |
| case 't': web_buf_append(&b, "\t", 1); q++; break; | |
| case 'u': { | |
| int v = web_hex4(q + 1); | |
| if (v < 0) { free(b.ptr); return NULL; } | |
| q += 5; | |
| if (v >= 0xd800 && v <= 0xdbff && q[0] == '\\' && q[1] == 'u') { | |
| int lo = web_hex4(q + 2); | |
| if (lo >= 0xdc00 && lo <= 0xdfff) { | |
| unsigned code = 0x10000 + (((unsigned)v - 0xd800) << 10) + | |
| ((unsigned)lo - 0xdc00); | |
| web_utf8_append(&b, code); | |
| q += 6; | |
| break; | |
| } | |
| } | |
| web_utf8_append(&b, (unsigned)v); | |
| break; | |
| } | |
| default: | |
| if (*q) web_buf_append(&b, q++, 1); | |
| break; | |
| } | |
| } | |
| if (*q != '"') { | |
| free(b.ptr); | |
| return NULL; | |
| } | |
| if (endp) *endp = q + 1; | |
| return web_buf_take(&b); | |
| } | |
| static char *web_json_get_string(const char *json, const char *key) { | |
| char pat[128]; | |
| snprintf(pat, sizeof(pat), "\"%s\"", key); | |
| const char *p = json; | |
| while ((p = strstr(p, pat)) != NULL) { | |
| p += strlen(pat); | |
| while (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n') p++; | |
| if (*p++ != ':') continue; | |
| while (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n') p++; | |
| if (*p == '"') return web_json_parse_string_at(p, NULL); | |
| } | |
| return NULL; | |
| } | |
| static char *web_cdp_eval_string(cdp_ws *ws, const char *expr, | |
| char *err, size_t err_len) { | |
| char *qexpr = web_json_quote(expr); | |
| web_buf params = {0}; | |
| web_buf_puts(¶ms, "{\"expression\":"); | |
| web_buf_puts(¶ms, qexpr); | |
| web_buf_puts(¶ms, ",\"returnByValue\":true,\"awaitPromise\":true,\"includeCommandLineAPI\":true}"); | |
| free(qexpr); | |
| char *params_s = web_buf_take(¶ms); | |
| char *resp = web_cdp_call(ws, "Runtime.evaluate", params_s, err, err_len); | |
| free(params_s); | |
| if (!resp) return NULL; | |
| if (strstr(resp, "\"exceptionDetails\"")) { | |
| web_set_err(err, err_len, "JavaScript evaluation failed"); | |
| free(resp); | |
| return NULL; | |
| } | |
| char *val = web_json_get_string(resp, "value"); | |
| free(resp); | |
| if (!val) web_set_err(err, err_len, "Runtime.evaluate did not return a string"); | |
| return val; | |
| } | |
| static bool web_wait_ready(cdp_ws *ws, char *err, size_t err_len) { | |
| const char *expr = "document.readyState"; | |
| for (int i = 0; i < 80; i++) { | |
| if (web_set_cancel_err(ws ? ws->web : NULL, err, err_len)) return false; | |
| char *state = web_cdp_eval_string(ws, expr, err, err_len); | |
| if (state && (!strcmp(state, "complete") || !strcmp(state, "interactive"))) { | |
| free(state); | |
| if (web_sleep_ms(ws ? ws->web : NULL, 800)) return true; | |
| web_set_err(err, err_len, "interrupted"); | |
| return false; | |
| } | |
| free(state); | |
| if (!web_sleep_ms(ws ? ws->web : NULL, 250)) { | |
| web_set_err(err, err_len, "interrupted"); | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| static bool web_cdp_navigate(cdp_ws *ws, const char *url, | |
| char *err, size_t err_len) { | |
| char *qurl = web_json_quote(url); | |
| web_buf params = {0}; | |
| web_buf_puts(¶ms, "{\"url\":"); | |
| web_buf_puts(¶ms, qurl); | |
| web_buf_puts(¶ms, "}"); | |
| free(qurl); | |
| char *params_s = web_buf_take(¶ms); | |
| char *resp = web_cdp_call(ws, "Page.navigate", params_s, err, err_len); | |
| free(params_s); | |
| if (!resp) return false; | |
| free(resp); | |
| return true; | |
| } | |
| static bool web_page_probe(cdp_ws *ws, char **href_out, char **ready_out, | |
| long *text_len_out, char *err, size_t err_len) { | |
| const char *expr = | |
| "location.href+'\\n'+document.readyState+'\\n'+" | |
| "((document.body&&document.body.innerText)||'').length"; | |
| char *probe = web_cdp_eval_string(ws, expr, err, err_len); | |
| if (!probe) return false; | |
| char *nl1 = strchr(probe, '\n'); | |
| char *nl2 = nl1 ? strchr(nl1 + 1, '\n') : NULL; | |
| if (!nl1 || !nl2) { | |
| free(probe); | |
| web_set_err(err, err_len, "page readiness probe returned malformed data"); | |
| return false; | |
| } | |
| *nl1 = '\0'; | |
| *nl2 = '\0'; | |
| if (href_out) *href_out = web_xstrdup(probe); | |
| if (ready_out) *ready_out = web_xstrdup(nl1 + 1); | |
| if (text_len_out) *text_len_out = strtol(nl2 + 1, NULL, 10); | |
| free(probe); | |
| return true; | |
| } | |
| static bool web_wait_navigated_ready(cdp_ws *ws, const char *url, | |
| char *err, size_t err_len) { | |
| (void)url; | |
| long last_len = -1; | |
| int stable = 0; | |
| bool saw_real_url = false; | |
| for (int i = 0; i < 100; i++) { | |
| if (web_set_cancel_err(ws ? ws->web : NULL, err, err_len)) return false; | |
| char *href = NULL; | |
| char *ready = NULL; | |
| long text_len = 0; | |
| bool ok = web_page_probe(ws, &href, &ready, &text_len, err, err_len); | |
| if (!ok) { | |
| free(href); | |
| free(ready); | |
| if (!web_sleep_ms(ws ? ws->web : NULL, 250)) { | |
| web_set_err(err, err_len, "interrupted"); | |
| return false; | |
| } | |
| continue; | |
| } | |
| bool real_url = href && href[0] && | |
| strcmp(href, "about:blank") && | |
| strncmp(href, "chrome://", 9); | |
| bool ready_state = ready && | |
| (!strcmp(ready, "complete") || !strcmp(ready, "interactive")); | |
| if (real_url) saw_real_url = true; | |
| if (text_len > 0 && text_len == last_len) stable++; | |
| else stable = 0; | |
| last_len = text_len; | |
| free(href); | |
| free(ready); | |
| if (saw_real_url && ready_state && text_len > 0 && stable >= 2) { | |
| if (web_sleep_ms(ws ? ws->web : NULL, 500)) return true; | |
| web_set_err(err, err_len, "interrupted"); | |
| return false; | |
| } | |
| if (saw_real_url && ready_state && i >= 24) return true; | |
| if (!web_sleep_ms(ws ? ws->web : NULL, 250)) { | |
| web_set_err(err, err_len, "interrupted"); | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| static bool web_cdp_prepare_page(cdp_ws *ws, char *err, size_t err_len) { | |
| char *resp = web_cdp_call(ws, "Page.enable", "{}", err, err_len); | |
| if (!resp) return false; | |
| free(resp); | |
| resp = web_cdp_call(ws, "Runtime.enable", "{}", err, err_len); | |
| if (!resp) return false; | |
| free(resp); | |
| web_cdp_call_optional(ws, "Emulation.setFocusEmulationEnabled", | |
| "{\"enabled\":true}"); | |
| web_cdp_call_optional(ws, "Emulation.setDeviceMetricsOverride", | |
| "{\"width\":1365,\"height\":900,\"deviceScaleFactor\":1,\"mobile\":false}"); | |
| return web_wait_ready(ws, err, err_len); | |
| } | |
| static bool web_scroll_dynamic_page(cdp_ws *ws, char *err, size_t err_len) { | |
| const char *expr = | |
| "(() => new Promise(resolve => {" | |
| "const root=()=>document.scrollingElement||document.documentElement||document.body;" | |
| "const blockSel='h1,h2,h3,h4,h5,h6,p,li,pre,blockquote,td,th,[id=\"content-text\"],[class*=\"comment-body\"],[class*=\"comment-content\"],[data-testid*=\"comment-text\"]';" | |
| "const lazySel='[onscroll],[loading=\"lazy\"],[data-src],[data-lazy],[class*=\"lazy\"],[class*=\"infinite\"],[class*=\"virtual\"],[role=\"feed\"],[id*=\"comment\"],[class*=\"comment\"],[data-testid*=\"comment\"]';" | |
| "const hookCount=()=>{let n=0;try{if(window.onscroll)n++;if(document.onscroll)n++;if(document.body&&document.body.onscroll)n++;}catch(e){}" | |
| "try{if(typeof getEventListeners==='function'){for(const o of [window,document,document.body]){if(!o)continue;const ev=getEventListeners(o);if(ev&&ev.scroll)n+=ev.scroll.length;}}}catch(e){}" | |
| "try{n+=document.querySelectorAll(lazySel).length;}catch(e){}return n;};" | |
| "const metrics=()=>{const r=root();return {" | |
| "height:r?r.scrollHeight:0," | |
| "view:innerHeight||900," | |
| "y:scrollY||(r&&r.scrollTop)||0," | |
| "text:((document.body&&document.body.innerText)||'').length," | |
| "links:document.links?document.links.length:0," | |
| "blocks:document.body?document.body.querySelectorAll(blockSel).length:0," | |
| "hooks:hookCount()};};" | |
| "const sig=m=>[m.height,m.text,m.links,m.blocks].join('|');" | |
| "const grew=(a,b)=>b.height>a.height+20||b.text>a.text+200||b.links>a.links+2||b.blocks>a.blocks+2;" | |
| "const scrollOnce=()=>{const r=root();if(!r)return;" | |
| "const h=Math.max(700,Math.floor((innerHeight||900)*0.85));" | |
| "window.scrollTo(0,Math.min(r.scrollHeight,(scrollY||r.scrollTop||0)+h));};" | |
| "let last=metrics(),lastSig=sig(last),same=0,steps=0;" | |
| "const scrollable=last.height>last.view*1.35;" | |
| "if(!scrollable||last.hooks===0){resolve('scroll skipped hooks='+last.hooks+' text='+last.text);return;}" | |
| "const tick=()=>{" | |
| "if(steps>=28){resolve('scrolled '+steps+' text='+last.text);return;}" | |
| "const before=last;" | |
| "scrollOnce();steps++;" | |
| "setTimeout(()=>{const now=metrics(),nowSig=sig(now);" | |
| "if(nowSig===lastSig)same++;else same=0;" | |
| "const loaded=grew(before,now);" | |
| "last=now;lastSig=nowSig;" | |
| "if(steps===1&&!loaded){resolve('scroll probe unchanged text='+now.text);return;}" | |
| "const atBottom=now.y+now.view+20>=now.height;" | |
| "if(same>=4||(atBottom&&same>=1)){resolve('scrolled '+steps+' text='+now.text);return;}" | |
| "tick();},900);" | |
| "};tick();" | |
| "}))()"; | |
| if (web_set_cancel_err(ws ? ws->web : NULL, err, err_len)) return false; | |
| char local_err[160] = {0}; | |
| char *res = web_cdp_eval_string(ws, expr, local_err, sizeof(local_err)); | |
| if (!res && web_err_is_interrupted(local_err)) { | |
| web_set_err(err, err_len, "interrupted"); | |
| return false; | |
| } | |
| free(res); | |
| if (web_set_cancel_err(ws ? ws->web : NULL, err, err_len)) return false; | |
| return true; | |
| } | |
| static char *web_chrome_executable(void) { | |
| const char *env = getenv("DS4_CHROME"); | |
| if (env && env[0]) return web_xstrdup(env); | |
| if (access("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", X_OK) == 0) | |
| return web_xstrdup("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"); | |
| if (access("/Applications/Chromium.app/Contents/MacOS/Chromium", X_OK) == 0) | |
| return web_xstrdup("/Applications/Chromium.app/Contents/MacOS/Chromium"); | |
| const char *paths[] = { | |
| "/usr/bin/google-chrome", | |
| "/usr/bin/google-chrome-stable", | |
| "/usr/bin/chromium", | |
| "/usr/bin/chromium-browser", | |
| "/snap/bin/chromium", | |
| "/opt/google/chrome/chrome", | |
| NULL | |
| }; | |
| for (int i = 0; paths[i]; i++) { | |
| if (access(paths[i], X_OK) == 0) return web_xstrdup(paths[i]); | |
| } | |
| const char *names[] = { | |
| "google-chrome", | |
| "google-chrome-stable", | |
| "chromium", | |
| "chromium-browser", | |
| NULL | |
| }; | |
| const char *pathenv = getenv("PATH"); | |
| if (pathenv) { | |
| char *path = web_xstrdup(pathenv); | |
| char *save = NULL; | |
| for (char *dir = strtok_r(path, ":", &save); dir; dir = strtok_r(NULL, ":", &save)) { | |
| for (int i = 0; names[i]; i++) { | |
| char candidate[PATH_MAX]; | |
| snprintf(candidate, sizeof(candidate), "%s/%s", dir[0] ? dir : ".", names[i]); | |
| if (access(candidate, X_OK) == 0) { | |
| char *res = web_xstrdup(candidate); | |
| free(path); | |
| return res; | |
| } | |
| } | |
| } | |
| free(path); | |
| } | |
| return web_xstrdup("google-chrome"); | |
| } | |
| static const char *web_macos_chrome_app_name(void) { | |
| if (getenv("DS4_CHROME")) return NULL; | |
| if (access("/Applications/Google Chrome.app", F_OK) == 0) | |
| return "Google Chrome"; | |
| if (access("/Applications/Chromium.app", F_OK) == 0) | |
| return "Chromium"; | |
| return NULL; | |
| } | |
| static bool web_spawn_chrome(ds4_web *web, char *err, size_t err_len) { | |
| if (!web_mkdir_p(web->profile_dir)) { | |
| web_set_err(err, err_len, "failed to create Chrome profile dir %s: %s", | |
| web->profile_dir, strerror(errno)); | |
| return false; | |
| } | |
| char *exe = web_chrome_executable(); | |
| const char *mac_app_name = web_macos_chrome_app_name(); | |
| bool launched_via_open = mac_app_name != NULL && access("/usr/bin/open", X_OK) == 0; | |
| bool launched_via_open = false; | |
| char port_arg[64], profile_arg[PATH_MAX + 64]; | |
| snprintf(port_arg, sizeof(port_arg), "--remote-debugging-port=%d", web->port); | |
| snprintf(profile_arg, sizeof(profile_arg), "--user-data-dir=%s", web->profile_dir); | |
| pid_t pid = fork(); | |
| if (pid < 0) { | |
| web_set_err(err, err_len, "failed to fork Chrome: %s", strerror(errno)); | |
| free(exe); | |
| return false; | |
| } | |
| if (pid == 0) { | |
| int nullfd = open("/dev/null", O_RDWR); | |
| if (nullfd >= 0) { | |
| dup2(nullfd, STDOUT_FILENO); | |
| dup2(nullfd, STDERR_FILENO); | |
| if (nullfd > 2) close(nullfd); | |
| } | |
| if (launched_via_open) { | |
| execlp("/usr/bin/open", "open", "-g", "-na", mac_app_name, | |
| "--args", port_arg, "--remote-allow-origins=*", | |
| profile_arg, "--no-first-run", "--no-default-browser-check", | |
| "--disable-sync", "--use-mock-keychain", "--password-store=basic", | |
| "--mute-audio", "about:blank", (char *)NULL); | |
| } else { | |
| execlp(exe, exe, port_arg, "--remote-allow-origins=*", | |
| profile_arg, "--no-first-run", "--no-default-browser-check", | |
| "--disable-sync", "--use-mock-keychain", "--password-store=basic", | |
| "--mute-audio", "about:blank", (char *)NULL); | |
| } | |
| if (geteuid() == 0) { | |
| execlp(exe, exe, port_arg, "--remote-allow-origins=*", | |
| profile_arg, "--no-first-run", "--no-default-browser-check", | |
| "--disable-sync", "--password-store=basic", "--no-sandbox", | |
| "--mute-audio", "about:blank", (char *)NULL); | |
| } else { | |
| execlp(exe, exe, port_arg, "--remote-allow-origins=*", | |
| profile_arg, "--no-first-run", "--no-default-browser-check", | |
| "--disable-sync", "--password-store=basic", | |
| "--mute-audio", "about:blank", (char *)NULL); | |
| } | |
| _exit(127); | |
| } | |
| free(exe); | |
| web->chrome_pid = pid; | |
| for (int i = 0; i < 80; i++) { | |
| if (web_set_cancel_err(web, err, err_len)) return false; | |
| if (web_cdp_alive(web)) { | |
| web_log(web, "Chrome browser session is ready"); | |
| return true; | |
| } | |
| int status = 0; | |
| pid_t rc = waitpid(pid, &status, WNOHANG); | |
| if (rc == pid) { | |
| web->chrome_pid = 0; | |
| if (launched_via_open) continue; | |
| web_set_err(err, err_len, "Chrome exited before CDP became ready"); | |
| return false; | |
| } | |
| if (!web_sleep_ms(web, 250)) { | |
| web_set_err(err, err_len, "interrupted"); | |
| return false; | |
| } | |
| } | |
| web_set_err(err, err_len, "Chrome did not expose CDP on port %d", web->port); | |
| return false; | |
| } | |
| static bool web_ensure_browser(ds4_web *web, char *err, size_t err_len) { | |
| if (web_cdp_alive(web)) return true; | |
| if (web->chrome_pid > 0) { | |
| int status = 0; | |
| waitpid(web->chrome_pid, &status, WNOHANG); | |
| web->chrome_pid = 0; | |
| } | |
| if (!web->browser_allowed) { | |
| if (!web->confirm) { | |
| web_set_err(err, err_len, | |
| "starting a visible Chrome browser requires interactive approval"); | |
| return false; | |
| } | |
| if (!web->confirm(web->confirm_privdata, | |
| "The web tool wants to start a visible Chrome browser. Allow? (y/n) ", | |
| err, err_len)) | |
| { | |
| if (err && !err[0]) web_set_err(err, err_len, "user denied Chrome browser start"); | |
| return false; | |
| } | |
| web->browser_allowed = true; | |
| } | |
| return web_spawn_chrome(web, err, err_len); | |
| } | |
| static void web_tab_free(web_tab *tab) { | |
| if (!tab) return; | |
| free(tab->id); | |
| free(tab->ws_url); | |
| tab->id = NULL; | |
| tab->ws_url = NULL; | |
| } | |
| static char *web_browser_ws_url(ds4_web *web, char *err, size_t err_len) { | |
| char *body = web_http_request("GET", web->port, "/json/version", err, err_len); | |
| if (!body) return NULL; | |
| char *ws = web_json_get_string(body, "webSocketDebuggerUrl"); | |
| free(body); | |
| if (!ws) web_set_err(err, err_len, "Chrome did not return a browser WebSocket URL"); | |
| return ws; | |
| } | |
| static bool web_open_tab(ds4_web *web, const char *url, web_tab *tab, | |
| char *err, size_t err_len) { | |
| memset(tab, 0, sizeof(*tab)); | |
| char *browser_url = web_browser_ws_url(web, err, err_len); | |
| if (!browser_url) return false; | |
| cdp_ws browser = {.fd = -1, .web = web}; | |
| if (web_ws_connect(browser_url, &browser, err, err_len) != 0) { | |
| free(browser_url); | |
| return false; | |
| } | |
| free(browser_url); | |
| char *qurl = web_json_quote(url); | |
| web_buf params = {0}; | |
| web_buf_puts(¶ms, "{\"url\":"); | |
| web_buf_puts(¶ms, qurl); | |
| web_buf_puts(¶ms, ",\"background\":true,\"newWindow\":false}"); | |
| free(qurl); | |
| char *params_s = web_buf_take(¶ms); | |
| char *resp = web_cdp_call(&browser, "Target.createTarget", | |
| params_s, err, err_len); | |
| free(params_s); | |
| web_ws_close(&browser); | |
| if (!resp) return false; | |
| tab->id = web_json_get_string(resp, "targetId"); | |
| free(resp); | |
| if (!tab->id) { | |
| web_tab_free(tab); | |
| web_set_err(err, err_len, "Chrome did not return a page target id"); | |
| return false; | |
| } | |
| char ws_url[PATH_MAX + 128]; | |
| snprintf(ws_url, sizeof(ws_url), "ws://127.0.0.1:%d/devtools/page/%s", | |
| web->port, tab->id); | |
| tab->ws_url = web_xstrdup(ws_url); | |
| return true; | |
| } | |
| static void web_close_tab(ds4_web *web, const web_tab *tab) { | |
| if (!web || !tab || !tab->id || !tab->id[0]) return; | |
| char *enc = web_url_encode(tab->id); | |
| web_buf path = {0}; | |
| web_buf_puts(&path, "/json/close/"); | |
| web_buf_puts(&path, enc); | |
| free(enc); | |
| char err[160] = {0}; | |
| char *path_s = web_buf_take(&path); | |
| char *body = web_http_request("GET", web->port, path_s, err, sizeof(err)); | |
| free(path_s); | |
| if (body) { | |
| free(body); | |
| } else if (err[0]) { | |
| web_log(web, err); | |
| } | |
| } | |
| static const char *web_click_google_consent_js = | |
| "(() => {" | |
| "const clean=s=>(s||'').replace(/\\s+/g,' ').trim();" | |
| "const pats=[/accept all/i,/i agree/i,/agree/i,/accetta tutto/i,/tout accepter/i,/aceptar todo/i,/alle akzeptieren/i];" | |
| "const els=[...document.querySelectorAll('button,[role=button],input[type=submit],a')];" | |
| "for (const el of els){const t=clean(el.innerText||el.value||el.textContent);" | |
| "if(!t)continue; if(pats.some(p=>p.test(t))){el.click(); return 'clicked '+t;}}" | |
| "return '';" | |
| "})()"; | |
| static const char *web_extract_search_js = | |
| "(() => {" | |
| "const clean=s=>(s||'').replace(/\\s+/g,' ').trim();" | |
| "const esc=s=>clean(s).replace(/\\\\/g,'\\\\\\\\').replace(/\\[/g,'\\\\[').replace(/\\]/g,'\\\\]').replace(/\\n/g,' ');" | |
| "const visible=el=>{const r=el.getBoundingClientRect();const st=getComputedStyle(el);return r.width>0&&r.height>0&&st.display!=='none'&&st.visibility!=='hidden'&&st.opacity!=='0';};" | |
| "const bad=h=>(/(^|\\.)google\\./.test(h)||/(^|\\.)gstatic\\./.test(h)||/(^|\\.)googleusercontent\\./.test(h));" | |
| "const lines=['# Google search results','',`URL: ${location.href}`,'','## Visible links'];" | |
| "const seen=new Set();" | |
| "for(const a of document.querySelectorAll('a[href]')){if(!visible(a))continue;let href=a.href||'';" | |
| "try{const u=new URL(href);if(u.pathname==='/url'&&u.searchParams.get('q'))href=u.searchParams.get('q');}catch{}" | |
| "let u;try{u=new URL(href);}catch{continue;}if(!/^https?:$/.test(u.protocol))continue;if(bad(u.hostname))continue;" | |
| "const text=esc(a.innerText||a.textContent);if(text.length<3)continue;if(seen.has(u.href))continue;seen.add(u.href);" | |
| "lines.push(`- [${text.slice(0,180)}](${u.href})`);if(seen.size>=20)break;}" | |
| "lines.push('','## Text snapshot',clean(document.body.innerText).slice(0,1200));" | |
| "return lines.join('\\n');" | |
| "})()"; | |
| static const char *web_extract_page_js = | |
| "(() => {" | |
| "const clean=s=>(s||'').replace(/\\s+/g,' ').trim();" | |
| "const esc=s=>clean(s).replace(/\\\\/g,'\\\\\\\\').replace(/\\[/g,'\\\\[').replace(/\\]/g,'\\\\]').replace(/\\n/g,' ');" | |
| "const visible=el=>{const r=el.getBoundingClientRect();const st=getComputedStyle(el);return r.width>0&&r.height>0&&st.display!=='none'&&st.visibility!=='hidden'&&st.opacity!=='0';};" | |
| "const inline=n=>{if(!n)return'';if(n.nodeType===3)return n.nodeValue;if(n.nodeType!==1)return'';const el=n;" | |
| "if(el.tagName==='SCRIPT'||el.tagName==='STYLE'||el.tagName==='NOSCRIPT')return'';" | |
| "if(el.tagName==='A'){const t=esc(el.innerText||el.textContent);const h=el.href||'';return t&&h?`[${t}](${h})`:t;}" | |
| "if(el.tagName==='CODE')return '`'+clean(el.innerText||el.textContent).replace(/`/g,'\\\\`')+'`';" | |
| "return [...el.childNodes].map(inline).join('');};" | |
| "const lines=[`# ${clean(document.title)||location.href}`,'',`URL: ${location.href}`,'','## Content'];" | |
| "const blocks=[...document.body.querySelectorAll('h1,h2,h3,h4,h5,h6,p,li,pre,blockquote,td,th,[id=\"content-text\"],[class*=\"comment-body\"],[class*=\"comment-content\"],[data-testid*=\"comment-text\"]')];" | |
| "const seen=new Set();" | |
| "for(const el of blocks){if(!visible(el))continue;let s='';const tag=el.tagName;" | |
| "if(/^H[1-6]$/.test(tag)){s='#'.repeat(Number(tag[1]))+' '+inline(el);}" | |
| "else if(tag==='LI'){s='- '+inline(el);}" | |
| "else if(tag==='PRE'){s='```\\n'+(el.innerText||el.textContent||'').trimEnd()+'\\n```';}" | |
| "else if(tag==='BLOCKQUOTE'){s='> '+clean(el.innerText||el.textContent);}" | |
| "else{s=inline(el);}s=s.trim();if(!s||seen.has(s))continue;seen.add(s);lines.push('',s);" | |
| "if(lines.join('\\n').length>900000){lines.push('','[Content truncated by browser extractor.]');break;}}" | |
| "lines.push('','## Visible links');let n=0;const linkSeen=new Set();" | |
| "for(const a of document.querySelectorAll('a[href]')){if(!visible(a))continue;const t=esc(a.innerText||a.textContent);if(t.length<3)continue;" | |
| "let u;try{u=new URL(a.href);}catch{continue;}if(!/^https?:$/.test(u.protocol)||linkSeen.has(u.href))continue;linkSeen.add(u.href);" | |
| "lines.push(`- [${t.slice(0,160)}](${u.href})`);if(++n>=80)break;}" | |
| "return lines.join('\\n');" | |
| "})()"; | |
| static char *web_run_page_js(ds4_web *web, const char *url, const char *js, | |
| bool dynamic_scroll, | |
| char *err, size_t err_len) { | |
| if (!web_ensure_browser(web, err, err_len)) return NULL; | |
| web_tab tab = {0}; | |
| if (!web_open_tab(web, "about:blank", &tab, err, err_len)) return NULL; | |
| cdp_ws ws = {.fd = -1, .web = web}; | |
| if (web_ws_connect(tab.ws_url, &ws, err, err_len) != 0) { | |
| web_close_tab(web, &tab); | |
| web_tab_free(&tab); | |
| return NULL; | |
| } | |
| if (!web_cdp_prepare_page(&ws, err, err_len)) { | |
| web_ws_close(&ws); | |
| web_close_tab(web, &tab); | |
| web_tab_free(&tab); | |
| return NULL; | |
| } | |
| if (!web_cdp_navigate(&ws, url, err, err_len) || | |
| !web_wait_navigated_ready(&ws, url, err, err_len)) | |
| { | |
| web_ws_close(&ws); | |
| web_close_tab(web, &tab); | |
| web_tab_free(&tab); | |
| return NULL; | |
| } | |
| char *clicked = web_cdp_eval_string(&ws, web_click_google_consent_js, err, err_len); | |
| if (clicked && clicked[0]) { | |
| web_log(web, clicked); | |
| if (!web_sleep_ms(web, 1500)) { | |
| free(clicked); | |
| web_set_err(err, err_len, "interrupted"); | |
| web_ws_close(&ws); | |
| web_close_tab(web, &tab); | |
| web_tab_free(&tab); | |
| return NULL; | |
| } | |
| char wait_err[160] = {0}; | |
| if (!web_wait_navigated_ready(&ws, url, wait_err, sizeof(wait_err)) && | |
| web_err_is_interrupted(wait_err)) | |
| { | |
| free(clicked); | |
| web_set_err(err, err_len, "interrupted"); | |
| web_ws_close(&ws); | |
| web_close_tab(web, &tab); | |
| web_tab_free(&tab); | |
| return NULL; | |
| } | |
| } | |
| free(clicked); | |
| if (dynamic_scroll && !web_scroll_dynamic_page(&ws, err, err_len)) { | |
| web_ws_close(&ws); | |
| web_close_tab(web, &tab); | |
| web_tab_free(&tab); | |
| return NULL; | |
| } | |
| char *out = web_cdp_eval_string(&ws, js, err, err_len); | |
| web_ws_close(&ws); | |
| web_close_tab(web, &tab); | |
| web_tab_free(&tab); | |
| return out; | |
| } | |
| ds4_web *ds4_web_create(const ds4_web_config *cfg) { | |
| ds4_web *web = web_xmalloc(sizeof(*web)); | |
| memset(web, 0, sizeof(*web)); | |
| const char *home = cfg && cfg->home_dir && cfg->home_dir[0] ? | |
| cfg->home_dir : getenv("HOME"); | |
| if (!home || !home[0]) home = "."; | |
| snprintf(web->home, sizeof(web->home), "%s", home); | |
| snprintf(web->profile_dir, sizeof(web->profile_dir), "%s/.ds4/browser", home); | |
| web->port = cfg && cfg->port > 0 ? cfg->port : DS4_WEB_DEFAULT_PORT; | |
| web->chrome_pid = 0; | |
| web->next_cdp_id = 1; | |
| if (cfg) { | |
| web->confirm = cfg->confirm; | |
| web->confirm_privdata = cfg->confirm_privdata; | |
| web->log = cfg->log; | |
| web->log_privdata = cfg->log_privdata; | |
| web->cancel = cfg->cancel; | |
| web->cancel_privdata = cfg->cancel_privdata; | |
| } | |
| return web; | |
| } | |
| void ds4_web_free(ds4_web *web) { | |
| if (!web) return; | |
| /* Do not kill Chrome. The browser profile is user-visible state and keeping | |
| * it alive makes repeated web tool calls cheaper and less suspicious. */ | |
| free(web); | |
| } | |
| char *ds4_web_google_search(ds4_web *web, const char *query, | |
| char *err, size_t err_len) { | |
| if (!web) { | |
| web_set_err(err, err_len, "web subsystem is not initialized"); | |
| return NULL; | |
| } | |
| if (!query || !query[0]) { | |
| web_set_err(err, err_len, "google_search requires query"); | |
| return NULL; | |
| } | |
| char *q = web_url_encode(query); | |
| web_buf url = {0}; | |
| web_buf_puts(&url, "https://www.google.com/search?q="); | |
| web_buf_puts(&url, q); | |
| free(q); | |
| char *url_s = web_buf_take(&url); | |
| char *out = web_run_page_js(web, url_s, web_extract_search_js, false, err, err_len); | |
| free(url_s); | |
| return out; | |
| } | |
| char *ds4_web_visit_page(ds4_web *web, const char *url, | |
| char *err, size_t err_len) { | |
| if (!web) { | |
| web_set_err(err, err_len, "web subsystem is not initialized"); | |
| return NULL; | |
| } | |
| if (!url || !url[0]) { | |
| web_set_err(err, err_len, "visit_page requires url"); | |
| return NULL; | |
| } | |
| return web_run_page_js(web, url, web_extract_page_js, true, err, err_len); | |
| } | |