Skip to content

Commit

Permalink
Merge d3f656c into 3558e00
Browse files Browse the repository at this point in the history
  • Loading branch information
Badgerati committed Mar 13, 2020
2 parents 3558e00 + d3f656c commit 2e362df
Show file tree
Hide file tree
Showing 10 changed files with 365 additions and 12 deletions.
118 changes: 118 additions & 0 deletions docs/Tutorials/Compression/Requests.md
@@ -0,0 +1,118 @@
# Requests

You can send Requests to your Pode server that use compression on the payload, such as a JSON payload compressed via GZip.

Pode supports the following compression methods:

* gzip
* deflate

There are a number of ways you can specify the compression type, and these are defined below. When your request uses compression, Pode will first decompress the payload, and then attempt to parse it if needed.

## Request

The most common way is to define the a request's compression type in the request's headers. The header to use depends on which web-server type you're using:

| Server Type | Header |
| ----------- | ------ |
| HttpListener | X-Transfer-Encoding |
| Pode | Transfer-Encoding |

HttpListener is the default web server, unless you specify `-Type Pode` on `Start-PodeServer`. The reason for HttpListener using a slightly different header to the normal, is because HttpListener doesn't properly support compression; it will error with a 501 if you set the `Transfer-Encoding` header to either `gzip` or `deflate`.

!!! note
The Pode server does also support `X-Transfer-Encoding`, but it will check `Transfer-Encoding` first.

## Route

Like content types, you can force a Route to use a specific transfer encoding by using the `-TransferEncoding` parameter on [`Add-PodeRoute`](../../../Functions/Routes/Add-PodeRoute). If specified, Pode will use this compression type to decompress the payload regardless if the header is present or not.

```powershell
Add-PodeRoute -Method Get -Path '/' -TransferEncoding gzip -ScriptBlock {
# logic
}
```

## Configuration

Using the `server.psd1` configuration file, you can define a default transfer encoding to use for every route, or you can define patterns to match multiple route paths to set transfer encodings on mass.

### Default

To define a default transfer encoding for everything, you can use the following configuration:

```powershell
@{
Web = @{
TransferEncoding = @{
Default = "gzip"
}
}
}
```

### Route Patterns

You can define patterns to match multiple route paths, and any route that matches (when created) will have the appropriate transfer encoding set.

For example, the following configuration in your `server.psd1` would bind all `/api` routes to `gzip`, and then all `/status` routes to `deflate`:

```powershell
@{
Web = @{
TransferEncoding = @{
Routes = @{
"/api/*" = "gzip"
"/status/*" = "deflate"
}
}
}
}
```

## Precedence

The transfer encoding that will be used is determined by the following order:

1. Being defined on the Route.
2. The Route matches a pattern defined in the configuration file.
3. A default transfer encoding is defined in the configuration file.
4. The transfer encoding is supplied on the web request.

## Example

The following is an example of sending a `gzip` encoded payload to some `/ping` route:

```powershell
# get the JSON message in bytes
$data = @{
Name = "Deepthought"
Age = 42
}
$message = ($data | ConvertTo-Json)
$bytes = [System.Text.Encoding]::UTF8.GetBytes($message)
# compress the message using gzip
$ms = New-Object -TypeName System.IO.MemoryStream
$gzip = New-Object System.IO.Compression.GZipStream($ms, [IO.Compression.CompressionMode]::Compress, $true)
$gzip.Write($bytes, 0, $bytes.Length)
$gzip.Close()
$ms.Position = 0
# Pode Server
Invoke-RestMethod `
-Method Post `
-Uri 'http://localhost:8080/ping' `
-Body $ms.ToArray() `
-TransferEncoding gzip `
-ContentType application/json
# HttpListener Server
Invoke-RestMethod `
-Method Post `
-Uri 'http://localhost:8080/ping' `
-Body $ms.ToArray() `
-ContentType application/json `
-Headers @{'X-Transfer-Encoding' = 'gzip'}
```
21 changes: 21 additions & 0 deletions examples/web-gzip-request.ps1
@@ -0,0 +1,21 @@
$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path)
Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop

# or just:
# Import-Module Pode

# create a server, and start listening on port 8085
Start-PodeServer -Threads 2 -Type Pode {

# listen on localhost:8085
Add-PodeEndpoint -Address * -Port 8085 -Protocol Http

New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging

# GET request that recieves gzip'd json
Add-PodeRoute -Method Post -Path '/users' -ScriptBlock {
param($e)
Write-PodeJsonResponse -Value $e.Data
}

}
16 changes: 16 additions & 0 deletions src/Private/Context.ps1
Expand Up @@ -242,6 +242,11 @@ function New-PodeContext
$ctx.Server.Middleware = @()
$ctx.Server.BodyParsers = @{}

