/** * Bare OS curl — HTTP(S) client using Fetch (not Daniel Stenberg's libcurl). * @see https://curl.se/docs/manpage.html for flag inspiration; behavior is a subset. */ import b4a from 'b4a' import path from '#host-path' import { DEFAULT_CURL_USER_AGENT, defaultFetchSaveName, isSupportedFetchUrl, normalizeFetchUrl, applyBareOsCurlResolveMap } from './http-fetch-url.js' import { assertFetchUrlHostAllowedByDnsPolicy } from './bare-os-dns-policy.js' import { ensureBareFetchGlobals, resolveBareOsFetchFn } from './bare-os-ensure-bare-fetch.js' function basicAuthHeader(user, pass) { const s = user + ':' + pass if (typeof btoa === 'function') return 'Basic ' + btoa(s) if (typeof Buffer !== 'undefined') return 'Basic ' + Buffer.from(s, 'utf8').toString('base64') throw new Error('curl: cannot encode Basic auth (no btoa/Buffer)') } /** * RFC 2183 / 5987 style Content-Disposition filename (best-effort). * @param {string | null} headerVal * @returns {string | null} basename-safe segment or null */ export function filenameFromContentDisposition(headerVal) { if (!headerVal || typeof headerVal !== 'string') return null const h = headerVal.trim() const star = h.match(/filename\*\s*=\s*(?:UTF-8''|utf-8'')?([^;\s]+)/i) if (star) { try { const raw = star[1].replace(/^["']|["']$/g, '') const decoded = decodeURIComponent(raw) const base = decoded.split(/[/\\]/).pop() return base || null } catch { const base = star[1].split(/[/\\]/).pop() return base || null } } const q = h.match(/filename\s*=\s*"((?:\\.|[^"\\])*)"/i) if (q) { const inner = q[1].replace(/\\(.)/g, '$1') const base = inner.split(/[/\\]/).pop() return base || null } const plain = h.match(/filename\s*=\s*([^;\s]+)/i) if (plain) { const base = plain[1] .replace(/^["']|["']$/g, '') .split(/[/\\]/) .pop() return base || null } return null } /** Reject absolute paths and `..` segments from server-provided names (-J). */ function safeRemoteFilename(name) { if (!name || typeof name !== 'string') return null const t = name.trim() if (!t || t.includes('..') || path.posix.isAbsolute(t)) return null const base = path.posix.basename(t) return base || null } function usage() { return ( 'usage: curl [options] URL...\n' + 'Bare OS curl is fetch-based, not libcurl. See man curl.' ) } /** * Split -H line into headers[] or record User-Agent for argv-order precedence with -A. * @param {string} line * @param {string[]} headers * @param {string[]} userAgentSequence * @returns {boolean} false if malformed */ function pushCurlHeaderLine(line, headers, userAgentSequence) { const colon = line.indexOf(':') if (colon < 1) return false const name = line.slice(0, colon).trim() const value = line.slice(colon + 1).trim() if (name.toLowerCase() === 'user-agent') userAgentSequence.push(value) else headers.push(line) return true } function utf8Encode(str) { // Bare may not define TextEncoder; b4a works on Node and Bare. return b4a.from(String(str), 'utf8') } /** @param {(string | Uint8Array)[]} parts */ function concatParts(parts) { let len = 0 for (const p of parts) { len += typeof p === 'string' ? utf8Encode(p).length : p.length } const out = new Uint8Array(len) let o = 0 for (const p of parts) { const u8 = typeof p === 'string' ? utf8Encode(p) : p out.set(u8, o) o += u8.length } return out } function expandWriteOut(fmt, vars) { return fmt.replace(/%\{([^}]+)\}/g, (_, key) => { if (Object.prototype.hasOwnProperty.call(vars, key)) return String(vars[key]) return '' }) } /** * Best-effort URL pieces for curl -w expansions. * @param {string} url */ function curlWriteOutUrlParts(url) { try { const u = new URL(String(url || '')) return { scheme: (u.protocol || '').replace(/:$/, ''), host: u.hostname || '', port: u.port || '', path: (u.pathname || '/') + (u.search || '') + (u.hash || '') } } catch { return { scheme: '', host: '', port: '', path: '' } } } const CURL_MAX_REDIRECTS = 50 /** Default persistent jar on the personal drive (JSON per CLI_PARITY). */ export const BARE_OS_CURL_COOKIE_JAR = '~/.config/bare-os/curl/cookies.json' function curlHostFromUrl(urlStr) { try { return new URL(urlStr).hostname } catch { return '' } } /** @returns {Record>} */ function parseCookieJarJson(text) { try { const o = JSON.parse(text) if (!o || typeof o !== 'object') return {} /** @type {Record>} */ const out = {} for (const [h, bag] of Object.entries(o)) { if (!bag || typeof bag !== 'object') continue out[String(h)] = {} for (const [k, v] of Object.entries(bag)) { out[String(h)][String(k)] = String(v) } } return out } catch { return {} } } /** * @param {{ readFile: (p: string) => Promise }} vfs * @param {string} path */ async function loadCookieJarFile(vfs, path) { const buf = await vfs.readFile(path) if (!buf) return {} return parseCookieJarJson(b4a.toString(buf, 'utf8')) } /** * @param {Record>} jar * @param {string} host * @param {string} line */ function mergeInlineCookieLine(jar, host, line) { if (!host) return for (const part of line.split(';')) { const p = part.trim() const eq = p.indexOf('=') if (eq < 1) continue const name = p.slice(0, eq).trim() const val = p.slice(eq + 1).trim() if (!jar[host]) jar[host] = {} jar[host][name] = val } } /** * @param {Record>} target * @param {Record>} add */ function mergeCookieJars(target, add) { for (const [h, bag] of Object.entries(add)) { if (!target[h]) target[h] = {} Object.assign(target[h], bag) } } /** * @param {Record>} jar * @param {string} host */ function cookieHeaderForHost(jar, host) { const bag = jar[host] if (!bag || !Object.keys(bag).length) return '' return Object.entries(bag) .map(([k, v]) => `${k}=${v}`) .join('; ') } /** * @param {Record>} jar * @param {string} host * @param {string} headerVal */ function applySetCookieHeader(jar, host, headerVal) { if (!host || !headerVal) return const first = String(headerVal).split(';')[0].trim() const eq = first.indexOf('=') if (eq < 1) return const name = first.slice(0, eq).trim() const val = first.slice(eq + 1).trim() if (!jar[host]) jar[host] = {} jar[host][name] = val } /** @param {Response} res */ function gatherSetCookieValues(res) { const h = res.headers if (typeof h.getSetCookie === 'function') return h.getSetCookie() const v = h.get('Set-Cookie') return v ? [v] : [] } /** * TLS extras for fetch: Node undici `dispatcher`, or `bareOsCurlTls` when `ctx.httpFetch` is set. * Host `httpFetch` may read `init.bareOsCurlTls` (`insecure`, optional `caPem`/`certPem`/`keyPem`). * @param {Record} ctx * @param {{ readFile: (p: string) => Promise }} vfs * @param {{ insecure?: boolean, cacertPath?: string | null, certPath?: string | null, keyPath?: string | null }} opts * @returns {Promise<{ dispatcher?: unknown, delegatedTls?: { insecure: boolean, caPem?: string, certPem?: string, keyPem?: string }, error?: string }>} */ async function buildTlsFetchExtras(ctx, vfs, opts) { const insecure = !!opts.insecure const cacertPath = opts.cacertPath || null const certPath = opts.certPath || null const keyPath = opts.keyPath || null if (!insecure && !cacertPath && !certPath && !keyPath) return {} if (!vfs || typeof vfs.readFile !== 'function') { return { error: 'curl: TLS file options require ctx.vfs.readFile' } } if (typeof ctx.httpFetch === 'function') { let caPem let certPem let keyPem if (cacertPath) { const buf = await vfs.readFile(cacertPath) if (!buf) return { error: 'curl: cannot read --cacert file' } caPem = b4a.toString(buf, 'utf8') } if (certPath) { const buf = await vfs.readFile(certPath) if (!buf) return { error: 'curl: cannot read --cert file' } certPem = b4a.toString(buf, 'utf8') } if (keyPath) { const buf = await vfs.readFile(keyPath) if (!buf) return { error: 'curl: cannot read --key file' } keyPem = b4a.toString(buf, 'utf8') } const env = ctx.vfs?.env const pinRaw = env && typeof env === 'object' ? String(env.BARE_OS_TLS_PIN_SHA256 || '').trim() : '' const pinnedSha256 = pinRaw ? pinRaw .split(/[\s,]+/) .map((s) => s.trim()) .filter(Boolean) : undefined return { delegatedTls: { insecure, ...(caPem !== undefined ? { caPem } : {}), ...(certPem !== undefined ? { certPem } : {}), ...(keyPem !== undefined ? { keyPem } : {}), ...(pinnedSha256 && pinnedSha256.length ? { pinnedSha256 } : {}) } } } try { const undici = await import('undici') const Agent = undici.Agent /** @type {Record} */ const connect = {} if (insecure) connect.rejectUnauthorized = false if (cacertPath) { const buf = await vfs.readFile(cacertPath) if (!buf) return { error: 'curl: cannot read --cacert file' } connect.ca = b4a.toString(buf, 'utf8') } if (certPath) { const buf = await vfs.readFile(certPath) if (!buf) return { error: 'curl: cannot read --cert file' } connect.cert = b4a.toString(buf, 'utf8') } if (keyPath) { const buf = await vfs.readFile(keyPath) if (!buf) return { error: 'curl: cannot read --key file' } connect.key = b4a.toString(buf, 'utf8') } return { dispatcher: new Agent({ connect }) } } catch (e) { const msg = e && e.message ? e.message : String(e) return { error: 'curl: TLS options need undici (Node): ' + msg } } } /** * curl -I -L: send HEAD first; after any 3xx, follow with GET (curl man: "GET is used after redirect"). * Using fetch(HEAD, redirect:'follow') keeps HEAD on each hop and breaks many CDNs; manual hops match curl. * @returns {{ response: Response, redirectCount: number }} */ async function fetchHeadWithLocationFollow( fetchFn, startUrl, hdr, signal, fetchExtra = {} ) { let url = startUrl let method = 'HEAD' let redirectCount = 0 for (let hop = 0; hop < CURL_MAX_REDIRECTS; hop++) { const res = await fetchFn(url, { method, headers: hdr, redirect: 'manual', signal, body: undefined, ...fetchExtra }) if (res.status >= 300 && res.status < 400) { const loc = res.headers.get('Location') if (!loc) return { response: res, redirectCount } let nextUrl try { nextUrl = new URL(loc, url).href } catch { return { response: res, redirectCount } } method = 'GET' redirectCount++ try { if (res.body && typeof res.body.cancel === 'function') await res.body.cancel() } catch { /* ignore */ } url = nextUrl continue } return { response: res, redirectCount } } return { response: new Response('', { status: 310, statusText: 'Too many redirects' }), redirectCount } } /** * @param {Record} ctx * @param {string[]} argv */ export async function runCurlCli(ctx, argv) { const vfs = ctx.vfs const args = argv.slice(1) let method = '' const headers = [] /** @type {string[]} -H User-Agent and -A/--user-agent in argv order; last wins */ const userAgentSequence = [] /** @type {string[]} */ const dataChunks = [] let dataBinary = false let headOnly = false let includeHeaders = false let silent = false let showError = false let failOnError = false let location = false let verbose = false /** @type {string | null} */ let outputPath = null let remoteName = false /** Use Content-Disposition filename with -O (curl -J); server-controlled path — only save under trusted cwd. */ let remoteHeaderName = false /** @type {string | null} */ let writeOut = null let maxTimeMs = 0 /** Whole-operation ceiling (Fetch has no separate connect phase; see CLI_PARITY.md). */ let connectTimeMs = 0 /** @type {string | null} */ let userColonPass = null /** @type {string | null} */ let jsonBody = null /** @type {string | null} */ let uploadPath = null let insecureTls = false /** @type {string | null} */ let cacertPath = null /** @type {string | null} */ let certPath = null /** @type {string | null} */ let keyPath = null /** @type {string | null} */ let cookieJarOut = null /** @type {string[]} */ const cookieSpecs = [] /** @type {string[]} */ const urls = [] let i = 0 while (i < args.length) { const a = args[i] if (a === '--') { i++ while (i < args.length) { urls.push(normalizeFetchUrl(String(args[i++]))) } break } if (!a.startsWith('-') || a === '-') { urls.push(normalizeFetchUrl(String(a))) i++ continue } if (a === '-h' || a === '--help') { ctx.console.log(usage()) return } if (a === '-V' || a === '--version') { ctx.console.log('curl (Bare OS fetch subset) 0.1 — not libcurl') return } if (a === '-X' || a === '--request') { if (i + 1 >= args.length) { ctx.console.error('curl: option requires an argument: ' + a) ctx.exitCode = 2 return } method = String(args[++i]).toUpperCase() i++ continue } if (a.startsWith('-X') && a.length > 2) { method = a.slice(2).toUpperCase() i++ continue } if (a === '-H' || a === '--header') { if (i + 1 >= args.length) { ctx.console.error('curl: option requires an argument: ' + a) ctx.exitCode = 2 return } const line = String(args[++i]) if (!pushCurlHeaderLine(line, headers, userAgentSequence)) { ctx.console.error('curl: malformed header: ' + line) ctx.exitCode = 2 return } i++ continue } if (a.startsWith('-H') && a.length > 2) { const line = a.slice(2) if (!pushCurlHeaderLine(line, headers, userAgentSequence)) { ctx.console.error('curl: malformed header: ' + line) ctx.exitCode = 2 return } i++ continue } if (a === '-A' || a === '--user-agent') { if (i + 1 >= args.length) { ctx.console.error('curl: option requires an argument: ' + a) ctx.exitCode = 2 return } userAgentSequence.push(String(args[++i])) i++ continue } if (a.startsWith('--user-agent=')) { userAgentSequence.push(a.slice('--user-agent='.length)) i++ continue } if (a.startsWith('-A') && a.length > 2) { userAgentSequence.push(a.slice(2)) i++ continue } if ( a === '-d' || a === '--data' || a === '--data-ascii' || a === '--data-binary' || a === '--data-raw' ) { if (a === '--data-binary') dataBinary = true if (i + 1 >= args.length) { ctx.console.error('curl: option requires an argument: ' + a) ctx.exitCode = 2 return } dataChunks.push(String(args[++i])) i++ continue } if (a.startsWith('-d') && a.length > 2) { dataChunks.push(a.slice(2)) i++ continue } if (a.startsWith('--data=')) { dataChunks.push(a.slice(7)) i++ continue } if (a === '--json') { if (i + 1 >= args.length) { ctx.console.error('curl: option requires an argument: --json') ctx.exitCode = 2 return } jsonBody = String(args[++i]) i++ continue } if (a === '-o' || a === '--output') { if (i + 1 >= args.length) { ctx.console.error('curl: option requires an argument: ' + a) ctx.exitCode = 2 return } outputPath = String(args[++i]) i++ continue } if (a === '-O' || a === '--remote-name') { remoteName = true i++ continue } if (a === '-J' || a === '--remote-header-name') { remoteHeaderName = true i++ continue } if (a === '-T' || a === '--upload-file') { if (i + 1 >= args.length) { ctx.console.error('curl: option requires an argument: ' + a) ctx.exitCode = 2 return } uploadPath = String(args[++i]) i++ continue } if (a === '-w' || a === '--write-out') { if (i + 1 >= args.length) { ctx.console.error('curl: option requires an argument: ' + a) ctx.exitCode = 2 return } writeOut = String(args[++i]) i++ continue } if (a === '-u' || a === '--user') { if (i + 1 >= args.length) { ctx.console.error('curl: option requires an argument: ' + a) ctx.exitCode = 2 return } userColonPass = String(args[++i]) i++ continue } if (a === '--max-time' || a === '-m') { if (i + 1 >= args.length) { ctx.console.error('curl: option requires an argument: ' + a) ctx.exitCode = 2 return } const sec = Number(args[++i]) if (!Number.isFinite(sec) || sec < 0) { ctx.console.error('curl: invalid --max-time') ctx.exitCode = 2 return } maxTimeMs = Math.round(sec * 1000) i++ continue } if (a === '--connect-timeout') { if (i + 1 >= args.length) { ctx.console.error( 'curl: option requires an argument: --connect-timeout' ) ctx.exitCode = 2 return } const sec = Number(args[++i]) if (!Number.isFinite(sec) || sec < 0) { ctx.console.error('curl: invalid --connect-timeout') ctx.exitCode = 2 return } connectTimeMs = Math.round(sec * 1000) i++ continue } if (a === '-k' || a === '--insecure') { insecureTls = true i++ continue } if (a === '--cacert') { if (i + 1 >= args.length) { ctx.console.error('curl: option requires an argument: --cacert') ctx.exitCode = 2 return } cacertPath = String(args[++i]) i++ continue } if (a === '--cert') { if (i + 1 >= args.length) { ctx.console.error('curl: option requires an argument: --cert') ctx.exitCode = 2 return } certPath = String(args[++i]) i++ continue } if (a === '--key') { if (i + 1 >= args.length) { ctx.console.error('curl: option requires an argument: --key') ctx.exitCode = 2 return } keyPath = String(args[++i]) i++ continue } if (a === '-b' || a === '--cookie') { if (i + 1 >= args.length) { ctx.console.error('curl: option requires an argument: ' + a) ctx.exitCode = 2 return } cookieSpecs.push(String(args[++i])) i++ continue } if (a.startsWith('-b') && a.length > 2) { cookieSpecs.push(a.slice(2)) i++ continue } if (a === '-c' || a === '--cookie-jar') { if (i + 1 >= args.length) { ctx.console.error('curl: option requires an argument: ' + a) ctx.exitCode = 2 return } cookieJarOut = String(args[++i]) i++ continue } if (a === '-I' || a === '--head') { headOnly = true i++ continue } if (a === '-i' || a === '--include') { includeHeaders = true i++ continue } if (a === '-s' || a === '--silent') { silent = true i++ continue } if (a === '-S' || a === '--show-error') { showError = true i++ continue } if (a === '-f' || a === '--fail') { failOnError = true i++ continue } if (a === '-L' || a === '--location') { location = true i++ continue } if (a === '-v' || a === '--verbose') { verbose = true i++ continue } if (a.startsWith('--')) { ctx.console.error('curl: unknown option: ' + a) ctx.exitCode = 2 return } const rest = a.slice(1) for (let j = 0; j < rest.length; j++) { const c = rest[j] switch (c) { case 'I': headOnly = true break case 'i': includeHeaders = true break case 's': silent = true break case 'S': showError = true break case 'f': failOnError = true break case 'L': location = true break case 'v': verbose = true break case 'O': remoteName = true break case 'J': remoteHeaderName = true break case 'k': insecureTls = true break case 'b': ctx.console.error( 'curl: bundled -b requires a separate argument (use -b cookie.txt)' ) ctx.exitCode = 2 return default: ctx.console.error('curl: invalid option -- ' + c) ctx.exitCode = 2 return } } i++ } if (urls.length === 0) { ctx.console.error(usage()) ctx.exitCode = 2 return } for (const u of urls) { if (!isSupportedFetchUrl(u)) { ctx.console.error( 'curl: URL rejected (need http(s)://, data:, or file://): ' + u ) ctx.exitCode = 2 return } try { assertFetchUrlHostAllowedByDnsPolicy(u, ctx.vfs?.env || {}) } catch (e) { ctx.console.error((e && e.message) || String(e)) ctx.exitCode = 2 return } } if (outputPath != null && remoteName) { ctx.console.error( 'curl: --remote-name (-O) and --output (-o) cannot be used together' ) ctx.exitCode = 2 return } if (remoteHeaderName && !remoteName) { ctx.console.error( 'curl: -J / --remote-header-name requires -O / --remote-name' ) ctx.exitCode = 2 return } const deadlineMs = maxTimeMs > 0 && connectTimeMs > 0 ? Math.min(maxTimeMs, connectTimeMs) : maxTimeMs > 0 ? maxTimeMs : connectTimeMs let m = method if (!m) { if (headOnly) m = 'HEAD' else if (uploadPath) m = 'PUT' else if (dataChunks.length > 0 || jsonBody) m = 'POST' else m = 'GET' } await ensureBareFetchGlobals(ctx) const fetchFn = resolveBareOsFetchFn(ctx) if (!fetchFn) { ctx.console.error( 'curl: no HTTP client (need global fetch, ctx.bare.fetch from /lib/bare/bundles, or bare-fetch on Pear/Bare)' ) ctx.exitCode = 1 return } if ((cookieSpecs.length > 0 || cookieJarOut) && !vfs) { ctx.console.error('curl: -b / -c require ctx.vfs') ctx.exitCode = 2 return } const tlsExtras = await buildTlsFetchExtras(ctx, vfs, { insecure: insecureTls, cacertPath, certPath, keyPath }) if (tlsExtras.error) { if (!silent || showError) ctx.console.error(tlsExtras.error) ctx.exitCode = 2 return } /** @type {Record} */ const fetchTlsExtra = {} if (tlsExtras.dispatcher) fetchTlsExtra.dispatcher = tlsExtras.dispatcher if (tlsExtras.delegatedTls) fetchTlsExtra.bareOsCurlTls = tlsExtras.delegatedTls const baseHdr = new Headers() for (const line of headers) { const colon = line.indexOf(':') if (colon < 1) { ctx.console.error('curl: malformed header: ' + line) ctx.exitCode = 2 return } baseHdr.set(line.slice(0, colon).trim(), line.slice(colon + 1).trim()) } if (userColonPass) { const colon = userColonPass.indexOf(':') const user = colon >= 0 ? userColonPass.slice(0, colon) : userColonPass const pass = colon >= 0 ? userColonPass.slice(colon + 1) : '' baseHdr.set('Authorization', basicAuthHeader(user, pass)) } { const ua = userAgentSequence.length ? userAgentSequence[userAgentSequence.length - 1] : DEFAULT_CURL_USER_AGENT baseHdr.set('User-Agent', ua) } /** @type {Record>} */ const sessionJar = {} if (vfs && cookieJarOut) { mergeCookieJars(sessionJar, await loadCookieJarFile(vfs, cookieJarOut)) } if (vfs && cookieSpecs.length) { for (const spec of cookieSpecs) { if (!spec.includes('=')) { mergeCookieJars(sessionJar, await loadCookieJarFile(vfs, spec)) } } } /** @type {string | Uint8Array | undefined} */ let body = undefined if (jsonBody) { if (!baseHdr.has('Content-Type')) baseHdr.set('Content-Type', 'application/json') body = jsonBody } else if (dataChunks.length > 0) { const joined = dataChunks.join('&') if (!baseHdr.has('Content-Type') && !dataBinary) baseHdr.set('Content-Type', 'application/x-www-form-urlencoded') body = joined } else if (uploadPath) { const buf = await vfs.readFile(uploadPath) if (!buf) { if (!silent || showError) ctx.console.error('curl: cannot read upload file: ' + uploadPath) ctx.exitCode = 26 return } body = new Uint8Array(buf) if (!baseHdr.has('Content-Type')) baseHdr.set('Content-Type', 'application/octet-stream') } let lastSize = 0 let lastUrl = '' for (let ui = 0; ui < urls.length; ui++) { const url = applyBareOsCurlResolveMap(urls[ui], vfs?.env) const reqHdr = new Headers(baseHdr) const host = curlHostFromUrl(url) if (vfs && cookieSpecs.length) { for (const spec of cookieSpecs) { if (spec.includes('=')) mergeInlineCookieLine(sessionJar, host, spec) } } const jarCookie = cookieHeaderForHost(sessionJar, host) if (jarCookie) { if (reqHdr.has('Cookie')) { reqHdr.set('Cookie', reqHdr.get('Cookie') + '; ' + jarCookie) } else { reqHdr.set('Cookie', jarCookie) } } /** @type {string | null} */ let outForUrl = null if (outputPath != null) { outForUrl = urls.length > 1 ? `${outputPath}.${ui}` : outputPath } else if (remoteName) { outForUrl = defaultFetchSaveName(url) } const ac = deadlineMs > 0 ? new AbortController() : null const t = deadlineMs > 0 ? setTimeout(() => { try { ac.abort() } catch { /* ignore */ } }, deadlineMs) : null let res /** @type {number} */ let numRedirects = 0 try { if (headOnly && location) { const { response, redirectCount } = await fetchHeadWithLocationFollow( fetchFn, url, reqHdr, ac ? ac.signal : undefined, fetchTlsExtra ) res = response numRedirects = redirectCount } else { res = await fetchFn(url, { method: m, headers: reqHdr, body: m === 'HEAD' || m === 'GET' ? undefined : body, redirect: location ? 'follow' : 'manual', signal: ac ? ac.signal : undefined, ...fetchTlsExtra }) } } catch (e) { if (t) clearTimeout(t) const msg = e && e.message ? e.message : String(e) if (!silent || showError) ctx.console.error('curl: (' + url + ') ' + msg) ctx.exitCode = 7 return } if (t) clearTimeout(t) lastUrl = res.url || url const cookieHost = curlHostFromUrl(lastUrl) || host for (const sc of gatherSetCookieValues(res)) { applySetCookieHeader(sessionJar, cookieHost, sc) } if (cookieJarOut && vfs) { await vfs.mkdir('~/.config/bare-os/curl', { recursive: true }) await vfs.writeFile( cookieJarOut, b4a.from(JSON.stringify(sessionJar), 'utf8') ) } if (remoteName && remoteHeaderName && res.ok && m !== 'HEAD') { const raw = filenameFromContentDisposition( res.headers.get('Content-Disposition') ) const safe = raw ? safeRemoteFilename(raw) : null if (safe) outForUrl = safe } if (!location && res.status >= 300 && res.status < 400) { const loc = res.headers.get('Location') if (loc && (m === 'GET' || m === 'HEAD')) { if (!silent || showError) ctx.console.error( 'curl: redirect not followed (use -L): ' + res.status + ' -> ' + loc ) } } const buf = m === 'HEAD' ? new Uint8Array(0) : new Uint8Array(await res.arrayBuffer()) lastSize = buf.length if (failOnError && !res.ok) { if (!silent || showError) ctx.console.error('curl: HTTP ' + res.status + ' for ' + url) ctx.exitCode = 22 return } if (verbose && !silent) { ctx.console.error('> ' + m + ' ' + url) res.headers.forEach((v, k) => { ctx.console.error('< ' + k + ': ' + v) }) } const chunks = [] if (m === 'HEAD' || (includeHeaders && m !== 'HEAD')) { const statusLine = 'HTTP/1.1 ' + res.status + ' ' + (res.statusText || '') chunks.push(statusLine + '\r\n') res.headers.forEach((v, k) => { chunks.push(k + ': ' + v + '\r\n') }) chunks.push('\r\n') } if (m !== 'HEAD') { chunks.push(buf) } const outBytes = concatParts(chunks) if (outForUrl) { await vfs.writeFile(outForUrl, outBytes) } else { ctx.console.log(ctx.b4a.toString(outBytes)) } if (writeOut) { const parts = curlWriteOutUrlParts(lastUrl) const line = expandWriteOut(writeOut, { http_code: res.status, response_code: res.status, url_effective: lastUrl, size_download: lastSize, num_redirects: numRedirects, content_type: res.headers.get('content-type') || '', method: String(m || ''), scheme: parts.scheme, host: parts.host, port: parts.port, path: parts.path }) ctx.console.log(line) } } ctx.exitCode = 0 }