Curl2PS.psm1
Class CurlCommand { [string]$RawCommand [string]$User [string]$Body [System.Uri]$URL [string]$Method [hashtable]$Headers = @{} [bool]$Verbose = $false CurlCommand( [string]$curlString ) { # Split at platform-dependent NewLine if ($curlString -match "\n") { $arr = $curlString -split "\n" $curlString = ($arr | ForEach-Object { $_.TrimEnd('\').Trim() }) -join ' ' } $this.RawCommand = $curlString # Set the default method in case one isn't set later $this.Method = 'Get' $splitParams = Invoke-Command -ScriptBlock ([scriptblock]::Create("parse $curlString")) if ($splitParams[0] -notin 'curl', 'curl.exe') { Throw "`$curlString does not start with 'curl' or 'curl.exe', which is necessary for correct parsing." } for ($x = 1; $x -lt $splitParams.Count; $x++) { # If this item is a parameter name, use it # The next item must be the parameter value # Unless the current item is a switch param # If not, increment $x so we skip the next one if ($splitParams[$x] -like '-*') { [string[]]$paramNames = $splitParams[$x].TrimStart('-') if ($splitParams[$x] -match '^-[a-zA-Z]+') { # multiple single char flags [string[]]$paramNames = $paramNames[0][0..$paramNames[0].Length] } $paramValue = $splitParams[$x + 1] foreach ($paramName in $paramNames) { switch ($paramName) { { 'H', 'header' -ccontains $_ } { # Headers $split = ($paramValue.Split(':') -replace '\\"', '"') $this.Headers[($split[0].Trim())] = (($split[1..$($split.count)] -join ':').Trim()) $x++ } { 'X', 'request' -ccontains $_ } { # Request type $this.Method = $paramValue.Trim() $x++ } { 'v', 'verbose' -ccontains $_ } { # Verbosity $this.Verbose = $true } { 'd', 'data' -ccontains $_ } { # Body if ($paramValue.Length -gt 0) { $this.Body = $paramValue.Trim() -replace '\\"', '"' } $x++ } { 'u', 'user' -ccontains $_ } { # Username if ($_ -like '*:*') { Add-BasicAuth -CurlCommand $this -Auth $paramValue } else { $this.User = $paramValue.Trim() } $x++ } 'url' { $x++ $this.URL = $paramValue.Trim() } { 'k', 'insecure' -ccontains $_ } { Write-Warning "'-k' and '--insecure' (both the same) do not have a PS 5.1 equivalent. In 6+ it is '-SkipCertificateCheck'. Curl2PS will ignore this until supported is added for PS version specific conversions." } { 's', 'silent' -ccontains $_ } { Write-Warning "'-s' and '--silent' (both the same) do not have a direct PS equivalent. However, the same can be achieved by piping to Out-Null. You can skip this without losing any functionality." } { 'abstract-unix-socket', 'alt-svc', 'anyauth', 'a', 'append', 'aws-sigv4', 'basic', 'cacert', 'capath', 'cert-status', 'cert-type', 'E', 'cert', 'ciphers', 'compressed-ssh', 'compressed', 'K', 'config', 'connect-timeout', 'connect-to', 'C', 'continue-at', 'c', 'cookie-jar', 'b', 'cookie', 'create-dirs', 'create-file-mode', 'crlf', 'crlfile', 'curves', 'data-ascii', 'data-binary', 'data-raw', 'data-urlencode', 'delegation', 'digest', 'disable-eprt', 'disable-epsv', 'q', 'disable', 'disallow-username-in-url', 'dns-interface', 'dns-ipv4-addr', 'dns-ipv6-addr', 'dns-servers', 'doh-cert-status', 'doh-insecure', 'doh-url', 'D', 'dump-header', 'egd-file', 'engine', 'etag-compare', 'etag-save', 'expect100-timeout', 'fail-early', 'fail-with-body', 'f', 'fail', 'false-start', 'form-escape', 'form-string', 'F', 'form', 'ftp-account', 'ftp-alternative-to-user', 'ftp-create-dirs', 'ftp-method', 'ftp-pasv', 'P', 'ftp-port', 'ftp-pret', 'ftp-skip-pasv-ip', 'ftp-ssl-ccc-mode', 'ftp-ssl-ccc', 'ftp-ssl-control', 'G', 'get', 'g', 'globoff', 'happy-eyeballs-timeout-ms', 'haproxy-protocol', 'I', 'head', 'h', 'help', 'hostpubmd5', 'hostpubsha256', 'hsts', 'http0.9', '0', 'http1.0', 'http1.1', 'http2-prior-knowledge', 'http2', 'http3', 'ignore-content-length', 'i', 'include', 'interface', '4', 'ipv4', '6', 'ipv6', 'json', 'j', 'junk-session-cookies', 'keepalive-time', 'key-type', 'key', 'krb', 'libcurl', 'limit-rate', 'l', 'list-only', 'local-port', 'location-trusted', 'L', 'location', 'login-options', 'mail-auth', 'mail-from', 'mail-rcpt-allowfails', 'mail-rcpt', 'M', 'manual', 'max-filesize', 'max-redirs', 'm', 'max-time', 'metalink', 'negotiate', 'netrc-file', 'netrc-optional', 'n', 'netrc', ':', 'next', 'no-alpn', 'N', 'no-buffer', '#', 'progress-bar', 'no-clobber', 'no-keepalive', 'no-npn', 'no-progress-meter', 'no-sessionid', 'noproxy', 'ntlm-wb', 'ntlm', 'oauth2-bearer', 'output-dir', 'o', 'output', 'parallel-immediate', 'parallel-max', 'Z', 'parallel', 'pass', 'path-as-is', 'pinnedpubkey', 'post301', 'post302', 'post303', 'preproxy', 'proto-default', 'proto-redir', 'proto', 'proxy-anyauth', 'proxy-basic', 'proxy-cacert', 'proxy-capath', 'proxy-cert-type', 'proxy-cert', 'proxy-ciphers', 'proxy-crlfile', 'proxy-digest', 'proxy-header', 'proxy-insecure', 'proxy-key-type', 'proxy-key', 'proxy-negotiate', 'proxy-ntlm', 'proxy-pass', 'proxy-pinnedpubkey', 'proxy-service-name', 'proxy-ssl-allow-beast', 'proxy-ssl-auto-client-cert', 'proxy-tls13-ciphers', 'proxy-tlsauthtype', 'proxy-tlspassword', 'proxy-tlsuser', 'proxy-tlsv1', 'U', 'proxy-user', 'x', 'proxy', 'proxy1.0', 'p', 'proxytunnel', 'pubkey', 'Q', 'quote', 'random-file', 'r', 'range', '500', 'rate', 'raw', 'e', 'referer', 'J', 'remote-header-name', 'remote-name-all', 'O', 'remote-name', 'R', 'remote-time', 'remove-on-error', 'request-target', 'resolve', 'retry-all-errors', 'retry-connrefused', 'retry-delay', 'retry-max-time', 'retry', 'sasl-authzid', 'sasl-ir', 'service-name', 'S', 'show-error', 'socks4', 'socks4a', 'socks5-basic', 'socks5-gssapi-nec', 'socks5-gssapi-service', 'socks5-gssapi', 'socks5-hostname', 'socks5', 'Y', 'speed-limit', 'y', 'speed-time', 'ssl-allow-beast', 'ssl-auto-client-cert', 'ssl-no-revoke', 'ssl-reqd', 'ssl-revoke-best-effort', 'ssl', '2', 'sslv2', '3', 'sslv3', 'stderr', 'styled-output', 'suppress-connect-headers', 'tcp-fastopen', 'tcp-nodelay', 't', 'telnet-option', 'tftp-blksize', 'tftp-no-options', 'z', 'time-cond', 'tls-max', 'tls13-ciphers', 'tlsauthtype', 'tlspassword', 'tlsuser', 'tlsv1.0', 'tlsv1.1', 'tlsv1.2', 'tlsv1.3', '1', 'tlsv1', 'tr-encoding', 'trace-ascii', 'trace-time', 'trace', 'unix-socket', 'T', 'upload-file', 'B', 'use-ascii', 'A', 'user-agent', 'V', 'version', 'w', 'write-out', 'xattr' -ccontains $_ } { # Valid, yet-unsupported parameters # retrieved from curl.se/docs/manpage.html using console script # ```js # var params = new Set(); # document.querySelectorAll("body > div.main > div.contents > p > span").forEach( function(e){ setTimeout( function(){ var param = e.innerText.match(/(?<=^--|^-).+/g); if(param){ var splitParam = param[0].split(', '); if (splitParam.length > 1) { splitParam.forEach( function(n){ params.add(n.replace(/^-+/g,'').replace(/\s.+$/g,'')) } )} else {params.add(splitParam[0].replace(/^-+/g,'').replace(/\s.+$/g,''))} } }, 300 ) } ) # var currentParams = new Set(['H', 'header', 'X', 'request', 'v', 'verbose', 'd', 'data', 'u', 'user']) # params = new Set([...params].filter(x => !currentParams.has(x))) # ``` # Then output the result: # ```js # Array.from(params).join("','") # ``` Write-Verbose "The current version of the module does not support '-$paramName'; however, future releases may implement this." Write-Information -MessageData "The parameter '-$paramName', although supplied, will not be sent to the IWR, because this feature is not yet implemented." -Tags 'Params' Write-Warning -Message "The parameter '-$paramName' is not yet supported. Please refer to curl man pages: https://curl.se/docs/manpage.html" } default { # Unknown Throw "Unknown parameter: '$paramName'. Cannot continue." } } } } elseif ($splitParams[$x] -match '^https?\:\/\/') { # Must be a url $this.URL = $splitParams[$x] } } # Check the url for basic auth if ($this.URL.UserInfo.Length -gt 1) { $this.AddBasicAuth($this.URL.UserInfo) } } AddBasicAuth( [string]$Auth ) { $encodedAuth = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($Auth)) $urlNoAuth = $this.URL.OriginalString -replace "$($Auth)@", '' $this.URL = [System.Uri]::new($urlNoAuth) $this.Headers['Authorization'] = "Basic $encodedAuth" } CompressJSON () { if ($this.Body.Length -gt 2) { try { $this.Body = $this.Body | ConvertFrom-Json | ConvertTo-Json -Depth 100 -Compress } catch { Write-Warning 'Unable to clean up the JSON.' } } else { Write-Warning 'There is no JSON body to clean.' } } [string] ToString() { return $this.RawCommand } [string] ToIRM() { $outString = 'Invoke-RestMethod' $outString += " -Method $($this.Method)" $outString += " -Uri '$($this.URL.ToString())'" if ($this.User.Length -gt 0) { $outString += " $($this.User)" } $outString += " -Verbose:`$$($this.Verbose.ToString().ToLowerInvariant())" if ($this.Headers.Keys) { #$outString += " -Headers ('$($this.Headers | ConvertTo-Json -Compress)' | ConvertFrom-Json)" $outString += " -Headers $(ConvertTo-HashtableString $this.Headers)" } if ($this.Body.Length -gt 0) { $outString += " -Body '$($this.Body)'" } return $outString } [hashtable] ToIRMSplat() { $out = @{} $out['Method'] = $this.Method $out['Uri'] = $this.URL.ToString() if ($this.User.Length -gt 0) { # will prompt for password $out['Credential'] = $this.User } if ($this.Body.Length -gt 0) { $out['Body'] = $this.Body } if ($this.Headers.Keys) { $out['Headers'] = $this.Headers } $out['Verbose'] = $this.Verbose return $out } } Function ConvertTo-HashtableString { param ( [Hashtable]$InputObject ) $strKeys = @() foreach ($key in $InputObject.Keys | Sort-Object) { $strKeys += " '$key' = '$($InputObject[$key])'" } $str = "@{`n" + ($strKeys -join "`n") + "`n}" $str } Function parse { $args } Function ConvertTo-IRM { [OutputType([System.Collections.Hashtable], ParameterSetName = 'asSplat')] [OutputType([System.String], ParameterSetName = 'asString')] [cmdletbinding( DefaultParameterSetName = 'asSplat' )] param ( [Parameter( Position = 0 )] [CurlCommand]$CurlCommand, [Parameter( ParameterSetName = 'asString' )] [switch]$CommandAsString, [switch]$CompressJSON ) if ($CompressJSON.IsPresent) { $CurlCommand.CompressJSON() } if ($CommandAsString.IsPresent) { $CurlCommand.ToIRM() } else { $CurlCommand.ToIRMSplat() } } Function Get-CurlCommand { [OutputType([CurlCommand])] param ( [ValidateScript({ $_.Trim() -match '^curl(?:\.exe)? ' })] [string]$CurlString ) [CurlCommand]::new($CurlString.Trim()) } |