UNPKG

curlconverter

Version:

convert curl commands to Python, JavaScript, Go, PHP and more

534 lines (485 loc) 14.3 kB
import { warnIfPartsIgnored } from "../Warnings.js"; import { Word, eq, mergeWords } from "../shell/Word.js"; import { parse, COMMON_SUPPORTED_ARGS } from "../parse.js"; import type { Request, Warnings } from "../parse.js"; export const supportedArgs = new Set([ ...COMMON_SUPPORTED_ARGS, "cookie-jar", "ipv4", "no-ipv4", "ipv6", "no-ipv6", "form", // not supported, more specific warning "form-string", "ciphers", "insecure", "cert", "cert-type", "key", "key-type", "cacert", "capath", "crlfile", "pinnedpubkey", "random-file", "egd-file", "hsts", "noproxy", // not fully supported "proxy", // not supported "proxy-user", // TODO: supported by all? "globoff", "no-globoff", "netrc", "netrc-optional", "timeout", "connect-timeout", "limit-rate", "compressed", "no-compressed", // Wget picks the auth and some it doesn't support but there's a more // specific error message for those. "anyauth", "no-anyauth", "digest", "no-digest", "aws-sigv4", "negotiate", "no-negotiate", "delegation", // GSS/kerberos // "service-name", // GSS/kerberos, not supported "ntlm", "no-ntlm", "ntlm-wb", "no-ntlm-wb", "no-digest", "output", "upload-file", "continue-at", "clobber", "remote-time", "keep-alive", "silent", "verbose", "next", ]); // TODO: check this // https://www.gnu.org/software/bash/manual/html_node/Quoting.html const regexDoubleEscape = /\$|`|"|\\|!/gu; const unprintableChars = /\p{C}|[^ \P{Z}]/gu; // TODO: there's probably more const regexAnsiCEscape = /\p{C}|[^ \P{Z}]|\\|'/gu; // https://unix.stackexchange.com/questions/270977/ const shellChars = /[\x02-\x09\x0b-\x1a\\#?`(){}[\]^*<=>~|; "!$&'\x82-\xff]/; export function reprStr(s: string, mustQuote = false): string { const containsUnprintableChars = unprintableChars.test(s); if (containsUnprintableChars) { return ( "$'" + s.replace(regexAnsiCEscape, (c: string) => { switch (c) { case "\x07": return "\\a"; case "\b": return "\\b"; case "\x1B": return "\\e"; case "\f": return "\\f"; case "\n": return "\\n"; case "\r": return "\\r"; case "\t": return "\\t"; case "\v": return "\\v"; case "\\": return "\\\\"; case "'": return "\\'"; // TODO: unnecessary? // case '"': // return '\\"'; // case "?": // return "\\?"; } const hex = (c.codePointAt(0) as number).toString(16); if (hex.length <= 2) { return "\\x" + hex.padStart(2, "0"); } if (hex.length <= 4) { return "\\u" + hex.padStart(4, "0"); } return "\\U" + hex.padStart(8, "0"); }) + "'" ); } if ( (s.includes('"') || s.includes("$") || s.includes("`") || s.includes("!") || mustQuote) && !s.includes("'") ) { return "'" + s + "'"; } if (shellChars.test(s) || mustQuote) { return ( '"' + s.replace(regexDoubleEscape, (c: string) => { switch (c[0]) { case "$": return "\\$"; case "`": return "\\`"; case '"': return '\\"'; case "\\": return "\\\\"; case "!": // ! might have an effect or it might not, depending on if // the command is in a file or pasted into the shell and other things. // If it's backslash escaped, the backslash is still not removed, a // safe way to escape it is to close the double quote, put the ! in // single quotes and then open the double quote again. return "\"'!'\""; } // Should never happen return c; }) + '"' ); } return s; } export function repr(w: Word): string { // TODO: put variables in the quotes const args: string[] = []; for (const [i, t] of w.tokens.entries()) { if (typeof t === "string") { // A string after a variable cannot be un-quoted // After a subcommand it's usually okay but that's more complex. args.push(reprStr(t, !!i)); } else if (t.type === "variable") { args.push(t.text); } else { // TODO: this will probably lead to syntax errors args.push(t.text); } } return args.join(""); } function requestToWget(request: Request, warnings: Warnings): string { warnIfPartsIgnored(request, warnings, { dataReadsFile: true, cookieFiles: true, multipleUrls: true, }); if ( request.dataReadsFile && request.dataArray && request.dataArray.length && (request.dataArray.length > 1 || (!(request.dataArray[0] instanceof Word) && request.dataArray[0].name)) ) { warnings.push([ "unsafe-data", "the generated data content is wrong, " + // TODO: might not come from "@" JSON.stringify("@" + request.dataReadsFile) + " means read the file " + JSON.stringify(request.dataReadsFile), ]); } const args: string[] = []; if ( request.urls[0].uploadFile || (request.dataArray && request.dataArray.length === 1 && !(request.dataArray[0] instanceof Word) && !request.dataArray[0].name) || request.data ) { if (!eq(request.urls[0].method, "POST")) { args.push("--method=" + repr(request.urls[0].method)); } } else if (!eq(request.urls[0].method, "GET")) { args.push("--method=" + repr(request.urls[0].method)); } if (request.urls.length > 1) { const uniqueMethods = new Set<string>( request.urls.map((u) => u.method.toString()), ); // TODO: add tons of checks/warnings that wget doesn't let you set things per-URL if (uniqueMethods.size > 1) { warnings.push([ "mixed-methods", "the input curl command uses multiple HTTP methods, which Wget doesn't support: " + request.urls .map((u) => JSON.stringify(u.method.toString())) .join(", "), ]); } } if (request.cookieFiles && request.cookieFiles.length) { for (const cookieFile of request.cookieFiles) { args.push("--load-cookies=" + repr(cookieFile)); } if (request.cookieFiles.length > 1) { const lastCookieFile = request.cookieFiles[request.cookieFiles.length - 1]; warnings.push([ "multiple-cookie-files", "Wget only supports reading cookies from one file, only the last one will be sent: " + lastCookieFile.toString(), ]); } } if (request.cookieJar) { args.push("--save-cookies=" + repr(request.cookieJar)); // TODO: --keep-session-cookies } if (request.headers.length) { for (const [headerName, headerValue] of request.headers) { args.push( "--header=" + repr(mergeWords(headerName, ": ", headerValue ?? "")), ); // TODO: there's also --referer, --user-agent and --content-disposition } } if (request.compressed !== undefined) { if (request.compressed) { args.push("--compression=auto"); } else { args.push("--compression=none"); } } if (request.maxRedirects && request.maxRedirects.toString() !== "20") { // TODO: escape/parse? args.push("--max-redirect=" + repr(request.maxRedirects)); } if (request.ipv4) { args.push("--inet4-only"); } if (request.ipv6) { args.push("--inet6-only"); } if (request.urls[0].auth) { const [user, password] = request.urls[0].auth; args.push("--user=" + repr(user)); if (password.toBool()) { args.push("--password=" + repr(password)); } if (request.authType === "basic") { args.push("--auth-no-challenge"); } if ( !["none", "basic", "digest", "ntlm", "ntlm-wb", "negotiate"].includes( request.authType, ) ) { warnings.push([ "wget-unsupported-auth", "Wget doesn't support " + request.authType + " authentication", ]); } } if (request.urls[0].uploadFile) { if ( eq(request.urls[0].uploadFile, "-") || eq(request.urls[0].uploadFile, ".") ) { warnings.push([ "wget-stdin", "Wget does not support uploading from stdin", ]); } if (eq(request.urls[0].method, "POST")) { args.push("--post-file=" + repr(request.urls[0].uploadFile)); } else { args.push("--body-file=" + repr(request.urls[0].uploadFile)); } } else if (request.multipartUploads) { warnings.push([ "multipart", "Wget does not support sending multipart/form-data", ]); } else if ( request.dataArray && request.dataArray.length === 1 && !(request.dataArray[0] instanceof Word) && !request.dataArray[0].name ) { if (eq(request.urls[0].method, "POST")) { args.push("--post-file=" + repr(request.dataArray[0].filename)); } else { args.push("--body-file=" + repr(request.dataArray[0].filename)); } } else if (request.data) { if (eq(request.urls[0].method, "POST")) { args.push("--post-data=" + repr(request.data)); } else { args.push("--body-data=" + repr(request.data)); } } // https://www.gnu.org/software/wget/manual/html_node/HTTPS-_0028SSL_002fTLS_0029-Options.html if (request.ciphers) { args.push("--ciphers=" + repr(request.ciphers)); } if (request.insecure) { args.push("--no-check-certificate"); } if (request.cert) { const [cert, password] = request.cert; args.push("--certificate=" + repr(cert)); if (password) { warnings.push([ "wget-cert-password", // TODO: is this true? "Wget does not support certificate passwords", ]); } } if (request.certType) { args.push("--certificate-type=" + repr(request.certType)); } if (request.key) { args.push("--private-key=" + repr(request.key)); } if (request.keyType) { args.push("--private-key-type=" + repr(request.keyType)); } if (request.cacert) { args.push("--ca-certificate=" + repr(request.cacert)); } if (request.capath) { args.push("--ca-directory=" + repr(request.capath)); } if (request.crlfile) { args.push("--crl-file=" + repr(request.crlfile)); } if (request.pinnedpubkey) { args.push("--pinnedpubkey=" + repr(request.pinnedpubkey)); } if (request.randomFile) { args.push("--random-file=" + repr(request.randomFile)); } if (request.egdFile) { args.push("--egd-file=" + repr(request.egdFile)); } if (request.hsts) { for (const hsts of request.hsts) { // TODO: warn multiple won't work args.push("--hsts-file=" + repr(hsts)); } } // TODO: --secure-protocol if (request.netrc === "ignored") { args.push("--no-netrc"); } if (request.noproxy) { if (eq(request.noproxy, "*")) { args.push("--no-proxy"); } else { warnings.push([ "wget-noproxy", "Wget does not support noproxy for specific hosts", ]); } } if (request.proxy) { warnings.push([ "wget-proxy", "Wget requires specifying proxies in environment variables: " + JSON.stringify(request.proxy.toString()), ]); } if (request.proxyAuth) { const [proxyUser, proxyPassword] = request.proxyAuth.split(":", 2); args.push("--proxy-user=" + repr(proxyUser)); if (proxyPassword && proxyPassword.toBool()) { args.push("--proxy-password=" + repr(proxyPassword)); } } if (request.timeout) { // TODO: warn that this is not for the whole request args.push("--timeout=" + repr(request.timeout)); } if (request.connectTimeout) { args.push("--connect-timeout=" + repr(request.connectTimeout)); } // TODO: curl's --speed-limit if (request.limitRate) { // TODO: they probably have different syntaxes args.push("--limit-rate=" + repr(request.limitRate)); } // TODO: this doesn't match up. Warn if multiple, etc. if (request.urls[0].output) { args.push("--output-document=" + repr(request.urls[0].output)); } else { args.push("--output-document -"); } if (request.clobber === false) { args.push("--no-clobber"); } // TODO: --create-dirs if (request.remoteTime === false) { args.push("--no-use-server-timestamps"); } if (request.continueAt) { if (eq(request.continueAt, "-")) { args.push("--continue"); } else { args.push("--start-pos=" + repr(request.continueAt)); } } if (request.keepAlive === false) { args.push("--no-http-keep-alive"); } // This is used only for FTP by Wget, we might as well add it even // though we don't support FTP. if (request.globoff) { // TODO: this isn't great, it has different meanings from curl to Wget // TODO: don't remove \[ and \] from original URL? args.push("--no-glob"); } // TODO: // curl: // --silent, --verbose, --no-verbose (the default) // --progress-meter (the default), --no-progress-meter, --progress-bar // --trace-ascii, --trace // wget: // --quiet, --verbose (the default), --no-verbose // --progress=bar (the default), --progress=dot // --debug if (request.silent) { args.push("--quiet"); } else if (request.verbose === false) { args.push("--no-verbose"); } for (const url of request.urls) { // TODO: wget probably has a different syntax args.push(repr(url.url)); } const multiline = args.length > 3 || args.reduce((a, b) => a + b.length, 0) > 80 - 5; const joiner = multiline ? " \\\n " : " "; return "wget " + args.join(joiner) + "\n"; } // TODO: --verbose ? export function _toWget(requests: Request[], warnings: Warnings = []): string { return requests.map((r) => requestToWget(r, warnings)).join("\n\n"); } export function toWgetWarn( curlCommand: string | string[], warnings: Warnings = [], ): [string, Warnings] { const requests = parse(curlCommand, supportedArgs, warnings); const wget = _toWget(requests, warnings); return [wget, warnings]; } export function toWget(curlCommand: string | string[]): string { return toWgetWarn(curlCommand)[0]; }