Curl2PS.psm1

if ($PSVersionTable.PSVersion.Major -lt 6) {
    $dlls2Load = @(
        'System.Collections.Immutable.dll'
        'System.Reflection.Metadata.dll'
        'Microsoft.CodeAnalysis.dll'
    )
    foreach ($dll in $dlls2Load){
        Add-Type -Path "$PSScriptRoot\dependencies\$dll"
    }
}
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 "$([System.Environment]::NewLine)") {
            $arr = $curlString -split "$([System.Environment]::NewLine)"
            $curlString = ($arr | ForEach-Object { $_.TrimEnd('\').TrimEnd(' ') }) -join ' '
        }
        $this.RawCommand = $curlString
        # Set the default method in case one isn't set later
        $this.Method = 'Get'

        $splitParams = [Microsoft.CodeAnalysis.CommandLineParser]::SplitCommandLineIntoArguments($curlString, $true)
        if ($splitParams[0].ToLowerInvariant() -ne 'curl') {
            Throw "`$curlString does not start with 'curl', 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 '-*') {
                $paramName = $splitParams[$x].TrimStart('-')
                $paramValue = $splitParams[$x + 1]
                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++
                    }
                    { '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','k','insecure','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','s','silent','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','url','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' will not be sent to the IWR."
                    }           
                    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"
    }

    [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){
        $strKeys += " '$key' = '$($InputObject[$key])'"
    }
    $str = "@{`n" + ($strKeys -join "`n") + "`n}"
    $str
}
Function ConvertTo-IRM {
    [OutputType([System.Collections.Hashtable], ParameterSetName = 'asSplat')]
    [OutputType([System.String], ParameterSetName = 'asString')]
    [cmdletbinding(
        DefaultParameterSetName = 'asSplat'
    )]
    param (
        [CurlCommand]$CurlCommand,
        [Parameter(
            ParameterSetName = 'asString'
        )]
        [switch]$CommandAsString
    )
    if ($CommandAsString.IsPresent) {
        $CurlCommand.ToIRM()
    } else {
        $CurlCommand.ToIRMSplat()
    }
}
Function Get-CurlCommand {
    [OutputType([CurlCommand])]
    param (
        [ValidateScript({ $_.ToLowerInvariant().Trim() -match '^curl ' })]
        [string]$CurlString
    )
    [CurlCommand]::new($CurlString.Trim())
}