Files
knutwurst--ps5-patchdl/src/patchdl_net.c
T
Knutwurst d87f473678 Keep started pieces across pause and restart
Pausing, a failed job or a restart of PatchDL dropped every piece that
was in flight; with pieces of several GB that is a lot to fetch again.
A stopped piece now syncs what arrived and records it in the sidecar
("have": "piece:bytes,..."), and the next run continues it with a
Range request. Only bytes confirmed by fdatasync are recorded, so a
crash can't make a resume trust data that never reached the disk. The
card and the download list count these bytes, and a restart still offers
Resume for them.

Also:
- Download again during a pause that landed while the manifest was
  loading takes the pause back instead of being ignored.
- A failed final flush (disk full) no longer reports a file download
  as done.
- Rejected manifests (too many pieces, gaps, empty) report
  bad_manifest instead of no reason.
2026-10-17 21:13:19 +02:00

1318 lines
50 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* SPDX-License-Identifier: GPL-3.0-or-later
* Copyright (C) 2026 Knutwurst
*
* PatchDL is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option)
* any later version. See the LICENSE file in the project root for details.
*/
#include "patchdl_net.h"
#include "patchdl_dns.h"
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <unistd.h>
#ifdef PATCHDL_HAVE_CURL
#include <curl/curl.h>
#include <openssl/evp.h>
#include "patchdl_ca.h"
#endif
/* Manifest sanity caps — reject anything bigger than a real PS5 patch. The
largest title we've seen tops out around 70 GB / 18 pieces. */
#define PATCHDL_MAX_PIECES 4096
#define PATCHDL_MAX_PIECE_BYTES (8ULL * 1024 * 1024 * 1024) /* 8 GiB */
#define PATCHDL_MAX_TOTAL_BYTES (200ULL * 1024 * 1024 * 1024) /* 200 GiB */
/* In-RAM buffer caps for full HTTP body fetches. version.xml is a few KB,
manifest JSON is a few MB at most — fail-closed beyond that. The
manifest cap is sized for ~4096 pieces × ~3 KB JSON each with plenty of
headroom; real PS5 manifests are 1-2 MB. */
#define PATCHDL_BUF_MAX_VERXML (4 * 1024 * 1024)
#define PATCHDL_BUF_MAX_MANIFEST (16 * 1024 * 1024)
#ifdef PATCHDL_HAVE_CURL
/* Replacement for fopen("wb"/"r+b") that refuses to follow a symlink at the
destination (would let a malicious symlink redirect the download) and pins
the new file's mode to 0600. Returns NULL on any open error. */
static FILE *
fopen_safe(const char *path, int rw_existing) {
int flags = O_CLOEXEC | O_NOFOLLOW;
int fd;
flags |= rw_existing ? O_RDWR : (O_WRONLY | O_CREAT | O_TRUNC);
FILE *fp;
fd = open(path, flags, 0600);
if (fd < 0) return NULL;
if (!(fp = fdopen(fd, rw_existing ? "r+b" : "wb"))) close(fd); /* fdopen keeps no fd on failure */
return fp;
}
#endif
#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",
"gs-sec.ww.np.dl.playstation.net", /* PS4 update lists */
NULL,
};
/* PS4 package manifests are published on gs2 over plain http only (https
there is a CloudFront edge without Sony's certificate). The console's own
installer checks the package signature, so a forged manifest can't get an
unsigned package installed. Everything else stays https-only. */
#define PLAIN_HTTP_HOST "gs2.ww.prod.dl.playstation.net"
static int
host_allowed(const char *host) {
size_t hlen = strlen(host);
/* DNS is case-insensitive; an upstream redirect to "SGST.prod..." would
otherwise drop out of the allowlist. */
for (int i = 0; ALLOWED_HOSTS[i]; i++) {
if (!strcasecmp(host, ALLOWED_HOSTS[i]))
return 1;
size_t alen = strlen(ALLOWED_HOSTS[i]);
if (hlen > alen + 1 &&
host[hlen - alen - 1] == '.' &&
!strcasecmp(host + hlen - alen, ALLOWED_HOSTS[i]))
return 1;
}
return 0;
}
/* Extract hostname from https://host/path or http://host/path */
/* Why the calling thread's last patchdl_http_get failed (or what status it
got), for error reports. */
static _Thread_local struct {
char step[24];
int curl_rc;
long http_code;
} g_last;
static void
set_last(const char *step, int curl_rc, long http_code) {
snprintf(g_last.step, sizeof g_last.step, "%s", step);
g_last.curl_rc = curl_rc;
g_last.http_code = http_code;
}
/* CURLOPT_RESOLVE entries for host on 443 and 80 from PatchDL's own
resolver (patchdl_dns.c), with every address it found. NULL when the host
can't be resolved. */
static struct curl_slist *
resolve_list_for(const char *host) {
patchdl_dns_answer_t a;
char entry[PATCHDL_DNS_ENTRY_MAX];
struct curl_slist *rl, *both;
unsigned turn = patchdl_dns_rotation();
if (patchdl_dns_lookup(host, &a) ||
patchdl_dns_resolve_entry(&a, host, 443, turn, entry, sizeof entry) ||
!(rl = curl_slist_append(NULL, entry)))
return NULL;
if (patchdl_dns_resolve_entry(&a, host, 80, turn, entry, sizeof entry) ||
!(both = curl_slist_append(rl, entry))) {
curl_slist_free_all(rl);
return NULL;
}
return both;
}
/* The host's addresses refused, dropped or stalled the connection: the next
request asks DNS again instead of reusing the cached answer, so a CDN edge
that stopped serving this console is left behind. Includes a transfer that
stalled mid-way (low-speed timeout): the edges rotate within seconds, and
a fresh answer usually names other ones. */
static void
note_curl_result(const char *host, CURLcode res) {
if (res == CURLE_COULDNT_CONNECT || res == CURLE_COULDNT_RESOLVE_HOST ||
res == CURLE_OPERATION_TIMEDOUT || res == CURLE_RECV_ERROR ||
res == CURLE_SEND_ERROR || res == CURLE_GOT_NOTHING)
patchdl_dns_forget(host);
}
void
patchdl_http_last_error(char *step, size_t step_sz, int *curl_rc, long *http_code) {
if (step && step_sz) snprintf(step, step_sz, "%s", g_last.step);
if (curl_rc) *curl_rc = g_last.curl_rc;
if (http_code) *http_code = g_last.http_code;
}
/* Manifest piece hashes: PS5 manifests carry SHA-256 (64 hex digits), PS4
manifests SHA-1 (40). */
static const EVP_MD *
digest_for(const char *hex) {
return (hex && strlen(hex) == 40) ? EVP_sha1() : EVP_sha256();
}
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;
}
/* ---------- 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;
/* Overflow guard before the cap check (b->size+total may wrap on 32-bit). */
if (total > (size_t)-1 - b->size - 1) return 0;
/* Cap accumulation so a hostile CDN can't drive unbounded RAM growth. */
if (b->max && b->size + total > b->max) return 0;
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;
}
static int http_get_once(const char *url, patchdl_buf_t *out);
int
patchdl_http_get(const char *url, patchdl_buf_t *out) {
return http_get_once(url, out);
}
/* Manifests, fetched when a download starts: a request that failed on the
network is tried twice more after a short pause, each time starting at
another CDN address, so a single dropped connection doesn't fail the
download before its first byte. HTTP errors, TLS failures and refused
hosts are final. (Update lists aren't retried: the background check walks
every game and would crawl when an edge is dead.) */
static int
http_get_retrying(const char *url, patchdl_buf_t *out) {
static const unsigned pause_s[] = { 2, 5 };
size_t max = out->max;
int rc = -1;
for (int attempt = 0; attempt < 3; attempt++) {
if (attempt) {
if (strcmp(g_last.step, "connect") && strcmp(g_last.step, "timeout") &&
strcmp(g_last.step, "network"))
break;
sleep(pause_s[attempt - 1]);
memset(out, 0, sizeof *out);
out->max = max;
}
if (!(rc = http_get_once(url, out))) break;
}
return rc;
}
static int
http_get_once(const char *url, patchdl_buf_t *out) {
CURL *curl;
CURLcode res;
char host[256];
struct curl_slist *resolve_list = NULL;
struct curl_blob ca_blob;
int plain = !strncmp(url, "http://", 7);
long http_code = 0;
set_last("", 0, 0);
if (url_host(url, host, sizeof(host))) { set_last("bad_url", 0, 0); return -1; }
if (!host_allowed(host)) { set_last("host_not_allowed", 0, 0); return -1; }
if (plain && strcasecmp(host, PLAIN_HTTP_HOST)) { set_last("http_not_allowed", 0, 0); return -1; }
if (!(resolve_list = resolve_list_for(host))) { set_last("dns", 0, 0); return -1; }
{
size_t caller_max = out->max;
memset(out, 0, sizeof(*out));
out->max = caller_max;
}
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);
/* Redirects must stay on HTTPS — host_allowed gates the initial URL, but
once libcurl follows a 302 we want the protocol pinned too. The _STR
variants replaced the bitfield options in libcurl 7.85. Plain http only
for the PS4 manifest host (see PLAIN_HTTP_HOST). */
curl_easy_setopt(curl, CURLOPT_PROTOCOLS_STR, plain ? "http" : "https");
curl_easy_setopt(curl, CURLOPT_REDIR_PROTOCOLS_STR, "https");
curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
curl_easy_setopt(curl, CURLOPT_USERAGENT, "patchdl/1.0");
res = curl_easy_perform(curl);
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
note_curl_result(host, res);
curl_easy_cleanup(curl);
curl_slist_free_all(resolve_list);
if (res != CURLE_OK) {
/* only "connect", "timeout" and "network" are worth another try */
set_last(res == CURLE_COULDNT_CONNECT ? "connect" :
res == CURLE_PEER_FAILED_VERIFICATION ||
res == CURLE_SSL_CONNECT_ERROR ? "tls"
: res == CURLE_UNSUPPORTED_PROTOCOL ||
res == CURLE_TOO_MANY_REDIRECTS ? "redirect"
: res == CURLE_WRITE_ERROR ? "bad_manifest" /* over the size cap */
: res == CURLE_OPERATION_TIMEDOUT ? "timeout" : "network",
(int)res, http_code);
free(out->data);
out->data = NULL;
out->size = 0;
return -1;
}
set_last(http_code >= 400 ? "http_status" : "", 0, http_code);
return 0;
}
/* ---------- external-host HTTP (no Sony CA, no allowlist) -------------- */
static int
http_external(const char *url, const char *method,
const char *content_type, const char *body,
patchdl_buf_t *out) {
#ifndef PATCHDL_HAVE_CURL
(void)url; (void)method; (void)content_type; (void)body; (void)out;
return -1;
#else
CURL *curl;
CURLcode res;
char host[256];
struct curl_slist *resolve_list = NULL;
struct curl_slist *headers = NULL;
if (url_host(url, host, sizeof(host))) return -1;
if (!(resolve_list = resolve_list_for(host))) return -1;
{
size_t caller_max = out->max;
memset(out, 0, sizeof(*out));
out->max = caller_max;
}
curl = curl_easy_init();
if (!curl) { curl_slist_free_all(resolve_list); return -1; }
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_SSL_VERIFYPEER, 0L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L);
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 3L);
curl_easy_setopt(curl, CURLOPT_PROTOCOLS_STR, "https");
curl_easy_setopt(curl, CURLOPT_REDIR_PROTOCOLS_STR, "https");
curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
curl_easy_setopt(curl, CURLOPT_USERAGENT,
"Mozilla/5.0 (PlayStation; PatchDL)");
if (body) {
if (content_type) {
char ct[256];
snprintf(ct, sizeof(ct), "Content-Type: %s", content_type);
headers = curl_slist_append(headers, ct);
}
headers = curl_slist_append(headers, "Accept: application/json");
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body);
}
res = curl_easy_perform(curl);
note_curl_result(host, res);
curl_easy_cleanup(curl);
curl_slist_free_all(resolve_list);
curl_slist_free_all(headers);
if (res != CURLE_OK) {
free(out->data);
out->data = NULL;
out->size = 0;
return -1;
}
return 0;
#endif
}
int
patchdl_http_get_external(const char *url, patchdl_buf_t *out) {
return http_external(url, "GET", NULL, NULL, out);
}
int
patchdl_http_post_external(const char *url, const char *content_type,
const char *body, patchdl_buf_t *out) {
return http_external(url, "POST", content_type, body, out);
}
/* 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];
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 (!(rl = resolve_list_for(host))) return -1;
if (verify) {
sink.md = EVP_MD_CTX_new();
if (!sink.md) { curl_slist_free_all(rl); return -1; } /* never skip a requested check */
EVP_DigestInit_ex(sink.md, digest_for(expected_sha256_hex), NULL);
}
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);
/* HTTPS pin removed on the streaming download path: the Sony CDN sometimes
302s a piece URL to a signed http:// edge inside its own infrastructure,
and refusing those redirects was breaking real-world downloads.
host_allowed + TLS-against-pinned-root on every leg already gate the
hosts we'll talk to. NOSIGNAL stays — it protects the worker from a
SIGPIPE on connection RST. */
curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
/* an error page (404, 503) must not be saved as the file */
curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1L);
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);
note_curl_result(host, res);
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];
int ok = EVP_DigestFinal_ex(sink.md, dig, &dlen);
EVP_MD_CTX_free(sink.md);
/* Fail-closed on a digest API failure — otherwise hex would be empty
and we'd silently report -2 with no diagnostic. */
if (ok != 1 || dlen == 0) return -2;
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_safe(dest_path, 0);
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);
if (fclose(fp) != 0 && !rc) rc = -1; /* the last flush failed (disk full) */
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);
}
/* Substring scan bounded to [p, limit). NULL limit means search to NUL.
Returns NULL if needle is not found before limit. */
static const char *
strstr_bounded(const char *p, const char *needle, const char *limit) {
const char *hit = strstr(p, needle);
if (!hit) return NULL;
if (limit && hit >= limit) return NULL;
return hit;
}
/* Read "key": "value" starting from p. Search and read are bounded by `limit`
(pass NULL to search to end of buffer). Decodes \\ \" \/ \n \r \t \b \f; any
other \X is copied without the backslash. \uXXXX is left as the raw 6 bytes
(we don't need Unicode for manifest fields). limit==NULL keeps legacy
end-of-string scope for callers that don't need the cap. */
static int
json_string_after(const char *p, const char *key, char *out, size_t out_sz,
const char *limit) {
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_bounded(p, needle, limit);
if (!q) return -1;
q += strlen(needle);
while ((!limit || q < limit) &&
(*q == ' ' || *q == '\t' || *q == '\r' || *q == '\n')) q++;
if (limit && q >= limit) return -1;
if (*q++ != ':') return -1;
while ((!limit || q < limit) &&
(*q == ' ' || *q == '\t' || *q == '\r' || *q == '\n')) q++;
if (limit && q >= limit) return -1;
if (*q++ != '"') return -1;
while ((!limit || q < limit) && *q && *q != '"' && n + 1 < out_sz) {
if (*q == '\\' && q[1] && (!limit || q + 1 < limit)) {
q++;
switch (*q) {
case '"': out[n++] = '"'; break;
case '\\': out[n++] = '\\'; break;
case '/': out[n++] = '/'; break;
case 'n': out[n++] = '\n'; break;
case 'r': out[n++] = '\r'; break;
case 't': out[n++] = '\t'; break;
case 'b': out[n++] = '\b'; break;
case 'f': out[n++] = '\f'; break;
default: out[n++] = *q; break; /* unknown escape: keep payload */
}
q++;
} else {
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,
const char *limit) {
char needle[48];
const char *q;
if (!p || !out) return -1;
snprintf(needle, sizeof(needle), "\"%s\"", key);
q = strstr_bounded(p, needle, limit);
if (!q) return -1;
q += strlen(needle);
while ((!limit || q < limit) &&
(*q == ' ' || *q == '\t' || *q == '\r' || *q == '\n')) q++;
if (limit && q >= limit) return -1;
if (*q++ != ':') return -1;
while ((!limit || q < limit) &&
(*q == ' ' || *q == '\t' || *q == '\r' || *q == '\n')) q++;
if (limit && q >= limit) return -1;
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;
memset(&manifest, 0, sizeof(manifest));
manifest.max = PATCHDL_BUF_MAX_MANIFEST;
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, NULL);
/* 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_safe(dest_path, 1);
if (fp) { fseek(fp, 0, SEEK_END); have = ftell(fp); if (have < 0) have = 0; }
}
if (!fp) { fp = fopen_safe(dest_path, 0); 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, '}');
const char *piece_limit = (obj_end && (!pieces_end || obj_end < pieces_end))
? obj_end : pieces_end;
if (json_string_after(p, "url", url, sizeof(url), piece_limit))
break;
json_u64_after(p, "fileSize", &expected, piece_limit);
have_offset = (json_u64_after(p, "fileOffset", &offset, piece_limit) == 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), piece_limit);
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),
piece_limit);
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. A resumed piece starts at
`resume` bytes; see piece_header_cb and piece_write_cb for how a server
that ignores the range is handled. */
typedef struct {
int fd;
long long base;
long long written;
long long size; /* the piece's length: never write past it */
long long resume; /* bytes of the piece already on disk */
long long range_start; /* from Content-Range, -1 if none */
int checked; /* first body bytes seen */
int bad_range; /* the server sent a different range */
CURL *curl;
EVP_MD_CTX *md;
const EVP_MD *md_type;
volatile long long *bytes_slot;
} piece_sink_t;
/* Feed [offset, offset+size) of fd into md. 0 on success, -1 on a read
error, -2 when *abort was set (pause or cancel) on the way. */
static int
digest_fd_region(EVP_MD_CTX *md, int fd, long long offset, long long size,
volatile int *abort) {
const size_t CHUNK = 1u << 20;
unsigned char *buf = malloc(CHUNK);
if (!buf) return -1;
while (size > 0) {
size_t want = size > (long long)CHUNK ? CHUNK : (size_t)size;
ssize_t got;
if (abort && *abort) { free(buf); return -2; }
got = pread(fd, buf, want, (off_t)offset);
if (got <= 0) { free(buf); return -1; }
EVP_DigestUpdate(md, buf, (size_t)got);
offset += got; size -= got;
}
free(buf);
return 0;
}
static size_t
piece_header_cb(char *b, size_t size, size_t nitems, void *ud) {
piece_sink_t *s = (piece_sink_t *)ud;
size_t n = size * nitems;
long long start;
/* A redirect's headers come first; only the last response counts. */
if (n > 5 && !strncmp(b, "HTTP/", 5)) s->range_start = -1;
if (n > 20 && !strncasecmp(b, "Content-Range:", 14) &&
sscanf(b + 14, " bytes %lld-", &start) == 1)
s->range_start = start;
return n;
}
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;
if (s->resume && !s->checked) {
long code = 0;
s->checked = 1;
curl_easy_getinfo(s->curl, CURLINFO_RESPONSE_CODE, &code);
if (code != 206) {
/* the server sent the whole piece: start it over in place */
s->written = 0;
if (s->md) EVP_DigestInit_ex(s->md, s->md_type, NULL);
} else if (s->range_start != s->resume) {
s->bad_range = 1;
return 0; /* not where we left off */
}
}
/* more than the piece holds would land in the next piece's bytes */
if (s->size > 0 && s->written + (long long)n > s->size) 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,
long long resume_from,
const char *expected_sha256_or_null,
patchdl_piece_ctx_t *ctx,
int *curl_rc_out, long *http_code_out) {
CURL *curl;
CURLcode res;
long http_code = 0;
char host[256];
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 (curl_rc_out) *curl_rc_out = 0;
if (http_code_out) *http_code_out = 0;
if (url_host(url, host, sizeof(host))) return -1;
if (!host_allowed(host)) return -1;
if (!(rl = resolve_list_for(host))) return -1;
if (resume_from < 0 || file_size <= 0 || resume_from >= file_size) resume_from = 0;
memset(&sink, 0, sizeof(sink));
sink.fd = fd;
sink.base = file_offset;
sink.size = file_size;
sink.resume = resume_from;
sink.written = resume_from;
sink.range_start = -1;
sink.bytes_slot = ctx ? ctx->bytes_slot : NULL;
if (sink.bytes_slot) *sink.bytes_slot = resume_from;
if (verify) {
sink.md_type = digest_for(expected_sha256_or_null);
sink.md = EVP_MD_CTX_new();
if (!sink.md) { curl_slist_free_all(rl); return -1; } /* never skip a requested check */
EVP_DigestInit_ex(sink.md, sink.md_type, NULL);
/* the part already on disk goes into the hash first */
if (sink.md && resume_from) {
int drc = digest_fd_region(sink.md, fd, file_offset, resume_from,
ctx ? ctx->abort : NULL);
if (drc == -2) { /* paused or cancelled meanwhile */
EVP_MD_CTX_free(sink.md);
curl_slist_free_all(rl);
return -1;
}
if (drc) { /* unreadable: fetch it all again */
EVP_DigestInit_ex(sink.md, sink.md_type, NULL);
sink.resume = sink.written = 0;
if (sink.bytes_slot) *sink.bytes_slot = 0;
}
}
}
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);
sink.curl = curl;
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, piece_write_cb);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &sink);
curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, piece_header_cb);
curl_easy_setopt(curl, CURLOPT_HEADERDATA, &sink);
if (sink.resume) {
char range[48];
snprintf(range, sizeof range, "%lld-%lld", sink.resume, file_size - 1);
curl_easy_setopt(curl, CURLOPT_RANGE, range);
}
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);
/* HTTPS pin removed on the streaming piece path — see the same change in
http_download_to_file_progress for the why. */
curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
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);
note_curl_result(host, res);
curl_easy_cleanup(curl);
curl_slist_free_all(rl);
if (curl_rc_out) *curl_rc_out = (int)res;
if (http_code_out) *http_code_out = http_code;
if (res != CURLE_OK) {
if (sink.md) EVP_MD_CTX_free(sink.md);
/* -3: the server can't continue where this piece stopped */
return sink.resume && (sink.bad_range || http_code == 416) ? -3 : -1;
}
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];
int ok = EVP_DigestFinal_ex(sink.md, dig, &dl);
EVP_MD_CTX_free(sink.md);
if (ok != 1 || dl == 0) return -2;
hex_encode(dig, dl, hex, sizeof(hex));
if (strcasecmp(hex, expected_sha256_or_null) != 0)
return -2; /* integrity mismatch */
}
/* durable before the caller sets the done bit; an error here means
the bytes may not be on disk, so the piece doesn't count */
if (fdatasync(fd) != 0) return -1;
return 0;
}
/* Read-only: hash a [offset, offset+size) region of fd into out_hex (>=65
bytes), with the algorithm `like_hex` is written in (see digest_for). Uses
pread so it doesn't disturb the fd offset. 0 on success. */
int
patchdl_hash_fd_region(int fd, long long offset, long long size,
const char *like_hex, 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, digest_for(like_hex), 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;
int ok = EVP_DigestFinal_ex(md, dig, &dl);
if (ok != 1 || dl == 0) { free(buf); EVP_MD_CTX_free(md); return -1; }
for (i = 0; i < dl; i++) snprintf(out_hex + 2 * i, 3, "%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));
memset(&buf, 0, sizeof(buf));
buf.max = PATCHDL_BUF_MAX_MANIFEST;
if (http_get_retrying(manifest_url, &buf)) return -1;
if (!buf.data || !buf.size) {
free(buf.data);
if (!g_last.step[0]) set_last("empty_reply", 0, g_last.http_code);
return -1;
}
pieces = strstr(buf.data, "\"pieces\"");
if (!pieces || !(pieces = strchr(pieces, '['))) {
free(buf.data);
if (!g_last.step[0]) set_last("bad_manifest", 0, g_last.http_code);
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); set_last("bad_manifest", 0, g_last.http_code); return -1; }
/* Sanity caps: refuse a manifest that would let a CDN drive multi-TB
allocations or millions of pieces. The biggest real PS5 patch we've
seen is ~70 GB / 18 pieces; these limits leave room to spare. */
if (cap > PATCHDL_MAX_PIECES) { free(buf.data); set_last("bad_manifest", 0, g_last.http_code); 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, '}');
const char *piece_limit = (obj_end && (!pieces_end || obj_end < pieces_end))
? obj_end : pieces_end;
if (json_string_after(p, "url", url, sizeof(url), piece_limit))
break;
json_u64_after(p, "fileSize", &sz, piece_limit);
if (json_u64_after(p, "fileOffset", &off, piece_limit) != 0)
off = (unsigned long long)running; /* no offset -> assume contiguous */
/* Validate tiling: pieces must be in order, contiguous, non-empty,
and each individually under the per-piece cap. */
if ((long long)off != running || sz == 0 || sz > PATCHDL_MAX_PIECE_BYTES ||
(unsigned long long)running + sz > PATCHDL_MAX_TOTAL_BYTES) {
patchdl_manifest_free(out);
free(buf.data);
set_last("bad_manifest", 0, g_last.http_code);
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), piece_limit);
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;
}
json_string_after(buf.data, "playgoChunkCrcUrl", out->crc_url,
sizeof(out->crc_url), NULL);
free(buf.data);
if (n == 0) { patchdl_manifest_free(out); set_last("bad_manifest", 0, g_last.http_code); 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};
int hostrc, allowrc, dnsrc = -1;
long http_code = 0;
CURLcode res = CURLE_OK;
int did_curl = 0;
patchdl_dns_answer_t a;
memset(&a, 0, sizeof a);
hostrc = url_host(url, host, sizeof(host));
allowrc = (hostrc == 0) ? host_allowed(host) : 0;
if (hostrc == 0) {
patchdl_dns_refresh(host);
dnsrc = patchdl_dns_lookup(host, &a);
}
if (hostrc == 0 && allowrc && dnsrc == 0) {
patchdl_buf_t buf;
memset(&buf, 0, sizeof(buf));
CURL *curl = curl_easy_init();
if (curl) {
char rs[PATCHDL_DNS_ENTRY_MAX];
struct curl_slist *rl;
struct curl_blob ca_blob;
rl = patchdl_dns_resolve_entry(&a, host, 443, 0, rs, sizeof rs)
? NULL : 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_PROTOCOLS_STR, "https");
curl_easy_setopt(curl, CURLOPT_REDIR_PROTOCOLS_STR, "https");
curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
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;
}
}
/* host, source and trail are plain ASCII from our own resolver; quotes
can't occur in them. */
snprintf(out_json, sz,
"{\"host\":\"%s\",\"host_ok\":%d,\"allowed\":%d,"
"\"dns_rc\":%d,\"ip\":\"%s\",\"dns_source\":\"%s\",\"dns_trail\":\"%s\","
"\"curl_ran\":%d,\"curl_rc\":%d,\"curl_err\":\"%s\",\"http_code\":%ld}",
host, hostrc == 0, allowrc, dnsrc, a.n ? a.ip[0] : "", a.source, a.trail,
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 */
}
void
patchdl_http_last_error(char *step, size_t step_sz, int *curl_rc, long *http_code) {
if (step && step_sz) snprintf(step, step_sz, "no_curl");
if (curl_rc) *curl_rc = 0;
if (http_code) *http_code = 0;
}
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);
}