# common support values
$ctx.Server.Supported = @{
TransferEncodings = @('gzip', 'deflate', 'x-gzip')
}

# endware that needs to run
$ctx.Server.Endware = @()

Expand Down Expand Up @@ -453,6 +458,10 @@ function Set-PodeWebConfiguration
Default = $Configuration.ContentType.Default
Routes = @{}
}
TransferEncoding = @{
Default = $Configuration.TransferEncoding.Default
Routes = @{}
}
}

# setup content type route patterns for forced content types
Expand All @@ -462,6 +471,13 @@ function Set-PodeWebConfiguration
$Context.Server.Web.ContentType.Routes[$_pattern] = $_type
}

# setup transfer encoding route patterns for forced transfer encodings
$Configuration.TransferEncoding.Routes.Keys | Where-Object { ![string]::IsNullOrWhiteSpace($_) } | ForEach-Object {
$_type = $Configuration.TransferEncoding.Routes[$_]
$_pattern = (Convert-PodePathPatternToRegex -Path $_ -NotSlashes)
$Context.Server.Web.TransferEncoding.Routes[$_pattern] = $_type
}

# setup content type route patterns for error pages
$Configuration.ErrorPages.Routes.Keys | Where-Object { ![string]::IsNullOrWhiteSpace($_) } | ForEach-Object {
$_type = $Configuration.ErrorPages.Routes[$_]
Expand Down
91 changes: 84 additions & 7 deletions src/Private/Helpers.ps1
Expand Up @@ -1040,6 +1040,59 @@ function Test-PodeValidNetworkFailure
return ($null -ne $match)
}

function Get-PodeTransferEncoding
{
param(
[Parameter()]
[string]
$TransferEncoding,

[switch]
$ThrowError
)

# return if no encoding
if ([string]::IsNullOrWhiteSpace($TransferEncoding)) {
return [string]::Empty
}

# get the encoding, and store invalid ones for later
$parts = @($TransferEncoding -isplit ',').Trim()
$normal = @('chunked', 'identity')
$invalid = @()

# if we see a supported one, return immediately. else build up invalid one
foreach ($part in $parts) {
if ($part -iin $PodeContext.Server.Supported.TransferEncodings) {
if ($part -ieq 'x-gzip') {
return 'gzip'
}

return $part
}

if ($part -iin $normal) {
continue
}

$invalid += $part
}

# if we have any invalid, throw a 415 error
if ($invalid.Length -gt 0) {
if ($ThrowError) {
$err = [System.Net.Http.HttpRequestException]::new()
$err.Data.Add('PodeStatusCode', 415)
throw $err
}

return $invalid[0]
}

# else, we're safe
return [string]::Empty
}

function Get-PodeEncodingFromContentType
{
param(
Expand All @@ -1052,10 +1105,10 @@ function Get-PodeEncodingFromContentType
return [System.Text.Encoding]::UTF8
}

$parts = $ContentType -isplit ';'
$parts = @($ContentType -isplit ';').Trim()

foreach ($part in $parts) {
if ($part.Trim().StartsWith('charset')) {
if ($part.StartsWith('charset')) {
return [System.Text.Encoding]::GetEncoding(($part -isplit '=')[1].Trim())
}
}
Expand All @@ -1071,7 +1124,11 @@ function ConvertFrom-PodeRequestContent

[Parameter()]
[string]
$ContentType
$ContentType,

[Parameter()]
[string]
$TransferEncoding
)

# get the requests content type and boundary
Expand Down Expand Up @@ -1102,11 +1159,31 @@ function ConvertFrom-PodeRequestContent
}

'pode' {
$Content = $Request.Body.Value
# if the request is compressed, attempt to uncompress it
if (![string]::IsNullOrWhiteSpace($TransferEncoding)) {
# create a compressed stream to decompress the req bytes
$ms = New-Object -TypeName System.IO.MemoryStream
$ms.Write($Request.Body.Bytes, 0, $Request.Body.Bytes.Length)
$ms.Seek(0, 0) | Out-Null
$stream = New-Object "System.IO.Compression.$($TransferEncoding)Stream"($ms, [System.IO.Compression.CompressionMode]::Decompress)

# read the decompressed bytes
$Content = Read-PodeStreamToEnd -Stream $stream -Encoding $Encoding
}
else {
$Content = $Request.Body.Value
}
}

