mirror of
https://github.com/curlconverter/curlconverter.git
synced 2022-05-22 02:35:29 +03:00
1253 lines
45 KiB
JavaScript
1253 lines
45 KiB
JavaScript
import URL from 'url'
|
|
|
|
import nunjucks from 'nunjucks'
|
|
|
|
import parser from './parser.js'
|
|
|
|
const env = nunjucks.configure(['templates/'], { // set folders with templates
|
|
autoescape: false
|
|
})
|
|
env.addFilter('isArr', something => Array.isArray(something))
|
|
env.addFilter('isString', something => typeof something === 'string')
|
|
|
|
const has = (obj, prop) => {
|
|
return Object.prototype.hasOwnProperty.call(obj, prop)
|
|
}
|
|
|
|
export class CCError extends Error {}
|
|
|
|
const pushProp = (obj, prop, value) => {
|
|
if (!has(obj, prop)) {
|
|
obj[prop] = []
|
|
}
|
|
obj[prop].push(value)
|
|
return obj
|
|
}
|
|
|
|
// BEGIN GENERATED CURL OPTIONS
|
|
const curlLongOpts = {
|
|
url: { type: 'string' },
|
|
'dns-ipv4-addr': { type: 'string' },
|
|
'dns-ipv6-addr': { type: 'string' },
|
|
'random-file': { type: 'string' },
|
|
'egd-file': { type: 'string' },
|
|
'oauth2-bearer': { type: 'string' },
|
|
'connect-timeout': { type: 'string' },
|
|
'doh-url': { type: 'string' },
|
|
ciphers: { type: 'string' },
|
|
'dns-interface': { type: 'string' },
|
|
'disable-epsv': { type: 'bool', name: 'epsv' },
|
|
'no-disable-epsv': { type: 'bool', name: 'epsv', expand: false },
|
|
'disallow-username-in-url': { type: 'bool' },
|
|
'no-disallow-username-in-url': { type: 'bool', name: 'disallow-username-in-url', expand: false },
|
|
epsv: { type: 'bool' },
|
|
'no-epsv': { type: 'bool', name: 'epsv', expand: false },
|
|
'dns-servers': { type: 'string' },
|
|
trace: { type: 'string' },
|
|
npn: { type: 'bool' },
|
|
'no-npn': { type: 'bool', name: 'npn', expand: false },
|
|
'trace-ascii': { type: 'string' },
|
|
alpn: { type: 'bool' },
|
|
'no-alpn': { type: 'bool', name: 'alpn', expand: false },
|
|
'limit-rate': { type: 'string' },
|
|
compressed: { type: 'bool' },
|
|
'no-compressed': { type: 'bool', name: 'compressed', expand: false },
|
|
'tr-encoding': { type: 'bool' },
|
|
'no-tr-encoding': { type: 'bool', name: 'tr-encoding', expand: false },
|
|
digest: { type: 'bool' },
|
|
'no-digest': { type: 'bool', name: 'digest', expand: false },
|
|
negotiate: { type: 'bool' },
|
|
'no-negotiate': { type: 'bool', name: 'negotiate', expand: false },
|
|
ntlm: { type: 'bool' },
|
|
'no-ntlm': { type: 'bool', name: 'ntlm', expand: false },
|
|
'ntlm-wb': { type: 'bool' },
|
|
'no-ntlm-wb': { type: 'bool', name: 'ntlm-wb', expand: false },
|
|
basic: { type: 'bool' },
|
|
'no-basic': { type: 'bool', name: 'basic', expand: false },
|
|
anyauth: { type: 'bool' },
|
|
'no-anyauth': { type: 'bool', name: 'anyauth', expand: false },
|
|
wdebug: { type: 'bool' },
|
|
'no-wdebug': { type: 'bool', name: 'wdebug', expand: false },
|
|
'ftp-create-dirs': { type: 'bool' },
|
|
'no-ftp-create-dirs': { type: 'bool', name: 'ftp-create-dirs', expand: false },
|
|
'create-dirs': { type: 'bool' },
|
|
'no-create-dirs': { type: 'bool', name: 'create-dirs', expand: false },
|
|
'create-file-mode': { type: 'string' },
|
|
'max-redirs': { type: 'string' },
|
|
'proxy-ntlm': { type: 'bool' },
|
|
'no-proxy-ntlm': { type: 'bool', name: 'proxy-ntlm', expand: false },
|
|
crlf: { type: 'bool' },
|
|
'no-crlf': { type: 'bool', name: 'crlf', expand: false },
|
|
stderr: { type: 'string' },
|
|
'aws-sigv4': { type: 'string' },
|
|
interface: { type: 'string' },
|
|
krb: { type: 'string' },
|
|
krb4: { type: 'string', name: 'krb' },
|
|
'haproxy-protocol': { type: 'bool' },
|
|
'no-haproxy-protocol': { type: 'bool', name: 'haproxy-protocol', expand: false },
|
|
'max-filesize': { type: 'string' },
|
|
'disable-eprt': { type: 'bool', name: 'eprt' },
|
|
'no-disable-eprt': { type: 'bool', name: 'eprt', expand: false },
|
|
eprt: { type: 'bool' },
|
|
'no-eprt': { type: 'bool', name: 'eprt', expand: false },
|
|
xattr: { type: 'bool' },
|
|
'no-xattr': { type: 'bool', name: 'xattr', expand: false },
|
|
'ftp-ssl': { type: 'bool', name: 'ssl' },
|
|
'no-ftp-ssl': { type: 'bool', name: 'ssl', expand: false },
|
|
ssl: { type: 'bool' },
|
|
'no-ssl': { type: 'bool', name: 'ssl', expand: false },
|
|
'ftp-pasv': { type: 'bool' },
|
|
'no-ftp-pasv': { type: 'bool', name: 'ftp-pasv', expand: false },
|
|
socks5: { type: 'string' },
|
|
'tcp-nodelay': { type: 'bool' },
|
|
'no-tcp-nodelay': { type: 'bool', name: 'tcp-nodelay', expand: false },
|
|
'proxy-digest': { type: 'bool' },
|
|
'no-proxy-digest': { type: 'bool', name: 'proxy-digest', expand: false },
|
|
'proxy-basic': { type: 'bool' },
|
|
'no-proxy-basic': { type: 'bool', name: 'proxy-basic', expand: false },
|
|
retry: { type: 'string' },
|
|
'retry-connrefused': { type: 'bool' },
|
|
'no-retry-connrefused': { type: 'bool', name: 'retry-connrefused', expand: false },
|
|
'retry-delay': { type: 'string' },
|
|
'retry-max-time': { type: 'string' },
|
|
'proxy-negotiate': { type: 'bool' },
|
|
'no-proxy-negotiate': { type: 'bool', name: 'proxy-negotiate', expand: false },
|
|
'ftp-account': { type: 'string' },
|
|
'proxy-anyauth': { type: 'bool' },
|
|
'no-proxy-anyauth': { type: 'bool', name: 'proxy-anyauth', expand: false },
|
|
'trace-time': { type: 'bool' },
|
|
'no-trace-time': { type: 'bool', name: 'trace-time', expand: false },
|
|
'ignore-content-length': { type: 'bool' },
|
|
'no-ignore-content-length': { type: 'bool', name: 'ignore-content-length', expand: false },
|
|
'ftp-skip-pasv-ip': { type: 'bool' },
|
|
'no-ftp-skip-pasv-ip': { type: 'bool', name: 'ftp-skip-pasv-ip', expand: false },
|
|
'ftp-method': { type: 'string' },
|
|
'local-port': { type: 'string' },
|
|
socks4: { type: 'string' },
|
|
socks4a: { type: 'string' },
|
|
'ftp-alternative-to-user': { type: 'string' },
|
|
'ftp-ssl-reqd': { type: 'bool', name: 'ssl-reqd' },
|
|
'no-ftp-ssl-reqd': { type: 'bool', name: 'ssl-reqd', expand: false },
|
|
'ssl-reqd': { type: 'bool' },
|
|
'no-ssl-reqd': { type: 'bool', name: 'ssl-reqd', expand: false },
|
|
sessionid: { type: 'bool' },
|
|
'no-sessionid': { type: 'bool', name: 'sessionid', expand: false },
|
|
'ftp-ssl-control': { type: 'bool' },
|
|
'no-ftp-ssl-control': { type: 'bool', name: 'ftp-ssl-control', expand: false },
|
|
'ftp-ssl-ccc': { type: 'bool' },
|
|
'no-ftp-ssl-ccc': { type: 'bool', name: 'ftp-ssl-ccc', expand: false },
|
|
'ftp-ssl-ccc-mode': { type: 'string' },
|
|
libcurl: { type: 'string' },
|
|
raw: { type: 'bool' },
|
|
'no-raw': { type: 'bool', name: 'raw', expand: false },
|
|
post301: { type: 'bool' },
|
|
'no-post301': { type: 'bool', name: 'post301', expand: false },
|
|
keepalive: { type: 'bool' },
|
|
'no-keepalive': { type: 'bool', name: 'keepalive', expand: false },
|
|
'socks5-hostname': { type: 'string' },
|
|
'keepalive-time': { type: 'string' },
|
|
post302: { type: 'bool' },
|
|
'no-post302': { type: 'bool', name: 'post302', expand: false },
|
|
noproxy: { type: 'string' },
|
|
'socks5-gssapi-nec': { type: 'bool' },
|
|
'no-socks5-gssapi-nec': { type: 'bool', name: 'socks5-gssapi-nec', expand: false },
|
|
'proxy1.0': { type: 'string' },
|
|
'tftp-blksize': { type: 'string' },
|
|
'mail-from': { type: 'string' },
|
|
'mail-rcpt': { type: 'string' },
|
|
'ftp-pret': { type: 'bool' },
|
|
'no-ftp-pret': { type: 'bool', name: 'ftp-pret', expand: false },
|
|
proto: { type: 'string' },
|
|
'proto-redir': { type: 'string' },
|
|
resolve: { type: 'string' },
|
|
delegation: { type: 'string' },
|
|
'mail-auth': { type: 'string' },
|
|
post303: { type: 'bool' },
|
|
'no-post303': { type: 'bool', name: 'post303', expand: false },
|
|
metalink: { type: 'bool' },
|
|
'no-metalink': { type: 'bool', name: 'metalink', expand: false },
|
|
'sasl-authzid': { type: 'string' },
|
|
'sasl-ir': { type: 'bool' },
|
|
'no-sasl-ir': { type: 'bool', name: 'sasl-ir', expand: false },
|
|
'test-event': { type: 'bool' },
|
|
'no-test-event': { type: 'bool', name: 'test-event', expand: false },
|
|
'unix-socket': { type: 'string' },
|
|
'path-as-is': { type: 'bool' },
|
|
'no-path-as-is': { type: 'bool', name: 'path-as-is', expand: false },
|
|
'socks5-gssapi-service': { type: 'string', name: 'proxy-service-name' },
|
|
'proxy-service-name': { type: 'string' },
|
|
'service-name': { type: 'string' },
|
|
'proto-default': { type: 'string' },
|
|
'expect100-timeout': { type: 'string' },
|
|
'tftp-no-options': { type: 'bool' },
|
|
'no-tftp-no-options': { type: 'bool', name: 'tftp-no-options', expand: false },
|
|
'connect-to': { type: 'string' },
|
|
'abstract-unix-socket': { type: 'string' },
|
|
'tls-max': { type: 'string' },
|
|
'suppress-connect-headers': { type: 'bool' },
|
|
'no-suppress-connect-headers': { type: 'bool', name: 'suppress-connect-headers', expand: false },
|
|
'compressed-ssh': { type: 'bool' },
|
|
'no-compressed-ssh': { type: 'bool', name: 'compressed-ssh', expand: false },
|
|
'happy-eyeballs-timeout-ms': { type: 'string' },
|
|
'retry-all-errors': { type: 'bool' },
|
|
'no-retry-all-errors': { type: 'bool', name: 'retry-all-errors', expand: false },
|
|
'http1.0': { type: 'bool' },
|
|
'http1.1': { type: 'bool' },
|
|
http2: { type: 'bool' },
|
|
'http2-prior-knowledge': { type: 'bool' },
|
|
http3: { type: 'bool' },
|
|
'http0.9': { type: 'bool' },
|
|
'no-http0.9': { type: 'bool', name: 'http0.9', expand: false },
|
|
tlsv1: { type: 'bool' },
|
|
'tlsv1.0': { type: 'bool' },
|
|
'tlsv1.1': { type: 'bool' },
|
|
'tlsv1.2': { type: 'bool' },
|
|
'tlsv1.3': { type: 'bool' },
|
|
'tls13-ciphers': { type: 'string' },
|
|
'proxy-tls13-ciphers': { type: 'string' },
|
|
sslv2: { type: 'bool' },
|
|
sslv3: { type: 'bool' },
|
|
ipv4: { type: 'bool' },
|
|
ipv6: { type: 'bool' },
|
|
append: { type: 'bool' },
|
|
'no-append': { type: 'bool', name: 'append', expand: false },
|
|
'user-agent': { type: 'string' },
|
|
cookie: { type: 'string' },
|
|
'alt-svc': { type: 'string' },
|
|
hsts: { type: 'string' },
|
|
'use-ascii': { type: 'bool' },
|
|
'no-use-ascii': { type: 'bool', name: 'use-ascii', expand: false },
|
|
'cookie-jar': { type: 'string' },
|
|
'continue-at': { type: 'string' },
|
|
data: { type: 'string' },
|
|
'data-raw': { type: 'string' },
|
|
'data-ascii': { type: 'string' },
|
|
'data-binary': { type: 'string' },
|
|
'data-urlencode': { type: 'string' },
|
|
'dump-header': { type: 'string' },
|
|
referer: { type: 'string' },
|
|
cert: { type: 'string' },
|
|
cacert: { type: 'string' },
|
|
'cert-type': { type: 'string' },
|
|
key: { type: 'string' },
|
|
'key-type': { type: 'string' },
|
|
pass: { type: 'string' },
|
|
engine: { type: 'string' },
|
|
capath: { type: 'string' },
|
|
pubkey: { type: 'string' },
|
|
hostpubmd5: { type: 'string' },
|
|
crlfile: { type: 'string' },
|
|
tlsuser: { type: 'string' },
|
|
tlspassword: { type: 'string' },
|
|
tlsauthtype: { type: 'string' },
|
|
'ssl-allow-beast': { type: 'bool' },
|
|
'no-ssl-allow-beast': { type: 'bool', name: 'ssl-allow-beast', expand: false },
|
|
'ssl-auto-client-cert': { type: 'bool' },
|
|
'no-ssl-auto-client-cert': { type: 'bool', name: 'ssl-auto-client-cert', expand: false },
|
|
'proxy-ssl-auto-client-cert': { type: 'bool' },
|
|
'no-proxy-ssl-auto-client-cert': { type: 'bool', name: 'proxy-ssl-auto-client-cert', expand: false },
|
|
pinnedpubkey: { type: 'string' },
|
|
'proxy-pinnedpubkey': { type: 'string' },
|
|
'cert-status': { type: 'bool' },
|
|
'no-cert-status': { type: 'bool', name: 'cert-status', expand: false },
|
|
'doh-cert-status': { type: 'bool' },
|
|
'no-doh-cert-status': { type: 'bool', name: 'doh-cert-status', expand: false },
|
|
'false-start': { type: 'bool' },
|
|
'no-false-start': { type: 'bool', name: 'false-start', expand: false },
|
|
'ssl-no-revoke': { type: 'bool' },
|
|
'no-ssl-no-revoke': { type: 'bool', name: 'ssl-no-revoke', expand: false },
|
|
'ssl-revoke-best-effort': { type: 'bool' },
|
|
'no-ssl-revoke-best-effort': { type: 'bool', name: 'ssl-revoke-best-effort', expand: false },
|
|
'tcp-fastopen': { type: 'bool' },
|
|
'no-tcp-fastopen': { type: 'bool', name: 'tcp-fastopen', expand: false },
|
|
'proxy-tlsuser': { type: 'string' },
|
|
'proxy-tlspassword': { type: 'string' },
|
|
'proxy-tlsauthtype': { type: 'string' },
|
|
'proxy-cert': { type: 'string' },
|
|
'proxy-cert-type': { type: 'string' },
|
|
'proxy-key': { type: 'string' },
|
|
'proxy-key-type': { type: 'string' },
|
|
'proxy-pass': { type: 'string' },
|
|
'proxy-ciphers': { type: 'string' },
|
|
'proxy-crlfile': { type: 'string' },
|
|
'proxy-ssl-allow-beast': { type: 'bool' },
|
|
'no-proxy-ssl-allow-beast': { type: 'bool', name: 'proxy-ssl-allow-beast', expand: false },
|
|
'login-options': { type: 'string' },
|
|
'proxy-cacert': { type: 'string' },
|
|
'proxy-capath': { type: 'string' },
|
|
'proxy-insecure': { type: 'bool' },
|
|
'no-proxy-insecure': { type: 'bool', name: 'proxy-insecure', expand: false },
|
|
'proxy-tlsv1': { type: 'bool' },
|
|
'socks5-basic': { type: 'bool' },
|
|
'no-socks5-basic': { type: 'bool', name: 'socks5-basic', expand: false },
|
|
'socks5-gssapi': { type: 'bool' },
|
|
'no-socks5-gssapi': { type: 'bool', name: 'socks5-gssapi', expand: false },
|
|
'etag-save': { type: 'string' },
|
|
'etag-compare': { type: 'string' },
|
|
curves: { type: 'string' },
|
|
fail: { type: 'bool' },
|
|
'no-fail': { type: 'bool', name: 'fail', expand: false },
|
|
'fail-early': { type: 'bool' },
|
|
'no-fail-early': { type: 'bool', name: 'fail-early', expand: false },
|
|
'styled-output': { type: 'bool' },
|
|
'no-styled-output': { type: 'bool', name: 'styled-output', expand: false },
|
|
'mail-rcpt-allowfails': { type: 'bool' },
|
|
'no-mail-rcpt-allowfails': { type: 'bool', name: 'mail-rcpt-allowfails', expand: false },
|
|
'fail-with-body': { type: 'bool' },
|
|
'no-fail-with-body': { type: 'bool', name: 'fail-with-body', expand: false },
|
|
form: { type: 'string' },
|
|
'form-string': { type: 'string' },
|
|
globoff: { type: 'bool' },
|
|
'no-globoff': { type: 'bool', name: 'globoff', expand: false },
|
|
get: { type: 'bool' },
|
|
'request-target': { type: 'string' },
|
|
help: { type: 'bool' },
|
|
'no-help': { type: 'bool', name: 'help', expand: false },
|
|
header: { type: 'string' },
|
|
'proxy-header': { type: 'string' },
|
|
include: { type: 'bool' },
|
|
'no-include': { type: 'bool', name: 'include', expand: false },
|
|
head: { type: 'bool' },
|
|
'no-head': { type: 'bool', name: 'head', expand: false },
|
|
'junk-session-cookies': { type: 'bool' },
|
|
'no-junk-session-cookies': { type: 'bool', name: 'junk-session-cookies', expand: false },
|
|
'remote-header-name': { type: 'bool' },
|
|
'no-remote-header-name': { type: 'bool', name: 'remote-header-name', expand: false },
|
|
insecure: { type: 'bool' },
|
|
'no-insecure': { type: 'bool', name: 'insecure', expand: false },
|
|
'doh-insecure': { type: 'bool' },
|
|
'no-doh-insecure': { type: 'bool', name: 'doh-insecure', expand: false },
|
|
config: { type: 'string' },
|
|
'list-only': { type: 'bool' },
|
|
'no-list-only': { type: 'bool', name: 'list-only', expand: false },
|
|
location: { type: 'bool' },
|
|
'no-location': { type: 'bool', name: 'location', expand: false },
|
|
'location-trusted': { type: 'bool' },
|
|
'no-location-trusted': { type: 'bool', name: 'location-trusted', expand: false },
|
|
'max-time': { type: 'string' },
|
|
manual: { type: 'bool' },
|
|
'no-manual': { type: 'bool', name: 'manual', expand: false },
|
|
netrc: { type: 'bool' },
|
|
'no-netrc': { type: 'bool', name: 'netrc', expand: false },
|
|
'netrc-optional': { type: 'bool' },
|
|
'no-netrc-optional': { type: 'bool', name: 'netrc-optional', expand: false },
|
|
'netrc-file': { type: 'string' },
|
|
buffer: { type: 'bool' },
|
|
'no-buffer': { type: 'bool', name: 'buffer', expand: false },
|
|
output: { type: 'string' },
|
|
'remote-name': { type: 'bool' },
|
|
'remote-name-all': { type: 'bool' },
|
|
'no-remote-name-all': { type: 'bool', name: 'remote-name-all', expand: false },
|
|
'output-dir': { type: 'string' },
|
|
proxytunnel: { type: 'bool' },
|
|
'no-proxytunnel': { type: 'bool', name: 'proxytunnel', expand: false },
|
|
'ftp-port': { type: 'string' },
|
|
disable: { type: 'bool' },
|
|
'no-disable': { type: 'bool', name: 'disable', expand: false },
|
|
quote: { type: 'string' },
|
|
range: { type: 'string' },
|
|
'remote-time': { type: 'bool' },
|
|
'no-remote-time': { type: 'bool', name: 'remote-time', expand: false },
|
|
silent: { type: 'bool' },
|
|
'no-silent': { type: 'bool', name: 'silent', expand: false },
|
|
'show-error': { type: 'bool' },
|
|
'no-show-error': { type: 'bool', name: 'show-error', expand: false },
|
|
'telnet-option': { type: 'string' },
|
|
'upload-file': { type: 'string' },
|
|
user: { type: 'string' },
|
|
'proxy-user': { type: 'string' },
|
|
verbose: { type: 'bool' },
|
|
'no-verbose': { type: 'bool', name: 'verbose', expand: false },
|
|
version: { type: 'bool' },
|
|
'no-version': { type: 'bool', name: 'version', expand: false },
|
|
'write-out': { type: 'string' },
|
|
proxy: { type: 'string' },
|
|
preproxy: { type: 'string' },
|
|
request: { type: 'string' },
|
|
'speed-limit': { type: 'string' },
|
|
'speed-time': { type: 'string' },
|
|
'time-cond': { type: 'string' },
|
|
parallel: { type: 'bool' },
|
|
'no-parallel': { type: 'bool', name: 'parallel', expand: false },
|
|
'parallel-max': { type: 'string' },
|
|
'parallel-immediate': { type: 'bool' },
|
|
'no-parallel-immediate': { type: 'bool', name: 'parallel-immediate', expand: false },
|
|
'progress-bar': { type: 'bool' },
|
|
'no-progress-bar': { type: 'bool', name: 'progress-bar', expand: false },
|
|
'progress-meter': { type: 'bool' },
|
|
'no-progress-meter': { type: 'bool', name: 'progress-meter', expand: false },
|
|
next: { type: 'bool' }
|
|
}
|
|
|
|
const curlShortOpts = {
|
|
0: 'http1.0',
|
|
1: 'tlsv1',
|
|
2: 'sslv2',
|
|
3: 'sslv3',
|
|
4: 'ipv4',
|
|
6: 'ipv6',
|
|
a: 'append',
|
|
A: 'user-agent',
|
|
b: 'cookie',
|
|
B: 'use-ascii',
|
|
c: 'cookie-jar',
|
|
C: 'continue-at',
|
|
d: 'data',
|
|
D: 'dump-header',
|
|
e: 'referer',
|
|
E: 'cert',
|
|
f: 'fail',
|
|
F: 'form',
|
|
g: 'globoff',
|
|
G: 'get',
|
|
h: 'help',
|
|
H: 'header',
|
|
i: 'include',
|
|
I: 'head',
|
|
j: 'junk-session-cookies',
|
|
J: 'remote-header-name',
|
|
k: 'insecure',
|
|
K: 'config',
|
|
l: 'list-only',
|
|
L: 'location',
|
|
m: 'max-time',
|
|
M: 'manual',
|
|
n: 'netrc',
|
|
N: 'no-buffer',
|
|
o: 'output',
|
|
O: 'remote-name',
|
|
p: 'proxytunnel',
|
|
P: 'ftp-port',
|
|
q: 'disable',
|
|
Q: 'quote',
|
|
r: 'range',
|
|
R: 'remote-time',
|
|
s: 'silent',
|
|
S: 'show-error',
|
|
t: 'telnet-option',
|
|
T: 'upload-file',
|
|
u: 'user',
|
|
U: 'proxy-user',
|
|
v: 'verbose',
|
|
V: 'version',
|
|
w: 'write-out',
|
|
x: 'proxy',
|
|
X: 'request',
|
|
Y: 'speed-limit',
|
|
y: 'speed-time',
|
|
z: 'time-cond',
|
|
Z: 'parallel',
|
|
'#': 'progress-bar',
|
|
':': 'next'
|
|
}
|
|
// END GENERATED CURL OPTIONS
|
|
|
|
// These are options that curl used to have.
|
|
// Those that don't conflict with the current options are supported by curlconverter.
|
|
// TODO: curl's --long-options can be shortened.
|
|
// For example if curl used to only have a single option, "--blah" then
|
|
// "--bla" "--bl" and "--b" all used to be valid options as well. If later
|
|
// "--blaz" was added, suddenly those 3 shortened options are removed (because
|
|
// they are now ambiguous).
|
|
// https://github.com/curlconverter/curlconverter/pull/280#issuecomment-931241328
|
|
const removedLongOpts = {
|
|
'ftp-ascii': { type: 'bool', name: 'use-ascii', removed: '7.10.7' },
|
|
port: { type: 'string', removed: '7.3' },
|
|
upload: { type: 'bool', removed: '7.7' },
|
|
continue: { type: 'bool', removed: '7.9' },
|
|
'3p-url': { type: 'string', removed: '7.16.0' },
|
|
'3p-user': { type: 'string', removed: '7.16.0' },
|
|
'3p-quote': { type: 'string', removed: '7.16.0' },
|
|
'http2.0': { type: 'bool', name: 'http2', removed: '7.36.0' },
|
|
'no-http2.0': { type: 'bool', name: 'http2', removed: '7.36.0' },
|
|
'telnet-options': { type: 'string', name: 'telnet-option', removed: '7.49.0' },
|
|
'http-request': { type: 'string', name: 'request', removed: '7.49.0' },
|
|
socks: { type: 'string', name: 'socks5', removed: '7.49.0' },
|
|
'capath ': { type: 'string', name: 'capath', removed: '7.49.0' }, // trailing space
|
|
ftpport: { type: 'string', name: 'ftp-port', removed: '7.49.0' },
|
|
environment: { type: 'bool', removed: '7.54.1' },
|
|
// These --no-<option> flags were automatically generated and never had any effect
|
|
'no-tlsv1': { type: 'bool', name: 'tlsv1', removed: '7.54.1' },
|
|
'no-tlsv1.2': { type: 'bool', name: 'tlsv1.2', removed: '7.54.1' },
|
|
'no-http2-prior-knowledge': { type: 'bool', name: 'http2-prior-knowledge', removed: '7.54.1' },
|
|
'no-ipv6': { type: 'bool', name: 'ipv6', removed: '7.54.1' },
|
|
'no-ipv4': { type: 'bool', name: 'ipv4', removed: '7.54.1' },
|
|
'no-sslv2': { type: 'bool', name: 'sslv2', removed: '7.54.1' },
|
|
'no-tlsv1.0': { type: 'bool', name: 'tlsv1.0', removed: '7.54.1' },
|
|
'no-tlsv1.1': { type: 'bool', name: 'tlsv1.1', removed: '7.54.1' },
|
|
'no-remote-name': { type: 'bool', name: 'remote-name', removed: '7.54.1' },
|
|
'no-sslv3': { type: 'bool', name: 'sslv3', removed: '7.54.1' },
|
|
'no-get': { type: 'bool', name: 'get', removed: '7.54.1' },
|
|
'no-http1.0': { type: 'bool', name: 'http1.0', removed: '7.54.1' },
|
|
'no-next': { type: 'bool', name: 'next', removed: '7.54.1' },
|
|
'no-tlsv1.3': { type: 'bool', name: 'tlsv1.3', removed: '7.54.1' },
|
|
'no-environment': { type: 'bool', name: 'environment', removed: '7.54.1' },
|
|
'no-http1.1': { type: 'bool', name: 'http1.1', removed: '7.54.1' },
|
|
'no-proxy-tlsv1': { type: 'bool', name: 'proxy-tlsv1', removed: '7.54.1' },
|
|
'no-http2': { type: 'bool', name: 'http2', removed: '7.54.1' }
|
|
}
|
|
for (const [opt, val] of Object.entries(removedLongOpts)) {
|
|
if (!has(val, 'name')) {
|
|
val.name = opt
|
|
}
|
|
}
|
|
// TODO: use this to warn users when they specify a short option that
|
|
// used to be for something else?
|
|
const changedShortOpts = {
|
|
p: 'used to be short for --port <port> (a since-deleted flag) until curl 7.3',
|
|
// TODO: some of these might be renamed options
|
|
t: 'used to be short for --upload (a since-deleted boolean flag) until curl 7.7',
|
|
c: 'used to be short for --continue (a since-deleted boolean flag) until curl 7.9',
|
|
// TODO: did -@ actually work?
|
|
'@': 'used to be short for --create-dirs until curl 7.10.7',
|
|
Z: 'used to be short for --max-redirs <num> until curl 7.10.7',
|
|
9: 'used to be short for --crlf until curl 7.10.8',
|
|
8: 'used to be short for --stderr <file> until curl 7.10.8',
|
|
7: 'used to be short for --interface <name> until curl 7.10.8',
|
|
6: 'used to be short for --krb <level> (which itself used to be --krb4 <level>) until curl 7.10.8',
|
|
// TODO: did these short options ever actually work?
|
|
5: 'used to be another way to specify the url until curl 7.10.8',
|
|
'*': 'used to be another way to specify the url until curl 7.49.0',
|
|
'~': 'used to be short for --xattr until curl 7.49.0'
|
|
}
|
|
|
|
// These options can be specified more than once, they
|
|
// are always returned as a list.
|
|
// Normally, if you specify some option more than once,
|
|
// curl will just take the last one.
|
|
// TODO: extract this from curl's source code?
|
|
const canBeList = new Set([
|
|
// TODO: unlike curl, we don't support multiple
|
|
// URLs and just take the last one.
|
|
'url',
|
|
'header', 'proxy-header',
|
|
'form',
|
|
'data', 'data-binary', 'data-ascii', 'data-raw', 'data-urlencode',
|
|
'mail-rcpt',
|
|
'resolve',
|
|
'connect-to',
|
|
'cookie',
|
|
'quote',
|
|
'telnet-option'
|
|
])
|
|
|
|
const shortened = {}
|
|
for (const [opt, val] of Object.entries(curlLongOpts)) {
|
|
if (!has(val, 'name')) {
|
|
val.name = opt
|
|
}
|
|
// curl lets you not type the full argument as long as it's unambiguous.
|
|
// So --sil instead of --silent is okay, --s is not.
|
|
// This doesn't apply to options starting with --no-
|
|
// Default 'expand' to true if not specified
|
|
const shouldExpand = !has(val, 'expand') || val.expand
|
|
delete val.expand
|
|
if (shouldExpand) {
|
|
for (let i = 1; i < opt.length; i++) {
|
|
const shortenedOpt = opt.slice(0, i)
|
|
pushProp(shortened, shortenedOpt, val)
|
|
}
|
|
}
|
|
}
|
|
for (const [shortenedOpt, vals] of Object.entries(shortened)) {
|
|
if (!has(curlLongOpts, shortenedOpt)) {
|
|
if (vals.length === 1) {
|
|
curlLongOpts[shortenedOpt] = vals[0]
|
|
} else if (vals.length > 1) {
|
|
// More than one option shortens to this, it's ambiguous
|
|
curlLongOpts[shortenedOpt] = null
|
|
}
|
|
}
|
|
}
|
|
for (const [removedOpt, val] of Object.entries(removedLongOpts)) {
|
|
if (!has(curlLongOpts, removedOpt)) {
|
|
curlLongOpts[removedOpt] = val
|
|
} else if (curlLongOpts[removedOpt] === null) {
|
|
// This happens with --socks because it became --socks5 and there are multiple options
|
|
// that start with "--socks"
|
|
// console.error("couldn't add removed option --" + removedOpt + " to curlLongOpts because it's already ambiguous")
|
|
// TODO: do we want to do this?
|
|
// curlLongOpts[removedOpt] = val
|
|
} else {
|
|
// Almost certainly a shortened form of a still-existing option
|
|
// This happens with --continue (now short for --continue-at)
|
|
// and --upload (now short for --upload-file)
|
|
// console.error("couldn't add removed option --" + removedOpt + ' to curlLongOpts because it already exists')
|
|
}
|
|
}
|
|
|
|
const toBoolean = opt => {
|
|
if (opt.startsWith('no-disable-')) {
|
|
return true
|
|
}
|
|
if (opt.startsWith('disable-') || opt.startsWith('no-')) {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
const parseWord = (str) => {
|
|
const BACKSLASHES = /\\./gs
|
|
const unescapeChar = (m) => m.charAt(1) === '\n' ? '' : m.charAt(1)
|
|
return str.replace(BACKSLASHES, unescapeChar)
|
|
}
|
|
const parseSingleQuoteString = (str) => {
|
|
const BACKSLASHES = /\\(\n|')/gs
|
|
const unescapeChar = (m) => m.charAt(1) === '\n' ? '' : m.charAt(1)
|
|
return str.slice(1, -1).replace(BACKSLASHES, unescapeChar)
|
|
}
|
|
const parseDoubleQuoteString = (str) => {
|
|
const BACKSLASHES = /\\(\n|\\|")/gs
|
|
const unescapeChar = (m) => m.charAt(1) === '\n' ? '' : m.charAt(1)
|
|
return str.slice(1, -1).replace(BACKSLASHES, unescapeChar)
|
|
}
|
|
// ANSI-C quoted strings look $'like this'.
|
|
// Not all shells have them but bash does
|
|
// https://www.gnu.org/software/bash/manual/html_node/ANSI_002dC-Quoting.html
|
|
//
|
|
// https://git.savannah.gnu.org/cgit/bash.git/tree/lib/sh/strtrans.c
|
|
const parseAnsiCString = (str) => {
|
|
const ANSI_BACKSLASHES = /\\(\\|a|b|e|E|f|n|r|t|v|'|"|\?|[0-7]{1,3}|x[0-9A-Fa-f]{1,2}|u[0-9A-Fa-f]{1,4}|U[0-9A-Fa-f]{1,8}|c.)/gs
|
|
const unescapeChar = (m) => {
|
|
switch (m.charAt(1)) {
|
|
case '\\':
|
|
return '\\'
|
|
case 'a':
|
|
return '\a' // eslint-disable-line
|
|
case 'b':
|
|
return '\b'
|
|
case 'e':
|
|
case 'E':
|
|
return '\x1B'
|
|
case 'f':
|
|
return '\f'
|
|
case 'n':
|
|
return '\n'
|
|
case 'r':
|
|
return '\r'
|
|
case 't':
|
|
return '\t'
|
|
case 'v':
|
|
return '\v'
|
|
case "'":
|
|
return "'"
|
|
case '"':
|
|
return '"'
|
|
case '?':
|
|
return '?'
|
|
case 'c':
|
|
// bash handles all characters by considering the first byte
|
|
// of its UTF-8 input and can produce invalid UTF-8, whereas
|
|
// JavaScript stores strings in UTF-16
|
|
if (m.codePointAt(2) > 127) {
|
|
throw new CCError("non-ASCII control character in ANSI-C quoted string: '\\u{" + m.codePointAt(2).toString(16) + "}'")
|
|
}
|
|
// If this produces a 0x00 (null) character, it will cause bash to
|
|
// terminate the string at that character, but we return the null
|
|
// character in the result.
|
|
return m[2] === '?' ? '\x7F' : String.fromCodePoint(m[2].toUpperCase().codePointAt(0) & 0b00011111)
|
|
case 'x':
|
|
case 'u':
|
|
case 'U':
|
|
// Hexadecimal character literal
|
|
// Unlike bash, this will error if the the code point is greater than 10FFFF
|
|
return String.fromCodePoint(parseInt(m.slice(2), 16))
|
|
case '0':
|
|
case '1':
|
|
case '2':
|
|
case '3':
|
|
case '4':
|
|
case '5':
|
|
case '6':
|
|
case '7':
|
|
// Octal character literal
|
|
return String.fromCodePoint(parseInt(m.slice(1), 8) % 256)
|
|
default:
|
|
// There must be a mis-match between ANSI_BACKSLASHES and the switch statement
|
|
throw new CCError('unhandled character in ANSI-C escape code: ' + JSON.stringify(m))
|
|
}
|
|
}
|
|
|
|
return str.slice(2, -1).replace(ANSI_BACKSLASHES, unescapeChar)
|
|
}
|
|
|
|
const tokenizeBashStr = (curlCommand) => {
|
|
const curlArgs = parser.parse(curlCommand)
|
|
// The AST must be in a nice format, i.e.
|
|
// (program
|
|
// (command
|
|
// name: (command_name (word))
|
|
// argument+: (
|
|
// word |
|
|
// string ('') |
|
|
// raw_string ("") |
|
|
// ansii_c_string ($'') |
|
|
// simple_expansion (variable_name))))
|
|
//
|
|
// TODO: support strings with variable expansions inside
|
|
// TODO: support prefixed variables, e.g. "MY_VAR=hello curl example.com"
|
|
// TODO: get only named children?
|
|
if (curlArgs.rootNode.type !== 'program') {
|
|
// TODO: better error message.
|
|
throw new CCError("expected a 'program' AST node, got " + curlArgs.rootNode.type + ' instead')
|
|
}
|
|
|
|
if (curlArgs.rootNode.childCount < 1) {
|
|
// TODO: better error message.
|
|
throw new CCError('empty "program" node')
|
|
}
|
|
|
|
// Get the curl call AST node. Skip comments
|
|
let command
|
|
for (const programChildNode of curlArgs.rootNode.children) {
|
|
if (programChildNode.type === 'comment') {
|
|
continue
|
|
} else if (programChildNode.type === 'command') {
|
|
command = programChildNode
|
|
// TODO: if there are more `command` nodes,
|
|
// warn that everything after the first one is ignored
|
|
break
|
|
} else {
|
|
// TODO: better error message.
|
|
throw new CCError("expected a 'command' AST node, got " + curlArgs.rootNode.firstChild.type + ' instead')
|
|
}
|
|
}
|
|
if (!command) {
|
|
// NOTE: if you add more node types in the `for` loop above, this error needs to be updated.
|
|
// We would probably need to keep track of the node types we've seen.
|
|
throw new CCError("expected a 'command' AST node, only found 'comment' nodes")
|
|
}
|
|
|
|
if (command.childCount < 1) {
|
|
// TODO: better error message.
|
|
throw new CCError('empty "command" node')
|
|
}
|
|
// TODO: add childrenForFieldName to tree-sitter node/web bindings and then
|
|
// use that here instead
|
|
// TODO: you can have variable_assignment before the actual command
|
|
// MY_VAR=foo curl example.com
|
|
const [cmdName, ...args] = command.children
|
|
if (cmdName.type !== 'command_name') {
|
|
// TODO: better error message.
|
|
throw new CCError("expected a 'command_name' AST node, got " + cmdName.type + ' instead')
|
|
}
|
|
|
|
const toVal = (node) => {
|
|
switch (node.type) {
|
|
case 'word':
|
|
case 'simple_expansion': // TODO: handle variables properly downstream
|
|
return parseWord(node.text)
|
|
case 'string':
|
|
return parseDoubleQuoteString(node.text)
|
|
case 'raw_string':
|
|
return parseSingleQuoteString(node.text)
|
|
case 'ansii_c_string':
|
|
return parseAnsiCString(node.text)
|
|
case 'concatenation':
|
|
// item[]=1 turns into item=1 if we don't do this
|
|
// https://github.com/tree-sitter/tree-sitter-bash/issues/104
|
|
if (node.children.every(n => n.type === 'word')) {
|
|
return node.text
|
|
}
|
|
return node.children.map(toVal).join('')
|
|
default:
|
|
// console.error(curlCommand)
|
|
// console.error(curlArgs.rootNode.toString())
|
|
throw new CCError('unexpected argument type ' + JSON.stringify(node.type) + '. Must be one of "word", "string", "raw_string", "ascii_c_string", "simple_expansion" or "concatenation"')
|
|
}
|
|
}
|
|
return [cmdName.text.trim(), ...args.map(toVal)]
|
|
}
|
|
|
|
const parseArgs = (args, opts) => {
|
|
const [longOpts, shortOpts] = opts || [curlLongOpts, curlShortOpts]
|
|
|
|
const parsedArguments = {}
|
|
for (let i = 0, stillflags = true; i < args.length; i++) {
|
|
let arg = args[i]
|
|
if (stillflags && arg.startsWith('-')) {
|
|
if (arg === '--') {
|
|
/* This indicates the end of the flags and thus enables the
|
|
following (URL) argument to start with -. */
|
|
stillflags = false
|
|
} else if (arg.startsWith('--')) {
|
|
const longArg = longOpts[arg.slice(2)]
|
|
if (longArg === null) {
|
|
throw new CCError('option ' + arg + ': is ambiguous')
|
|
}
|
|
if (typeof longArg === 'undefined') {
|
|
// TODO: extract a list of deleted arguments to check here
|
|
throw new CCError('option ' + arg + ': is unknown')
|
|
}
|
|
|
|
if (longArg.type === 'string') {
|
|
if (i + 1 < args.length) {
|
|
i++
|
|
pushProp(parsedArguments, longArg.name, args[i])
|
|
} else {
|
|
throw new CCError('option ' + arg + ': requires parameter')
|
|
}
|
|
} else {
|
|
parsedArguments[longArg.name] = toBoolean(arg.slice(2)) // TODO: all shortened args work correctly?
|
|
}
|
|
} else {
|
|
// Short option. These can look like
|
|
// -X POST -> {request: 'POST'}
|
|
// or
|
|
// -XPOST -> {request: 'POST'}
|
|
// or multiple options
|
|
// -ABCX POST
|
|
// -> {A: true, B: true, C: true, request: 'POST'}
|
|
// or multiple options and a value for the last one
|
|
// -ABCXPOST
|
|
// -> {A: true, B: true, C: true, request: 'POST'}
|
|
|
|
// "-" passed to curl on its own raises an error,
|
|
// curlconverter's command line uses it to read from stdin
|
|
if (arg.length === 1) {
|
|
if (has(shortOpts, '')) {
|
|
arg = ['-', '']
|
|
} else {
|
|
throw new CCError('option ' + arg + ': is unknown')
|
|
}
|
|
}
|
|
for (let j = 1; j < arg.length; j++) {
|
|
if (!has(shortOpts, arg[j])) {
|
|
if (has(changedShortOpts, arg[j])) {
|
|
throw new CCError('option ' + arg + ': ' + changedShortOpts[arg[j]])
|
|
}
|
|
// TODO: there are a few deleted short options we could report
|
|
throw new CCError('option ' + arg + ': is unknown')
|
|
}
|
|
const shortFor = shortOpts[arg[j]]
|
|
const longArg = longOpts[shortFor]
|
|
if (longArg.type === 'string') {
|
|
let val
|
|
if (j + 1 < arg.length) {
|
|
// treat -XPOST as -X POST
|
|
val = arg.slice(j + 1)
|
|
j = arg.length
|
|
} else if (i + 1 < args.length) {
|
|
i++
|
|
val = args[i]
|
|
} else {
|
|
throw new CCError('option ' + arg + ': requires parameter')
|
|
}
|
|
pushProp(parsedArguments, longArg.name, val)
|
|
} else {
|
|
// Use shortFor because -N is short for --no-buffer
|
|
// and we want to end up with {buffer: false}
|
|
parsedArguments[longArg.name] = toBoolean(shortFor)
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
pushProp(parsedArguments, 'url', arg)
|
|
}
|
|
}
|
|
|
|
for (const [arg, values] of Object.entries(parsedArguments)) {
|
|
if (Array.isArray(values) && !canBeList.has(arg)) {
|
|
parsedArguments[arg] = values[values.length - 1]
|
|
}
|
|
}
|
|
return parsedArguments
|
|
}
|
|
|
|
export const parseQueryString = (s) => {
|
|
// if url is 'example.com?' => s is ''
|
|
// if url is 'example.com' => s is null
|
|
if (!s) {
|
|
return [null, null]
|
|
}
|
|
|
|
const asList = []
|
|
for (const param of s.split('&')) {
|
|
const [key, val] = param.split(/=(.*)/s, 2)
|
|
let decodedKey
|
|
let decodedVal
|
|
try {
|
|
decodedKey = decodeURIComponent(key)
|
|
decodedVal = val === undefined ? null : decodeURIComponent(val)
|
|
} catch (e) {
|
|
if (e instanceof URIError) {
|
|
// Query string contains invalid percent encoded characters,
|
|
// we cannot properly convert it.
|
|
return [null, null]
|
|
}
|
|
throw e
|
|
}
|
|
try {
|
|
// If the query string doesn't round-trip, we cannot properly convert it.
|
|
// TODO: this is too strict. Ideally we want to check how each runtime/library
|
|
// percent encodes query strings. For example, a %27 character in the input query
|
|
// string will be decoded to a ' but won't be re-encoded into a %27 by encodeURIComponent
|
|
const roundTripKey = encodeURIComponent(decodedKey)
|
|
const roundTripVal = encodeURIComponent(decodedVal)
|
|
if ((roundTripKey !== key && roundTripKey.replace('%20', '+') !== key) ||
|
|
(decodedVal && (roundTripVal !== val && roundTripVal.replace('%20', '+') !== val))) {
|
|
return [null, null]
|
|
}
|
|
} catch (e) {
|
|
if (e instanceof URIError) {
|
|
return [null, null]
|
|
}
|
|
throw e
|
|
}
|
|
asList.push([decodedKey, decodedVal])
|
|
}
|
|
|
|
// Group keys
|
|
const asDict = {}
|
|
let prevKey = null
|
|
for (const [key, val] of asList) {
|
|
if (prevKey === key) {
|
|
asDict[key].push(val)
|
|
} else {
|
|
if (!has(asDict, key)) {
|
|
asDict[key] = [val]
|
|
} else {
|
|
// If there's a repeated key with a different key between
|
|
// one of its repetitions, there is no way to represent
|
|
// this query string as a dictionary.
|
|
return [asList, null]
|
|
}
|
|
}
|
|
prevKey = key
|
|
}
|
|
|
|
// Convert lists with 1 element to the element
|
|
for (const [key, val] of Object.entries(asDict)) {
|
|
if (val.length === 1) {
|
|
asDict[key] = val[0]
|
|
}
|
|
}
|
|
|
|
return [asList, asDict]
|
|
}
|
|
|
|
const buildRequest = parsedArguments => {
|
|
// TODO: handle multiple URLs
|
|
if (!parsedArguments.url || !parsedArguments.url.length) {
|
|
// TODO: better error message (could be parsing fail)
|
|
throw new CCError('no URL specified!')
|
|
}
|
|
let url = parsedArguments.url[parsedArguments.url.length - 1]
|
|
|
|
let headers
|
|
if (parsedArguments.header) {
|
|
if (!headers) {
|
|
headers = []
|
|
}
|
|
for (const header of parsedArguments.header) {
|
|
const [name, value] = header.split(/:(.*)/s, 2)
|
|
headers.push([name, value ? value.replace(/^ /, '') : ''])
|
|
}
|
|
}
|
|
const capitalizeHeaders = !headers || headers.some(h => h[0] !== h[0].toLowerCase()) || !headers.length
|
|
|
|
let cookies
|
|
const cookieHeaders = (headers || []).filter(h => h[0].toLowerCase() === 'cookie')
|
|
if (cookieHeaders.length === 1) {
|
|
const parsedCookies = parseCookiesStrict(cookieHeaders[0][1])
|
|
if (parsedCookies) {
|
|
cookies = parsedCookies
|
|
}
|
|
} else if (cookieHeaders.length === 0) {
|
|
// If there is a Cookie header, --cookies is ignored
|
|
if (parsedArguments.cookie) {
|
|
// TODO: a --cookie without a = character reads from it as a filename
|
|
const cookieString = parsedArguments.cookie.join(';')
|
|
if (!headers) {
|
|
headers = []
|
|
}
|
|
headers.push([capitalizeHeaders ? 'Cookie' : 'cookie', cookieString])
|
|
cookies = parseCookies(cookieString, false)
|
|
}
|
|
}
|
|
|
|
if (parsedArguments['user-agent']) {
|
|
if (!headers) {
|
|
headers = []
|
|
}
|
|
headers.push([capitalizeHeaders ? 'User-Agent' : 'user-agent', parsedArguments['user-agent']])
|
|
}
|
|
|
|
if (parsedArguments.referer) {
|
|
if (!headers) {
|
|
headers = []
|
|
}
|
|
// referer can be ";auto" or followed by ";auto", we ignore that.
|
|
const referer = parsedArguments.referer.replace(/;auto$/, '')
|
|
if (referer) {
|
|
headers.push([capitalizeHeaders ? 'Referer' : 'referer', referer])
|
|
}
|
|
}
|
|
|
|
let multipartUploads
|
|
if (parsedArguments.form) {
|
|
multipartUploads = []
|
|
parsedArguments.form.forEach(multipartArgument => {
|
|
// -F is the most complicated option, we just assume it looks
|
|
// like key=value and some generators handle value being @filepath
|
|
// TODO: https://curl.se/docs/manpage.html#-F
|
|
const [key, value] = multipartArgument.split(/=(.*)/s, 2)
|
|
multipartUploads.push([key, value || ''])
|
|
})
|
|
}
|
|
|
|
// TODO: don't lower case method,
|
|
// curl expects you to uppercase always, if you do -X PoSt, that's
|
|
// what it will put as the method and we should do the same.
|
|
// TODO: read curl's source to figure out precedence rules.
|
|
let method
|
|
if (parsedArguments.head) {
|
|
method = 'head'
|
|
} else if (has(parsedArguments, 'request') &&
|
|
parsedArguments.request !== 'null') { // Safari adds `-Xnull` if it can't determine the request type
|
|
method = parsedArguments.request.toLowerCase()
|
|
} else if (parsedArguments['upload-file']) { // --upload-file '' doesn't do anything.
|
|
method = 'put'
|
|
} else if ((has(parsedArguments, 'data') ||
|
|
has(parsedArguments, 'data-ascii') ||
|
|
has(parsedArguments, 'data-binary') ||
|
|
has(parsedArguments, 'data-raw') ||
|
|
has(parsedArguments, 'form')) && !(parsedArguments.get)) {
|
|
method = 'post'
|
|
} else {
|
|
method = 'get'
|
|
}
|
|
|
|
const urlObject = URL.parse(url) // eslint-disable-line
|
|
// if GET request with data, convert data to query string
|
|
// NB: the -G flag does not change the http verb. It just moves the data into the url.
|
|
// TODO: this probably has a lot of mismatches with curl
|
|
if (parsedArguments.get) {
|
|
urlObject.query = urlObject.query ? urlObject.query : ''
|
|
if (has(parsedArguments, 'data')) {
|
|
let urlQueryString = ''
|
|
|
|
if (url.indexOf('?') < 0) {
|
|
url += '?'
|
|
} else {
|
|
urlQueryString += '&'
|
|
}
|
|
|
|
urlQueryString += parsedArguments.data.join('&')
|
|
urlObject.query += urlQueryString
|
|
// TODO: url and urlObject will be different if url has an #id
|
|
url += urlQueryString
|
|
delete parsedArguments.data
|
|
}
|
|
}
|
|
if (urlObject.query && urlObject.query.endsWith('&')) {
|
|
urlObject.query = urlObject.query.slice(0, -1)
|
|
}
|
|
const [queryAsList, queryAsDict] = parseQueryString(urlObject.query)
|
|
// Most software libraries don't let you distinguish between a=&b= and a&b,
|
|
// so if we get an `a&b`-type query string, don't bother.
|
|
const request = { url }
|
|
if (!queryAsList || queryAsList.some((p) => p[1] === null)) {
|
|
request.urlWithoutQuery = url // TODO: rename?
|
|
} else {
|
|
urlObject.search = null // Clean out the search/query portion.
|
|
request.urlWithoutQuery = URL.format(urlObject)
|
|
|
|
if (queryAsList.length > 0) {
|
|
request.query = queryAsList
|
|
if (queryAsDict) {
|
|
request.queryDict = queryAsDict
|
|
}
|
|
}
|
|
}
|
|
|
|
if (cookies) {
|
|
// generators that use .cookies need to do
|
|
// deleteHeader(request, 'cookie')
|
|
request.cookies = cookies
|
|
}
|
|
|
|
if (parsedArguments.compressed) {
|
|
request.compressed = true
|
|
}
|
|
|
|
if (headers) {
|
|
request.headers = headers
|
|
}
|
|
request.method = method
|
|
|
|
if (multipartUploads) {
|
|
request.multipartUploads = multipartUploads
|
|
}
|
|
// TODO: all of these could be specified in the same command.
|
|
// They also need to maintain order.
|
|
// TODO: do all of these allow @file?
|
|
// TODO: set Content-Type downstream for some of these
|
|
if (parsedArguments.data) {
|
|
request.data = parsedArguments.data
|
|
} else if (parsedArguments['data-binary']) {
|
|
request.data = parsedArguments['data-binary']
|
|
request.isDataBinary = true
|
|
} else if (parsedArguments['data-ascii']) {
|
|
request.data = parsedArguments['data-ascii']
|
|
} else if (parsedArguments['data-raw']) {
|
|
request.data = parsedArguments['data-raw']
|
|
request.isDataRaw = true
|
|
} else if (parsedArguments['data-urlencode']) {
|
|
// TODO: this doesn't exactly match curl
|
|
// all '&' and all but the first '=' need to be escaped
|
|
request.data = parsedArguments['data-urlencode']
|
|
}
|
|
|
|
if (parsedArguments.user) {
|
|
const [user, pass] = parsedArguments.user.split(/:(.*)/s, 2)
|
|
request.auth = [user, pass || '']
|
|
}
|
|
if (parsedArguments.digest) {
|
|
request.digest = parsedArguments.digest
|
|
}
|
|
if (has(request, 'data')) {
|
|
if (request.data.length > 1) {
|
|
request.dataArray = request.data
|
|
request.data = request.data.join('&')
|
|
} else {
|
|
request.data = request.data[0]
|
|
}
|
|
}
|
|
|
|
if (parsedArguments.insecure) {
|
|
request.insecure = true
|
|
}
|
|
// TODO: if the URL doesn't start with https://, curl doesn't verify
|
|
// certificates, etc.
|
|
if (parsedArguments.cert) {
|
|
// --key has no effect if --cert isn't passed
|
|
request.cert = parsedArguments.key ? [parsedArguments.cert, parsedArguments.key] : parsedArguments.cert
|
|
}
|
|
if (parsedArguments.cacert) {
|
|
request.cacert = parsedArguments.cacert
|
|
}
|
|
if (parsedArguments.capath) {
|
|
request.capath = parsedArguments.capath
|
|
}
|
|
if (parsedArguments.output) {
|
|
request.output = parsedArguments.output
|
|
}
|
|
|
|
return request
|
|
}
|
|
|
|
const parseCurlCommand = (curlCommand) => {
|
|
const [cmdName, ...args] = Array.isArray(curlCommand) ? curlCommand : tokenizeBashStr(curlCommand)
|
|
if (typeof cmdName === 'undefined') {
|
|
const errorMsg = Array.isArray(curlCommand) ? 'no arguments provided' : 'failed to parse input'
|
|
throw new CCError(errorMsg)
|
|
}
|
|
if (cmdName.trim() !== 'curl') {
|
|
const shortenedCmdName = cmdName.length > 30 ? cmdName.slice(0, 27) + '...' : cmdName
|
|
if (cmdName.startsWith('curl ')) {
|
|
throw new CCError('command should begin with a single token "curl" but instead begins with ' + JSON.stringify(shortenedCmdName))
|
|
} else {
|
|
throw new CCError('command should begin with "curl" but instead begins with ' + JSON.stringify(shortenedCmdName))
|
|
}
|
|
}
|
|
|
|
const parsedArguments = parseArgs(args)
|
|
return buildRequest(parsedArguments)
|
|
}
|
|
|
|
const serializeCookies = cookieDict => {
|
|
let cookieString = ''
|
|
let i = 0
|
|
const cookieCount = Object.keys(cookieDict).length
|
|
for (const cookieName in cookieDict) {
|
|
const cookieValue = cookieDict[cookieName]
|
|
cookieString += cookieName + '=' + cookieValue
|
|
if (i < cookieCount - 1) {
|
|
cookieString += '; '
|
|
}
|
|
i++
|
|
}
|
|
return cookieString
|
|
}
|
|
|
|
// Gets the first header, matching case-insensitively
|
|
const getHeader = (request, header) => {
|
|
if (!request.headers) {
|
|
return undefined
|
|
}
|
|
const lookup = header.toLowerCase()
|
|
for (const [h, v] of request.headers) {
|
|
if (h.toLowerCase() === lookup) {
|
|
return v
|
|
}
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
const hasHeader = (request, header) => {
|
|
return !!countHeader(request, header)
|
|
}
|
|
|
|
const deleteHeader = (request, header) => {
|
|
if (!request.headers) {
|
|
return
|
|
}
|
|
const lookup = header.toLowerCase()
|
|
for (let i = request.headers.length - 1; i >= 0; i--) {
|
|
if (request.headers[i][0].toLowerCase() === lookup) {
|
|
request.headers.splice(i, 1)
|
|
}
|
|
}
|
|
}
|
|
|
|
const countHeader = (request, header) => {
|
|
let count = 0
|
|
const lookup = header.toLowerCase()
|
|
for (const h of (request.headers || [])) {
|
|
if (h[0].toLowerCase() === lookup) {
|
|
count += 1
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
|
|
const parseCookiesStrict = (cookieString) => {
|
|
const cookies = []
|
|
for (let cookie of cookieString.split(';')) {
|
|
cookie = cookie.replace(/^ /, '')
|
|
const [name, value] = cookie.split(/=(.*)/s, 2)
|
|
if (value === undefined) {
|
|
return null
|
|
}
|
|
cookies.push([name, value])
|
|
}
|
|
return cookies
|
|
}
|
|
|
|
const parseCookies = (cookieString) => {
|
|
const cookies = []
|
|
for (let cookie of cookieString.split(';')) {
|
|
cookie = cookie.trim()
|
|
if (!cookie) {
|
|
continue
|
|
}
|
|
const [name, value] = cookie.split(/=(.*)/s, 2)
|
|
cookies.push([name, value || ''])
|
|
}
|
|
return cookies
|
|
}
|
|
|
|
export {
|
|
curlLongOpts,
|
|
curlShortOpts,
|
|
parseArgs,
|
|
buildRequest,
|
|
parseCurlCommand,
|
|
serializeCookies,
|
|
getHeader,
|
|
hasHeader,
|
|
deleteHeader,
|
|
has
|
|
}
|