#include "patchdl_net.h" #include #include #include #include #include #include #include #include #include #include #include #ifdef PATCHDL_HAVE_CURL #include #include #include "patchdl_ca.h" #endif #define DNS_SERVER "1.1.1.1" #define DNS_PORT 53 #define DNS_TIMEOUT_MS 3000 #ifdef PATCHDL_HAVE_CURL static const char *ALLOWED_HOSTS[] = { "sgst.prod.dl.playstation.net", "gst.prod.dl.playstation.net", "gs2.ww.prod.dl.playstation.net", "prosperopatches.com", NULL, }; static int host_allowed(const char *host) { size_t hlen = strlen(host); for (int i = 0; ALLOWED_HOSTS[i]; i++) { if (!strcmp(host, ALLOWED_HOSTS[i])) return 1; size_t alen = strlen(ALLOWED_HOSTS[i]); if (hlen > alen + 1 && host[hlen - alen - 1] == '.' && !strcmp(host + hlen - alen, ALLOWED_HOSTS[i])) return 1; } return 0; } /* Extract hostname from https://host/path or http://host/path */ static int url_host(const char *url, char *out, size_t out_sz) { const char *p = strstr(url, "://"); const char *end; size_t len; if (!p) return -1; p += 3; end = strpbrk(p, "/:?"); len = end ? (size_t)(end - p) : strlen(p); if (len == 0 || len >= out_sz) return -1; memcpy(out, p, len); out[len] = '\0'; return 0; } /* ---------- raw UDP DNS resolver ---------------------------------------- */ /* Diagnostics from the last dns_resolve() call (read by patchdl_net_diag). step: 0=ok 1=socket 2=sendto 3=recv<12 4=no-answer. */ static int g_dns_step, g_dns_errno, g_dns_n, g_dns_ancount; static int dns_encode_name(const char *host, uint8_t *buf, size_t bufsz) { size_t pos = 0; const char *p = host; while (*p) { const char *dot = strchr(p, '.'); size_t lablen = dot ? (size_t)(dot - p) : strlen(p); if (lablen > 63 || pos + 1 + lablen + 1 >= bufsz) return -1; buf[pos++] = (uint8_t)lablen; memcpy(buf + pos, p, lablen); pos += lablen; p += lablen; if (*p == '.') p++; } buf[pos++] = 0; return (int)pos; } static int dns_resolve(const char *host, char *ip_out, size_t ip_sz) { int fd; struct sockaddr_in srv; uint8_t pkt[512], resp[512]; ssize_t n; int name_len; size_t pos; struct timeval tv; g_dns_step = 0; g_dns_errno = 0; g_dns_n = 0; g_dns_ancount = 0; fd = socket(AF_INET, SOCK_DGRAM, 0); if (fd < 0) { g_dns_step = 1; g_dns_errno = errno; return -1; } tv.tv_sec = DNS_TIMEOUT_MS / 1000; tv.tv_usec = (DNS_TIMEOUT_MS % 1000) * 1000; setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); memset(&srv, 0, sizeof(srv)); srv.sin_family = AF_INET; srv.sin_port = htons(DNS_PORT); srv.sin_addr.s_addr = inet_addr(DNS_SERVER); memset(pkt, 0, sizeof(pkt)); pkt[0] = 0xDE; pkt[1] = 0xAD; /* ID */ pkt[2] = 0x01; pkt[3] = 0x00; /* standard query, RD */ pkt[4] = 0x00; pkt[5] = 0x01; /* QDCOUNT = 1 */ pos = 12; name_len = dns_encode_name(host, pkt + pos, sizeof(pkt) - pos - 4); if (name_len < 0) { close(fd); return -1; } pos += (size_t)name_len; pkt[pos++] = 0x00; pkt[pos++] = 0x01; /* QTYPE = A */ pkt[pos++] = 0x00; pkt[pos++] = 0x01; /* QCLASS = IN */ if (sendto(fd, pkt, pos, 0, (struct sockaddr *)&srv, sizeof(srv)) < 0) { g_dns_step = 2; g_dns_errno = errno; close(fd); return -1; } n = recv(fd, resp, sizeof(resp), 0); g_dns_n = (int)n; if (n < 0) g_dns_errno = errno; close(fd); if (n < 12) { g_dns_step = 3; return -1; } uint16_t ancount = (uint16_t)((resp[6] << 8) | resp[7]); g_dns_ancount = ancount; if (!ancount) { g_dns_step = 4; return -1; } /* Skip header and question section */ pos = 12; while (pos < (size_t)n) { if (!resp[pos]) { pos++; break; } if ((resp[pos] & 0xC0) == 0xC0) { pos += 2; break; } pos += 1 + resp[pos]; } if (pos + 4 > (size_t)n) { g_dns_step = 5; return -1; } pos += 4; /* QTYPE + QCLASS */ /* Walk every answer RR; the CDN returns a CNAME chain, so skip non-A records and return the first A (type 1, rdlen 4). */ for (int a = 0; a < ancount && pos < (size_t)n; a++) { if ((resp[pos] & 0xC0) == 0xC0) { pos += 2; } else { while (pos < (size_t)n && resp[pos]) pos += 1 + resp[pos]; pos++; } if (pos + 10 > (size_t)n) break; uint16_t type = (uint16_t)((resp[pos] << 8) | resp[pos + 1]); pos += 2; pos += 6; /* CLASS + TTL */ uint16_t rdlen = (uint16_t)((resp[pos] << 8) | resp[pos + 1]); pos += 2; if (type == 1 && rdlen == 4 && pos + 4 <= (size_t)n) { snprintf(ip_out, ip_sz, "%u.%u.%u.%u", resp[pos], resp[pos + 1], resp[pos + 2], resp[pos + 3]); return 0; } pos += rdlen; /* skip this RR's data (e.g. a CNAME target) */ } g_dns_step = 6; /* no A record among the answers */ return -1; } /* Cache resolved IPs by host. The batch fetch hits the same CDN host dozens of times; a burst of identical raw-UDP queries gets dropped/rate-limited, so resolve once (with retries) and reuse. */ static pthread_mutex_t dns_cache_mtx = PTHREAD_MUTEX_INITIALIZER; static struct { char host[256]; char ip[INET_ADDRSTRLEN]; } dns_cache[64]; static int dns_cache_n; static int dns_lookup(const char *host, char *ip_out, size_t ip_sz) { int rc = -1; pthread_mutex_lock(&dns_cache_mtx); for (int i = 0; i < dns_cache_n; i++) { if (!strcmp(dns_cache[i].host, host)) { strncpy(ip_out, dns_cache[i].ip, ip_sz - 1); ip_out[ip_sz - 1] = '\0'; pthread_mutex_unlock(&dns_cache_mtx); return 0; } } /* Cold miss: resolve while HOLDING the cache lock (single-flight). N pool workers needing the same CDN host would otherwise each blast Sony's rate-limited resolver; this way one resolves and the rest get the cache. It also serializes dns_resolve so its diagnostic globals can't be raced. (This is the DNS lock, independent of the pool lock.) */ for (int attempt = 0; attempt < 4 && rc; attempt++) rc = dns_resolve(host, ip_out, ip_sz); if (!rc && dns_cache_n < (int)(sizeof(dns_cache) / sizeof(dns_cache[0]))) { strncpy(dns_cache[dns_cache_n].host, host, sizeof(dns_cache[0].host) - 1); strncpy(dns_cache[dns_cache_n].ip, ip_out, sizeof(dns_cache[0].ip) - 1); dns_cache_n++; } pthread_mutex_unlock(&dns_cache_mtx); return rc; } /* ---------- HTTP GET via curl ------------------------------------------- */ static size_t write_cb(void *ptr, size_t size, size_t nmemb, void *userdata) { patchdl_buf_t *b = userdata; size_t total = size * nmemb; char *newp = realloc(b->data, b->size + total + 1); if (!newp) return 0; b->data = newp; memcpy(b->data + b->size, ptr, total); b->size += total; b->data[b->size] = '\0'; return total; } int patchdl_http_get(const char *url, patchdl_buf_t *out) { CURL *curl; CURLcode res; char host[256], ip[INET_ADDRSTRLEN]; char resolve_str[512]; struct curl_slist *resolve_list = NULL; struct curl_blob ca_blob; if (url_host(url, host, sizeof(host))) return -1; if (!host_allowed(host)) return -1; if (dns_lookup(host, ip, sizeof(ip))) return -1; snprintf(resolve_str, sizeof(resolve_str), "%s:443:%s", host, ip); resolve_list = curl_slist_append(NULL, resolve_str); /* Also cover port 80 in case a redirect lands there */ char resolve_80[512]; snprintf(resolve_80, sizeof(resolve_80), "%s:80:%s", host, ip); resolve_list = curl_slist_append(resolve_list, resolve_80); memset(out, 0, sizeof(*out)); curl = curl_easy_init(); if (!curl) { curl_slist_free_all(resolve_list); return -1; } /* Trust only the embedded SCEI DNAS Root (Sony's private CDN CA). CAINFO_BLOB keeps the cert in memory — no file written anywhere. */ ca_blob.data = (void *)PATCHDL_SCEI_DNAS_ROOT_PEM; ca_blob.len = strlen(PATCHDL_SCEI_DNAS_ROOT_PEM); ca_blob.flags = CURL_BLOB_COPY; curl_easy_setopt(curl, CURLOPT_URL, url); curl_easy_setopt(curl, CURLOPT_RESOLVE, resolve_list); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb); curl_easy_setopt(curl, CURLOPT_WRITEDATA, out); curl_easy_setopt(curl, CURLOPT_CAINFO_BLOB, &ca_blob); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L); /* Some Sony CDN edges still serve SHA-1-signed leaf certs, which OpenSSL 3.x rejects at the default security level. Lower it so the chain still verifies against the pinned SCEI root. */ curl_easy_setopt(curl, CURLOPT_SSL_CIPHER_LIST, "DEFAULT@SECLEVEL=0"); curl_easy_setopt(curl, CURLOPT_TIMEOUT, 15L); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 3L); curl_easy_setopt(curl, CURLOPT_USERAGENT, "patchdl/1.0"); res = curl_easy_perform(curl); curl_easy_cleanup(curl); curl_slist_free_all(resolve_list); if (res != CURLE_OK) { free(out->data); out->data = NULL; out->size = 0; return -1; } return 0; } /* Write sink: tees the body to the file and, when verifying, into a running SHA-256. curl always calls with size==1, so nmemb is the byte count. */ typedef struct { FILE *fp; EVP_MD_CTX *md; /* NULL when not verifying */ } write_sink_t; static size_t file_write_cb(void *ptr, size_t size, size_t nmemb, void *userdata) { write_sink_t *s = (write_sink_t *)userdata; size_t written = fwrite(ptr, size, nmemb, s->fp); if (s->md && written) EVP_DigestUpdate(s->md, ptr, written * size); return written; } /* Hex-encode a digest, lowercase. */ static void hex_encode(const unsigned char *d, unsigned int len, char *out, size_t out_sz) { static const char hexd[] = "0123456789abcdef"; unsigned int i; for (i = 0; i < len && (2u * i + 2u) < out_sz; i++) { out[2 * i] = hexd[(d[i] >> 4) & 0xf]; out[2 * i + 1] = hexd[d[i] & 0xf]; } out[2 * i] = '\0'; } typedef struct { patchdl_download_progress_cb cb; void *ctx; long long base; long long total; } progress_state_t; static int curl_progress_cb(void *clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) { progress_state_t *p = (progress_state_t *)clientp; long long total; (void)ultotal; (void)ulnow; if (!p || !p->cb) return 0; total = p->total > 0 ? p->total : (long long)dltotal; /* A non-zero return aborts the transfer (CURLE_ABORTED_BY_CALLBACK), which is how a cancel request stops a piece mid-flight. */ return p->cb(p->ctx, p->base + (long long)dlnow, total); } /* Returns 0 on success, -1 on download/network failure, -2 when an expected SHA-256 was given and the downloaded bytes did not match it, -3 when a byte range was requested (range_start>0) but the server ignored it (no HTTP 206). When range_start>0 the body is appended at the file's current position, so the caller must have it positioned at range_start and must not verify. */ static int http_download_to_file_progress(const char *url, FILE *fp, long long *bytes_out, progress_state_t *progress, const char *expected_sha256_hex, long long range_start) { CURL *curl; CURLcode res; char host[256], ip[INET_ADDRSTRLEN], rs443[512], rs80[512]; char range_hdr[48]; long http_code = 0; struct curl_slist *rl = NULL; struct curl_blob ca_blob; curl_off_t dl = 0; write_sink_t sink = { fp, NULL }; int verify = (expected_sha256_hex && expected_sha256_hex[0]); if (bytes_out) *bytes_out = 0; if (url_host(url, host, sizeof(host))) return -1; if (!host_allowed(host)) return -1; if (dns_lookup(host, ip, sizeof(ip))) return -1; if (verify) { sink.md = EVP_MD_CTX_new(); if (sink.md) EVP_DigestInit_ex(sink.md, EVP_sha256(), NULL); } snprintf(rs443, sizeof(rs443), "%s:443:%s", host, ip); rl = curl_slist_append(NULL, rs443); snprintf(rs80, sizeof(rs80), "%s:80:%s", host, ip); rl = curl_slist_append(rl, rs80); ca_blob.data = (void *)PATCHDL_SCEI_DNAS_ROOT_PEM; ca_blob.len = strlen(PATCHDL_SCEI_DNAS_ROOT_PEM); ca_blob.flags = CURL_BLOB_COPY; curl = curl_easy_init(); if (!curl) { curl_slist_free_all(rl); if (sink.md) EVP_MD_CTX_free(sink.md); return -1; } curl_easy_setopt(curl, CURLOPT_URL, url); curl_easy_setopt(curl, CURLOPT_RESOLVE, rl); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, file_write_cb); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &sink); curl_easy_setopt(curl, CURLOPT_CAINFO_BLOB, &ca_blob); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L); curl_easy_setopt(curl, CURLOPT_SSL_CIPHER_LIST, "DEFAULT@SECLEVEL=0"); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 5L); curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 20L); /* No total timeout (patches can be large); abort only on a long stall. */ curl_easy_setopt(curl, CURLOPT_LOW_SPEED_LIMIT, 1024L); curl_easy_setopt(curl, CURLOPT_LOW_SPEED_TIME, 30L); curl_easy_setopt(curl, CURLOPT_USERAGENT, "patchdl/1.0"); if (range_start > 0) { snprintf(range_hdr, sizeof(range_hdr), "%lld-", range_start); curl_easy_setopt(curl, CURLOPT_RANGE, range_hdr); } if (progress && progress->cb) { curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L); curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, curl_progress_cb); curl_easy_setopt(curl, CURLOPT_XFERINFODATA, progress); } res = curl_easy_perform(curl); curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); curl_easy_getinfo(curl, CURLINFO_SIZE_DOWNLOAD_T, &dl); curl_easy_cleanup(curl); curl_slist_free_all(rl); if (res != CURLE_OK) { if (sink.md) EVP_MD_CTX_free(sink.md); return -1; } /* Asked for a byte range but the server sent the whole file (no 206): the caller must drop the piece and re-fetch it whole. */ if (range_start > 0 && http_code != 206) { if (sink.md) EVP_MD_CTX_free(sink.md); return -3; } if (sink.md) { unsigned char dig[EVP_MAX_MD_SIZE]; unsigned int dlen = 0; char hex[2 * EVP_MAX_MD_SIZE + 1]; EVP_DigestFinal_ex(sink.md, dig, &dlen); EVP_MD_CTX_free(sink.md); hex_encode(dig, dlen, hex, sizeof(hex)); if (strcasecmp(hex, expected_sha256_hex) != 0) return -2; /* integrity mismatch */ } if (bytes_out) *bytes_out = (long long)dl; return 0; } int patchdl_http_download_progress(const char *url, const char *dest_path, long long *bytes_out, patchdl_download_progress_cb cb, void *ctx) { FILE *fp = fopen(dest_path, "wb"); progress_state_t progress = { cb, ctx, 0, 0 }; int rc; if (!fp) return -1; rc = http_download_to_file_progress(url, fp, bytes_out, &progress, NULL, 0); fclose(fp); if (rc) { unlink(dest_path); return -1; } return 0; } int patchdl_http_download(const char *url, const char *dest_path, long long *bytes_out) { return patchdl_http_download_progress(url, dest_path, bytes_out, NULL, NULL); } static int json_string_after(const char *p, const char *key, char *out, size_t out_sz) { char needle[48]; const char *q; size_t n = 0; if (!p || !out || out_sz == 0) return -1; out[0] = '\0'; snprintf(needle, sizeof(needle), "\"%s\"", key); q = strstr(p, needle); if (!q) return -1; q += strlen(needle); while (*q == ' ' || *q == '\t' || *q == '\r' || *q == '\n') q++; if (*q++ != ':') return -1; while (*q == ' ' || *q == '\t' || *q == '\r' || *q == '\n') q++; if (*q++ != '"') return -1; while (*q && *q != '"' && n + 1 < out_sz) { if (*q == '\\' && q[1]) q++; out[n++] = *q++; } out[n] = '\0'; return n ? 0 : -1; } static int json_u64_after(const char *p, const char *key, unsigned long long *out) { char needle[48]; const char *q; if (!p || !out) return -1; snprintf(needle, sizeof(needle), "\"%s\"", key); q = strstr(p, needle); if (!q) return -1; q += strlen(needle); while (*q == ' ' || *q == '\t' || *q == '\r' || *q == '\n') q++; if (*q++ != ':') return -1; while (*q == ' ' || *q == '\t' || *q == '\r' || *q == '\n') q++; if (*q < '0' || *q > '9') return -1; *out = strtoull(q, NULL, 10); return 0; } int patchdl_http_download_manifest_progress(const char *manifest_url, const char *dest_path, long long *bytes_out, patchdl_download_progress_cb cb, void *ctx, int verify, int resume) { patchdl_buf_t manifest; const char *pieces, *pieces_end, *p; FILE *fp = NULL; long long total = 0, have = 0; unsigned long long manifest_total = 0; int count = 0, started, rc = -1; if (bytes_out) *bytes_out = 0; if (patchdl_http_get(manifest_url, &manifest)) return -1; if (!manifest.data || !manifest.size) { free(manifest.data); return -1; } pieces = strstr(manifest.data, "\"pieces\""); if (!pieces || !(pieces = strchr(pieces, '['))) { free(manifest.data); return -1; } /* Bound the scan to the pieces array; otherwise a later "url" key in the manifest (e.g. playgoChunkCrcUrl) could be appended as a bogus piece. */ pieces_end = strchr(pieces, ']'); json_u64_after(manifest.data, "originalFileSize", &manifest_total); /* Resume: reopen the existing partial and keep its bytes; else start clean. Fully-downloaded pieces are skipped; the one piece that was only partially written continues mid-piece via an HTTP byte range (with a fall back to re-fetching it whole if the CDN ignores the range). */ if (resume) { fp = fopen(dest_path, "r+b"); if (fp) { fseek(fp, 0, SEEK_END); have = ftell(fp); if (have < 0) have = 0; } } if (!fp) { fp = fopen(dest_path, "wb"); have = 0; } if (!fp) { free(manifest.data); return -1; } started = (have <= 0); p = pieces; while ((p = strstr(p, "\"url\"")) && (!pieces_end || p < pieces_end)) { char url[768]; char hash[80] = {0}; long long got = 0, range_start = 0; unsigned long long expected = 0; unsigned long long offset = 0; int have_offset, drc; const char *want_hash; const char *obj_end = strchr(p, '}'); if (json_string_after(p, "url", url, sizeof(url))) break; json_u64_after(p, "fileSize", &expected); have_offset = (json_u64_after(p, "fileOffset", &offset) == 0); /* Piece already fully present from a previous run: skip the download. */ if (!started && have_offset && expected && have >= (long long)(offset + expected)) { total = (long long)(offset + expected); count++; if (cb && cb(ctx, total, manifest_total ? (long long)manifest_total : total)) goto done; p = obj_end ? obj_end + 1 : p + 5; continue; } /* First piece to (re)download while resuming. If part of it is already on disk, resume WITHIN it with a byte range; otherwise drop any stray bytes and fetch it whole. After this, every piece is fetched whole. */ if (!started) { if (have_offset && expected && have > (long long)offset && have < (long long)(offset + expected)) { range_start = have - (long long)offset; /* this piece's bytes on disk */ fseek(fp, 0, SEEK_END); /* append at `have` */ total = have; } else { long long start_at = have_offset ? (long long)offset : 0; fflush(fp); if (ftruncate(fileno(fp), (off_t)start_at) != 0) goto done; /* can't resume cleanly; keep partial */ fseek(fp, 0, SEEK_END); total = start_at; } started = 1; } /* Whole pieces are concatenated in array order; a ranged (partial) piece starts mid-piece, so the contiguity guard applies only to whole ones. */ if (have_offset && range_start == 0 && offset != (unsigned long long)total) goto done; /* A ranged piece can't be hashed (only its tail is fetched). */ want_hash = NULL; if (range_start == 0 && verify) { json_string_after(p, "hashValue", hash, sizeof(hash)); want_hash = hash[0] ? hash : NULL; } { progress_state_t progress = { cb, ctx, total, manifest_total ? (long long)manifest_total : 0 }; /* drc: 0 ok, -1 network/cancel, -2 SHA-256, -3 range ignored. */ drc = http_download_to_file_progress(url, fp, &got, &progress, want_hash, range_start); if (drc == -3) { /* Server ignored the range: drop the piece and fetch it whole. */ fflush(fp); if (ftruncate(fileno(fp), (off_t)offset) != 0) goto done; fseek(fp, 0, SEEK_END); total = (long long)offset; range_start = 0; if (verify) { json_string_after(p, "hashValue", hash, sizeof(hash)); want_hash = hash[0] ? hash : NULL; } progress.base = total; drc = http_download_to_file_progress(url, fp, &got, &progress, want_hash, 0); } } if (drc) { if (drc == -2) rc = -2; goto done; } /* range_start + got = this piece's bytes now on disk. */ if (expected && (unsigned long long)(range_start + got) != expected) goto done; total += got; /* A non-zero callback return between pieces means cancel requested. */ if (cb && cb(ctx, total, manifest_total ? (long long)manifest_total : total)) goto done; count++; p = obj_end ? obj_end + 1 : p + 5; } if (count > 0) { rc = 0; if (bytes_out) *bytes_out = total; } done: fclose(fp); free(manifest.data); /* Keep the partial on failure so it can be resumed; the caller deletes it on cancel or on a corrupt-verify (-2). */ return rc; } int patchdl_http_download_manifest(const char *manifest_url, const char *dest_path, long long *bytes_out) { return patchdl_http_download_manifest_progress(manifest_url, dest_path, bytes_out, NULL, NULL, 0, 0); } /* ---- global init + parallel piece download (connection pool) ----------- */ void patchdl_net_global_init(void) { curl_global_init(CURL_GLOBAL_ALL); } void patchdl_net_global_cleanup(void) { curl_global_cleanup(); } /* Write sink for one piece: pwrite at a fixed base offset (concurrent non-overlapping pieces of the same fd are safe), tee into SHA-256 if asked, and publish bytes-so-far for live progress. */ typedef struct { int fd; long long base; long long written; EVP_MD_CTX *md; volatile long long *bytes_slot; } piece_sink_t; static size_t piece_write_cb(void *ptr, size_t size, size_t nmemb, void *ud) { piece_sink_t *s = (piece_sink_t *)ud; size_t n = size * nmemb; ssize_t w; if (n == 0) return 0; w = pwrite(s->fd, ptr, n, (off_t)(s->base + s->written)); if (w < 0 || (size_t)w != n) return 0; /* short write -> curl errors out */ if (s->md) EVP_DigestUpdate(s->md, ptr, n); s->written += (long long)n; if (s->bytes_slot) *s->bytes_slot = s->written; return n; } static int piece_xfer_cb(void *clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) { volatile int *abort_flag = (volatile int *)clientp; (void)dltotal; (void)dlnow; (void)ultotal; (void)ulnow; return (abort_flag && *abort_flag) ? 1 : 0; /* non-zero aborts the transfer */ } int patchdl_http_download_piece(const char *url, int fd, long long file_offset, long long file_size, const char *expected_sha256_or_null, patchdl_piece_ctx_t *ctx) { CURL *curl; CURLcode res; long http_code = 0; char host[256], ip[INET_ADDRSTRLEN], rs443[512], rs80[512]; struct curl_slist *rl = NULL; struct curl_blob ca_blob; piece_sink_t sink; int verify = (expected_sha256_or_null && expected_sha256_or_null[0]); if (url_host(url, host, sizeof(host))) return -1; if (!host_allowed(host)) return -1; if (dns_lookup(host, ip, sizeof(ip))) return -1; memset(&sink, 0, sizeof(sink)); sink.fd = fd; sink.base = file_offset; sink.bytes_slot = ctx ? ctx->bytes_slot : NULL; if (verify) { sink.md = EVP_MD_CTX_new(); if (sink.md) EVP_DigestInit_ex(sink.md, EVP_sha256(), NULL); } snprintf(rs443, sizeof(rs443), "%s:443:%s", host, ip); rl = curl_slist_append(NULL, rs443); snprintf(rs80, sizeof(rs80), "%s:80:%s", host, ip); rl = curl_slist_append(rl, rs80); ca_blob.data = (void *)PATCHDL_SCEI_DNAS_ROOT_PEM; ca_blob.len = strlen(PATCHDL_SCEI_DNAS_ROOT_PEM); ca_blob.flags = CURL_BLOB_COPY; curl = curl_easy_init(); if (!curl) { curl_slist_free_all(rl); if (sink.md) EVP_MD_CTX_free(sink.md); return -1; } curl_easy_setopt(curl, CURLOPT_URL, url); curl_easy_setopt(curl, CURLOPT_RESOLVE, rl); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, piece_write_cb); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &sink); curl_easy_setopt(curl, CURLOPT_CAINFO_BLOB, &ca_blob); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L); curl_easy_setopt(curl, CURLOPT_SSL_CIPHER_LIST, "DEFAULT@SECLEVEL=0"); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 5L); curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1L); /* 4xx/5xx -> error, no body written */ curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 20L); curl_easy_setopt(curl, CURLOPT_LOW_SPEED_LIMIT, 1024L); curl_easy_setopt(curl, CURLOPT_LOW_SPEED_TIME, 30L); curl_easy_setopt(curl, CURLOPT_USERAGENT, "patchdl/1.0"); if (ctx && ctx->abort) { curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L); curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, piece_xfer_cb); curl_easy_setopt(curl, CURLOPT_XFERINFODATA, (void *)ctx->abort); } res = curl_easy_perform(curl); curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); curl_easy_cleanup(curl); curl_slist_free_all(rl); if (res != CURLE_OK) { if (sink.md) EVP_MD_CTX_free(sink.md); return -1; /* network error / abort */ } if (file_size > 0 && sink.written != file_size) { if (sink.md) EVP_MD_CTX_free(sink.md); return -1; /* short or over-long -> failed */ } if (sink.md) { unsigned char dig[EVP_MAX_MD_SIZE]; unsigned int dl = 0; char hex[2 * EVP_MAX_MD_SIZE + 1]; EVP_DigestFinal_ex(sink.md, dig, &dl); EVP_MD_CTX_free(sink.md); hex_encode(dig, dl, hex, sizeof(hex)); if (strcasecmp(hex, expected_sha256_or_null) != 0) return -2; /* integrity mismatch */ } fdatasync(fd); /* durable before the caller sets the done bit */ return 0; } /* Read-only: SHA-256 a [offset, offset+size) region of fd into out_hex (>=65 bytes). Uses pread so it doesn't disturb the fd offset. 0 on success. */ int patchdl_sha256_fd_region(int fd, long long offset, long long size, char *out_hex) { EVP_MD_CTX *md; unsigned char *buf; long long pos = offset, remaining = size; const size_t CHUNK = 1u << 20; out_hex[0] = '\0'; if (fd < 0 || size < 0) return -1; md = EVP_MD_CTX_new(); if (!md) return -1; buf = malloc(CHUNK); if (!buf) { EVP_MD_CTX_free(md); return -1; } EVP_DigestInit_ex(md, EVP_sha256(), NULL); while (remaining > 0) { size_t want = remaining > (long long)CHUNK ? CHUNK : (size_t)remaining; ssize_t got = pread(fd, buf, want, (off_t)pos); if (got <= 0) { free(buf); EVP_MD_CTX_free(md); return -1; } EVP_DigestUpdate(md, buf, (size_t)got); pos += got; remaining -= got; } { unsigned char dig[EVP_MAX_MD_SIZE]; unsigned int dl = 0, i; EVP_DigestFinal_ex(md, dig, &dl); for (i = 0; i < dl; i++) sprintf(out_hex + 2 * i, "%02x", dig[i]); out_hex[2 * dl] = '\0'; } free(buf); EVP_MD_CTX_free(md); return 0; } void patchdl_manifest_free(patchdl_manifest_t *m) { if (!m || !m->pieces) return; for (int i = 0; i < m->count; i++) free(m->pieces[i].url); free(m->pieces); m->pieces = NULL; m->count = 0; } int patchdl_fetch_manifest(const char *manifest_url, patchdl_manifest_t *out) { patchdl_buf_t buf; const char *pieces, *pieces_end, *p; int cap = 0, n = 0; long long running = 0; memset(out, 0, sizeof(*out)); if (patchdl_http_get(manifest_url, &buf)) return -1; if (!buf.data || !buf.size) { free(buf.data); return -1; } pieces = strstr(buf.data, "\"pieces\""); if (!pieces || !(pieces = strchr(pieces, '['))) { free(buf.data); return -1; } pieces_end = strchr(pieces, ']'); for (p = pieces; (p = strstr(p, "\"url\"")) && (!pieces_end || p < pieces_end); p += 5) cap++; if (cap <= 0) { free(buf.data); return -1; } out->pieces = calloc((size_t)cap, sizeof(patchdl_piece_t)); if (!out->pieces) { free(buf.data); return -1; } p = pieces; while ((p = strstr(p, "\"url\"")) && (!pieces_end || p < pieces_end) && n < cap) { char url[768] = {0}; unsigned long long sz = 0, off = 0; const char *obj_end = strchr(p, '}'); if (json_string_after(p, "url", url, sizeof(url))) break; json_u64_after(p, "fileSize", &sz); if (json_u64_after(p, "fileOffset", &off) != 0) off = (unsigned long long)running; /* no offset -> assume contiguous */ /* Validate tiling: pieces must be in order, contiguous, non-empty. */ if ((long long)off != running || sz == 0) { patchdl_manifest_free(out); free(buf.data); return -1; } out->pieces[n].url = strdup(url); out->pieces[n].offset = (long long)off; out->pieces[n].size = (long long)sz; json_string_after(p, "hashValue", out->pieces[n].hash, sizeof(out->pieces[n].hash)); if (!out->pieces[n].url) { patchdl_manifest_free(out); free(buf.data); return -1; } running += (long long)sz; n++; out->count = n; /* keep current so manifest_free frees exactly n */ p = obj_end ? obj_end + 1 : p + 5; } free(buf.data); if (n == 0) { patchdl_manifest_free(out); return -1; } out->total = running; /* authoritative assembled size */ return 0; } void patchdl_net_diag(const char *url, char *out_json, size_t sz) { char host[256] = {0}, ip[INET_ADDRSTRLEN] = {0}; int hostrc, allowrc, dnsrc; long http_code = 0; CURLcode res = CURLE_OK; int did_curl = 0; hostrc = url_host(url, host, sizeof(host)); allowrc = (hostrc == 0) ? host_allowed(host) : 0; dnsrc = (hostrc == 0) ? dns_resolve(host, ip, sizeof(ip)) : -1; if (hostrc == 0 && allowrc && dnsrc == 0) { patchdl_buf_t buf; memset(&buf, 0, sizeof(buf)); CURL *curl = curl_easy_init(); if (curl) { char rs[512]; struct curl_slist *rl; struct curl_blob ca_blob; snprintf(rs, sizeof(rs), "%s:443:%s", host, ip); rl = curl_slist_append(NULL, rs); ca_blob.data = (void *)PATCHDL_SCEI_DNAS_ROOT_PEM; ca_blob.len = strlen(PATCHDL_SCEI_DNAS_ROOT_PEM); ca_blob.flags = CURL_BLOB_COPY; curl_easy_setopt(curl, CURLOPT_URL, url); curl_easy_setopt(curl, CURLOPT_RESOLVE, rl); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buf); curl_easy_setopt(curl, CURLOPT_CAINFO_BLOB, &ca_blob); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L); curl_easy_setopt(curl, CURLOPT_SSL_CIPHER_LIST, "DEFAULT@SECLEVEL=0"); curl_easy_setopt(curl, CURLOPT_TIMEOUT, 15L); res = curl_easy_perform(curl); curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); curl_easy_cleanup(curl); curl_slist_free_all(rl); free(buf.data); did_curl = 1; } } snprintf(out_json, sz, "{\"host\":\"%s\",\"host_ok\":%d,\"allowed\":%d," "\"dns_rc\":%d,\"ip\":\"%s\"," "\"dns_step\":%d,\"dns_errno\":%d,\"dns_recv\":%d,\"dns_ancount\":%d," "\"curl_ran\":%d,\"curl_rc\":%d,\"curl_err\":\"%s\",\"http_code\":%ld}", host, hostrc == 0, allowrc, dnsrc, ip, g_dns_step, g_dns_errno, g_dns_n, g_dns_ancount, did_curl, (int)res, did_curl ? curl_easy_strerror(res) : "n/a", http_code); } #else /* !PATCHDL_HAVE_CURL */ int patchdl_http_get(const char *url, patchdl_buf_t *out) { (void)url; (void)out; return -1; /* curl not available in this build; set CURL_DIR= to enable */ } int patchdl_http_download(const char *url, const char *dest_path, long long *bytes_out) { (void)url; (void)dest_path; if (bytes_out) *bytes_out = 0; return -1; } int patchdl_http_download_progress(const char *url, const char *dest_path, long long *bytes_out, patchdl_download_progress_cb cb, void *ctx) { (void)cb; (void)ctx; return patchdl_http_download(url, dest_path, bytes_out); } int patchdl_http_download_manifest(const char *manifest_url, const char *dest_path, long long *bytes_out) { (void)manifest_url; (void)dest_path; if (bytes_out) *bytes_out = 0; return -1; } int patchdl_http_download_manifest_progress(const char *manifest_url, const char *dest_path, long long *bytes_out, patchdl_download_progress_cb cb, void *ctx, int verify, int resume) { (void)cb; (void)ctx; (void)verify; (void)resume; return patchdl_http_download_manifest(manifest_url, dest_path, bytes_out); } void patchdl_net_diag(const char *url, char *out_json, size_t sz) { (void)url; snprintf(out_json, sz, "{\"error\":\"curl not built\"}"); } #endif /* PATCHDL_HAVE_CURL */ patchdl_buf_t * patchdl_buf_new(void) { return calloc(1, sizeof(patchdl_buf_t)); } void patchdl_buf_free(patchdl_buf_t *b) { if (!b) return; free(b->data); free(b); }