default {
$Content = Read-PodeStreamToEnd -Stream $Request.InputStream -Encoding $Encoding
# if the request is compressed, attempt to uncompress it
if (![string]::IsNullOrWhiteSpace($TransferEncoding)) {
$stream = New-Object "System.IO.Compression.$($TransferEncoding)Stream"($Request.InputStream, [System.IO.Compression.CompressionMode]::Decompress)
$Content = Read-PodeStreamToEnd -Stream $stream -Encoding $Encoding
}
else {
$Content = Read-PodeStreamToEnd -Stream $Request.InputStream -Encoding $Encoding
}
}
}

Expand Down Expand Up @@ -1195,8 +1272,8 @@ function ConvertFrom-PodeRequestContent
$type = ConvertFrom-PodeBytesToString -Bytes $Lines[$bIndex+2] -Encoding $Encoding -RemoveNewLine

$Result.Files.Add($fields.filename, @{
'ContentType' = (@($type -isplit ':')[1].Trim());
'Bytes' = $null;
ContentType = @($type -isplit ':')[1].Trim()
Bytes = $null
})

$bytes = @()
Expand Down
1 change: 1 addition & 0 deletions src/Private/Mappers.ps1
Expand Up @@ -172,6 +172,7 @@ function Get-PodeContentType
'.gsm' { return 'audio/x-gsm' }
'.gtar' { return 'application/x-gtar' }
'.gz' { return 'application/x-gzip' }
'.gzip' { return 'application/x-gzip' }
'.h' { return 'text/plain' }
'.hdf' { return 'application/x-hdf' }
'.hdml' { return 'text/x-hdml' }
Expand Down
11 changes: 9 additions & 2 deletions src/Private/Middleware.ps1
Expand Up @@ -198,6 +198,11 @@ function Get-PodeRouteValidateMiddleware
$e.ContentType = $route.ContentType
}

# override the transfer encoding from the route if it's not empty
if (![string]::IsNullOrWhiteSpace($route.TransferEncoding)) {
$e.TransferEncoding = $route.TransferEncoding
}

# set the content type for any pages for the route if it's not empty
$e.ErrorType = $route.ErrorType

Expand All @@ -214,7 +219,7 @@ function Get-PodeBodyMiddleware

try {
# attempt to parse that data
$result = ConvertFrom-PodeRequestContent -Request $e.Request -ContentType $e.ContentType
$result = ConvertFrom-PodeRequestContent -Request $e.Request -ContentType $e.ContentType -TransferEncoding $e.TransferEncoding

# set session data
$e.Data = $result.Data
Expand Down Expand Up @@ -277,7 +282,9 @@ function Get-PodeCookieMiddleware

$value = [string]::Empty
if ($atoms.Length -gt 1) {
$value = ($atoms[1..($atoms.Length - 1)] -join ([string]::Empty))
foreach ($atom in $atoms[1..($atoms.Length - 1)]) {
$value += $atom
}
}

$e.Cookies[$atoms[0]] = [System.Net.Cookie]::new($atoms[0], $value)
Expand Down
8 changes: 8 additions & 0 deletions src/Private/PodeServer.ps1
Expand Up @@ -156,6 +156,7 @@ function Invoke-PodeSocketHandler
Streamed = $true
Route = $null
Timestamp = [datetime]::UtcNow
TransferEncoding = $null
}

# set pode in server response header
Expand Down Expand Up @@ -206,12 +207,19 @@ function Invoke-PodeSocketHandler
Protocol = $req_info.Protocol
ProtocolVersion = ($req_info.Protocol -isplit '/')[1]
ContentEncoding = (Get-PodeEncodingFromContentType -ContentType $req_info.Headers['Content-Type'])
TransferEncoding = (Get-PodeTransferEncoding -TransferEncoding $req_info.Headers['Transfer-Encoding'] -ThrowError)
}

# if the transfer encoding is empty, attempt X-Transfer-Encoding for support from HttpListener
if ([string]::IsNullOrWhiteSpace($WebEvent.Request.TransferEncoding)) {
$WebEvent.Request.TransferEncoding = (Get-PodeTransferEncoding -TransferEncoding $req_info.Headers['X-Transfer-Encoding'] -ThrowError)
}

$WebEvent.Path = $req_info.Uri.AbsolutePath
$WebEvent.Method = $req_info.Method.ToLowerInvariant()
$WebEvent.Endpoint = $req_info.Headers['Host']
$WebEvent.ContentType = $req_info.Headers['Content-Type']
$WebEvent.TransferEncoding = $WebEvent.Request.TransferEncoding

# parse the query string and convert it to a hashtable
$WebEvent.Query = (Convert-PodeQueryStringToHashTable -Uri $req_info.Query)
Expand Down

0 comments on commit 2e362df

Please sign in to comment.