Curl2PS.psm1

Class Curl2PSParameterDefinition {
    [string]$Type
    [string]$ParameterName
    [PSObject]$Value
}
Class Curl2PSParameterTransformer {
    [string]$Description
    [version]$MinimumVersion
    [string]$ParameterName
    [string]$Type
    [scriptblock]$Value
    [string]$Warning
    [Curl2PSParameterTransformer]$AdditionalParameters

    Curl2PSArgumentDefinition() {}
}
Function ConvertTo-Curl2PSParameter {
    [OutputType([Curl2PSParameterDefinition])]
    param (
        [string]$ParamValue,
        [string]$ParamName
    )
    if ($config.ParameterTransformers.Keys -ccontains $paramName) {
        # if the argument value is a string, locate the correct argument in the config
        $ogParamName = $paramName
        if ($config.ParameterTransformers[$paramName] -is [string]) {
            $paramName = $config.ParameterTransformers[$paramName]
        }
        # if the argument is an array, get the one with highest met minimum version
        if ($config.ParameterTransformers[$paramName] -is [array]) {
            $argConfig = $null
            foreach ($argument in $config.ParameterTransformers[$paramName]) {
                if ($null -eq $argConfig -and -not $argument.MinimumVersion) {
                    $argConfig = $argument
                }
                if ($argument.MinimumVersion -and [version]$argument.MinimumVersion -lt $PSVersionTable.PSVersion -and $argument.MinimumVersion -gt $argConfig.MinimumVersion) {
                    $argConfig = $argument
                }
            }
        } else {
            $argConfig = $config.ParameterTransformers[$paramName]
        }
        # minimum version check (i.e. SkipCertificateCheck)
        if ($argConfig.MinimumVersion -and [version]$argConfig.MinimumVersion -gt $PSVersionTable.PSVersion) {
            Write-Warning "The parameter $ogParamName is not supported in this version of PowerShell. Minimum version required: $($argConfig.MinimumVersion)"
            continue
        }
        # invoke the config's script block to return the value
        $out = Invoke-Command -ScriptBlock $argConfig.Value -ArgumentList $paramValue
        $data = [Curl2PSParameterDefinition]@{
            Type          = $argConfig.Type
            ParameterName = $argConfig.ParameterName
            Value         = $out
        }
        # headers are a special case, as they are a hashtable of key/value pairs and sometimes represent other parameters for Invoke-RestMethod
        if ($data.ParameterName -eq 'Headers' -and $config.Headers.Keys -contains $data.Value.Keys[0]) {
            $key = $data.Value.Keys[0]
            if ($config.Headers[$key].Keys -notcontains 'MinimumVersion' -or [version]$config.Headers[$key].MinimumVersion -lt $PSVersionTable.PSVersion) {
                $data = [Curl2PSParameterDefinition]@{
                    Type          = $config.Headers[$key].Type
                    ParameterName = $config.Headers[$key].ParameterName
                    Value         = $data.Value.Values[0]
                }
            }
        }
        $data
        if ($argConfig.AdditionalParameters) {
            foreach ($addParam in $argConfig.AdditionalParameters) {
                [Curl2PSParameterDefinition]@{
                    Type          = $addParam.Type
                    ParameterName = $addParam.ParameterName
                    Value         = Invoke-Command -ScriptBlock $addParam.Value -ArgumentList $paramValue
                }
            }
        }
        if ($argConfig.Warning.Length -gt 0) {
            Write-Warning "For param '$($ogParamName)': $($argConfig.Warning)"
        }
    } else {
        Write-Warning "'$paramName' may be a valid cURL parameter, but it has not yet been implemented in Curl2PS. Feel free to open a feature request at https://github.com/theposhwolf/curl2ps/issues"
    }
}
Function ConvertTo-HashtableString {
    param (
        [Hashtable]$InputObject,
        [int]$Depth = 0,
        [switch]$IsForm
    )
    $strKeys = @()
    $indent = " " * $Depth  # Indentation based on depth
    foreach ($key in $InputObject.Keys | Sort-Object) {
        $value = $InputObject[$key]
        if ($value -is [Hashtable]) {
            # recursively process nested hashtable
            $nestedHashtableString = ConvertTo-HashtableString -InputObject $value -Depth ($Depth + 1)
            $strKeys += "$indent '$key' = $nestedHashtableString"
        } else {
            # depth based nesting
            if ($IsForm.IsPresent -and $value -like 'Get-Item *') {
                $strKeys += "$indent '$key' = $value"
            } else {
                $strKeys += "$indent '$key' = '$value'"
            }
        }
    }
    $str = "$indent@{`n" + ($strKeys -join "`n") + "`n$indent}"
    $str
}
Function Invoke-GetItemInHashtable {
    param (
        [Hashtable]$ht
    )
    # create a new hashtable to store the resolved values
    $newHt = @{}
    foreach ($key in $ht.Keys) {
        $value = $ht[$key]
        if ($value -is [Hashtable]) {
            # recursively process nested hashtables and add the result to the new hashtable
            $newHt[$key] = Invoke-GetItemInHashtable -ht $value
        } elseif ($value -is [string] -and $value.StartsWith('Get-Item ')) {
            # curl uses the @ symbol in -F to denote files to send. Invoke-RestMethod
            # then expects the FileInfo object (Get-Item) so we need to execute it.
            $scriptBlock = [scriptblock]::Create($value)
            $newHt[$key] = & $scriptBlock
        } else {
            # copy values as-is
            $newHt[$key] = $value
        }
    }
    return $newHt  # Return the new hashtable
}
Function parse {
    $args
}
Function ConvertTo-Curl2PSSplat {
    [OutputType([hashtable])]
    param (
        [Parameter(
            Mandatory,
            ValueFromPipeline
        )]
        [Curl2PSParameterDefinition]$Parameter
    )
    Begin {
        $splat = @{}
    }
    Process {
        if ($Parameter.Type -eq 'Hashtable') {
            $ht = @{}
            if ($splat.Keys -contains $Parameter.ParameterName) {
                foreach ($key in $splat[$Parameter.ParameterName].Keys) {
                    $ht[$key] = $splat[$Parameter.ParameterName][$key]
                }
            }
            foreach ($key in $Parameter.Value.Keys) {
                $ht[$key] = $Parameter.Value[$key]
            }
            try {
                $convertedHt = Invoke-GetItemInHashtable $ht
            } catch {
                $convertedHt = $ht
            }
            $splat[$Parameter.ParameterName] = $convertedHt
        } else {
            $splat[$Parameter.ParameterName] = $Parameter.Value
        }
    }
    End {
        $splat
    }
}
Function ConvertTo-Curl2PSString {
    [OutputType([string])]
    param (
        [Parameter(
            Mandatory,
            ValueFromPipeline
        )]
        [Curl2PSParameterDefinition]$Parameter
    )
    Begin {
        $baseStr = "Invoke-RestMethod -Uri '{URI}' -Method {METHOD}"
        $hts = @{}
    }
    Process {
        switch ($Parameter.Type) {
            'String' {
                if ($Parameter.ParameterName -eq 'Uri') {
                    $baseStr = $baseStr -replace '\{URI\}', $Parameter.Value
                } elseif ($Parameter.ParameterName -eq 'Method') {
                    $baseStr = $baseStr -replace '\{METHOD\}', $Parameter.Value
                } else {
                    $baseStr += " -$($Parameter.ParameterName) '$($Parameter.Value)'"
                }
            }
            'Hashtable' {
                if ($hts.Keys -notcontains $Parameter.ParameterName) {
                    $hts[$Parameter.ParameterName] = @{}
                }
                foreach ($key in $Parameter.Value.Keys) {
                    $hts[$Parameter.ParameterName][$key] = $Parameter.Value[$key]
                }
            }
            'Switch' {
                $baseStr += " -$($Parameter.ParameterName):`$$($Parameter.Value.ToString().ToLower())"
            }
            'PSCredential' {
                $cred = $Parameter.Value
                if ($cred.GetNetworkCredential().Password.Length -gt 0) {
                    Write-Warning 'This output possibly includes a plaintext password, please treat this securely.'
                    $authStr = "`$cred = [PSCredential]::new('$($cred.UserName)', (ConvertTo-SecureString '$($cred.GetNetworkCredential().Password)' -AsPlainText -Force))`n"
                } else {
                    "`$cred = Get-Credential -UserName '$($cred.UserName)' -Message 'Please input the password for user $($cred.UserName)'`n"
                }
                $baseStr = $authStr + $baseStr + " -Credential `$cred"
            }
            default {
                $baseStr += " -$($Parameter.ParameterName) $($Parameter.Value)"
            }
        }
    }
    End {
        foreach ($key in $hts.Keys) {
            $baseStr += "-$($key) $(ConvertTo-HashtableString $hts[$key] -IsForm:($key -eq 'Form'))"
        }
        $baseStr
    }
}
Function ConvertTo-IRM {
    [OutputType([hashtable], ParameterSetName = 'splat')]
    [OutputType([string], ParameterSetName = 'string')]
    [cmdletbinding(
        DefaultParameterSetName = 'splat'
    )]
    param (
        [Parameter(
            Position = 0
        )]
        [string]$CurlCommand,
        [Parameter(
            ParameterSetName = 'asString'
        )]
        [switch]$CommandAsString,
        [switch]$CompressJSON
    )

    if ($CompressJSON.IsPresent) {
        Write-Warning 'The CompressJSON switch is no longer valid.'
    }

    if ($CommandAsString.IsPresent) {
        Invoke-Curl2PS -CurlString $CurlCommand -AsString
    } else {
        Invoke-Curl2PS -CurlString $CurlCommand
    }
}
Function Invoke-Curl2PS {
    [OutputType([hashtable], ParameterSetName = 'splat')]
    [OutputType([string], ParameterSetName = 'string')]
    [OutputType([Curl2PSParameterDefinition[]], ParameterSetName = 'raw')]
    [cmdletbinding(
        DefaultParameterSetName = 'splat'
    )]
    param (
        [Parameter(
            Mandatory,
            Position = 0
        )]
        [string]$CurlString,
        [Parameter(
            ParameterSetName = 'string'
        )]
        [switch]$AsString,
        [Parameter(
            ParameterSetName = 'raw'
        )]
        [switch]$Raw
    )
    if ($CurlString -match "\n") {
        $arr = $CurlString -split "\n"
        $CurlString = ($arr | ForEach-Object { $_.TrimEnd('\').Trim() }) -join ' '
    }

    $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."
    }
    [Curl2PSParameterDefinition[]]$parameters = 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]
            }
            # grab the value
            $paramValue = $splitParams[$x + 1]
            foreach ($paramName in $paramNames) {
                ConvertTo-Curl2PSParameter -ParamName $paramName -ParamValue $paramValue
            }
        } elseif ($splitParams[$x].Trim() -match '^https?\:\/\/') {
            # the url in curl is the last parameter, so we need to check if it is a valid URL
            [System.Uri]$uri = $splitParams[$x]
            if ($uri.UserInfo.Length -gt 0) {

                ConvertTo-Curl2PSParameter -ParamName 'u' -ParamValue $uri.UserInfo

                [System.Uri]$uri = $uri.OriginalString -replace "$($uri.UserInfo)@", ''
            }
            [Curl2PSParameterDefinition]@{
                Type          = 'String'
                ParameterName = 'Uri'
                Value         = $uri.OriginalString
            } 
        }
    }

    # if no explicit method, assume GET
    if ($parameters.ParameterName -notcontains 'Method') {
        $parameters += [Curl2PSParameterDefinition]@{
            Type          = 'String'
            ParameterName = 'Method'
            Value         = 'Get'
        }
    }

    if ($PSCmdlet.ParameterSetName -eq 'splat') {
        # generate a splat representation of the parameters
        $parameters | ConvertTo-Curl2PSSplat
    } elseif ($PSCmdlet.ParameterSetName -eq 'string') {
        # generate a string representation of the Invoke-RestMethod command
        $parameters | ConvertTo-Curl2PSString
    } elseif ($PSCmdlet.ParameterSetName -eq 'raw') {
        $parameters
    }
}
$script:config = @{
    ParameterTransformers = @{
        "H"        = [Curl2PSParameterTransformer]@{
            ParameterName = "Headers"
            Description   = "Headers are passed to curl as name:value and Invoke-RestMethod takes them as a hashtable."
            Type          = "Hashtable"
            Value         = {
                $split = ($args[0].Split(':') -replace '\\"', '"')
                @{
                    ($split[0].Trim()) = (($split[1..$($split.count)] -join ':').Trim())
                }
            }
        }
        "header"   = "H"
        "X"        = [Curl2PSParameterTransformer]@{
            ParameterName = "Method"
            Description   = "Method is simply a string."
            Type          = "String"
            Value         = {
                $args[0].Trim()
            }
        }
        "request"  = "X"
        "d"        = [Curl2PSParameterTransformer]@{
            ParameterName = "Body"
            Description   = "Body is a string, some curl json escapes the double quote, so that is removed."
            Type          = "String"
            Value         = {
                $args[0].Trim() -replace '\\"', '"'
            }
        }
        "data"     = "d"
        "url"      = [Curl2PSParameterTransformer]@{
            ParameterName = "Uri"
            Description   = "Uri is simply a string."
            Type          = "String"
            Value         = {
                $args[0].Trim()
            }
        }
        "k"        = [Curl2PSParameterTransformer]@{
            MinimumVersion = "6.0"
            ParameterName  = "SkipCertificateCheck"
            Description    = "If -k or --insecure is present, -SkipCertificateCheck is always true."
            Type           = "Switch"
            Value          = {
                $true
            }
        }
        "insecure" = "k"
        "v"        = [Curl2PSParameterTransformer]@{
            ParameterName = "Verbose"
            Description   = "If -v or --verbose is present, use -Verbose in Invoke-RestMethod."
            Type          = "Switch"
            Value         = {
                $true
            }
        }
        "verbose"  = "v"
        "u"        = @(
            [Curl2PSParameterTransformer]@{
                ParameterName = "Headers"
                Description   = "Supported in all versions of PowerShell, we can convert basic auth to an Authorization header and pass that."
                Type          = "Hashtable"
                Value         = {
                    $encodedAuth = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($args[0]))
                    @{
                        Authorization = "Basic $encodedAuth"
                    }
                }
            },
            [Curl2PSParameterTransformer]@{
                MinimumVersion       = "7.0"
                ParameterName        = "Credential"
                Description          = "Starting in PowerShell 7.0 (unsure on version), basic auth can be passed using a combination of -Credential and '-Authentication Basic'"
                Type                 = "PSCredential"
                Value                = {
                    $user = $args[0]
                    if ($user -like '*:*') {
                        $split = $user.Split(':')
                        if ($split[1].Length -gt 0) {
                            [pscredential]::new($split[0], (ConvertTo-SecureString $split[1] -AsPlainText -Force))
                        } else {
                            [pscredential]::new($split[0], [securestring]::new())
                        }
                    } else {
                        Write-Warning "Unable to handle the user authentication value. Unrecognized format."
                    }
                }
                AdditionalParameters = @{
                    ParameterName = "Authentication"
                    Type          = "String"
                    Value         = {
                        "Basic"
                    }
                }
            }
        )
        "user"     = "u"
        "F"        = [Curl2PSParameterTransformer]@{
            MinimumVersion = "7.0"
            ParameterName  = "Form"
            Description    = "Form is passed as '-F name=value' in curl and needs to be converted to a hashtable for PowerShell."
            Type           = "Hashtable"
            Value          = {
                $ht = @{}
                $formData = $args[0].TrimStart('"').TrimEnd('"')
                $split = $formData.Split('=')
                $split[1] = $split[1] -replace '@', 'Get-Item '
                if ($split[1] -like '{*}') {
                    $ht[$split[0]] = $split[1] | ConvertFrom-Json -AsHashtable
                } else {
                    $ht[$split[0]] = $split[1]
                }
                $ht
            }
            Warning        = "Form support needs testing! If this works or not, please share your feedback: https://github.com/theposhwolf/curl2ps/issues"
        }
        "form"     = "F"
    }
    Headers               = @{
        "Content-Type" = @{
            MinimumVersion = "7.0"
            ParameterName  = "ContentType"
            Type           = "String"
        }
    }
}