diff --git a/docs/Tutorials/Middleware/Types/Security.md b/docs/Tutorials/Middleware/Types/Security.md
index 69f3d60f7..0adee8603 100644
--- a/docs/Tutorials/Middleware/Types/Security.md
+++ b/docs/Tutorials/Middleware/Types/Security.md
@@ -2,7 +2,7 @@
The security headers middleware runs at the beginning of every request, and if any security headers are defined they will be added onto the response.
-The following headers are currently supported, but you can add custom header values:
+The following headers are currently supported, but you can add custom header values via [`Add-PodeSecurityHeader`](../../../../Functions/Security/Add-PodeSecurityHeader) for any missing:
* Access-Control-Max-Age
* Access-Control-Allow-Methods
@@ -13,6 +13,7 @@ The following headers are currently supported, but you can add custom header val
* Cross-Origin-Opener-Policy
* Strict-Transport-Security
* Content-Security-Policy
+* Content-Security-Policy-Report-Only
* X-XSS-Protection
* Permissions-Policy
* X-Frame-Options
@@ -37,21 +38,21 @@ To remove all configured values, use [`Remove-PodeSecurity`](../../../../Functio
The following values are used for each header when the `Simple` type is supplied:
-| Name | Value |
-| ---- | ----- |
-| Access-Control-Max-Age | 7200 |
-| Access-Control-Allow-Origin | * |
-| Access-Control-Allow-Methods | * |
-| Access-Control-Allow-Headers | * |
-| Cross-Origin-Embedder-Policy | require-corp |
-| Cross-Origin-Resource-Policy | same-origin |
-| Cross-Origin-Opener-Policy | same-origin |
-| Content-Security-Policy | default-src 'self' |
-| X-XSS-Protection | 0 |
-| Permissions-Policy | accelerometer=(), autoplay=(self), camera=(), display-capture=(self), fullscreen=(self), geolocation=(self), gyroscope=(self), magnetometer=(self), microphone=(), payment=(), picture-in-picture=(self), sync-xhr=(), usb=() |
-| X-Frame-Options | SAMEORIGIN |
-| X-Content-Type-Options | nosniff |
-| Referred-Policy | strict-origin |
+| Name | Value |
+| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| Access-Control-Max-Age | 7200 |
+| Access-Control-Allow-Origin | * |
+| Access-Control-Allow-Methods | * |
+| Access-Control-Allow-Headers | * |
+| Cross-Origin-Embedder-Policy | require-corp |
+| Cross-Origin-Resource-Policy | same-origin |
+| Cross-Origin-Opener-Policy | same-origin |
+| Content-Security-Policy | default-src 'self' |
+| X-XSS-Protection | 0 |
+| Permissions-Policy | accelerometer=(), autoplay=(self), camera=(), display-capture=(self), fullscreen=(self), geolocation=(self), gyroscope=(self), magnetometer=(self), microphone=(), payment=(), picture-in-picture=(self), sync-xhr=(), usb=() |
+| X-Frame-Options | SAMEORIGIN |
+| X-Content-Type-Options | nosniff |
+| Referred-Policy | strict-origin |
The Server header is also hidden.
@@ -59,22 +60,22 @@ The Server header is also hidden.
The following values are used for each header when the `Strict` type is supplied:
-| Name | Value |
-| ---- | ----- |
-| Access-Control-Max-Age | 7200 |
-| Access-Control-Allow-Methods | * |
-| Access-Control-Allow-Origin | * |
-| Access-Control-Allow-Headers | * |
-| Cross-Origin-Embedder-Policy | require-corp |
-| Cross-Origin-Resource-Policy | same-origin |
-| Cross-Origin-Opener-Policy | same-origin |
-| Strict-Transport-Security | max-age=31536000; includeSubDomains |
-| Content-Security-Policy | default-src 'self' |
-| X-XSS-Protection | 0 |
-| Permissions-Policy | accelerometer=(), autoplay=(self), camera=(), display-capture=(self), fullscreen=(self), geolocation=(self), gyroscope=(self), magnetometer=(self), microphone=(), payment=(), picture-in-picture=(self), sync-xhr=(), usb=() |
-| X-Frame-Options | DENY |
-| X-Content-Type-Options | nosniff |
-| Referred-Policy | no-referrer |
+| Name | Value |
+| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| Access-Control-Max-Age | 7200 |
+| Access-Control-Allow-Methods | * |
+| Access-Control-Allow-Origin | * |
+| Access-Control-Allow-Headers | * |
+| Cross-Origin-Embedder-Policy | require-corp |
+| Cross-Origin-Resource-Policy | same-origin |
+| Cross-Origin-Opener-Policy | same-origin |
+| Strict-Transport-Security | max-age=31536000; includeSubDomains |
+| Content-Security-Policy | default-src 'self' |
+| X-XSS-Protection | 0 |
+| Permissions-Policy | accelerometer=(), autoplay=(self), camera=(), display-capture=(self), fullscreen=(self), geolocation=(self), gyroscope=(self), magnetometer=(self), microphone=(), payment=(), picture-in-picture=(self), sync-xhr=(), usb=() |
+| X-Frame-Options | DENY |
+| X-Content-Type-Options | nosniff |
+| Referred-Policy | no-referrer |
The Server header is also hidden.
@@ -153,12 +154,14 @@ The following functions exist:
* [`Set-PodeSecurityContentSecurityPolicy`](../../../../Functions/Security/Set-PodeSecurityContentSecurityPolicy)
* [`Remove-PodeSecurityContentSecurityPolicy`](../../../../Functions/Security/Remove-PodeSecurityContentSecurityPolicy)
-The `Content-Security-Policy` header controls a whitelist of approved sourced from which the browser can load resoures. For example:
+The `Content-Security-Policy` header controls a whitelist of approved sources from which the browser can load resources. For example:
```powershell
Set-PodeSecurityContentSecurityPolicy -Default 'self' -Image 'self', 'data'
```
+By supplying the `-ReportOnly` switch, the `Content-Security-Policy-Report-Only` header will be used instead.
+
### Permissions Policy
The following functions exist:
diff --git a/docs/Tutorials/WebEvent.md b/docs/Tutorials/WebEvent.md
index b90920a1c..aa8568129 100644
--- a/docs/Tutorials/WebEvent.md
+++ b/docs/Tutorials/WebEvent.md
@@ -70,44 +70,44 @@ These are the properties available for `$WebEvent.Request`
!!! warning
Changing properties on this object could cause errors, unwanted behaviour, or a full server crash. Only edit them if you know what you're doing. Same for calling any methods.
-| Name | Type | Description | Example |
-| ----------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- |
-| Address | string | The address being used by the Request. This will favour hostnames over IPs | - |
-| AllowClientCertificate | bool | Whether Pode should expect, and process, and client certificates | - |
-| AwaitingBody | bool | If the request is chunked, this flags if Pode is still awaiting for the whole body to be sent | - |
-| Body | string | The textually encoded version of the RawBody | - |
-| Certificate | X509Certificate | The certificate being used for SSL connections. Usually defined from [`Add-PodeEndpoint`](../../Functions/Core/Add-PodeEndpoint) | - |
-| ClientCertificate | X509Certificate2 | If being used, the client certificate supplied on the Request | - |
-| ClientCertificateErrors | SslPolicyErrors | Contains any errors that might have occurred while validating the client certificate. Pode ignores these by default, so they will need checking the [Client Certificate Authenication](../Authentication/Methods/ClientCertificate) | - |
-| CloseImmediately | bool | Whether this Request should be closed immediately. Used internally, you'll likely never see this set to true | - |
-| ContentEncoding | Encoding | The encoding used for the content | UTF8 |
-| ContentLength | int | The size of the content in the Request's payload | - |
-| ContentType | string | The type of content being supplied in the Request's payload | application/json |
-| Error | HttpRequestException | Contains any errors thrown internally, that will be bubbled back up to Pode for logging | |
-| Form | PodeForm | Contains information about any form elements sent in the Request | - |
-| Headers | Hashtable | A collectio of every header sent in the Request | - |
-| Host | string | The ip/hostname used for the Request | 127.0.0.1, example.com |
-| HttpMethod | string | The HTTP method of the current Request | GET, POST, etc. |
-| InputStream | Stream | The stream used to read the inbound connection's data | - |
-| IsAborted | bool | Whether the Request should be aborted. Used internally, you'll likely never see this set to true | - |
-| IsDisposed | bool | Whether the current Request is disposed | - |
-| IsProcessable | bool | Whether this Request should be processed. Used internally, you'll likely never see this set to false | - |
-| IsSsl | bool | Whether the connection is currently over SSL or not | - |
-| KeepAlive | bool | Whether the connection should be kept alive, or terminated after use | - |
-| LocalEndPoint | EndPoint | Details about the local connection | - |
-| Protocol | string | The protocol type being used | HTTP/1.1 |
-| Protocols | SslProtocols | The SSL protocols allowed to be used for connections | SSL3, TLS1.2 |
-| ProtocolVersion | string | The protocol version of the protocol type | 1.1 |
-| QueryString | NameValueCollection | A collection of the key/values supplied on the Request's query string | - |
-| RawBody | byte[] | The raw bytes of the Request's payload | - |
-| RemoteEndPoint | EndPoint | Details about the remote connection | - |
-| Scheme | string | The connection scheme being used | HTTP, HTTPS, etc. |
-| SslUpgraded | bool | Whether this connection has been upgraded to SSL. Used for implicit connections | - |
-| TlsMode | PodeTlsMode | Whether the connection is using implicit or explicit TLS | - |
-| TransferEncoding | string | The transfer encoding used for the content | gzip, chunked, identity |
-| Url | Uri | The whole Request URL that was made | http://example.com?name=value |
-| UrlReferrer | string | The referred of the Request | - |
-| UserAgent | string | The user agent of where the Request originated | - |
+| Name | Type | Description | Example |
+| ----------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------- |
+| Address | string | The address being used by the Request. This will favour hostnames over IPs | - |
+| AllowClientCertificate | bool | Whether Pode should expect, and process, and client certificates | - |
+| AwaitingBody | bool | If the request is chunked, this flags if Pode is still awaiting for the whole body to be sent | - |
+| Body | string | The textually encoded version of the RawBody | - |
+| Certificate | X509Certificate | The certificate being used for SSL connections. Usually defined from [`Add-PodeEndpoint`](../../Functions/Core/Add-PodeEndpoint) | - |
+| ClientCertificate | X509Certificate2 | If being used, the client certificate supplied on the Request | - |
+| ClientCertificateErrors | SslPolicyErrors | Contains any errors that might have occurred while validating the client certificate. Pode ignores these by default, so they will need checking the [Client Certificate Authentication](../Authentication/Methods/ClientCertificate) | - |
+| CloseImmediately | bool | Whether this Request should be closed immediately. Used internally, you'll likely never see this set to true | - |
+| ContentEncoding | Encoding | The encoding used for the content | UTF8 |
+| ContentLength | int | The size of the content in the Request's payload | - |
+| ContentType | string | The type of content being supplied in the Request's payload | application/json |
+| Error | PodeRequestException | Contains any errors thrown internally, that will be bubbled back up to Pode for logging | |
+| Form | PodeForm | Contains information about any form elements sent in the Request | - |
+| Headers | Hashtable | A collection of every header sent in the Request | - |
+| Host | string | The ip/hostname used for the Request | 127.0.0.1, example.com |
+| HttpMethod | string | The HTTP method of the current Request | GET, POST, etc. |
+| InputStream | Stream | The stream used to read the inbound connection's data | - |
+| IsAborted | bool | Whether the Request should be aborted. Used internally, you'll likely never see this set to true | - |
+| IsDisposed | bool | Whether the current Request is disposed | - |
+| IsProcessable | bool | Whether this Request should be processed. Used internally, you'll likely never see this set to false | - |
+| IsSsl | bool | Whether the connection is currently over SSL or not | - |
+| KeepAlive | bool | Whether the connection should be kept alive, or terminated after use | - |
+| LocalEndPoint | EndPoint | Details about the local connection | - |
+| Protocol | string | The protocol type being used | HTTP/1.1 |
+| Protocols | SslProtocols | The SSL protocols allowed to be used for connections | SSL3, TLS1.2 |
+| ProtocolVersion | string | The protocol version of the protocol type | 1.1 |
+| QueryString | NameValueCollection | A collection of the key/values supplied on the Request's query string | - |
+| RawBody | byte[] | The raw bytes of the Request's payload | - |
+| RemoteEndPoint | EndPoint | Details about the remote connection | - |
+| Scheme | string | The connection scheme being used | HTTP, HTTPS, etc. |
+| SslUpgraded | bool | Whether this connection has been upgraded to SSL. Used for implicit connections | - |
+| TlsMode | PodeTlsMode | Whether the connection is using implicit or explicit TLS | - |
+| TransferEncoding | string | The transfer encoding used for the content | gzip, chunked, identity |
+| Url | Uri | The whole Request URL that was made | http://example.com?name=value |
+| UrlReferrer | string | The referred of the Request | - |
+| UserAgent | string | The user agent of where the Request originated | - |
### Response
@@ -130,7 +130,7 @@ These are the properties available for `$WebEvent.Response`
| SendChunked | bool | Whether or not the response should be sent back in chunks | - |
| Sent | bool | Whether or not this Response has already been sent tot the client | - |
| StatusCode | int | The status code to send back to the client | 200, 401, 500, etc. |
-| StatusDescription | string | The statuc description to send back, based on the status code | OK, Not Found, etc. |
+| StatusDescription | string | The status description to send back, based on the status code | OK, Not Found, etc. |
## Customise
diff --git a/docs/release-notes.md b/docs/release-notes.md
index 969c65a6d..c97ffdb58 100644
--- a/docs/release-notes.md
+++ b/docs/release-notes.md
@@ -1,5 +1,39 @@
# Release Notes
+## v2.11.1
+
+Date: 3rd November 2024
+
+```plain
+### Enhancements
+* #1409: Adds new/missing CSP parameters in security headers
+
+### Bugs
+* #1407: 'Initialize-PodeOpenApiTable' fails to initialize OpenAPI table when 'DefaultDefinitionTag' param is Null (thanks @mdaneri!)
+* #1413: Fixes Test-PodeCacheStorage being called with incorrect -Key parameter (thanks @willgladstone!)
+* #1418: Fix for ConvertTo-PodeYaml failing, when passed object contains a Key named "Count" (thanks @mdaneri!)
+* #1420: Fix for OpenAPI Component Properties, Server Endpoint, and Path Filtering Issues (thanks @mdaneri!)
+* #1423: Fix OpenAPI Route Path Conversion for Placeholder Unescaping and Relocate Function (#1422) (thanks @mdaneri!)
+* #1424: Refactor Byte Conversion Functions (thanks @mdaneri!)
+* #1424: Remove Duplicate Task Process Definitions (thanks @mdaneri!)
+* #1427: Fixes "collection modified" error when cleaning up sessions
+* #1430: Add-PodeOAComponentRequestBody now supports hashtable/ordered input (thanks @mdaneri!)
+* #1430: Fix OpenAPI Route parameters so they respect the DefinitionTag (thanks @mdaneri!)
+* #1430: More appropriately order responses, parameters, and request bodies in OpenAPI (thanks @mdaneri!)
+* #1430: Order Routes in OpenAPI specification by creation, and HTTP methods in a more conventional order (thanks @mdaneri!)
+* #1434: Fixes the clean-up of old log files (thanks @DoLearnWhileAlive!)
+* #1436: Add -AllowNonStandardBody Parameter to Enable Request Bodies for Non-Standard HTTP Methods in OpenAPI (thanks @mdaneri!)
+* #1438: Fixes the unnecessary Error Logging on HTTP Request Timeouts (thanks @mdaneri!)
+* #1439: Don't setup the Caching Housekeeper Timer in a Serverless context
+* #1440: Fixes error message appearing when using self-signed certificates on localhost
+
+### Security
+* #1428: Migrates Stream functions into the .NET Listener
+
+### Packaging
+* #1404: Add Pester Test to Check for Duplicate Function Definitions (thanks @mdaneri!)
+```
+
## v2.11.0
Date: 29th September 2024
diff --git a/examples/OpenApi-SimplePotato.ps1 b/examples/OpenApi-SimplePotato.ps1
new file mode 100644
index 000000000..0e3b412f6
--- /dev/null
+++ b/examples/OpenApi-SimplePotato.ps1
@@ -0,0 +1,97 @@
+<#
+.SYNOPSIS
+ Sets up a Pode server with OpenAPI documentation, request logging, and routes for handling 'potato' requests.
+
+.DESCRIPTION
+ This script configures a Pode server to listen on a specified port, enables both request and error logging,
+ and sets up OpenAPI documentation. It defines routes for fetching 'potato' data with responses in both
+ JSON and plain text. OpenAPI documentation is exposed via Swagger and other viewers.
+
+.EXAMPLE
+ ./PodeServer-OpenApi.ps1
+
+ Invoke-RestMethod -Uri http://localhost:8080/api/v4.2/potato -Method Get
+
+.LINK
+ https://github.com/Badgerati/Pode
+
+.NOTES
+ This is an example Pode server setup that demonstrates OpenAPI integration.
+ Author: Pode Team
+ License: MIT License
+#>
+
+# Try to import the Pode module from the source if available, otherwise use the installed version
+try {
+ # Determine the script path and Pode module path
+ $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path)
+ $podePath = Split-Path -Parent -Path $ScriptPath
+
+ # Import Pode from local source if available, otherwise from installed modules
+ if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) {
+ Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop
+ }
+ else {
+ Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop
+ }
+}
+catch {
+ throw
+}
+
+# Start the Pode server
+Start-PodeServer {
+
+ # Enable terminal logging for requests and errors
+ New-PodeLoggingMethod -Terminal | Enable-PodeRequestLogging
+ New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging
+
+ # Define the endpoint for the server
+ Add-PodeEndpoint -Address 127.0.0.1 -Port 8080 -Protocol Http
+
+ # Initialize OpenAPI with basic configuration
+ Enable-PodeOpenApi -Path '/docs/openapi' -DefinitionTag 'potato' -DisableMinimalDefinitions
+
+ # Set OpenAPI info
+ Add-PodeOAInfo -Title 'Potato sample - OpenAPI 3.0' `
+ -Version 1.0.17 `
+ -Description 'This is a simple "potato" API with the OpenAPI 3.0 specification.' -DefinitionTag 'potato'
+
+ # Define the OpenAPI server endpoint
+ Add-PodeOAServerEndpoint -url '/api' -Description 'default endpoint' -DefinitionTag 'potato'
+
+ # External documentation link for OpenAPI
+ $extDoc = New-PodeOAExternalDoc -Description 'Find out more about Swagger' -Url 'http://swagger.io'
+ $extDoc | Add-PodeOAExternalDoc
+
+ # Enable OpenAPI viewers
+ Enable-PodeOAViewer -Type Swagger -Path '/docs/swagger' -Title 'Swagger' -DefinitionTag 'potato'
+ Enable-PodeOAViewer -Bookmarks -Path '/docs' -Title 'Bookmark' -DefinitionTag 'potato'
+ Enable-PodeOAViewer -Editor -Path '/docs/editor' -Title 'Editor' -DefinitionTag 'potato'
+
+ # Select OpenAPI definition tag
+ Select-PodeOADefinition -tag 'potato' -ScriptBlock {
+
+ # Define routes within the '/api' group
+ Add-PodeRouteGroup -Path '/api' -Routes {
+
+ # JSON output route
+ Add-PodeRoute -Method Get -Path '/v4.2/:potato' -ScriptBlock {
+ Write-PodeJsonResponse -Value @{Potato = $WebEvent.Parameters['potato'] } -StatusCode 400
+ } -Passthru | Set-PodeOARouteInfo -Summary 'Json output' -Description 'Returns JSON response' -OperationId 'json' -Passthru | `
+ Set-PodeOARequest -PassThru -Parameters (
+ New-PodeOAStringProperty -Name 'potato' -Description 'Potato Name' -Required |
+ ConvertTo-PodeOAParameter -In Path -Required
+ )
+
+ # Plain text output route
+ Add-PodeRoute -Method Get -Path '/:potato' -ScriptBlock {
+ Write-PodeTextResponse -Value $WebEvent.Parameters['potato'] -StatusCode 200
+ } -Passthru | Set-PodeOARouteInfo -Summary 'Text output' -Description 'Returns plain text response' -OperationId 'text' -Passthru | `
+ Set-PodeOARequest -PassThru -Parameters (
+ New-PodeOAStringProperty -Name 'potato' -Description 'Potato Name' -Required |
+ ConvertTo-PodeOAParameter -In Path -Required
+ )
+ }
+ }
+}
diff --git a/examples/OpenApi-TuttiFrutti.ps1 b/examples/OpenApi-TuttiFrutti.ps1
index 358906a22..778597aef 100644
--- a/examples/OpenApi-TuttiFrutti.ps1
+++ b/examples/OpenApi-TuttiFrutti.ps1
@@ -92,7 +92,6 @@ Some useful links:
Add-PodeOAServerEndpoint -url '/api/v3' -Description 'default endpoint' -DefinitionTag 'v3', 'v3.1'
-
Add-PodeOAInfo -Title 'Swagger Petstore - OpenAPI 3.0' -Version 1.0.17 -Description $InfoDescription -TermsOfService 'http://swagger.io/terms/' -LicenseName 'Apache 2.0' `
-LicenseUrl 'http://www.apache.org/licenses/LICENSE-2.0.html' -ContactName 'API Support' -ContactEmail 'apiteam@swagger.io' -DefinitionTag 'v3'
@@ -303,7 +302,7 @@ Some useful links:
New-PodeOAStringProperty -Name 'message' | New-PodeOAIntProperty -Name 'code'-Format Int32 | New-PodeOAObjectProperty | Add-PodeOAComponentSchema -Name 'ErrorModel'
- Add-PodeRoute -PassThru -Method Get -Path '/peta/:id' -ScriptBlock {
+ Add-PodeRoute -PassThru -Method Get -Path '/peta/:id' -OADefinitionTag 'v3.1' -ScriptBlock {
Write-PodeJsonResponse -Value (Get-Pet -Id $WebEvent.Parameters['id']) -StatusCode 200
} |
Set-PodeOARouteInfo -Summary 'Find pets by ID' -Description 'Returns pets based on ID' -OperationId 'getPetsById' -PassThru |
@@ -421,9 +420,9 @@ Some useful links:
New-PodeOAExample -ContentType 'text/plain' -Name 'user' -Summary 'User Example in Plain text' -ExternalValue 'http://foo.bar/examples/user-example.txt' |
New-PodeOAExample -ContentType '*/*' -Name 'user' -Summary 'User example in other forma' -ExternalValue 'http://foo.bar/examples/user-example.whatever'
Select-PodeOADefinition -Tag 'v3' -Scriptblock {
- Add-PodeRouteGroup -Path '/api/v4' -Routes {
+ Add-PodeRouteGroup -Path '/api/v3/private' -Routes {
- Add-PodeRoute -PassThru -Method Put -Path '/pat/:petId' -ScriptBlock {
+ Add-PodeRoute -PassThru -Method Put,Post -Path '/pat/:petId' -ScriptBlock {
$JsonPet = ConvertTo-Json $WebEvent.data
if ( Update-Pet -Id $WebEvent.Parameters['petId'] -Data $JsonPet) {
Write-PodeJsonResponse -Value @{} -StatusCode 200
@@ -431,7 +430,7 @@ Some useful links:
else {
Write-PodeJsonResponse -Value @{} -StatusCode 405
}
- } | Set-PodeOARouteInfo -Summary 'Updates a pet in the store with form data' -Tags 'pet' -OperationId 'updatePasdadaetWithForm' -PassThru |
+ } | Set-PodeOARouteInfo -Summary 'Updates a pet in the store with form data' -Tags 'pet' -PassThru |
Set-PodeOARequest -Parameters @(
(New-PodeOAStringProperty -Name 'petId' -Description 'ID of pet that needs to be updated' | ConvertTo-PodeOAParameter -In Path -Required)
) -RequestBody (
@@ -441,6 +440,9 @@ Some useful links:
Add-PodeOAResponse -StatusCode 200 -Description 'Pet updated.' -Content (@{ 'application/json' = '' ; 'application/xml' = '' }) -PassThru |
Add-PodeOAResponse -StatusCode 405 -Description 'Method Not Allowed' -Content (@{ 'application/json' = '' ; 'application/xml' = '' })
+
+
+
Add-PodeRoute -PassThru -Method Put -Path '/paet/:petId' -ScriptBlock {
$JsonPet = ConvertTo-Json $WebEvent.data
if ( Update-Pet -Id $WebEvent.Parameters['id'] -Data $JsonPet) {
@@ -563,7 +565,7 @@ Some useful links:
} -PassThru | Set-PodeOARouteInfo -Summary 'Shutdown the server' -PassThru | Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation'
- Add-PodeRouteGroup -Path '/api/v3' -Routes {
+ Add-PodeRouteGroup -Path '/api/v3' -Routes {
#PUT
Add-PodeRoute -PassThru -Method Put -Path '/pet' -ScriptBlock {
$JsonPet = ConvertTo-Json $WebEvent.data
diff --git a/examples/PetStore/Petstore-OpenApi.ps1 b/examples/PetStore/Petstore-OpenApi.ps1
index bf2578bf5..ed78f7f96 100644
--- a/examples/PetStore/Petstore-OpenApi.ps1
+++ b/examples/PetStore/Petstore-OpenApi.ps1
@@ -92,15 +92,20 @@ Start-PodeServer -Threads 1 -ScriptBlock {
Initialize-Pet
Initialize-Order
Initialize-Users
- # attempt to re-initialise the state (will do nothing if the file doesn't exist)
+ # attempt to re-initialise the state (will do nothing if the file doesn't exist)
Restore-PodeState -Path $script:PetDataJson
}
# Configure Pode server endpoints
if ((Get-PodeConfig).Protocol -eq 'Https') {
- $Certificate = Join-Path -Path $CertsPath -ChildPath (Get-PodeConfig).Certificate
- $CertificateKey = Join-Path -Path $CertsPath -ChildPath (Get-PodeConfig).CertificateKey
- Add-PodeEndpoint -Address (Get-PodeConfig).Address -Port (Get-PodeConfig).RestFulPort -Protocol Https -Certificate $Certificate -CertificateKey $CertificateKey -CertificatePassword (Get-PodeConfig).CertificatePassword -Default
+ if ((Get-PodeConfig).SelfSignedCertificate) {
+ Add-PodeEndpoint -Address (Get-PodeConfig).Address -Port (Get-PodeConfig).RestFulPort -Protocol Https -SelfSigned -Default
+ }
+ else {
+ $Certificate = Join-Path -Path $CertsPath -ChildPath (Get-PodeConfig).Certificate
+ $CertificateKey = Join-Path -Path $CertsPath -ChildPath (Get-PodeConfig).CertificateKey
+ Add-PodeEndpoint -Address (Get-PodeConfig).Address -Port (Get-PodeConfig).RestFulPort -Protocol Https -Certificate $Certificate -CertificateKey $CertificateKey -CertificatePassword (Get-PodeConfig).CertificatePassword -Default
+ }
}
else {
Add-PodeEndpoint -Address (Get-PodeConfig).Address -Port (Get-PodeConfig).RestFulPort -Protocol Http -Default
diff --git a/examples/PetStore/Petstore-OpenApiMultiTag.ps1 b/examples/PetStore/Petstore-OpenApiMultiTag.ps1
index 932b3b0aa..5ffcea4e7 100644
--- a/examples/PetStore/Petstore-OpenApiMultiTag.ps1
+++ b/examples/PetStore/Petstore-OpenApiMultiTag.ps1
@@ -88,9 +88,16 @@ Start-PodeServer -Threads 1 -ScriptBlock {
if ((Get-PodeConfig).Protocol -eq 'Https') {
- $Certificate = Join-Path -Path $CertsPath -ChildPath (Get-PodeConfig).Certificate
- $CertificateKey = Join-Path -Path $CertsPath -ChildPath (Get-PodeConfig).CertificateKey
- Add-PodeEndpoint -Address (Get-PodeConfig).Address -Port (Get-PodeConfig).RestFulPort -Protocol Https -Certificate $Certificate -CertificateKey $CertificateKey -CertificatePassword (Get-PodeConfig).CertificatePassword -Default
+ if ((Get-PodeConfig).SelfSignedCertificate) {
+ Add-PodeEndpoint -Address (Get-PodeConfig).Address -Port (Get-PodeConfig).RestFulPort -Protocol Https -SelfSigned -Default -Name 'endpoint_v3'
+ Add-PodeEndpoint -Address (Get-PodeConfig).Address -Port ((Get-PodeConfig).RestFulPort + 1) -Protocol Https -SelfSigned -Default -Name 'endpoint_v3.1'
+ }
+ else {
+ $Certificate = Join-Path -Path $CertsPath -ChildPath (Get-PodeConfig).Certificate
+ $CertificateKey = Join-Path -Path $CertsPath -ChildPath (Get-PodeConfig).CertificateKey
+ Add-PodeEndpoint -Address (Get-PodeConfig).Address -Port (Get-PodeConfig).RestFulPort -Protocol Https -Certificate $Certificate -CertificateKey $CertificateKey -CertificatePassword (Get-PodeConfig).CertificatePassword -Default -Name 'endpoint_v3'
+ Add-PodeEndpoint -Address (Get-PodeConfig).Address -Port ((Get-PodeConfig).RestFulPort + 1) -Protocol Https -Certificate $Certificate -CertificateKey $CertificateKey -CertificatePassword (Get-PodeConfig).CertificatePassword -Default -Name 'endpoint_v3.1'
+ }
}
else {
Add-PodeEndpoint -Address (Get-PodeConfig).Address -Port (Get-PodeConfig).RestFulPort -Protocol Http -Default -Name 'endpoint_v3'
@@ -134,7 +141,6 @@ Some useful links:
Add-PodeOAServerEndpoint -url '/api/v3' -Description 'V3 Endpoint' -DefinitionTag 'v3.0.3'
Add-PodeOAServerEndpoint -url '/api/v3' -Description 'V3.1 Endpoint' -DefinitionTag 'v3.1'
- Add-PodeOAServerEndpoint -url '/api' -Description 'Default Endpoint' -DefinitionTag 'v3.0.3','v3.1'
#OpenAPI 3.0
Enable-PodeOAViewer -Type Swagger -Path '/docs/swagger' -DefinitionTag 'v3.0.3'
@@ -202,8 +208,6 @@ Some useful links:
New-PodeAuthScheme -Basic -Realm 'PetStore' | Add-PodeAuth -Name 'Basic' -Sessionless -ScriptBlock {
param($username, $password)
- write-host $username
- write-host $password
# here you'd check a real user storage, this is just for example
if ($username -eq 'morty' -and $password -eq 'pickle') {
diff --git a/examples/PetStore/server.psd1 b/examples/PetStore/server.psd1
index 844eed9d3..de82d77dc 100644
--- a/examples/PetStore/server.psd1
+++ b/examples/PetStore/server.psd1
@@ -1,17 +1,18 @@
@{
- RestFulPort = 8081
- Protocol = 'Http'
- Address = 'localhost'
- Certificate = 'Certificate.pem'
- CertificateKey = 'CertificateKey.key'
- CertificatePassword = 'password@01'
- SessionsTtlMinutes = 360
- Server = @{
- Timeout = 60
- BodySize = 100MB
+ RestFulPort = 8081
+ Protocol = 'Http'
+ Address = 'localhost'
+ Certificate = 'Certificate.pem'
+ CertificateKey = 'CertificateKey.key'
+ CertificatePassword = 'password@01'
+ SessionsTtlMinutes = 360
+ SelfSignedCertificate = $false
+ Server = @{
+ Timeout = 60
+ BodySize = 100MB
}
- Web=@{
- OpenApi=@{
+ Web = @{
+ OpenApi = @{
DefaultDefinitionTag = 'v3.0.3'
}
}
diff --git a/examples/Web-AuthBasic.ps1 b/examples/Web-AuthBasic.ps1
index 25815359a..e0a886a02 100644
--- a/examples/Web-AuthBasic.ps1
+++ b/examples/Web-AuthBasic.ps1
@@ -75,6 +75,7 @@ Start-PodeServer -Threads 2 {
return @{ Message = 'Invalid details supplied' }
}
+
# POST request to get current user (since there's no session, authentication will always happen)
Add-PodeRoute -Method Post -Path '/users' -Authentication 'Validate' -ScriptBlock {
Write-PodeJsonResponse -Value @{
diff --git a/examples/Web-PagesHttps.ps1 b/examples/Web-PagesHttps.ps1
index 8c6dabcc3..155b16c8b 100644
--- a/examples/Web-PagesHttps.ps1
+++ b/examples/Web-PagesHttps.ps1
@@ -50,6 +50,8 @@ catch { throw }
# create a server, flagged to generate a self-signed cert for dev/testing
Start-PodeServer {
+ New-PodeLoggingMethod -Terminal | Enable-PodeRequestLogging
+ New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging
# bind to ip/port and set as https with self-signed cert
switch ($CertType) {
diff --git a/examples/Web-UsePodeAuth.ps1 b/examples/Web-UsePodeAuth.ps1
new file mode 100644
index 000000000..d72c8ff20
--- /dev/null
+++ b/examples/Web-UsePodeAuth.ps1
@@ -0,0 +1,93 @@
+<#
+.SYNOPSIS
+ A PowerShell script to set up a Pode server with API key authentication and various route configurations.
+
+.DESCRIPTION
+ Sets up a Pode server that listens on a specified port, enables request and error logging, and configures API key
+ authentication. The script uses the `Use-PodeAuth` function to load and apply the authentication defined in
+ `./auth/SampleAuth.ps1`. The authentication is applied globally to the API routes using `Add-PodeAuthMiddleware`.
+ The script defines a route to fetch a list of users, which requires authentication using a specified location
+ for the API key (Header, Query, or Cookie).
+
+.PARAMETER Location
+ Specifies where the API key is expected. Valid values are 'Header', 'Query', and 'Cookie'. Default is 'Header'.
+
+.EXAMPLE
+ To run the sample:
+
+ ```powershell
+ ./Web-UsePodeAuth.ps1
+ ```
+ Then, make a request to get users with an API key:
+
+ ```powershell
+ Invoke-RestMethod -Uri 'http://localhost:8081/api/users' -Method Get -Headers @{ 'X-API-KEY' = 'test-api-key' }
+ ```
+
+.LINK
+ https://github.com/Badgerati/Pode/blob/develop/examples/Web-UsePodeAuth.ps1
+
+.NOTES
+ The `Use-PodeAuth` function is used to load the authentication script located at `./auth/SampleAuth.ps1`. The
+ authentication is then enforced using `Add-PodeAuthMiddleware` to protect the `/api/*` routes.
+
+.NOTES
+ Author: Pode Team
+ License: MIT License
+#>
+
+param(
+ [Parameter()]
+ [ValidateSet('Header', 'Query', 'Cookie')]
+ [string]
+ $Location = 'Header'
+)
+
+try {
+ # Determine the script path and Pode module path
+ $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path)
+ $podePath = Split-Path -Parent -Path $ScriptPath
+
+ # Import the Pode module from the source path if it exists, otherwise from installed modules
+ if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) {
+ Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop
+ }
+ else {
+ Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop
+ }
+}
+catch { throw }
+
+# or just:
+# Import-Module Pode
+
+# create a server, and start listening on port 8081
+Start-PodeServer -Threads 2 {
+
+ # listen on localhost:8081
+ Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http
+
+ New-PodeLoggingMethod -File -Name 'requests' | Enable-PodeRequestLogging
+ New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging
+
+ Use-PodeAuth
+
+ Add-PodeAuthMiddleware -Name 'globalAuthValidation' -Authentication 'Validate' -Route '/api/*'
+
+ # GET request to get list of users (since there's no session, authentication will always happen)
+ Add-PodeRoute -Method Get -Path '/api/users' -ScriptBlock {
+ Write-PodeJsonResponse -Value @{
+ Users = @(
+ @{
+ Name = 'Deep Thought'
+ Age = 42
+ },
+ @{
+ Name = 'Leeroy Jenkins'
+ Age = 1337
+ }
+ )
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/examples/auth/SampleAuth.ps1 b/examples/auth/SampleAuth.ps1
new file mode 100644
index 000000000..0c2f230cc
--- /dev/null
+++ b/examples/auth/SampleAuth.ps1
@@ -0,0 +1,17 @@
+# setup bearer auth
+New-PodeAuthScheme -ApiKey -Location $Location | Add-PodeAuth -Name 'Validate' -Sessionless -ScriptBlock {
+ param($key)
+
+ # here you'd check a real user storage, this is just for example
+ if ($key -ieq 'test-api-key') {
+ return @{
+ User = @{
+ ID = 'M0R7Y302'
+ Name = 'Morty'
+ Type = 'Human'
+ }
+ }
+ }
+
+ return $null
+}
\ No newline at end of file
diff --git a/pode.build.ps1 b/pode.build.ps1
index 2d2be077d..c3f5eaeb2 100644
--- a/pode.build.ps1
+++ b/pode.build.ps1
@@ -291,7 +291,7 @@ Task PrintChecksum {
Task ChocoDeps -If (Test-PodeBuildIsWindows) {
if (!(Test-PodeBuildCommand 'choco')) {
Set-ExecutionPolicy Bypass -Scope Process -Force
- Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
+ Invoke-Expression ([System.Net.WebClient]::new().DownloadString('https://chocolatey.org/install.ps1'))
}
}
@@ -799,29 +799,41 @@ Task SetupPowerShell {
osx = "powershell-$($PowerShellVersion)-$($os)-$($arch).tar.gz"
})[$os]
- # build the blob name
- $blobName = "v$($PowerShellVersion -replace '\.', '-')"
+ # build the URL
+ $urls = @{
+ Old = "https://pscoretestdata.blob.core.windows.net/v$($PowerShellVersion -replace '\.', '-')/$($packageName)"
+ New = "https://powershellinfraartifacts-gkhedzdeaghdezhr.z01.azurefd.net/install/v$($PowerShellVersion)/$($packageName)"
+ }
# download the package to a temp location
$outputFile = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath $packageName
$downloadParams = @{
- Uri = "https://pscoretestdata.blob.core.windows.net/$($blobName)/$($packageName)"
+ Uri = $urls.New
OutFile = $outputFile
ErrorAction = 'Stop'
}
- Write-Host "Downloading $($packageName) from $($downloadParams.Uri)"
Write-Host "Output file: $($outputFile)"
- # retry the download 3 times, with a sleep of 10s between each attempt
+ # retry the download 6 times, with a sleep of 10s between each attempt, and altering between old and new URLs
$counter = 0
$success = $false
do {
try {
$counter++
- Write-Host "Attempt $($counter) of 3"
+ Write-Host "Attempt $($counter) of 6"
+
+ # use new URL for odd attempts, and old URL for even attempts
+ if ($counter % 2 -eq 0) {
+ $downloadParams.Uri = $urls.Old
+ }
+ else {
+ $downloadParams.Uri = $urls.New
+ }
+ # download the package
+ Write-Host "Attempting download of $($packageName) from $($downloadParams.Uri)"
Invoke-WebRequest @downloadParams
$success = $true
@@ -829,11 +841,11 @@ Task SetupPowerShell {
}
catch {
$success = $false
- if ($counter -ge 3) {
- throw "Failed to download PowerShell package after 3 attempts. Error: $($_.Exception.Message)"
+ if ($counter -ge 6) {
+ throw "Failed to download PowerShell package after 6 attempts. Error: $($_.Exception.Message)"
}
- Start-Sleep -Seconds 10
+ Start-Sleep -Seconds 5
}
} while (!$success)
diff --git a/src/Listener/PodeCompressionType.cs b/src/Listener/PodeCompressionType.cs
new file mode 100644
index 000000000..459470bc8
--- /dev/null
+++ b/src/Listener/PodeCompressionType.cs
@@ -0,0 +1,8 @@
+namespace Pode
+{
+ public enum PodeCompressionType
+ {
+ Gzip,
+ Deflate
+ }
+}
\ No newline at end of file
diff --git a/src/Listener/PodeContext.cs b/src/Listener/PodeContext.cs
index 52af8cd66..48be3955c 100644
--- a/src/Listener/PodeContext.cs
+++ b/src/Listener/PodeContext.cs
@@ -9,56 +9,96 @@
namespace Pode
{
+ ///
+ /// Represents the context for a Pode request, including state management, request handling, and response processing.
+ ///
public class PodeContext : PodeProtocol, IDisposable
{
+ // Unique identifier for the context.
public string ID { get; private set; }
+
+ // Represents the incoming request.
public PodeRequest Request { get; private set; }
+
+ // Represents the outgoing response.
public PodeResponse Response { get; private set; }
+
+ // Listener associated with the context.
public PodeListener Listener { get; private set; }
+
+ // The socket for the current connection.
public Socket Socket { get; private set; }
+
+ // The Pode socket associated with the context.
public PodeSocket PodeSocket { get; private set; }
+
+ // Timestamp when the context was created.
public DateTime Timestamp { get; private set; }
+
+ // Data storage for request-specific metadata.
public Hashtable Data { get; private set; }
+
+ // The name of the endpoint associated with the socket.
public string EndpointName => PodeSocket.Name;
+ // Object used for thread-safety.
private object _lockable = new object();
+ // State of the context.
private PodeContextState _state;
public PodeContextState State
{
get => _state;
private set
{
- if (_state != PodeContextState.Timeout || value == PodeContextState.Closed)
+ // Only allow changing from Timeout if transitioning to Closed or Error.
+ if (_state != PodeContextState.Timeout || value == PodeContextState.Closed || value == PodeContextState.Error)
{
_state = value;
}
}
}
+ // Determines if the context should be closed immediately.
public bool CloseImmediately => State == PodeContextState.Error
|| State == PodeContextState.Closing
|| State == PodeContextState.Timeout
|| Request.CloseImmediately;
+ // Determines if the context is associated with a WebSocket.
public new bool IsWebSocket => base.IsWebSocket || (IsUnknown && PodeSocket.IsWebSocket);
public bool IsWebSocketUpgraded => IsWebSocket && Request is PodeSignalRequest;
+
+ // Determines if the context is associated with SMTP.
public new bool IsSmtp => base.IsSmtp || (IsUnknown && PodeSocket.IsSmtp);
+
+ // Determines if the context is associated with HTTP.
public new bool IsHttp => base.IsHttp || (IsUnknown && PodeSocket.IsHttp);
+ // Strongly typed request properties for different protocols.
public PodeSmtpRequest SmtpRequest => (PodeSmtpRequest)Request;
public PodeHttpRequest HttpRequest => (PodeHttpRequest)Request;
public PodeSignalRequest SignalRequest => (PodeSignalRequest)Request;
+ // Determines if the connection should be kept alive.
public bool IsKeepAlive => (Request.IsKeepAlive && Response.SseScope != PodeSseScope.Local) || Response.SseScope == PodeSseScope.Global;
- public bool IsErrored => State == PodeContextState.Error || State == PodeContextState.SslError;
+
+ // Flags for different context states.
+ public bool IsErrored => State == PodeContextState.Error;
public bool IsTimeout => State == PodeContextState.Timeout;
public bool IsClosed => State == PodeContextState.Closed;
public bool IsOpened => State == PodeContextState.Open;
+ // Token and timer for managing request timeouts.
public CancellationTokenSource ContextTimeoutToken { get; private set; }
private Timer TimeoutTimer;
+ ///
+ /// Initializes a new PodeContext with the given socket, PodeSocket, and listener.
+ ///
+ /// The socket used for the current connection.
+ /// The PodeSocket managing this context.
+ /// The PodeListener associated with this context.
public PodeContext(Socket socket, PodeSocket podeSocket, PodeListener listener)
{
ID = PodeHelpers.NewGuid();
@@ -72,37 +112,64 @@ public PodeContext(Socket socket, PodeSocket podeSocket, PodeListener listener)
State = PodeContextState.New;
}
+ ///
+ /// Initializes the request and response for the context.
+ ///
+ /// A Task representing the async operation.
public async Task Initialise()
{
NewResponse();
await NewRequest().ConfigureAwait(false);
}
+ ///
+ /// Callback for handling request timeouts.
+ ///
+ /// An object containing state information for the callback.
private void TimeoutCallback(object state)
{
- if (Response.SseEnabled || Request.IsWebSocket)
+ try
{
- return;
- }
+ PodeHelpers.WriteErrorMessage("TimeoutCallback triggered", Listener, PodeLoggingLevel.Debug, this);
+
+ if (Response.SseEnabled || Request.IsWebSocket)
+ {
+ PodeHelpers.WriteErrorMessage("Timeout ignored due to SSE/WebSocket", Listener, PodeLoggingLevel.Debug, this);
+ return;
+ }
- ContextTimeoutToken.Cancel();
- State = PodeContextState.Timeout;
+ PodeHelpers.WriteErrorMessage($"Request timeout reached: {Listener.RequestTimeout} seconds", Listener, PodeLoggingLevel.Warning, this);
- Response.StatusCode = 408;
- Request.Error = new HttpRequestException("Request timeout");
- Request.Error.Data.Add("PodeStatusCode", 408);
+ ContextTimeoutToken.Cancel();
+ State = PodeContextState.Timeout;
- Dispose();
+ Response.StatusCode = 408;
+ Request.Error = new PodeRequestException($"Request timeout [ContextId: {this.ID}]", 408);
+
+ Dispose();
+ PodeHelpers.WriteErrorMessage($"Request timeout reached: Dispose", Listener, PodeLoggingLevel.Debug, this);
+ }
+ catch (Exception ex)
+ {
+ PodeHelpers.WriteErrorMessage($"Exception in TimeoutCallback: {ex}", Listener, PodeLoggingLevel.Error);
+ }
}
+ ///
+ /// Creates a new response object for the current context.
+ ///
private void NewResponse()
{
Response = new PodeResponse(this);
}
+ ///
+ /// Creates a new request object based on the socket type.
+ ///
+ /// A Task representing the async operation.
private async Task NewRequest()
{
- // create a new request
+ // Create a new request based on the socket type.
switch (PodeSocket.Type)
{
case PodeProtocolType.Smtp:
@@ -118,28 +185,13 @@ private async Task NewRequest()
break;
}
- // attempt to open the request stream
- try
- {
- await Request.Open(CancellationToken.None).ConfigureAwait(false);
- State = PodeContextState.Open;
- }
- catch (AggregateException aex)
- {
- PodeHelpers.HandleAggregateException(aex, Listener, PodeLoggingLevel.Debug, true);
- State = Request.InputStream == default(Stream)
- ? PodeContextState.Error
- : PodeContextState.SslError;
- }
- catch (Exception ex)
- {
- PodeHelpers.WriteException(ex, Listener, PodeLoggingLevel.Debug);
- State = Request.InputStream == default(Stream)
- ? PodeContextState.Error
- : PodeContextState.SslError;
- }
+ // Attempt to open the request stream.
+ await Request.Open(CancellationToken.None).ConfigureAwait(false);
+ State = Request.State == PodeStreamState.Open
+ ? PodeContextState.Open
+ : PodeContextState.Error;
- // if request is SMTP or TCP, send ACK if available
+ // If the request is SMTP or TCP, send acknowledgment if available.
if (IsOpened)
{
if (PodeSocket.IsSmtp)
@@ -153,6 +205,9 @@ private async Task NewRequest()
}
}
+ ///
+ /// Sets the context type based on the request type and socket type.
+ ///
private void SetContextType()
{
if (!IsUnknown && !(base.IsHttp && Request.IsWebSocket))
@@ -160,70 +215,68 @@ private void SetContextType()
return;
}
- // depending on socket type, either:
+ // Depending on socket type, set the appropriate protocol type.
switch (PodeSocket.Type)
{
- // - only allow smtp
case PodeProtocolType.Smtp:
if (!Request.IsSmtp)
{
- throw new HttpRequestException("Request is not Smtp");
+ throw new PodeRequestException("Request is not Smtp", 422);
}
-
Type = PodeProtocolType.Smtp;
break;
- // - only allow tcp
case PodeProtocolType.Tcp:
if (!Request.IsTcp)
{
- throw new HttpRequestException("Request is not Tcp");
+ throw new PodeRequestException("Request is not Tcp", 422);
}
-
Type = PodeProtocolType.Tcp;
break;
- // - only allow http
case PodeProtocolType.Http:
if (Request.IsWebSocket)
{
- throw new HttpRequestException("Request is not Http");
+ throw new PodeRequestException("Request is not Http", 422);
}
-
Type = PodeProtocolType.Http;
break;
- // - only allow web-socket
case PodeProtocolType.Ws:
if (!Request.IsWebSocket)
{
- throw new HttpRequestException("Request is not for a WebSocket");
+ throw new PodeRequestException("Request is not for a WebSocket", 422);
}
-
Type = PodeProtocolType.Ws;
break;
- // - allow http and web-socket
case PodeProtocolType.HttpAndWs:
Type = Request.IsWebSocket ? PodeProtocolType.Ws : PodeProtocolType.Http;
break;
}
}
+ ///
+ /// Cancels the request timeout by disposing of the timeout timer.
+ ///
public void CancelTimeout()
{
TimeoutTimer.Dispose();
}
+ ///
+ /// Handles receiving data for the current request.
+ ///
+ /// A Task representing the async operation.
public async Task Receive()
{
try
{
- // start timeout
+ // Start timeout
ContextTimeoutToken = new CancellationTokenSource();
TimeoutTimer = new Timer(TimeoutCallback, null, Listener.RequestTimeout * 1000, Timeout.Infinite);
- // start receiving
+ // Start receiving data.
State = PodeContextState.Receiving;
try
{
@@ -232,7 +285,12 @@ public async Task Receive()
SetContextType();
await EndReceive(close).ConfigureAwait(false);
}
- catch (OperationCanceledException) { }
+ catch (OperationCanceledException ex) when (ContextTimeoutToken.IsCancellationRequested)
+ {
+ PodeHelpers.WriteErrorMessage("Request timed out during receive operation", Listener, PodeLoggingLevel.Warning, this);
+ State = PodeContextState.Timeout; // Explicitly set the state to Timeout
+ Request.Error = new PodeRequestException("Request timed out", ex, 408);
+ }
}
catch (Exception ex)
{
@@ -242,6 +300,11 @@ public async Task Receive()
}
}
+ ///
+ /// Ends the receiving process and handles the context based on whether it should be closed.
+ ///
+ /// Whether the context should be closed after receiving.
+ /// A Task representing the async operation.
public async Task EndReceive(bool close)
{
State = close ? PodeContextState.Closing : PodeContextState.Received;
@@ -253,6 +316,9 @@ public async Task EndReceive(bool close)
await PodeSocket.HandleContext(this).ConfigureAwait(false);
}
+ ///
+ /// Starts receiving data by creating a new response and setting the state.
+ ///
public void StartReceive()
{
NewResponse();
@@ -261,34 +327,39 @@ public void StartReceive()
PodeHelpers.WriteErrorMessage($"Socket listening", Listener, PodeLoggingLevel.Verbose, this);
}
+ ///
+ /// Upgrades the connection to a WebSocket.
+ ///
+ /// The client identifier for the WebSocket connection.
+ /// A Task representing the async operation.
+ /// Thrown if the request cannot be upgraded to a WebSocket.
public async Task UpgradeWebSocket(string clientId = null)
{
PodeHelpers.WriteErrorMessage($"Upgrading Websocket", Listener, PodeLoggingLevel.Verbose, this);
- // websocket
if (!IsWebSocket)
{
- throw new HttpRequestException("Cannot upgrade a non-websocket request");
+ throw new PodeRequestException("Cannot upgrade a non-websocket request", 412);
}
- // set a default clientId
+ // Set a default clientId if none is provided.
if (string.IsNullOrWhiteSpace(clientId))
{
clientId = PodeHelpers.NewGuid();
}
- // set the status of the response
+ // Set the status of the response to indicate protocol switching.
Response.StatusCode = 101;
Response.StatusDescription = "Switching Protocols";
- // get the socket key from the request
+ // Get the socket key from the request.
var socketKey = $"{HttpRequest.Headers["Sec-WebSocket-Key"]}".Trim();
- // make the socket accept hash
+ // Create the socket accept hash.
var crypto = SHA1.Create();
var socketHash = Convert.ToBase64String(crypto.ComputeHash(System.Text.Encoding.UTF8.GetBytes($"{socketKey}{PodeHelpers.WEB_SOCKET_MAGIC_KEY}")));
- // compile the headers
+ // Compile headers for the response.
Response.Headers.Clear();
Response.Headers.Set("Connection", "Upgrade");
Response.Headers.Set("Upgrade", "websocket");
@@ -299,21 +370,29 @@ public async Task UpgradeWebSocket(string clientId = null)
Response.Headers.Set("X-Pode-ClientId", clientId);
}
- // send message to upgrade web socket
+ // Send response to upgrade to WebSocket.
await Response.Send().ConfigureAwait(false);
- // add open web socket to listener
+ // Add the upgraded WebSocket to the listener.
var signal = new PodeSignal(this, HttpRequest.Url.AbsolutePath, clientId);
Request = new PodeSignalRequest(HttpRequest, signal);
Listener.AddSignal(SignalRequest.Signal);
PodeHelpers.WriteErrorMessage($"Websocket upgraded", Listener, PodeLoggingLevel.Verbose, this);
}
+ ///
+ /// Disposes of the resources used by the context.
+ ///
public void Dispose()
{
- Dispose(Request.Error != default(HttpRequestException));
+ Dispose(Request.Error != default(PodeRequestException));
+ GC.SuppressFinalize(this);
}
+ ///
+ /// Disposes of the resources used by the context, with an option to force disposal.
+ ///
+ /// Whether to force the disposal of resources.
public void Dispose(bool force)
{
lock (_lockable)
@@ -324,36 +403,33 @@ public void Dispose(bool force)
if (IsClosed)
{
PodeSocket.RemovePendingSocket(Socket);
- Request.Dispose();
- Response.Dispose();
- ContextTimeoutToken.Dispose();
- TimeoutTimer.Dispose();
+ Request?.Dispose();
+ Response?.Dispose();
+ DisposeTimeoutResources();
return;
}
var _awaitingBody = false;
- // send the response and close, only close request if not keep alive
try
{
- // dispose timeout token
- ContextTimeoutToken.Dispose();
- TimeoutTimer.Dispose();
+ // Dispose timeout resources
+ DisposeTimeoutResources();
- // error or timeout?
+ // Set error status code if context is errored.
if (IsErrored)
{
- Response.StatusCode = 500;
+ Response.StatusCode = Request.IsAborted ? Request.Error.StatusCode : 500;
}
- // are we awaiting for more info?
+ // Determine if the HTTP request is awaiting more data.
if (IsHttp)
{
_awaitingBody = HttpRequest.AwaitingBody && !IsErrored && !IsTimeout;
}
- // only send a response if Http
- if (IsHttp && State != PodeContextState.SslError && !_awaitingBody)
+ // Send response if HTTP and not awaiting body.
+ if (IsHttp && Request.IsOpen && !_awaitingBody)
{
if (IsTimeout)
{
@@ -365,13 +441,13 @@ public void Dispose(bool force)
}
}
- // if it was smtp, and it was processable, RESET!
+ // Reset SMTP request if it was processable.
if (IsSmtp && Request.IsProcessable)
{
SmtpRequest.Reset();
}
- // dispose of request if not KeepAlive, and not waiting for body
+ // Dispose of request and response if not keep-alive or forced.
if (!_awaitingBody && (!IsKeepAlive || force))
{
State = PodeContextState.Closed;
@@ -389,19 +465,35 @@ public void Dispose(bool force)
Response.Dispose();
}
}
- catch { }
-
- // if keep-alive, or awaiting body, setup for re-receive
- if ((_awaitingBody || (IsKeepAlive && !IsErrored && !IsTimeout && !Response.SseEnabled)) && !force)
+ catch (Exception ex)
{
- PodeHelpers.WriteErrorMessage($"Re-receiving Request", Listener, PodeLoggingLevel.Verbose, this);
- StartReceive();
+ PodeHelpers.WriteException(ex, Listener, PodeLoggingLevel.Error);
}
- else
+ finally
{
- PodeSocket.RemovePendingSocket(Socket);
+ // Handle re-receiving or socket clean-up.
+ if ((_awaitingBody || (IsKeepAlive && !IsErrored && !IsTimeout && !Response.SseEnabled)) && !force)
+ {
+ PodeHelpers.WriteErrorMessage($"Re-receiving Request", Listener, PodeLoggingLevel.Verbose, this);
+ StartReceive();
+ }
+ else
+ {
+ PodeSocket.RemovePendingSocket(Socket);
+ }
}
}
}
+
+ ///
+ /// Disposes timeout-related resources.
+ ///
+ private void DisposeTimeoutResources()
+ {
+ ContextTimeoutToken?.Dispose();
+ TimeoutTimer?.Dispose();
+ ContextTimeoutToken = null;
+ TimeoutTimer = null;
+ }
}
}
\ No newline at end of file
diff --git a/src/Listener/PodeContextState.cs b/src/Listener/PodeContextState.cs
index 081c4bfa9..868cb0bad 100644
--- a/src/Listener/PodeContextState.cs
+++ b/src/Listener/PodeContextState.cs
@@ -9,7 +9,6 @@ public enum PodeContextState
Closing,
Closed,
Error,
- SslError,
Timeout
}
}
\ No newline at end of file
diff --git a/src/Listener/PodeEndpoint.cs b/src/Listener/PodeEndpoint.cs
index ceaf167d3..f316b1ac5 100644
--- a/src/Listener/PodeEndpoint.cs
+++ b/src/Listener/PodeEndpoint.cs
@@ -56,12 +56,7 @@ public void Listen()
public bool Accept(SocketAsyncEventArgs args)
{
- if (IsDisposed)
- {
- throw new ObjectDisposedException("PodeEndpoint disposed");
- }
-
- return Socket.AcceptAsync(args);
+ return IsDisposed ? throw new ObjectDisposedException("PodeEndpoint disposed") : Socket.AcceptAsync(args);
}
public void Dispose()
diff --git a/src/Listener/PodeForm.cs b/src/Listener/PodeForm.cs
index b740e06c6..e267e19b1 100644
--- a/src/Listener/PodeForm.cs
+++ b/src/Listener/PodeForm.cs
@@ -53,7 +53,7 @@ public static PodeForm Parse(byte[] bytes, string contentType, Encoding contentE
}
else
{
- throw new HttpRequestException("No multipart/form-data boundary found");
+ throw new PodeRequestException("No multipart/form-data boundary found");
}
// get the boundary start/end
@@ -91,7 +91,7 @@ private static PodeForm ParseHttp(PodeForm form, List lines, List b
currentLineIndex = boundaryLineIndexes[i] + 1;
// parse headers until we see a blank line
- while (!string.IsNullOrWhiteSpace((currentLine = GetLineString(lines[currentLineIndex], contentEncoding))))
+ while (!string.IsNullOrWhiteSpace(currentLine = GetLineString(lines[currentLineIndex], contentEncoding)))
{
currentLineIndex++;
@@ -107,13 +107,13 @@ private static PodeForm ParseHttp(PodeForm form, List lines, List b
currentLineIndex++;
// get the content disposition fields
- if (!headers.ContainsKey("Content-Disposition"))
+ if (!headers.TryGetValue("Content-Disposition", out string contentDispHeader))
{
- throw new HttpRequestException("No Content-Disposition found in multipart/form-data");
+ throw new PodeRequestException("No Content-Disposition found in multipart/form-data");
}
// foreach (var line in disposition.Split(';'))
- foreach (var line in headers["Content-Disposition"].Split(';'))
+ foreach (var line in contentDispHeader.Split(';'))
{
var atoms = line.Split('=');
if (atoms.Length == 2)
@@ -123,7 +123,7 @@ private static PodeForm ParseHttp(PodeForm form, List lines, List b
}
// is this just a regular data field?
- if (!fields.ContainsKey("filename"))
+ if (!fields.TryGetValue("filename", out string filenameField))
{
// add the data item as name=value
form.Data.Add(new PodeFormData(fields["name"], GetLineString(lines[currentLineIndex], contentEncoding)));
@@ -136,15 +136,15 @@ private static PodeForm ParseHttp(PodeForm form, List lines, List b
var currentData = form.Data.FirstOrDefault(x => x.Key == fields["name"]);
if (currentData == default(PodeFormData))
{
- form.Data.Add(new PodeFormData(fields["name"], fields["filename"]));
+ form.Data.Add(new PodeFormData(fields["name"], filenameField));
}
else
{
- currentData.AddValue(fields["filename"]);
+ currentData.AddValue(filenameField);
}
// do we actually have a filename?
- if (string.IsNullOrWhiteSpace(fields["filename"]))
+ if (string.IsNullOrWhiteSpace(filenameField))
{
continue;
}
@@ -173,7 +173,7 @@ private static PodeForm ParseHttp(PodeForm form, List lines, List b
}
// add a file item for filename=stream [+name/content-type]
- form.Files.Add(new PodeFormFile(fields["filename"], stream, fields["name"], headers["Content-Type"].Trim()));
+ form.Files.Add(new PodeFormFile(filenameField, stream, fields["name"], headers["Content-Type"].Trim()));
}
}
diff --git a/src/Listener/PodeHelpers.cs b/src/Listener/PodeHelpers.cs
index 44a7e8f95..657f57668 100644
--- a/src/Listener/PodeHelpers.cs
+++ b/src/Listener/PodeHelpers.cs
@@ -7,6 +7,8 @@
using System.Runtime.Versioning;
using System.Threading.Tasks;
using System.Threading;
+using System.Text;
+using System.IO.Compression;
namespace Pode
{
@@ -40,7 +42,7 @@ public static bool IsNetFramework
}
}
- public static void WriteException(Exception ex, PodeConnector connector = default(PodeConnector), PodeLoggingLevel level = PodeLoggingLevel.Error)
+ public static void WriteException(Exception ex, PodeConnector connector = default, PodeLoggingLevel level = PodeLoggingLevel.Error)
{
if (ex == default(Exception))
{
@@ -64,7 +66,7 @@ public static bool IsNetFramework
}
}
- public static void HandleAggregateException(AggregateException aex, PodeConnector connector = default(PodeConnector), PodeLoggingLevel level = PodeLoggingLevel.Error, bool handled = false)
+ public static void HandleAggregateException(AggregateException aex, PodeConnector connector = default, PodeLoggingLevel level = PodeLoggingLevel.Error, bool handled = false)
{
try
{
@@ -88,7 +90,7 @@ public static bool IsNetFramework
}
}
- public static void WriteErrorMessage(string message, PodeConnector connector = default(PodeConnector), PodeLoggingLevel level = PodeLoggingLevel.Error, PodeContext context = default(PodeContext))
+ public static void WriteErrorMessage(string message, PodeConnector connector = default, PodeLoggingLevel level = PodeLoggingLevel.Error, PodeContext context = default)
{
// do nothing if no message
if (string.IsNullOrWhiteSpace(message))
@@ -138,7 +140,11 @@ public static async Task WriteTo(MemoryStream stream, byte[] array, int startInd
// Perform the asynchronous write operation
if (count > 0)
{
+#if NETCOREAPP2_1_OR_GREATER
+ await stream.WriteAsync(array.AsMemory(startIndex, count), cancellationToken).ConfigureAwait(false);
+#else
await stream.WriteAsync(array, startIndex, count, cancellationToken).ConfigureAwait(false);
+#endif
}
}
@@ -189,7 +195,7 @@ public static List ConvertToByteLines(byte[] bytes)
{
var lines = new List();
var index = 0;
- var nextIndex = 0;
+ int nextIndex;
while ((nextIndex = Array.IndexOf(bytes, NEW_LINE_BYTE, index)) > 0)
{
@@ -212,5 +218,95 @@ public static List Subset(List list, int startIndex, int endIndex)
{
return Subset(list.ToArray(), startIndex, endIndex).ToList();
}
+
+ public static byte[] ConvertStreamToBytes(Stream stream)
+ {
+ // we need to copy the stream to a memory stream and then return the bytes
+ using (var memory = new MemoryStream())
+ {
+ stream.CopyTo(memory);
+ return memory.ToArray();
+ }
+ }
+
+ public static string ConvertBytesToString(byte[] bytes, bool removeNewLines = false)
+ {
+ // return empty string if no bytes
+ if (bytes == default(byte[]) || bytes.Length == 0)
+ {
+ return string.Empty;
+ }
+
+ // convert the bytes to a string
+ var str = Encoding.UTF8.GetString(bytes);
+
+ // remove new lines if needed
+ if (removeNewLines)
+ {
+ return str.Trim(NEW_LINE_ARRAY);
+ }
+
+ return str;
+ }
+
+ public static string ReadStreamToEnd(Stream stream, Encoding encoding = default)
+ {
+ // return empty string if no stream
+ if (stream == default(Stream))
+ {
+ return string.Empty;
+ }
+
+ // set the encoding if not provided
+ if (encoding == default(Encoding))
+ {
+ encoding = Encoding.UTF8;
+ }
+
+ // read the stream to the end
+ using (var reader = new StreamReader(stream, encoding))
+ {
+ return reader.ReadToEnd();
+ }
+ }
+
+ // decompress bytes into either a gzip or deflate stream, and return the string
+ public static string DecompressBytes(byte[] bytes, PodeCompressionType type, Encoding encoding = default)
+ {
+ var stream = CompressStream(new MemoryStream(bytes), type, CompressionMode.Decompress);
+ return ReadStreamToEnd(stream, encoding);
+ }
+
+ // compress bytes into either a gzip or deflate stream, and return the bytes
+ public static byte[] CompressBytes(byte[] bytes, PodeCompressionType type)
+ {
+ var ms = new MemoryStream();
+
+ using (var stream = CompressStream(ms, type, CompressionMode.Compress))
+ {
+ stream.Write(bytes, 0, bytes.Length);
+ }
+
+ ms.Position = 0;
+ return ms.ToArray();
+ }
+
+ // compress stream into either a gzip or deflate stream
+ public static Stream CompressStream(Stream stream, PodeCompressionType type, CompressionMode mode)
+ {
+ var leaveOpen = mode == CompressionMode.Compress;
+
+ switch (type)
+ {
+ case PodeCompressionType.Gzip:
+ return new GZipStream(stream, mode, leaveOpen);
+
+ case PodeCompressionType.Deflate:
+ return new DeflateStream(stream, mode, leaveOpen);
+
+ default:
+ return stream;
+ }
+ }
}
}
\ No newline at end of file
diff --git a/src/Listener/PodeHttpRequest.cs b/src/Listener/PodeHttpRequest.cs
index 1f1f42875..1742f0b17 100644
--- a/src/Listener/PodeHttpRequest.cs
+++ b/src/Listener/PodeHttpRequest.cs
@@ -59,8 +59,7 @@ public string Body
public override bool CloseImmediately
{
- get => string.IsNullOrWhiteSpace(HttpMethod)
- || (IsWebSocket && !HttpMethod.Equals("GET", StringComparison.InvariantCultureIgnoreCase));
+ get => !IsHttpMethodValid();
}
public override bool IsProcessable
@@ -106,7 +105,7 @@ protected override bool ValidateInput(byte[] bytes)
if (reqMeta.Length != 3)
{
- throw new HttpRequestException($"Invalid request line: {reqLine} [{reqMeta.Length}]");
+ throw new PodeRequestException($"Invalid request line: {reqLine} [{reqMeta.Length}]");
}
IsRequestLineValid = true;
@@ -166,7 +165,7 @@ protected override async Task Parse(byte[] bytes, CancellationToken cancel
// parse the body
await ParseBody(bytes, newline, bodyIndex, cancellationToken).ConfigureAwait(false);
- AwaitingBody = ContentLength > 0 && BodyStream.Length < ContentLength && Error == default(HttpRequestException);
+ AwaitingBody = ContentLength > 0 && BodyStream.Length < ContentLength && Error == default(PodeRequestException);
if (!AwaitingBody)
{
@@ -192,14 +191,14 @@ private int ParseHeaders(string[] reqLines)
var reqMeta = reqLines[0].Trim().Split(' ');
if (reqMeta.Length != 3)
{
- throw new HttpRequestException($"Invalid request line: {reqLines[0]} [{reqMeta.Length}]");
+ throw new PodeRequestException($"Invalid request line: {reqLines[0]} [{reqMeta.Length}]");
}
// http method
- HttpMethod = reqMeta[0].Trim();
+ HttpMethod = reqMeta[0].Trim().ToUpper();
if (!PodeHelpers.HTTP_METHODS.Contains(HttpMethod))
{
- throw new HttpRequestException($"Invalid request HTTP method: {HttpMethod}");
+ throw new PodeRequestException($"Invalid request HTTP method: {HttpMethod}", 405);
}
// query string
@@ -214,7 +213,7 @@ private int ParseHeaders(string[] reqLines)
Protocol = (reqMeta[2] ?? "HTTP/1.1").Trim();
if (!Protocol.StartsWith("HTTP/"))
{
- throw new HttpRequestException($"Invalid request version: {Protocol}");
+ throw new PodeRequestException($"Invalid request version: {Protocol}", 505);
}
ProtocolVersion = Protocol.Split('/')[1];
@@ -252,7 +251,7 @@ private int ParseHeaders(string[] reqLines)
// check the host header
if (string.IsNullOrWhiteSpace(Host) || !Context.PodeSocket.CheckHostname(Host))
{
- throw new HttpRequestException($"Invalid Host header: {Host}");
+ throw new PodeRequestException($"Invalid Host header: {Host}");
}
// build the URL
@@ -325,7 +324,7 @@ private async Task ParseBody(byte[] bytes, string newline, int start, Cancellati
// if chunked, and we have a content-length, fail
if (isChunked && ContentLength > 0)
{
- throw new HttpRequestException($"Cannot supply a Content-Length and a chunked Transfer-Encoding");
+ throw new PodeRequestException($"Cannot supply a Content-Length and a chunked Transfer-Encoding", 409);
}
// parse for chunked
@@ -378,9 +377,7 @@ private async Task ParseBody(byte[] bytes, string newline, int start, Cancellati
if (BodyStream.Length > Context.Listener.RequestBodySize)
{
AwaitingBody = false;
- var err = new HttpRequestException("Payload too large");
- err.Data.Add("PodeStatusCode", 413);
- throw err;
+ throw new PodeRequestException("Payload too large", 413);
}
}
@@ -389,6 +386,21 @@ public void ParseFormData()
Form = PodeForm.Parse(RawBody, ContentType, ContentEncoding);
}
+ public bool IsHttpMethodValid()
+ {
+ if (string.IsNullOrWhiteSpace(HttpMethod) || !PodeHelpers.HTTP_METHODS.Contains(HttpMethod))
+ {
+ return false;
+ }
+
+ if (IsWebSocket && HttpMethod != "GET")
+ {
+ return false;
+ }
+
+ return true;
+ }
+
public override void PartialDispose()
{
if (BodyStream != default(MemoryStream))
diff --git a/src/Listener/PodeItemQueue.cs b/src/Listener/PodeItemQueue.cs
index a5f23ba9a..bec11bc1e 100644
--- a/src/Listener/PodeItemQueue.cs
+++ b/src/Listener/PodeItemQueue.cs
@@ -20,11 +20,9 @@ public PodeItemQueue()
ProcessingItems = new List();
}
- public T Get(CancellationToken cancellationToken = default(CancellationToken))
+ public T Get(CancellationToken cancellationToken = default)
{
- var item = (cancellationToken == default(CancellationToken)
- ? Items.Take()
- : Items.Take(cancellationToken));
+ var item = Items.Take(cancellationToken == default ? CancellationToken.None : cancellationToken);
lock (ProcessingItems)
{
@@ -34,11 +32,11 @@ public PodeItemQueue()
return item;
}
- public Task GetAsync(CancellationToken cancellationToken = default(CancellationToken))
+ public Task GetAsync(CancellationToken cancellationToken = default)
{
- return (cancellationToken == default(CancellationToken)
+ return cancellationToken == default
? Task.Factory.StartNew(() => Get())
- : Task.Factory.StartNew(() => Get(cancellationToken), cancellationToken));
+ : Task.Factory.StartNew(() => Get(cancellationToken), cancellationToken);
}
public void Add(T item)
diff --git a/src/Listener/PodeListener.cs b/src/Listener/PodeListener.cs
index 64d8c1a06..46bea89b3 100644
--- a/src/Listener/PodeListener.cs
+++ b/src/Listener/PodeListener.cs
@@ -196,13 +196,13 @@ public void CloseSseConnection(string name, string[] groups, string[] clientIds)
public bool TestSseConnectionExists(string name, string clientId)
{
// check name
- if (!ServerEvents.ContainsKey(name))
+ if (!ServerEvents.TryGetValue(name, out IDictionary value))
{
return false;
}
// check clientId
- if (!string.IsNullOrEmpty(clientId) && !ServerEvents[name].ContainsKey(clientId))
+ if (!string.IsNullOrEmpty(clientId) && !value.ContainsKey(clientId))
{
return false;
}
diff --git a/src/Listener/PodeRequest.cs b/src/Listener/PodeRequest.cs
index bc3bfc07e..f36a1d938 100644
--- a/src/Listener/PodeRequest.cs
+++ b/src/Listener/PodeRequest.cs
@@ -12,41 +12,65 @@
namespace Pode
{
+ ///
+ /// Represents an incoming request in Pode, handling different protocols, SSL/TLS upgrades, and client communication.
+ ///
public class PodeRequest : PodeProtocol, IDisposable
{
+ // Endpoint information for remote and local addresses
public EndPoint RemoteEndPoint { get; private set; }
public EndPoint LocalEndPoint { get; private set; }
+
+ // SSL/TLS properties
public bool IsSsl { get; private set; }
public bool SslUpgraded { get; private set; }
public bool IsKeepAlive { get; protected set; }
+
+ // Flags indicating request characteristics and handling status
public virtual bool CloseImmediately { get => false; }
public virtual bool IsProcessable { get => true; }
+ // Input stream for incoming request data
public Stream InputStream { get; private set; }
+ public PodeStreamState State { get; private set; }
+ public bool IsOpen => State == PodeStreamState.Open;
+
+ // Certificate properties
public X509Certificate Certificate { get; private set; }
public bool AllowClientCertificate { get; private set; }
public PodeTlsMode TlsMode { get; private set; }
public X509Certificate2 ClientCertificate { get; set; }
public SslPolicyErrors ClientCertificateErrors { get; set; }
public SslProtocols Protocols { get; private set; }
- public HttpRequestException Error { get; set; }
- public bool IsAborted => Error != default(HttpRequestException);
+
+ // Error handling for request processing
+ public PodeRequestException Error { get; set; }
+ public bool IsAborted => Error != default(PodeRequestException);
public bool IsDisposed { get; private set; }
+ // Address and Scheme properties for the request
public virtual string Address => Context.PodeSocket.HasHostnames
? $"{Context.PodeSocket.Hostname}:{((IPEndPoint)LocalEndPoint).Port}"
: $"{((IPEndPoint)LocalEndPoint).Address}:{((IPEndPoint)LocalEndPoint).Port}";
public virtual string Scheme => SslUpgraded ? $"{Context.PodeSocket.Type}s" : $"{Context.PodeSocket.Type}";
+ // Socket and Context associated with the request
private Socket Socket;
protected PodeContext Context;
- protected static UTF8Encoding Encoding = new UTF8Encoding();
+ // Encoding and buffer for handling incoming data
+ protected static UTF8Encoding Encoding = new UTF8Encoding();
private byte[] Buffer;
private MemoryStream BufferStream;
private const int BufferSize = 16384;
+ ///
+ /// Initializes a new instance of the PodeRequest class.
+ ///
+ /// The socket used for communication.
+ /// The PodeSocket managing this request.
+ /// The PodeContext associated with this request.
public PodeRequest(Socket socket, PodeSocket podeSocket, PodeContext context)
{
Socket = socket;
@@ -58,8 +82,13 @@ public PodeRequest(Socket socket, PodeSocket podeSocket, PodeContext context)
AllowClientCertificate = podeSocket.AllowClientCertificate;
Protocols = podeSocket.Protocols;
Context = context;
+ State = PodeStreamState.New;
}
+ ///
+ /// Initializes a new instance of the PodeRequest class by copying properties from another request.
+ ///
+ /// The PodeRequest to copy properties from.
public PodeRequest(PodeRequest request)
{
IsSsl = request.IsSsl;
@@ -74,56 +103,106 @@ public PodeRequest(PodeRequest request)
AllowClientCertificate = request.AllowClientCertificate;
Protocols = request.Protocols;
TlsMode = request.TlsMode;
+ State = request.State;
}
+ ///
+ /// Opens the socket stream, upgrading to SSL/TLS if necessary.
+ ///
+ /// Token to monitor for cancellation requests.
+ /// A Task representing the async operation.
public async Task Open(CancellationToken cancellationToken)
{
- // open the socket's stream
- InputStream = new NetworkStream(Socket, true);
- if (!IsSsl || TlsMode == PodeTlsMode.Explicit)
+ try
{
- // if not ssl, use the main network stream
- return;
+ // Open the input stream for the socket
+ InputStream = new NetworkStream(Socket, true);
+
+ // Upgrade to SSL if necessary
+ if (!IsSsl || TlsMode == PodeTlsMode.Explicit)
+ {
+ // If not SSL, use the main network stream
+ State = PodeStreamState.Open;
+ return;
+ }
+
+ // Upgrade to SSL if necessary
+ await UpgradeToSSL(cancellationToken).ConfigureAwait(false);
}
+ catch (Exception ex)
+ {
+ if (ex is AggregateException)
+ {
+ PodeHelpers.HandleAggregateException(ex as AggregateException, Context.Listener, PodeLoggingLevel.Debug, true);
+ }
+ else
+ {
+ PodeHelpers.WriteException(ex, Context.Listener, PodeLoggingLevel.Debug);
+ }
- // otherwise, convert the stream to an ssl stream
- await UpgradeToSSL(cancellationToken).ConfigureAwait(false);
+ State = PodeStreamState.Error;
+ Error = new PodeRequestException(ex, 502);
+ }
}
+ ///
+ /// Upgrades the current connection to SSL/TLS.
+ ///
+ /// Token to monitor for cancellation requests.
+ /// A Task representing the async operation.
public async Task UpgradeToSSL(CancellationToken cancellationToken)
{
- // if we've already upgraded, return
if (SslUpgraded)
{
- return;
+ State = PodeStreamState.Open;
+ return; // Already upgraded
}
- // create the ssl stream
+ // Create an SSL stream for secure communication
var ssl = new SslStream(InputStream, false, new RemoteCertificateValidationCallback(ValidateCertificateCallback));
+ // Authenticate the SSL stream, handling cancellation and exceptions
using (cancellationToken.Register(() => ssl.Dispose()))
{
try
{
- // authenticate the stream
+ // Authenticate the SSL stream
await ssl.AuthenticateAsServerAsync(Certificate, AllowClientCertificate, Protocols, false).ConfigureAwait(false);
- // if we've upgraded, set the stream
+ // Set InputStream to the upgraded SSL stream
InputStream = ssl;
SslUpgraded = true;
+ State = PodeStreamState.Open;
+ }
+ catch (Exception ex) when (ex is OperationCanceledException || ex is IOException || ex is ObjectDisposedException)
+ {
+ PodeHelpers.WriteException(ex, Context.Listener, PodeLoggingLevel.Verbose);
+ State = PodeStreamState.Error;
+ Error = new PodeRequestException(ex, 500);
+ }
+ catch (AuthenticationException ex)
+ {
+ PodeHelpers.WriteException(ex, Context.Listener, PodeLoggingLevel.Debug);
+ State = PodeStreamState.Error;
+ Error = new PodeRequestException(ex, 400);
}
- catch (OperationCanceledException) { }
- catch (IOException) { }
- catch (ObjectDisposedException) { }
catch (Exception ex)
{
PodeHelpers.WriteException(ex, Context.Listener, PodeLoggingLevel.Error);
- Error = new HttpRequestException(ex.Message, ex);
- Error.Data.Add("PodeStatusCode", 502);
+ State = PodeStreamState.Error;
+ Error = new PodeRequestException(ex, 502);
}
}
}
+ ///
+ /// Callback to validate client certificates during the SSL handshake.
+ ///
+ /// The sender of the callback.
+ /// The client certificate to validate.
+ /// The chain of the certificate.
+ /// Any SSL policy errors found.
+ /// True if the certificate is valid; otherwise, false.
private bool ValidateCertificateCallback(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
ClientCertificateErrors = sslPolicyErrors;
@@ -135,8 +214,19 @@ private bool ValidateCertificateCallback(object sender, X509Certificate certific
return true;
}
+ ///
+ /// Receives data from the input stream and processes it.
+ ///
+ /// Token to monitor for cancellation requests.
+ /// A Task representing the async operation, with a boolean indicating whether the connection should be closed.
public async Task Receive(CancellationToken cancellationToken)
{
+ // Check if the stream is open
+ if (State != PodeStreamState.Open)
+ {
+ return false;
+ }
+
try
{
Error = default;
@@ -148,23 +238,34 @@ public async Task Receive(CancellationToken cancellationToken)
while (true)
{
- // read the input stream
+#if NETCOREAPP2_1_OR_GREATER
+ // Read data from the input stream
+ var read = await InputStream.ReadAsync(Buffer.AsMemory(0, BufferSize), cancellationToken).ConfigureAwait(false);
+ if (read <= 0)
+ {
+ break;
+ }
+
+ // Write the data to the buffer stream
+ await BufferStream.WriteAsync(Buffer.AsMemory(0, read), cancellationToken).ConfigureAwait(false);
+#else
+ // Read data from the input stream
var read = await InputStream.ReadAsync(Buffer, 0, BufferSize, cancellationToken).ConfigureAwait(false);
if (read <= 0)
{
break;
}
- // write the buffer to the stream
+ // Write the data to the buffer stream
await BufferStream.WriteAsync(Buffer, 0, read, cancellationToken).ConfigureAwait(false);
+#endif
- // if we have more data, or the input is invalid, continue
+ // Validate and parse the data if available
if (Socket.Available > 0 || !ValidateInput(BufferStream.ToArray()))
{
continue;
}
- // parse the buffer
if (!await Parse(BufferStream.ToArray(), cancellationToken).ConfigureAwait(false))
{
BufferStream.SetLength(0);
@@ -178,18 +279,23 @@ public async Task Receive(CancellationToken cancellationToken)
return close;
}
}
- catch (OperationCanceledException) { }
- catch (IOException) { }
- catch (HttpRequestException httpex)
+ catch (OperationCanceledException ex)
+ {
+ PodeHelpers.WriteException(ex, Context.Listener, PodeLoggingLevel.Verbose);
+ }
+ catch (IOException ex)
+ {
+ PodeHelpers.WriteException(ex, Context.Listener, PodeLoggingLevel.Verbose);
+ }
+ catch (PodeRequestException ex)
{
- PodeHelpers.WriteException(httpex, Context.Listener, PodeLoggingLevel.Error);
- Error = httpex;
+ PodeHelpers.WriteException(ex, Context.Listener, PodeLoggingLevel.Error);
+ Error = ex;
}
catch (Exception ex)
{
PodeHelpers.WriteException(ex, Context.Listener, PodeLoggingLevel.Error);
- Error = new HttpRequestException(ex.Message, ex);
- Error.Data.Add("PodeStatusCode", 400);
+ Error = new PodeRequestException(ex, 500);
}
finally
{
@@ -199,24 +305,48 @@ public async Task Receive(CancellationToken cancellationToken)
return false;
}
+ ///
+ /// Reads data from the input stream until the specified bytes are found.
+ ///
+ /// The bytes to check for in the input stream.
+ /// Token to monitor for cancellation requests.
+ /// A Task representing the async operation, with a string containing the data read.
public async Task Read(byte[] checkBytes, CancellationToken cancellationToken)
{
+ // Check if the stream is open
+ if (State != PodeStreamState.Open)
+ {
+ return string.Empty;
+ }
+
+ // Read data from the input stream until the check bytes are found
var buffer = new byte[BufferSize];
using (var bufferStream = new MemoryStream())
{
while (true)
{
- // read the input stream
+#if NETCOREAPP2_1_OR_GREATER
+ // Read data from the input stream
+ var read = await InputStream.ReadAsync(buffer.AsMemory(0, BufferSize), cancellationToken).ConfigureAwait(false);
+ if (read <= 0)
+ {
+ break;
+ }
+
+ // Write the data to the buffer stream
+ await bufferStream.WriteAsync(buffer.AsMemory(0, read), cancellationToken).ConfigureAwait(false);
+#else
+ // Read data from the input stream
var read = await InputStream.ReadAsync(buffer, 0, BufferSize, cancellationToken).ConfigureAwait(false);
if (read <= 0)
{
break;
}
- // write the buffer to the stream
+ // Write the data to the buffer stream
await bufferStream.WriteAsync(buffer, 0, read, cancellationToken).ConfigureAwait(false);
-
- // if we have more data, or the input is invalid, continue
+#endif
+ // Validate the input data
if (Socket.Available > 0 || !ValidateInputInternal(bufferStream.ToArray(), checkBytes))
{
continue;
@@ -229,27 +359,30 @@ public async Task Read(byte[] checkBytes, CancellationToken cancellation
}
}
- private bool ValidateInputInternal(byte[] bytes, byte[] checkBytes)
+ ///
+ /// Validates the input bytes against the specified check bytes.
+ ///
+ /// The bytes to validate.
+ /// The bytes to check against.
+ /// True if validation is successful, otherwise false.
+ private static bool ValidateInputInternal(byte[] bytes, byte[] checkBytes)
{
- // we need more bytes!
if (bytes.Length == 0)
{
- return false;
+ return false; // Need more bytes
}
- // do we have any checkBytes?
if (checkBytes == default(byte[]) || checkBytes.Length == 0)
{
- return true;
+ return true; // No specific bytes to check
}
- // check bytes against checkBytes length
if (bytes.Length < checkBytes.Length)
{
- return false;
+ return false; // Not enough bytes
}
- // expect to end with checkBytes?
+ // Check if the input ends with checkBytes
for (var i = 0; i < checkBytes.Length; i++)
{
if (bytes[bytes.Length - (checkBytes.Length - i)] != checkBytes[i])
@@ -261,16 +394,31 @@ private bool ValidateInputInternal(byte[] bytes, byte[] checkBytes)
return true;
}
+ ///
+ /// Parses the received bytes. This method should be implemented in derived classes.
+ ///
+ /// The bytes to parse.
+ /// Token to monitor for cancellation requests.
+ /// A Task representing the async operation, returning true if parsing was successful.
+ /// Thrown when called directly from PodeRequest.
protected virtual Task Parse(byte[] bytes, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
+ ///
+ /// Validates the incoming input bytes. Can be overridden by derived classes.
+ ///
+ /// The bytes to validate.
+ /// True if validation is successful, otherwise false.
protected virtual bool ValidateInput(byte[] bytes)
{
return true;
}
+ ///
+ /// Partially disposes resources used during request processing.
+ ///
public virtual void PartialDispose()
{
if (BufferStream != default(MemoryStream))
@@ -282,6 +430,9 @@ public virtual void PartialDispose()
Buffer = default;
}
+ ///
+ /// Disposes of the request and its associated resources.
+ ///
public virtual void Dispose()
{
if (IsDisposed)
@@ -298,6 +449,7 @@ public virtual void Dispose()
if (InputStream != default(Stream))
{
+ State = PodeStreamState.Closed;
InputStream.Dispose();
InputStream = default;
}
@@ -306,4 +458,4 @@ public virtual void Dispose()
PodeHelpers.WriteErrorMessage($"Request disposed", Context.Listener, PodeLoggingLevel.Verbose, Context);
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Listener/PodeRequestException.cs b/src/Listener/PodeRequestException.cs
new file mode 100644
index 000000000..7578d8072
--- /dev/null
+++ b/src/Listener/PodeRequestException.cs
@@ -0,0 +1,48 @@
+using System;
+using System.Net.Http;
+
+namespace Pode
+{
+ public class PodeRequestException : HttpRequestException
+ {
+ // the status code of the exception
+#if NETCOREAPP2_1_OR_GREATER
+ public new int StatusCode { get; private set; } = 400;
+#else
+ public int StatusCode { get; private set; } = 400;
+#endif
+
+ // is the exception a timeout status code (408?
+ public bool IsTimeout => StatusCode == 408;
+
+ // is the exception a client error status code (4xx)?
+ public bool IsClientError => StatusCode >= 400 && StatusCode < 500;
+
+ // is the exception a server error status code (5xx)?
+ public bool IsServerError => StatusCode >= 500 && StatusCode < 600;
+
+ // the logging level of the exception
+ public PodeLoggingLevel LoggingLevel => IsClientError ? PodeLoggingLevel.Debug : PodeLoggingLevel.Error;
+
+
+ // constructors
+ public PodeRequestException(int statusCode = default)
+ : this(string.Empty, null, statusCode) { }
+
+ public PodeRequestException(string message, int statusCode = default)
+ : this(message, null, statusCode) { }
+
+ public PodeRequestException(Exception exception, int statusCode = default)
+ : this(exception.Message, exception, statusCode) { }
+
+ public PodeRequestException(string message, Exception innerException, int statusCode = default)
+ : base(message, innerException)
+ {
+ if (statusCode > 0)
+ {
+ StatusCode = statusCode;
+ }
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/src/Listener/PodeResponse.cs b/src/Listener/PodeResponse.cs
index 786545994..3bd9ed78d 100644
--- a/src/Listener/PodeResponse.cs
+++ b/src/Listener/PodeResponse.cs
@@ -76,7 +76,7 @@ public string HttpResponseLine
get => $"{((PodeHttpRequest)Request).Protocol} {StatusCode} {StatusDescription}{PodeHelpers.NEW_LINE}";
}
- private static UTF8Encoding Encoding = new UTF8Encoding();
+ private static readonly UTF8Encoding Encoding = new UTF8Encoding();
public PodeResponse(PodeContext context)
{
@@ -351,7 +351,11 @@ public async Task Write(byte[] buffer, bool flush = false)
try
{
+#if NETCOREAPP2_1_OR_GREATER
+ await Request.InputStream.WriteAsync(buffer.AsMemory(), Context.Listener.CancellationToken).ConfigureAwait(false);
+#else
await Request.InputStream.WriteAsync(buffer, 0, buffer.Length, Context.Listener.CancellationToken).ConfigureAwait(false);
+#endif
if (flush)
{
@@ -382,7 +386,7 @@ private void SetDefaultHeaders()
{
if (ContentLength64 == 0)
{
- ContentLength64 = (OutputStream.Length > 0 ? OutputStream.Length : 0);
+ ContentLength64 = OutputStream.Length > 0 ? OutputStream.Length : 0;
}
}
diff --git a/src/Listener/PodeSmtpRequest.cs b/src/Listener/PodeSmtpRequest.cs
index 6bb1290d5..219a88640 100644
--- a/src/Listener/PodeSmtpRequest.cs
+++ b/src/Listener/PodeSmtpRequest.cs
@@ -257,7 +257,7 @@ protected override async Task Parse(byte[] bytes, CancellationToken cancel
break;
default:
- throw new HttpRequestException("Invalid SMTP command");
+ throw new PodeRequestException("Invalid SMTP command");
}
return true;
diff --git a/src/Listener/PodeSocket.cs b/src/Listener/PodeSocket.cs
index 7c337c9e3..335166558 100644
--- a/src/Listener/PodeSocket.cs
+++ b/src/Listener/PodeSocket.cs
@@ -8,11 +8,16 @@
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
using System.IO;
+using System.Net.Http;
namespace Pode
{
+ ///
+ /// Represents a PodeSocket, managing communication, incoming connections, and context handling.
+ ///
public class PodeSocket : PodeProtocol, IDisposable
{
+ // Properties related to socket configuration and certificates.
public string Name { get; private set; }
public List Hostnames { get; private set; }
public IList Endpoints { get; private set; }
@@ -24,13 +29,19 @@ public class PodeSocket : PodeProtocol, IDisposable
public bool CRLFMessageEnd { get; set; }
public bool DualMode { get; private set; }
- private ConcurrentQueue AcceptConnections;
- private IDictionary PendingSockets;
+ // Queue for handling connections asynchronously.
+ private readonly ConcurrentQueue AcceptConnections;
+ // Dictionary to keep track of pending socket connections.
+ private readonly Dictionary PendingSockets;
+
+ // Listener associated with the current PodeSocket.
private PodeListener Listener;
+ // Property to determine if the socket is using SSL.
public bool IsSsl => Certificate != default(X509Certificate);
+ // Timeout for receiving data on the socket.
private int _receiveTimeout;
public int ReceiveTimeout
{
@@ -40,17 +51,31 @@ public int ReceiveTimeout
_receiveTimeout = value;
foreach (var ep in Endpoints)
{
- ep.ReceiveTimeout = value;
+ ep.ReceiveTimeout = value; // Set receive timeout on all endpoints.
}
}
}
- public bool HasHostnames => Hostnames.Any();
+ // Property to determine if hostnames are set.
+ public bool HasHostnames => Hostnames.Count != 0;
public string Hostname => HasHostnames ? Hostnames[0] : Endpoints[0].IPAddress.ToString();
+ ///
+ /// Initializes a new instance of the PodeSocket class.
+ ///
+ /// The name of the socket.
+ /// The IP addresses associated with the socket.
+ /// The port on which the socket listens.
+ /// The SSL protocols to be used.
+ /// The protocol type.
+ /// The SSL certificate (optional).
+ /// Indicates whether client certificates are allowed.
+ /// The TLS mode to use.
+ /// Whether to enable IPv4 and IPv6 dual mode.
public PodeSocket(string name, IPAddress[] ipAddress, int port, SslProtocols protocols, PodeProtocolType type, X509Certificate certificate = null, bool allowClientCertificate = false, PodeTlsMode tlsMode = PodeTlsMode.Implicit, bool dualMode = false)
: base(type)
{
+ // Initialize properties.
Name = name;
Certificate = certificate;
AllowClientCertificate = allowClientCertificate;
@@ -63,51 +88,72 @@ public PodeSocket(string name, IPAddress[] ipAddress, int port, SslProtocols pro
PendingSockets = new Dictionary();
Endpoints = new List();
+ // Create PodeEndpoint instances for each provided IP address.
foreach (var addr in ipAddress)
{
Endpoints.Add(new PodeEndpoint(this, addr, port, dualMode));
}
}
+ ///
+ /// Binds a PodeListener to the current socket.
+ ///
+ /// The listener to bind.
public void BindListener(PodeListener listener)
{
Listener = listener;
}
+ ///
+ /// Binds the socket to all available endpoints.
+ ///
public void Listen()
{
foreach (var ep in Endpoints)
{
- ep.Listen();
+ ep.Listen(); // Start listening on each endpoint.
}
}
+ ///
+ /// Starts listening for connections on all endpoints.
+ ///
public void Start()
{
foreach (var ep in Endpoints)
{
+ // Start each endpoint in a new task, running asynchronously.
_ = Task.Run(() => StartEndpoint(ep), Listener.CancellationToken);
}
}
+ ///
+ /// Starts listening for connections on a specific endpoint.
+ ///
+ /// The endpoint to start listening on.
private void StartEndpoint(PodeEndpoint endpoint)
{
+ // Exit if the endpoint is disposed or if cancellation is requested.
if (endpoint.IsDisposed || Listener.CancellationToken.IsCancellationRequested)
{
return;
}
+ // Attempt to retrieve an available SocketAsyncEventArgs from the queue, or create a new one if unavailable.
if (!AcceptConnections.TryDequeue(out SocketAsyncEventArgs args))
{
args = NewAcceptConnection();
}
+ // Set properties for accepting a connection.
args.AcceptSocket = default;
args.UserToken = endpoint;
+
bool raised;
try
{
+ // Start accepting a new connection.
raised = endpoint.Accept(args);
}
catch (ObjectDisposedException)
@@ -115,22 +161,28 @@ private void StartEndpoint(PodeEndpoint endpoint)
return;
}
+ // If the operation completed synchronously, process the accepted connection.
if (!raised)
{
ProcessAccept(args);
}
}
+ ///
+ /// Starts receiving data from an accepted socket.
+ ///
+ /// The accepted socket.
+ /// A Task representing the async operation.
private async Task StartReceive(Socket acceptedSocket)
{
- // add the socket to pending
+ // Add the socket to pending sockets.
AddPendingSocket(acceptedSocket);
- // create the context
+ // Create the context for the connection.
var context = new PodeContext(acceptedSocket, this, Listener);
PodeHelpers.WriteErrorMessage($"Opening Receive", Listener, PodeLoggingLevel.Verbose, context);
- // initialise the context
+ // Initialize the context.
await context.Initialise().ConfigureAwait(false);
if (context.IsErrored)
{
@@ -138,43 +190,62 @@ private async Task StartReceive(Socket acceptedSocket)
return;
}
- // start receiving data
+ // Start receiving data.
StartReceive(context);
}
+ ///
+ /// Starts receiving data for a specific context.
+ ///
+ /// The context to start receiving for.
public void StartReceive(PodeContext context)
{
PodeHelpers.WriteErrorMessage($"Starting Receive", Listener, PodeLoggingLevel.Verbose, context);
try
{
+ // Run the receive operation asynchronously in a new task.
_ = Task.Run(async () => await context.Receive().ConfigureAwait(false), Listener.CancellationToken);
}
- catch (OperationCanceledException) { }
- catch (IOException) { }
+ catch (OperationCanceledException ex)
+ {
+ // Handle cancellation.
+ PodeHelpers.WriteException(ex, Listener, PodeLoggingLevel.Verbose);
+ }
+ catch (IOException ex)
+ {
+ // Handle I/O exceptions.
+ PodeHelpers.WriteException(ex, Listener, PodeLoggingLevel.Verbose);
+ }
catch (AggregateException aex)
{
+ // Handle aggregated exceptions.
PodeHelpers.HandleAggregateException(aex, Listener, PodeLoggingLevel.Error, true);
context.Socket.Close();
}
catch (Exception ex)
{
+ // Handle any other exceptions.
PodeHelpers.WriteException(ex, Listener);
context.Socket.Close();
}
}
+ ///
+ /// Processes an accepted connection.
+ ///
+ /// The SocketAsyncEventArgs containing the connection details.
private void ProcessAccept(SocketAsyncEventArgs args)
{
- // get details
+ // Get details about the accepted connection.
var accepted = args.AcceptSocket;
var endpoint = (PodeEndpoint)args.UserToken;
var error = args.SocketError;
- // start the socket again
+ // Start accepting new connections for the endpoint.
StartEndpoint(endpoint);
- // close socket if not successful, or if listener is stopped - close now!
+ // If the connection was not successful or the listener is stopped, close the socket.
if ((accepted == default(Socket)) || (error != SocketError.Success) || (!Listener.IsConnected))
{
if (error != SocketError.Success)
@@ -182,55 +253,70 @@ private void ProcessAccept(SocketAsyncEventArgs args)
PodeHelpers.WriteErrorMessage($"Closing accepting socket: {error}", Listener, PodeLoggingLevel.Debug);
}
- // close socket
+ // Close socket if it was accepted but there's an error.
if (accepted != default(Socket))
{
accepted.Close();
}
}
-
- // valid connection
else
{
- // start receive
+ // Start receiving data from the accepted connection.
try
{
_ = Task.Run(async () => await StartReceive(accepted), Listener.CancellationToken).ConfigureAwait(false);
}
- catch (OperationCanceledException) { }
- catch (IOException) { }
+ catch (OperationCanceledException ex)
+ {
+ // Handle cancellation.
+ PodeHelpers.WriteException(ex, Listener, PodeLoggingLevel.Verbose);
+ }
+ catch (IOException ex)
+ {
+ // Handle I/O exceptions.
+ PodeHelpers.WriteException(ex, Listener, PodeLoggingLevel.Verbose);
+ }
catch (AggregateException aex)
{
+ // Handle aggregated exceptions.
PodeHelpers.HandleAggregateException(aex, Listener, PodeLoggingLevel.Error, true);
}
catch (Exception ex)
{
+ // Handle any other exceptions.
PodeHelpers.WriteException(ex, Listener);
}
}
- // add args back to connections
+ // Add the SocketAsyncEventArgs back to the queue for reuse.
ClearSocketAsyncEvent(args);
AcceptConnections.Enqueue(args);
}
+ ///
+ /// Handles the context, processing and disposing it if needed.
+ ///
+ /// The PodeContext representing the connection context.
public async Task HandleContext(PodeContext context)
{
try
{
- // add context to be processed?
+ // Determine if the context should be processed.
var process = true;
- // if we need to exit now, dispose and exit
+ // If the context should be closed immediately, dispose it.
if (context.CloseImmediately)
{
- PodeHelpers.WriteException(context.Request.Error, Listener);
+ // Check if the request is aborted with a non-StatusCode of 408 (Request Timeout).
+ if (context.Request.IsAborted)
+ {
+ PodeHelpers.WriteException(context.Request.Error, Listener, context.Request.Error.LoggingLevel);
+ }
+
context.Dispose(true);
process = false;
}
-
- // if it's a websocket, upgrade it, then add context back for re-receiving
- else if (context.IsWebSocket)
+ else if (context.IsWebSocket) // Handle WebSocket upgrade and context disposal.
{
if (!context.IsWebSocketUpgraded)
{
@@ -244,9 +330,7 @@ public async Task HandleContext(PodeContext context)
context.Dispose();
}
}
-
- // if it's an email, re-receive unless processable
- else if (context.IsSmtp)
+ else if (context.IsSmtp) // Handle SMTP context disposal.
{
if (!context.Request.IsProcessable)
{
@@ -254,9 +338,7 @@ public async Task HandleContext(PodeContext context)
context.Dispose();
}
}
-
- // if it's http and awaiting the body
- else if (context.IsHttp)
+ else if (context.IsHttp) // Handle HTTP context disposal if awaiting body.
{
if (context.HttpRequest.AwaitingBody)
{
@@ -265,7 +347,7 @@ public async Task HandleContext(PodeContext context)
}
}
- // add the context for processing
+ // Add the context for processing.
if (process)
{
if (context.IsWebSocket)
@@ -283,10 +365,15 @@ public async Task HandleContext(PodeContext context)
}
catch (Exception ex)
{
+ // Log any exceptions that occur while handling the context.
PodeHelpers.WriteException(ex, Listener);
}
}
+ ///
+ /// Creates a new instance of SocketAsyncEventArgs for accepting connections.
+ ///
+ /// A new SocketAsyncEventArgs instance.
private SocketAsyncEventArgs NewAcceptConnection()
{
lock (AcceptConnections)
@@ -297,35 +384,53 @@ private SocketAsyncEventArgs NewAcceptConnection()
}
}
+ ///
+ /// Handles the completion of an accept operation.
+ ///
+ /// The object that triggered the event.
+ /// The SocketAsyncEventArgs with the connection details.
private void Accept_Completed(object sender, SocketAsyncEventArgs e)
{
ProcessAccept(e);
}
+ ///
+ /// Adds a socket to the list of pending sockets.
+ ///
+ /// The socket to add.
private void AddPendingSocket(Socket socket)
{
lock (PendingSockets)
{
var socketId = socket.GetHashCode().ToString();
+#if NETCOREAPP2_1_OR_GREATER
+ PendingSockets.TryAdd(socketId, socket);
+#else
if (!PendingSockets.ContainsKey(socketId))
{
PendingSockets.Add(socketId, socket);
}
+#endif
}
}
+ ///
+ /// Removes a socket from the list of pending sockets.
+ ///
+ /// The socket to remove.
public void RemovePendingSocket(Socket socket)
{
lock (PendingSockets)
{
- var socketId = socket.GetHashCode().ToString();
- if (PendingSockets.ContainsKey(socketId))
- {
- PendingSockets.Remove(socketId);
- }
+ PendingSockets.Remove(socket.GetHashCode().ToString());
}
}
+ ///
+ /// Checks if a given hostname matches the socket's hostnames.
+ ///
+ /// The hostname to check.
+ /// True if the hostname matches, otherwise false.
public bool CheckHostname(string hostname)
{
if (!HasHostnames)
@@ -337,36 +442,50 @@ public bool CheckHostname(string hostname)
return Hostnames.Any(x => x.Equals(_name, StringComparison.InvariantCultureIgnoreCase));
}
+ ///
+ /// Disposes of the resources used by the PodeSocket.
+ ///
public void Dispose()
{
- // close endpoints
- foreach (var ep in Endpoints)
- {
- ep.Dispose();
- }
-
- Endpoints.Clear();
-
- // close receiving contexts/sockets
try
{
- var _sockets = PendingSockets.Values.ToArray();
- for (var i = 0; i < _sockets.Length; i++)
+ // Close all endpoints.
+ foreach (var ep in Endpoints)
{
- CloseSocket(_sockets[i]);
+ ep.Dispose();
}
- PendingSockets.Clear();
+ Endpoints.Clear();
+
+ // Close all pending sockets.
+ try
+ {
+ var _sockets = PendingSockets.Values.ToArray();
+ for (var i = 0; i < _sockets.Length; i++)
+ {
+ CloseSocket(_sockets[i]);
+ }
+
+ PendingSockets.Clear();
+ }
+ catch (Exception ex)
+ {
+ PodeHelpers.WriteException(ex, Listener);
+ }
}
- catch (Exception ex)
+ finally
{
- PodeHelpers.WriteException(ex, Listener);
+ GC.SuppressFinalize(this);
}
}
+ ///
+ /// Merges another PodeSocket's properties into the current socket.
+ ///
+ /// The PodeSocket to merge from.
public void Merge(PodeSocket socket)
{
- // check for extra hostnames
+ // Merge hostnames if present.
if (socket.HasHostnames)
{
Hostnames.AddRange(socket.Hostnames);
@@ -375,25 +494,38 @@ public void Merge(PodeSocket socket)
socket.Dispose();
}
+ ///
+ /// Closes a socket connection.
+ ///
+ /// The socket to close.
public static void CloseSocket(Socket socket)
{
- // if connected, shut it down
+ // If connected, shut down the socket.
if (socket.Connected)
{
socket.Shutdown(SocketShutdown.Both);
}
- // dispose
+ // Dispose of the socket.
socket.Close();
socket.Dispose();
}
- private void ClearSocketAsyncEvent(SocketAsyncEventArgs e)
+ ///
+ /// Clears the SocketAsyncEventArgs instance for reuse.
+ ///
+ /// The SocketAsyncEventArgs instance to clear.
+ private static void ClearSocketAsyncEvent(SocketAsyncEventArgs e)
{
e.AcceptSocket = default;
e.UserToken = default;
}
+ ///
+ /// Determines if the provided object is equal to the current PodeSocket.
+ ///
+ /// The object to compare with.
+ /// True if equal, otherwise false.
public new bool Equals(object obj)
{
var _socket = (PodeSocket)obj;
@@ -411,4 +543,4 @@ private void ClearSocketAsyncEvent(SocketAsyncEventArgs e)
return true;
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Listener/PodeStreamState.cs b/src/Listener/PodeStreamState.cs
new file mode 100644
index 000000000..829ab40b3
--- /dev/null
+++ b/src/Listener/PodeStreamState.cs
@@ -0,0 +1,10 @@
+namespace Pode
+{
+ public enum PodeStreamState
+ {
+ New,
+ Open,
+ Closed,
+ Error
+ }
+}
\ No newline at end of file
diff --git a/src/Locales/ar/Pode.psd1 b/src/Locales/ar/Pode.psd1
index 978646dab..1c7ff7daf 100644
--- a/src/Locales/ar/Pode.psd1
+++ b/src/Locales/ar/Pode.psd1
@@ -288,7 +288,8 @@
taskProcessDoesNotExistExceptionMessage = 'عملية المهمة غير موجودة: {0}'
scheduleProcessDoesNotExistExceptionMessage = 'عملية الجدول الزمني غير موجودة: {0}'
definitionTagChangeNotAllowedExceptionMessage = 'لا يمكن تغيير علامة التعريف لمسار.'
- getRequestBodyNotAllowedExceptionMessage = 'لا يمكن أن تحتوي عمليات {0} على محتوى الطلب.'
+ getRequestBodyNotAllowedExceptionMessage = "'{0}' لا يمكن أن يحتوي على جسم الطلب. استخدم -AllowNonStandardBody لتجاوز هذا التقييد."
fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "الدالة '{0}' لا تقبل مصفوفة كمدخل لأنبوب البيانات."
unsupportedStreamCompressionEncodingExceptionMessage = 'تشفير الضغط غير مدعوم للتشفير {0}'
+ LocalEndpointConflictExceptionMessage = "تم تعريف كل من '{0}' و '{1}' كنقاط نهاية محلية لـ OpenAPI، لكن يُسمح فقط بنقطة نهاية محلية واحدة لكل تعريف API."
}
diff --git a/src/Locales/de/Pode.psd1 b/src/Locales/de/Pode.psd1
index 4d70c92aa..c90847e01 100644
--- a/src/Locales/de/Pode.psd1
+++ b/src/Locales/de/Pode.psd1
@@ -288,7 +288,8 @@
taskProcessDoesNotExistExceptionMessage = "Der Aufgabenprozess '{0}' existiert nicht."
scheduleProcessDoesNotExistExceptionMessage = "Der Aufgabenplanerprozess '{0}' existiert nicht."
definitionTagChangeNotAllowedExceptionMessage = 'Definitionstag für eine Route kann nicht geändert werden.'
- getRequestBodyNotAllowedExceptionMessage = '{0}-Operationen können keinen Anforderungstext haben.'
+ getRequestBodyNotAllowedExceptionMessage = "'{0}' Operationen dürfen keinen Anfragekörper haben. Verwenden Sie -AllowNonStandardBody, um diese Einschränkung zu umgehen."
fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "Die Funktion '{0}' akzeptiert kein Array als Pipeline-Eingabe."
unsupportedStreamCompressionEncodingExceptionMessage = 'Die Stream-Komprimierungskodierung wird nicht unterstützt: {0}'
+ LocalEndpointConflictExceptionMessage = "Sowohl '{0}' als auch '{1}' sind als lokale OpenAPI-Endpunkte definiert, aber es ist nur ein lokaler Endpunkt pro API-Definition erlaubt."
}
\ No newline at end of file
diff --git a/src/Locales/en-us/Pode.psd1 b/src/Locales/en-us/Pode.psd1
index 85ef1c3a5..56340e18f 100644
--- a/src/Locales/en-us/Pode.psd1
+++ b/src/Locales/en-us/Pode.psd1
@@ -288,7 +288,7 @@
taskProcessDoesNotExistExceptionMessage = 'Task process does not exist: {0}'
scheduleProcessDoesNotExistExceptionMessage = 'Schedule process does not exist: {0}'
definitionTagChangeNotAllowedExceptionMessage = 'Definition Tag for a Route cannot be changed.'
- getRequestBodyNotAllowedExceptionMessage = '{0} operations cannot have a Request Body.'
+ getRequestBodyNotAllowedExceptionMessage = "'{0}' operations cannot have a Request Body. Use -AllowNonStandardBody to override this restriction."
fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "The function '{0}' does not accept an array as pipeline input."
unsupportedStreamCompressionEncodingExceptionMessage = 'Unsupported stream compression encoding: {0}'
-}
\ No newline at end of file
+ LocalEndpointConflictExceptionMessage = "Both '{0}' and '{1}' are defined as local OpenAPI endpoints, but only one local endpoint is allowed per API definition."}
\ No newline at end of file
diff --git a/src/Locales/en/Pode.psd1 b/src/Locales/en/Pode.psd1
index 4aa2f34f7..44a1ee102 100644
--- a/src/Locales/en/Pode.psd1
+++ b/src/Locales/en/Pode.psd1
@@ -288,7 +288,8 @@
taskProcessDoesNotExistExceptionMessage = 'Task process does not exist: {0}'
scheduleProcessDoesNotExistExceptionMessage = 'Schedule process does not exist: {0}'
definitionTagChangeNotAllowedExceptionMessage = 'Definition Tag for a Route cannot be changed.'
- getRequestBodyNotAllowedExceptionMessage = '{0} operations cannot have a Request Body.'
+ getRequestBodyNotAllowedExceptionMessage = "'{0}' operations cannot have a Request Body. Use -AllowNonStandardBody to override this restriction."
fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "The function '{0}' does not accept an array as pipeline input."
unsupportedStreamCompressionEncodingExceptionMessage = 'Unsupported stream compression encoding: {0}'
+ LocalEndpointConflictExceptionMessage = "Both '{0}' and '{1}' are defined as local OpenAPI endpoints, but only one local endpoint is allowed per API definition."
}
\ No newline at end of file
diff --git a/src/Locales/es/Pode.psd1 b/src/Locales/es/Pode.psd1
index 409357fe4..9c2ee1194 100644
--- a/src/Locales/es/Pode.psd1
+++ b/src/Locales/es/Pode.psd1
@@ -288,7 +288,8 @@
taskProcessDoesNotExistExceptionMessage = "El proceso de la tarea '{0}' no existe."
scheduleProcessDoesNotExistExceptionMessage = "El proceso del programación '{0}' no existe."
definitionTagChangeNotAllowedExceptionMessage = 'La etiqueta de definición para una Route no se puede cambiar.'
- getRequestBodyNotAllowedExceptionMessage = 'Las operaciones {0} no pueden tener un cuerpo de solicitud.'
+ getRequestBodyNotAllowedExceptionMessage = "Las operaciones '{0}' no pueden tener un cuerpo de solicitud. Use -AllowNonStandardBody para evitar esta restricción."
fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "La función '{0}' no acepta una matriz como entrada de canalización."
unsupportedStreamCompressionEncodingExceptionMessage = 'La codificación de compresión de transmisión no es compatible: {0}'
+ LocalEndpointConflictExceptionMessage = "Tanto '{0}' como '{1}' están definidos como puntos finales locales de OpenAPI, pero solo se permite un punto final local por definición de API."
}
\ No newline at end of file
diff --git a/src/Locales/fr/Pode.psd1 b/src/Locales/fr/Pode.psd1
index c543ae136..2c9dfc579 100644
--- a/src/Locales/fr/Pode.psd1
+++ b/src/Locales/fr/Pode.psd1
@@ -288,8 +288,8 @@
taskProcessDoesNotExistExceptionMessage = "Le processus de la tâche '{0}' n'existe pas."
scheduleProcessDoesNotExistExceptionMessage = "Le processus de l'horaire '{0}' n'existe pas."
definitionTagChangeNotAllowedExceptionMessage = 'Le tag de définition pour une Route ne peut pas être modifié.'
- getRequestBodyNotAllowedExceptionMessage = 'Les opérations {0} ne peuvent pas avoir de corps de requête.'
+ getRequestBodyNotAllowedExceptionMessage = "Les opérations '{0}' ne peuvent pas avoir de corps de requête. Utilisez -AllowNonStandardBody pour contourner cette restriction."
fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "La fonction '{0}' n'accepte pas un tableau en tant qu'entrée de pipeline."
unsupportedStreamCompressionEncodingExceptionMessage = "La compression de flux {0} n'est pas prise en charge."
-}
-
+ LocalEndpointConflictExceptionMessage = "Les deux '{0}' et '{1}' sont définis comme des points de terminaison locaux pour OpenAPI, mais un seul point de terminaison local est autorisé par définition d'API."
+}
\ No newline at end of file
diff --git a/src/Locales/it/Pode.psd1 b/src/Locales/it/Pode.psd1
index cbc7ebb2c..999bf85c3 100644
--- a/src/Locales/it/Pode.psd1
+++ b/src/Locales/it/Pode.psd1
@@ -288,7 +288,8 @@
taskProcessDoesNotExistExceptionMessage = "Il processo dell'attività '{0}' non esiste."
scheduleProcessDoesNotExistExceptionMessage = "Il processo della programma '{0}' non esiste."
definitionTagChangeNotAllowedExceptionMessage = 'Il tag di definizione per una Route non può essere cambiato.'
- getRequestBodyNotAllowedExceptionMessage = 'Le operazioni {0} non possono avere un corpo della richiesta.'
+ getRequestBodyNotAllowedExceptionMessage = "Le operazioni '{0}' non possono avere un corpo della richiesta. Utilizzare -AllowNonStandardBody per aggirare questa restrizione."
fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "La funzione '{0}' non accetta una matrice come input della pipeline."
unsupportedStreamCompressionEncodingExceptionMessage = 'La compressione dello stream non è supportata per la codifica {0}'
+ LocalEndpointConflictExceptionMessage = "Sia '{0}' che '{1}' sono definiti come endpoint locali OpenAPI, ma è consentito solo un endpoint locale per definizione API."
}
\ No newline at end of file
diff --git a/src/Locales/ja/Pode.psd1 b/src/Locales/ja/Pode.psd1
index 5af361d24..e65627c59 100644
--- a/src/Locales/ja/Pode.psd1
+++ b/src/Locales/ja/Pode.psd1
@@ -288,7 +288,8 @@
taskProcessDoesNotExistExceptionMessage = 'タスクプロセスが存在しません: {0}'
scheduleProcessDoesNotExistExceptionMessage = 'スケジュールプロセスが存在しません: {0}'
definitionTagChangeNotAllowedExceptionMessage = 'Routeの定義タグは変更できません。'
- getRequestBodyNotAllowedExceptionMessage = '{0}操作にはリクエストボディを含めることはできません。'
+ getRequestBodyNotAllowedExceptionMessage = "'{0}' 操作にはリクエストボディを含めることはできません。-AllowNonStandardBody を使用してこの制限を回避してください。"
fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "関数 '{0}' は配列をパイプライン入力として受け付けません。"
unsupportedStreamCompressionEncodingExceptionMessage = 'サポートされていないストリーム圧縮エンコーディングが提供されました: {0}'
+ LocalEndpointConflictExceptionMessage = "'{0}' と '{1}' は OpenAPI のローカルエンドポイントとして定義されていますが、API 定義ごとに 1 つのローカルエンドポイントのみ許可されます。"
}
\ No newline at end of file
diff --git a/src/Locales/ko/Pode.psd1 b/src/Locales/ko/Pode.psd1
index 26dc8a116..f64f0c61f 100644
--- a/src/Locales/ko/Pode.psd1
+++ b/src/Locales/ko/Pode.psd1
@@ -288,7 +288,8 @@
taskProcessDoesNotExistExceptionMessage = '작업 프로세스가 존재하지 않습니다: {0}'
scheduleProcessDoesNotExistExceptionMessage = '스케줄 프로세스가 존재하지 않습니다: {0}'
definitionTagChangeNotAllowedExceptionMessage = 'Route에 대한 정의 태그는 변경할 수 없습니다.'
- getRequestBodyNotAllowedExceptionMessage = '{0} 작업에는 요청 본문이 있을 수 없습니다.'
+ getRequestBodyNotAllowedExceptionMessage = "'{0}' 작업은 요청 본문을 가질 수 없습니다. 이 제한을 무시하려면 -AllowNonStandardBody를 사용하세요."
fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "함수 '{0}'은(는) 배열을 파이프라인 입력으로 받지 않습니다."
unsupportedStreamCompressionEncodingExceptionMessage = '지원되지 않는 스트림 압축 인코딩: {0}'
+ LocalEndpointConflictExceptionMessage = "'{0}' 와 '{1}' 는 OpenAPI 로컬 엔드포인트로 정의되었지만, API 정의당 하나의 로컬 엔드포인트만 허용됩니다."
}
\ No newline at end of file
diff --git a/src/Locales/nl/Pode.psd1 b/src/Locales/nl/Pode.psd1
index 8e88fe7d5..d7933a0d9 100644
--- a/src/Locales/nl/Pode.psd1
+++ b/src/Locales/nl/Pode.psd1
@@ -288,7 +288,8 @@
taskProcessDoesNotExistExceptionMessage = "Taakproces '{0}' bestaat niet."
scheduleProcessDoesNotExistExceptionMessage = "Schema-proces '{0}' bestaat niet."
definitionTagChangeNotAllowedExceptionMessage = 'Definitietag voor een route kan niet worden gewijzigd.'
- getRequestBodyNotAllowedExceptionMessage = '{0}-operaties kunnen geen Request Body hebben.'
+ getRequestBodyNotAllowedExceptionMessage = "'{0}' operaties kunnen geen aanvraagbody hebben. Gebruik -AllowNonStandardBody om deze beperking te omzeilen."
fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "De functie '{0}' accepteert geen array als pipeline-invoer."
unsupportedStreamCompressionEncodingExceptionMessage = 'Niet-ondersteunde streamcompressie-encodering: {0}'
+ LocalEndpointConflictExceptionMessage = "Zowel '{0}' als '{1}' zijn gedefinieerd als lokale OpenAPI-eindpunten, maar er is slechts één lokaal eindpunt per API-definitie toegestaan."
}
\ No newline at end of file
diff --git a/src/Locales/pl/Pode.psd1 b/src/Locales/pl/Pode.psd1
index afbcb3dc2..cd632c469 100644
--- a/src/Locales/pl/Pode.psd1
+++ b/src/Locales/pl/Pode.psd1
@@ -288,7 +288,8 @@
taskProcessDoesNotExistExceptionMessage = "Proces zadania '{0}' nie istnieje."
scheduleProcessDoesNotExistExceptionMessage = "Proces harmonogramu '{0}' nie istnieje."
definitionTagChangeNotAllowedExceptionMessage = 'Tag definicji dla Route nie może zostać zmieniony.'
- getRequestBodyNotAllowedExceptionMessage = 'Operacje {0} nie mogą mieć treści żądania.'
+ getRequestBodyNotAllowedExceptionMessage = "Operacje '{0}' nie mogą zawierać treści żądania. Użyj -AllowNonStandardBody, aby obejść to ograniczenie."
fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "Funkcja '{0}' nie akceptuje tablicy jako wejścia potoku."
unsupportedStreamCompressionEncodingExceptionMessage = 'Kodowanie kompresji strumienia nie jest obsługiwane: {0}'
+ LocalEndpointConflictExceptionMessage = "Zarówno '{0}', jak i '{1}' są zdefiniowane jako lokalne punkty końcowe OpenAPI, ale na jedną definicję API dozwolony jest tylko jeden lokalny punkt końcowy."
}
\ No newline at end of file
diff --git a/src/Locales/pt/Pode.psd1 b/src/Locales/pt/Pode.psd1
index af6d8731b..a0604c179 100644
--- a/src/Locales/pt/Pode.psd1
+++ b/src/Locales/pt/Pode.psd1
@@ -288,7 +288,8 @@
taskProcessDoesNotExistExceptionMessage = "O processo da tarefa '{0}' não existe."
scheduleProcessDoesNotExistExceptionMessage = "O processo do cronograma '{0}' não existe."
definitionTagChangeNotAllowedExceptionMessage = 'A Tag de definição para uma Route não pode ser alterada.'
- getRequestBodyNotAllowedExceptionMessage = 'As operações {0} não podem ter um corpo de solicitação.'
+ getRequestBodyNotAllowedExceptionMessage = "As operações '{0}' não podem ter um corpo de solicitação. Use -AllowNonStandardBody para contornar essa restrição."
fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "A função '{0}' não aceita uma matriz como entrada de pipeline."
unsupportedStreamCompressionEncodingExceptionMessage = 'A codificação de compressão de fluxo não é suportada.'
+ LocalEndpointConflictExceptionMessage = "Tanto '{0}' quanto '{1}' estão definidos como endpoints locais do OpenAPI, mas apenas um endpoint local é permitido por definição de API."
}
\ No newline at end of file
diff --git a/src/Locales/zh/Pode.psd1 b/src/Locales/zh/Pode.psd1
index 9e7652920..26b013c95 100644
--- a/src/Locales/zh/Pode.psd1
+++ b/src/Locales/zh/Pode.psd1
@@ -288,7 +288,8 @@
taskProcessDoesNotExistExceptionMessage = "任务进程 '{0}' 不存在。"
scheduleProcessDoesNotExistExceptionMessage = "计划进程 '{0}' 不存在。"
definitionTagChangeNotAllowedExceptionMessage = 'Route的定义标签无法更改。'
- getRequestBodyNotAllowedExceptionMessage = '{0} 操作不能包含请求体。'
+ getRequestBodyNotAllowedExceptionMessage = "'{0}' 操作无法包含请求体。使用 -AllowNonStandardBody 以解除此限制。"
fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "函数 '{0}' 不接受数组作为管道输入。"
unsupportedStreamCompressionEncodingExceptionMessage = '不支持的流压缩编码: {0}'
+ LocalEndpointConflictExceptionMessage = "'{0}' 和 '{1}' 都被定义为 OpenAPI 的本地端点,但每个 API 定义仅允许一个本地端点。"
}
\ No newline at end of file
diff --git a/src/Private/Caching.ps1 b/src/Private/Caching.ps1
index 30c76e50b..1a35074c8 100644
--- a/src/Private/Caching.ps1
+++ b/src/Private/Caching.ps1
@@ -95,7 +95,8 @@ function Clear-PodeCacheInternal {
}
function Start-PodeCacheHousekeeper {
- if (![string]::IsNullOrEmpty((Get-PodeCacheDefaultStorage))) {
+ # if we have a custom default storage, or we're in serverless mode, then we don't need to run the housekeeper
+ if (![string]::IsNullOrEmpty((Get-PodeCacheDefaultStorage)) -or $PodeContext.Server.IsServerless) {
return
}
diff --git a/src/Private/Context.ps1 b/src/Private/Context.ps1
index 4e38b56d8..288d4f0ab 100644
--- a/src/Private/Context.ps1
+++ b/src/Private/Context.ps1
@@ -319,16 +319,18 @@ function New-PodeContext {
}
# routes for pages and api
- $ctx.Server.Routes = @{
- 'connect' = [ordered]@{}
- 'delete' = [ordered]@{}
+ $ctx.Server.Routes = [ordered]@{
+# common methods
'get' = [ordered]@{}
+ 'post' = [ordered]@{}
+ 'put' = [ordered]@{}
+ 'patch' = [ordered]@{}
+ 'delete' = [ordered]@{}
+# other methods
+ 'connect' = [ordered]@{}
'head' = [ordered]@{}
'merge' = [ordered]@{}
'options' = [ordered]@{}
- 'patch' = [ordered]@{}
- 'post' = [ordered]@{}
- 'put' = [ordered]@{}
'trace' = [ordered]@{}
'static' = [ordered]@{}
'signal' = [ordered]@{}
diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1
index 0bdc9237b..8e4e9f37d 100644
--- a/src/Private/Helpers.ps1
+++ b/src/Private/Helpers.ps1
@@ -1370,9 +1370,7 @@ function New-PodeRequestException {
$StatusCode
)
- $err = [System.Net.Http.HttpRequestException]::new()
- $err.Data.Add('PodeStatusCode', $StatusCode)
- return $err
+ return [PodeRequestException]::new($StatusCode)
}
function ConvertTo-PodeResponseContent {
@@ -1535,14 +1533,7 @@ function ConvertFrom-PodeRequestContent {
else {
# if the request is compressed, attempt to uncompress it
if (![string]::IsNullOrWhiteSpace($TransferEncoding)) {
- # create a compressed stream to decompress the req bytes
- $ms = [System.IO.MemoryStream]::new()
- $ms.Write($Request.RawBody, 0, $Request.RawBody.Length)
- $null = $ms.Seek(0, 0)
- $stream = Get-PodeCompressionStream -InputStream $ms -Encoding $TransferEncoding -Mode Decompress
-
- # read the decompressed bytes
- $Content = Read-PodeStreamToEnd -Stream $stream -Encoding $Request.ContentEncoding
+ $Content = [PodeHelpers]::DecompressBytes($Request.RawBody, $TransferEncoding, $Request.ContentEncoding)
}
else {
$Content = $Request.Body
@@ -3573,7 +3564,7 @@ function ConvertTo-PodeYamlInternal {
}
'hashtable' {
- if ($InputObject.Count -gt 0 ) {
+ if ($InputObject.GetEnumerator().MoveNext()) {
$index = 0
$string = [System.Text.StringBuilder]::new()
foreach ($item in $InputObject.Keys) {
@@ -3599,7 +3590,7 @@ function ConvertTo-PodeYamlInternal {
}
'pscustomobject' {
- if ($InputObject.Count -gt 0 ) {
+ if ($InputObject.PSObject.Properties.Count -gt 0) {
$index = 0
$string = [System.Text.StringBuilder]::new()
foreach ($item in ($InputObject | Get-Member -MemberType Properties | Select-Object -ExpandProperty Name)) {
diff --git a/src/Private/Logging.ps1 b/src/Private/Logging.ps1
index 27db3e4fa..e2c11f81c 100644
--- a/src/Private/Logging.ps1
+++ b/src/Private/Logging.ps1
@@ -68,7 +68,7 @@ function Get-PodeLoggingFileMethod {
$null = Get-ChildItem -Path $options.Path -Filter '*.log' -Force |
Where-Object { $_.CreationTime -lt $date } |
- Remove-Item $_ -Force
+ Remove-Item -Force
$options.NextClearDown = [DateTime]::Now.Date.AddDays(1)
}
@@ -484,4 +484,4 @@ function Test-PodeLoggerBatch {
$null = Invoke-PodeScriptBlock -ScriptBlock $logger.Method.ScriptBlock -Arguments $_args -UsingVariables $logger.Method.UsingVariables -Splat
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Private/Mappers.ps1 b/src/Private/Mappers.ps1
index 810146ae5..2fe4be46b 100644
--- a/src/Private/Mappers.ps1
+++ b/src/Private/Mappers.ps1
@@ -706,6 +706,7 @@ function Get-PodeStatusDescription {
440 { return 'Login Time-out' }
450 { return 'Blocked by Windows Parental Controls' }
451 { return 'Unavailable For Legal Reasons' }
+ 495 { return 'SSL Certificate Error' }
500 { return 'Internal Server Error' }
501 { return 'Not Implemented' }
502 { return 'Bad Gateway' }
diff --git a/src/Private/OpenApi.ps1 b/src/Private/OpenApi.ps1
index 2acb348d3..d00073600 100644
--- a/src/Private/OpenApi.ps1
+++ b/src/Private/OpenApi.ps1
@@ -814,14 +814,14 @@ function Set-PodeOpenApiRouteValue {
if ($Route.OpenApi.OperationId) {
$pm.operationId = $Route.OpenApi.OperationId
}
- if ($Route.OpenApi.Parameters) {
- $pm.parameters = $Route.OpenApi.Parameters
+ if ($Route.OpenApi.Parameters[$DefinitionTag]) {
+ $pm.parameters = $Route.OpenApi.Parameters[$DefinitionTag]
}
- if ($Route.OpenApi.RequestBody.$DefinitionTag) {
- $pm.requestBody = $Route.OpenApi.RequestBody.$DefinitionTag
+ if ($Route.OpenApi.RequestBody[$DefinitionTag]) {
+ $pm.requestBody = $Route.OpenApi.RequestBody[$DefinitionTag]
}
- if ($Route.OpenApi.CallBacks.$DefinitionTag) {
- $pm.callbacks = $Route.OpenApi.CallBacks.$DefinitionTag
+ if ($Route.OpenApi.CallBacks[$DefinitionTag]) {
+ $pm.callbacks = $Route.OpenApi.CallBacks[$DefinitionTag]
}
if ($Route.OpenApi.Servers) {
$pm.servers = $Route.OpenApi.Servers
@@ -848,8 +848,8 @@ function Set-PodeOpenApiRouteValue {
}
}
}
- if ($Route.OpenApi.Responses.$DefinitionTag ) {
- $pm.responses = $Route.OpenApi.Responses.$DefinitionTag
+ if ($Route.OpenApi.Responses[$DefinitionTag] ) {
+ $pm.responses = $Route.OpenApi.Responses[$DefinitionTag]
}
else {
# Set responses or default to '204 No Content' if not specified
@@ -1125,31 +1125,32 @@ function Get-PodeOpenApiDefinitionInternal {
$filter = ''
}
- foreach ($method in $PodeContext.Server.Routes.Keys) {
- foreach ($path in ($PodeContext.Server.Routes[$method].Keys | Sort-Object)) {
- # does it match the route?
- if ($path -inotmatch $filter) {
- continue
- }
- # the current route
- $_routes = @($PodeContext.Server.Routes[$method][$path])
- if ( $MetaInfo -and $MetaInfo.RestrictRoutes) {
- $_routes = @(Get-PodeRouteByUrl -Routes $_routes -EndpointName $EndpointName)
- }
- # continue if no routes
- if (($_routes.Length -eq 0) -or ($null -eq $_routes[0])) {
- continue
- }
+ foreach ($path in $PodeContext.Server.OpenAPI.Routes) {
+ # does it match the route?
+ if ($path -inotmatch $filter) {
+ continue
+ }
+ foreach ($method in $PodeContext.Server.Routes.Keys) {
+ $_routes = $PodeContext.Server.Routes[$method][$path]
+
+ if ($null -eq $_routes) { continue }
- # get the first route for base definition
+ if ( $MetaInfo -and $MetaInfo.RestrictRoutes) {
+ $_routes = @(Get-PodeRouteByUrl -Routes $_routes -EndpointName $EndpointName)
+ }
$_route = $_routes[0]
# check if the route has to be published
- if (($_route.OpenApi.Swagger -and $_route.OpenApi.DefinitionTag -contains $DefinitionTag ) -or $Definition.hiddenComponents.enableMinimalDefinitions) {
+ if (($_route.OpenApi.Swagger -and ($_route.OpenApi.DefinitionTag -contains $DefinitionTag) ) -or $Definition.hiddenComponents.enableMinimalDefinitions) {
#remove the ServerUrl part
if ( $localEndpoint) {
- $_route.OpenApi.Path = $_route.OpenApi.Path.replace($localEndpoint, '')
+ if ($_route.Path.StartsWith($localEndpoint)) {
+ $_route.OpenApi.Path = $_route.OpenApi.Path.replace($localEndpoint, '')
+ }
+ else {
+ continue
+ }
}
# do nothing if it has no responses set
if ($_route.OpenApi.Responses.Count -eq 0) {
@@ -1171,37 +1172,34 @@ function Get-PodeOpenApiDefinitionInternal {
$def.paths[$_route.OpenApi.Path][$method] = $pm
# add any custom server endpoints for route
- foreach ($_route in $_routes) {
-
- if ($_route.OpenApi.Servers.count -gt 0) {
- if ($null -eq $def.paths[$_route.OpenApi.Path][$method].servers) {
- $def.paths[$_route.OpenApi.Path][$method].servers = @()
- }
- if ($localEndpoint) {
- $def.paths[$_route.OpenApi.Path][$method].servers += $Definition.servers[0]
- }
+ if ($_route.OpenApi.Servers.count -gt 0) {
+ if ($null -eq $def.paths[$_route.OpenApi.Path][$method].servers) {
+ $def.paths[$_route.OpenApi.Path][$method].servers = @()
}
- if (![string]::IsNullOrWhiteSpace($_route.Endpoint.Address) -and ($_route.Endpoint.Address -ine '*:*')) {
+ if ($localEndpoint) {
+ $def.paths[$_route.OpenApi.Path][$method].servers += $Definition.servers[0]
+ }
+ }
+ if (![string]::IsNullOrWhiteSpace($_route.Endpoint.Address) -and ($_route.Endpoint.Address -ine '*:*')) {
- if ($null -eq $def.paths[$_route.OpenApi.Path][$method].servers) {
- $def.paths[$_route.OpenApi.Path][$method].servers = @()
- }
+ if ($null -eq $def.paths[$_route.OpenApi.Path][$method].servers) {
+ $def.paths[$_route.OpenApi.Path][$method].servers = @()
+ }
- $serverDef = $null
- if (![string]::IsNullOrWhiteSpace($_route.Endpoint.Name)) {
- $serverDef = [ordered]@{
- url = (Get-PodeEndpointByName -Name $_route.Endpoint.Name).Url
- }
+ $serverDef = $null
+ if (![string]::IsNullOrWhiteSpace($_route.Endpoint.Name)) {
+ $serverDef = [ordered]@{
+ url = (Get-PodeEndpointByName -Name $_route.Endpoint.Name).Url
}
- else {
- $serverDef = [ordered]@{
- url = "$($_route.Endpoint.Protocol)://$($_route.Endpoint.Address)"
- }
+ }
+ else {
+ $serverDef = [ordered]@{
+ url = "$($_route.Endpoint.Protocol)://$($_route.Endpoint.Address)"
}
+ }
- if ($null -ne $serverDef) {
- $def.paths[$_route.OpenApi.Path][$method].servers += $serverDef
- }
+ if ($null -ne $serverDef) {
+ $def.paths[$_route.OpenApi.Path][$method].servers += $serverDef
}
}
}
@@ -1372,17 +1370,26 @@ This is an internal function and may change in future releases of Pode.
function Initialize-PodeOpenApiTable {
param(
[string]
- $DefaultDefinitionTag = 'default'
+ $DefaultDefinitionTag
)
+ # Check if the provided definition tag is null or empty. If so, set it to 'default'.
+ if ([string]::IsNullOrEmpty($DefaultDefinitionTag)) {
+ $DefaultDefinitionTag = 'default'
+ }
+
# Initialization of the OpenAPI table with default settings
+ # Create a hashtable named $OpenAPI to hold various OpenAPI-related configurations and data.
$OpenAPI = @{
+ # Initialize a stack to manage the Definition Tag selection.
DefinitionTagSelectionStack = [System.Collections.Generic.Stack[System.Object]]::new()
+ Routes = @()
}
- # Set the currently selected definition tag
+ # Set the currently selected definition tag to the provided or default tag.
$OpenAPI['SelectedDefinitionTag'] = $DefaultDefinitionTag
- # Initialize the Definitions dictionary with a base OpenAPI object for the selected definition tag
+ # Initialize the Definitions dictionary with a base OpenAPI object for the selected definition tag.
+ # The base OpenAPI object is created using the Get-PodeOABaseObject function.
$OpenAPI['Definitions'] = @{ $OpenAPI['SelectedDefinitionTag'] = Get-PodeOABaseObject }
# Return the initialized OpenAPI table
@@ -2257,4 +2264,111 @@ function Test-PodeOAComponentInternal {
return $true
}
}
+}
+
+
+
+
+<#
+.SYNOPSIS
+ Converts a Pode route path into an OpenAPI-compliant route path format.
+
+.DESCRIPTION
+ This internal function takes a Pode route path and replaces placeholders with OpenAPI-style placeholders.
+ Specifically, it converts Pode route placeholders (e.g., `:id`) to OpenAPI placeholders (e.g., `{id}`).
+
+.PARAMETER Path
+ The Pode route path that contains placeholders to be converted to the OpenAPI format.
+
+.RETURNS
+ The converted OpenAPI-compliant route path as a string.
+
+.NOTES
+ This is an internal function and may change in future releases of Pode.
+#>
+function ConvertTo-PodeOARoutePath {
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]
+ $Path
+ )
+
+ return ([regex]::Unescape((Resolve-PodePlaceholder -Path $Path -Pattern '\:(?[\w]+)' -Prepend '{' -Append '}')))
+}
+
+<#
+.SYNOPSIS
+ Tests and validates the OpenAPI Definition Tag for a specific route in Pode.
+
+.DESCRIPTION
+ This function ensures that the OpenAPI Definition Tag for a route is correctly configured.
+ If the route already has an OpenAPI Definition Tag configured, it verifies if the new tag is allowed.
+ If the OpenAPI Definition Tag has not been configured, it validates and sets the provided tag.
+
+.PARAMETER Route
+ A hashtable representing the route that is being tested for the OpenAPI Definition Tag.
+
+.PARAMETER DefinitionTag
+ An optional array of strings representing the Definition Tag(s) to be tested and assigned.
+
+.RETURNS
+ Returns the validated DefinitionTag for the route.
+
+.EXAMPLE
+ $Route = @{
+ OpenApi = @{
+ IsDefTagConfigured = $false
+ DefinitionTag = @()
+ }
+ }
+ $DefinitionTag = @('tag1', 'tag2')
+ Test-PodeRouteOADefinitionTag -Route $Route -DefinitionTag $DefinitionTag
+
+.NOTES
+ This is an internal function and may change in future releases of Pode.
+#>
+function Test-PodeRouteOADefinitionTag {
+ param(
+ [Parameter(Mandatory = $true )]
+ [ValidateNotNullOrEmpty()]
+ [hashtable ]
+ $Route,
+
+ [string[]]
+ $DefinitionTag
+ )
+ # Check if the OpenAPI Definition Tag is already configured
+ if ($Route.OpenApi.IsDefTagConfigured) {
+ # If a DefinitionTag is provided
+ if ($DefinitionTag) {
+ # Loop through each element in $DefinitionTag
+ if ($DefinitionTag | ForEach-Object {
+
+ # Check if the current element exists in the already configured DefinitionTag
+ if (!($Route.OpenApi.DefinitionTag -contains $_)) {
+ # If any element in $DefinitionTag is not present in the configured DefinitionTag, throw an exception
+ throw ($PodeLocale.definitionTagChangeNotAllowedExceptionMessage)
+ }
+ # Return $true for each element to continue the check
+ $true
+ }
+ ) {
+ # If all elements in $DefinitionTag are present in the configured DefinitionTag, assign it to $oaDefinitionTag
+ return $DefinitionTag
+ }
+ }
+
+ return $Route.OpenApi.DefinitionTag
+ }
+ # If the OpenAPI Definition Tag is not configured yet
+
+ # Validate the provided DefinitionTag and assign it to $oaDefinitionTag
+ $oaDefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag
+ # Set the validated DefinitionTag as the OpenAPI DefinitionTag
+ $Route.OpenApi.DefinitionTag = $oaDefinitionTag
+ # Mark the OpenAPI DefinitionTag as configured
+ $Route.OpenApi.IsDefTagConfigured = $true
+
+
+ return $oaDefinitionTag
}
\ No newline at end of file
diff --git a/src/Private/PodeServer.ps1 b/src/Private/PodeServer.ps1
index 776438654..c954631e5 100644
--- a/src/Private/PodeServer.ps1
+++ b/src/Private/PodeServer.ps1
@@ -182,11 +182,11 @@ function Start-PodeWebServer {
# if we have an sse clientId, verify it and then set details in WebEvent
if ($WebEvent.Request.HasSseClientId) {
if (!(Test-PodeSseClientIdValid)) {
- throw [System.Net.Http.HttpRequestException]::new("The X-PODE-SSE-CLIENT-ID value is not valid: $($WebEvent.Request.SseClientId)")
+ throw [Pode.PodeRequestException]::new("The X-PODE-SSE-CLIENT-ID value is not valid: $($WebEvent.Request.SseClientId)")
}
if (![string]::IsNullOrEmpty($WebEvent.Request.SseClientName) -and !(Test-PodeSseClientId -Name $WebEvent.Request.SseClientName -ClientId $WebEvent.Request.SseClientId)) {
- throw [System.Net.Http.HttpRequestException]::new("The SSE Connection being referenced via the X-PODE-SSE-NAME and X-PODE-SSE-CLIENT-ID headers does not exist: [$($WebEvent.Request.SseClientName)] $($WebEvent.Request.SseClientId)")
+ throw [Pode.PodeRequestException]::new("The SSE Connection being referenced via the X-PODE-SSE-NAME and X-PODE-SSE-CLIENT-ID headers does not exist: [$($WebEvent.Request.SseClientName)] $($WebEvent.Request.SseClientId)", 404)
}
$WebEvent.Sse = @{
@@ -237,12 +237,12 @@ function Start-PodeWebServer {
catch [System.OperationCanceledException] {
$_ | Write-PodeErrorLog -Level Debug
}
- catch [System.Net.Http.HttpRequestException] {
+ catch [Pode.PodeRequestException] {
if ($Response.StatusCode -ge 500) {
$_.Exception | Write-PodeErrorLog -CheckInnerException
}
- $code = [int]($_.Exception.Data['PodeStatusCode'])
+ $code = $_.Exception.StatusCode
if ($code -le 0) {
$code = 400
}
diff --git a/src/Private/Routes.ps1 b/src/Private/Routes.ps1
index 319852e5c..91eeba90b 100644
--- a/src/Private/Routes.ps1
+++ b/src/Private/Routes.ps1
@@ -368,17 +368,7 @@ function Get-PodeRouteByUrl {
return $null
}
-
-function ConvertTo-PodeOpenApiRoutePath {
- param(
- [Parameter(Mandatory = $true)]
- [string]
- $Path
- )
-
- return (Resolve-PodePlaceholder -Path $Path -Pattern '\:(?[\w]+)' -Prepend '{' -Append '}')
-}
-
+
<#
.SYNOPSIS
Updates a Pode route path to ensure proper formatting.
diff --git a/src/Private/Security.ps1 b/src/Private/Security.ps1
index c5f475db4..eabbaa38c 100644
--- a/src/Private/Security.ps1
+++ b/src/Private/Security.ps1
@@ -1037,18 +1037,32 @@ function Protect-PodeContentSecurityKeyword {
$Name = $Name.ToLowerInvariant()
$keywords = @(
+ # standard keywords
'none',
'self',
+ 'strict-dynamic',
+ 'report-sample',
+ 'inline-speculation-rules',
+
+ # unsafe keywords
'unsafe-inline',
- 'unsafe-eval'
+ 'unsafe-eval',
+ 'unsafe-hashes',
+ 'wasm-unsafe-eval'
)
$schemes = @(
'http',
'https',
+ 'data',
+ 'blob',
+ 'filesystem',
+ 'mediastream',
'ws',
'wss',
- 'data',
+ 'ftp',
+ 'mailto',
+ 'tel',
'file'
)
@@ -1183,8 +1197,20 @@ function Set-PodeSecurityContentSecurityPolicyInternal {
Protect-PodeContentSecurityKeyword -Name 'base-uri' -Value $Params.BaseUri -Append:$Append
Protect-PodeContentSecurityKeyword -Name 'form-action' -Value $Params.FormAction -Append:$Append
Protect-PodeContentSecurityKeyword -Name 'frame-ancestors' -Value $Params.FrameAncestor -Append:$Append
+ Protect-PodeContentSecurityKeyword -Name 'fenched-frame-src' -Value $Params.FencedFrame -Append:$Append
+ Protect-PodeContentSecurityKeyword -Name 'prefetch-src' -Value $Params.Prefetch -Append:$Append
+ Protect-PodeContentSecurityKeyword -Name 'script-src-attr' -Value $Params.ScriptAttr -Append:$Append
+ Protect-PodeContentSecurityKeyword -Name 'script-src-elem' -Value $Params.ScriptElem -Append:$Append
+ Protect-PodeContentSecurityKeyword -Name 'style-src-attr' -Value $Params.StyleAttr -Append:$Append
+ Protect-PodeContentSecurityKeyword -Name 'style-src-elem' -Value $Params.StyleElem -Append:$Append
+ Protect-PodeContentSecurityKeyword -Name 'worker-src' -Value $Params.Worker -Append:$Append
)
+ # add "report-uri" if supplied
+ if (![string]::IsNullOrWhiteSpace($Params.ReportUri)) {
+ $values += "report-uri $($Params.ReportUri)".Trim()
+ }
+
if (![string]::IsNullOrWhiteSpace($Params.Sandbox) -and ($Params.Sandbox -ine 'None')) {
$values += "sandbox $($Params.Sandbox.ToLowerInvariant())".Trim()
}
@@ -1202,7 +1228,13 @@ function Set-PodeSecurityContentSecurityPolicyInternal {
# Add the Content Security Policy header to the response or relevant context. This cmdlet
# sets the HTTP header with the name 'Content-Security-Policy' and the constructed value.
- Add-PodeSecurityHeader -Name 'Content-Security-Policy' -Value $value
+ # if ReportOnly is set, the header name is set to 'Content-Security-Policy-Report-Only'.
+ $header = 'Content-Security-Policy'
+ if ($Params.ReportOnly) {
+ $header = 'Content-Security-Policy-Report-Only'
+ }
+
+ Add-PodeSecurityHeader -Name $header -Value $value
# this is done to explicitly disable XSS auditors in modern browsers
# as having it enabled has now been found to cause more vulnerabilities
diff --git a/src/Private/Server.ps1 b/src/Private/Server.ps1
index ea4fa1f87..2472c1f91 100644
--- a/src/Private/Server.ps1
+++ b/src/Private/Server.ps1
@@ -206,11 +206,10 @@ function Start-PodeInternalServer {
}
}
}
-
}
}
catch {
- throw $_.Exception
+ throw
}
}
diff --git a/src/Private/Sessions.ps1 b/src/Private/Sessions.ps1
index 4e9bc870c..a3768d4b4 100644
--- a/src/Private/Sessions.ps1
+++ b/src/Private/Sessions.ps1
@@ -290,7 +290,7 @@ function Set-PodeSessionInMemClearDown {
# remove sessions that have expired, or where the parent is gone
$now = [DateTime]::UtcNow
- foreach ($key in $store.Memory.Keys) {
+ foreach ($key in $store.Memory.Keys.Clone()) {
# expired
if ($store.Memory[$key].Expiry -lt $now) {
$null = $store.Memory.Remove($key)
diff --git a/src/Private/Streams.ps1 b/src/Private/Streams.ps1
deleted file mode 100644
index 261ca3586..000000000
--- a/src/Private/Streams.ps1
+++ /dev/null
@@ -1,326 +0,0 @@
-function Read-PodeStreamToEnd {
- param(
- [Parameter()]
- $Stream,
-
- [Parameter()]
- $Encoding = [System.Text.Encoding]::UTF8
- )
-
- if ($null -eq $Stream) {
- return [string]::Empty
- }
-
- return (Use-PodeStream -Stream ([System.IO.StreamReader]::new($Stream, $Encoding)) {
- return $args[0].ReadToEnd()
- })
-}
-
-function Read-PodeByteLineFromByteArray {
- param(
- [Parameter(Mandatory = $true)]
- [byte[]]
- $Bytes,
-
- [Parameter()]
- $Encoding = [System.Text.Encoding]::UTF8,
-
- [Parameter()]
- [int]
- $StartIndex = 0,
-
- [switch]
- $IncludeNewLine
- )
-
- $nlBytes = Get-PodeNewLineByte -Encoding $Encoding
-
- # attempt to find \n
- $index = [array]::IndexOf($Bytes, $nlBytes.NewLine, $StartIndex)
- $fIndex = $index
-
- # if not including new line, remove any trailing \r and \n
- if (!$IncludeNewLine) {
- $fIndex--
-
- if ($Bytes[$fIndex] -eq $nlBytes.Return) {
- $fIndex--
- }
- }
-
- # grab the portion of the bytes array - which is our line
- return @{
- Bytes = $Bytes[$StartIndex..$fIndex]
- StartIndex = $StartIndex
- EndIndex = $index
- }
-}
-
-function Get-PodeByteLinesFromByteArray {
- param(
- [Parameter(Mandatory = $true)]
- [byte[]]
- $Bytes,
-
- [Parameter()]
- $Encoding = [System.Text.Encoding]::UTF8,
-
- [switch]
- $IncludeNewLine
- )
-
- # lines
- $lines = @()
- $nlBytes = Get-PodeNewLineByte -Encoding $Encoding
-
- # attempt to find \n
- $index = 0
- while (($nextIndex = [array]::IndexOf($Bytes, $nlBytes.NewLine, $index)) -gt 0) {
- $fIndex = $nextIndex
-
- # if not including new line, remove any trailing \r and \n
- if (!$IncludeNewLine) {
- $fIndex--
- if ($Bytes[$fIndex] -eq $nlBytes.Return) {
- $fIndex--
- }
- }
-
- # add the line, and get the next one
- $lines += , $Bytes[$index..$fIndex]
- $index = $nextIndex + 1
- }
-
- return $lines
-}
-<#
-.SYNOPSIS
- Converts a stream to a byte array.
-
-.DESCRIPTION
- The `ConvertFrom-PodeValueToByteArray` function reads data from a stream and converts it to a byte array.
- It's useful for scenarios where you need to work with binary data from a stream.
-
-.PARAMETER Stream
- Specifies the input stream to convert. This parameter is mandatory.
-
-.OUTPUTS
- Returns a byte array containing the data read from the input stream.
-
-.EXAMPLE
- # Example usage:
- # Read data from a file stream and convert it to a byte array
- $stream = [System.IO.File]::OpenRead("C:\path\to\file.bin")
- $byteArray = ConvertFrom-PodeValueToByteArray -Stream $stream
- $stream.Close()
-
-.NOTES
- This is an internal function and may change in future releases of Pode.
-#>
-function ConvertFrom-PodeValueToByteArray {
- param(
- [Parameter(Mandatory = $true)]
- $Stream
- )
-
- # Initialize a buffer to read data in chunks
- $buffer = [byte[]]::new(64 * 1024)
- $ms = [System.IO.MemoryStream]::new()
- $read = 0
-
- # Read data from the stream and write it to the memory stream
- while (($read = $Stream.Read($buffer, 0, $buffer.Length)) -gt 0) {
- $ms.Write($buffer, 0, $read)
- }
-
- # Close the memory stream and return the byte array
- $ms.Close()
- return $ms.ToArray()
-}
-<#
-.SYNOPSIS
- Converts a string value to a byte array using the specified encoding.
-
-.DESCRIPTION
- The `ConvertFrom-PodeValueToByteArray` function takes a string value and converts it to a byte array.
- You can specify the desired encoding (default is UTF-8).
-
-.PARAMETER Value
- Specifies the input string value to convert.
-
-.PARAMETER Encoding
- Specifies the encoding to use when converting the string to bytes.
- Default value is UTF-8.
-
-.OUTPUTS
- Returns a byte array containing the encoded representation of the input string.
-
-.EXAMPLE
- # Example usage:
- $inputString = "Hello, world!"
- $byteArray = ConvertFrom-PodeValueToByteArray -Value $inputString
- # Now you can work with the byte array as needed.
-
-.NOTES
- This is an internal function and may change in future releases of Pode.
-#>
-function ConvertFrom-PodeValueToByteArray {
- param(
- [Parameter()]
- [string]
- $Value,
-
- [Parameter()]
- $Encoding = [System.Text.Encoding]::UTF8
- )
-
- return $Encoding.GetBytes($Value)
-}
-
-function ConvertFrom-PodeBytesToString {
- param(
- [Parameter()]
- [byte[]]
- $Bytes,
-
- [Parameter()]
- $Encoding = [System.Text.Encoding]::UTF8,
-
- [switch]
- $RemoveNewLine
- )
-
- if (($null -eq $Bytes) -or ($Bytes.Length -eq 0)) {
- return $Bytes
- }
-
- $value = $Encoding.GetString($Bytes)
- if ($RemoveNewLine) {
- $value = $value.Trim("`r`n")
- }
-
- return $value
-}
-
-<#
-.SYNOPSIS
- Retrieves information about newline characters in different encodings.
-
-.DESCRIPTION
- The `Get-PodeNewLineByte` function returns a hashtable containing information about newline characters.
- It calculates the byte values for newline (`n`) and carriage return (`r`) based on the specified encoding (default is UTF-8).
-
-.PARAMETER Encoding
- Specifies the encoding to use when calculating newline and carriage return byte values.
- Default value is UTF-8.
-
-.OUTPUTS
- Returns a hashtable with the following keys:
- - `NewLine`: Byte value for newline character (`n`).
- - `Return`: Byte value for carriage return character (`r`).
-
-.EXAMPLE
- Get-PodeNewLineByte -Encoding [System.Text.Encoding]::ASCII
- # Returns the byte values for newline and carriage return in ASCII encoding.
-
-.NOTES
- This is an internal function and may change in future releases of Pode.
-#>
-function Get-PodeNewLineByte {
- [CmdletBinding()]
- [OutputType([hashtable])]
- param(
- [Parameter()]
- $Encoding = [System.Text.Encoding]::UTF8
- )
-
- return @{
- NewLine = @($Encoding.GetBytes("`n"))[0]
- Return = @($Encoding.GetBytes("`r"))[0]
- }
-}
-
-function Test-PodeByteArrayIsBoundary {
- param(
- [Parameter()]
- [byte[]]
- $Bytes,
-
- [Parameter()]
- [string]
- $Boundary,
-
- [Parameter()]
- $Encoding = [System.Text.Encoding]::UTF8
- )
-
- # if no bytes, return
- if ($Bytes.Length -eq 0) {
- return $false
- }
-
- # if length difference >3, return (ie, 2 offset for `r`n)
- if (($Bytes.Length - $Boundary.Length) -gt 3) {
- return $false
- }
-
- # check if bytes starts with the boundary
- return (ConvertFrom-PodeBytesToString $Bytes $Encoding).StartsWith($Boundary)
-}
-
-function Remove-PodeNewLineBytesFromArray {
- param(
- [Parameter()]
- $Bytes,
-
- [Parameter()]
- $Encoding = [System.Text.Encoding]::UTF8
- )
-
- $nlBytes = Get-PodeNewLineByte -Encoding $Encoding
- $length = $Bytes.Length - 1
-
- if ($Bytes[$length] -eq $nlBytes.NewLine) {
- $length--
- }
-
- if ($Bytes[$length] -eq $nlBytes.Return) {
- $length--
- }
-
- return $Bytes[0..$length]
-}
-
-function Get-PodeCompressionStream {
- param (
- [Parameter(Mandatory = $true)]
- [System.IO.Stream]
- $InputStream,
-
- [Parameter(Mandatory = $true)]
- [ValidateSet('gzip', 'deflate')]
- [string]
- $Encoding,
-
- [Parameter(Mandatory = $true)]
- [System.IO.Compression.CompressionMode]
- $Mode
- )
-
- $leaveOpen = $Mode -eq [System.IO.Compression.CompressionMode]::Compress
-
- switch ($Encoding.ToLower()) {
- 'gzip' {
- return [System.IO.Compression.GZipStream]::new($InputStream, $Mode, $leaveOpen)
- }
-
- 'deflate' {
- return [System.IO.Compression.DeflateStream]::new($InputStream, $Mode, $leaveOpen)
- }
-
- default {
- # Unsupported stream compression encoding: $Encoding
- throw ($PodeLocale.unsupportedStreamCompressionEncodingExceptionMessage -f $Encoding)
- }
- }
-}
diff --git a/src/Public/Caching.ps1 b/src/Public/Caching.ps1
index 3be0951d8..93f7cef5b 100644
--- a/src/Public/Caching.ps1
+++ b/src/Public/Caching.ps1
@@ -52,7 +52,7 @@ function Get-PodeCache {
}
# used custom storage
- if (Test-PodeCacheStorage -Key $Storage) {
+ if (Test-PodeCacheStorage -Name $Storage) {
return (Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Cache.Storage[$Storage].Get -Arguments @($Key, $Metadata.IsPresent) -Splat -Return)
}
@@ -150,7 +150,7 @@ function Set-PodeCache {
}
# used custom storage
- elseif (Test-PodeCacheStorage -Key $Storage) {
+ elseif (Test-PodeCacheStorage -Name $Storage) {
$null = Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Cache.Storage[$Storage].Set -Arguments @($Key, $InputObject, $Ttl) -Splat
}
@@ -204,7 +204,7 @@ function Test-PodeCache {
}
# used custom storage
- if (Test-PodeCacheStorage -Key $Storage) {
+ if (Test-PodeCacheStorage -Name $Storage) {
return (Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Cache.Storage[$Storage].Test -Arguments @($Key) -Splat -Return)
}
@@ -255,7 +255,7 @@ function Remove-PodeCache {
}
# used custom storage
- elseif (Test-PodeCacheStorage -Key $Storage) {
+ elseif (Test-PodeCacheStorage -Name $Storage) {
$null = Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Cache.Storage[$Storage].Remove -Arguments @($Key) -Splat
}
@@ -301,7 +301,7 @@ function Clear-PodeCache {
}
# used custom storage
- elseif (Test-PodeCacheStorage -Key $Storage) {
+ elseif (Test-PodeCacheStorage -Name $Storage) {
$null = Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Cache.Storage[$Storage].Clear
}
@@ -554,4 +554,4 @@ function Get-PodeCacheDefaultTtl {
param()
return $PodeContext.Server.Cache.DefaultTtl
-}
\ No newline at end of file
+}
diff --git a/src/Public/OAComponents.ps1 b/src/Public/OAComponents.ps1
index 3bbc6eab4..a829f5274 100644
--- a/src/Public/OAComponents.ps1
+++ b/src/Public/OAComponents.ps1
@@ -316,7 +316,9 @@ function Add-PodeOAComponentRequestBody {
[Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
[Alias('ContentSchemas')]
- [hashtable]
+ [ValidateScript({
+ ($_ -is [hashtable]) -or ($_ -is [System.Collections.Specialized.OrderedDictionary])
+ })]
$Content,
[Parameter()]
@@ -344,6 +346,15 @@ function Add-PodeOAComponentRequestBody {
}
$DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag
+ if ($Content -is [hashtable]) {
+ $orderedHashtable = [ordered]@{}
+
+ foreach ($key in $Content.Keys | Sort-Object) {
+ $orderedHashtable[$key] = $Content[$key]
+ }
+ $Content = $orderedHashtable
+ }
+
foreach ($tag in $DefinitionTag) {
$param = [ordered]@{ content = ($Content | ConvertTo-PodeOAObjectSchema -DefinitionTag $tag) }
@@ -780,9 +791,9 @@ function Add-PodeOAComponentPathItem {
Method = $Method.ToLower()
NotPrepared = $true
OpenApi = @{
- Responses = $null
- Parameters = $null
- RequestBody = $null
+ Responses = [ordered]@{}
+ Parameters = [ordered]@{}
+ RequestBody = [ordered]@{}
callbacks = [ordered]@{}
Authentication = @()
Servers = @()
diff --git a/src/Public/OAProperties.ps1 b/src/Public/OAProperties.ps1
index 8c16b8bb1..322bc2f21 100644
--- a/src/Public/OAProperties.ps1
+++ b/src/Public/OAProperties.ps1
@@ -1942,12 +1942,6 @@ If supplied, the schema will be included in a response but not in a request
.PARAMETER WriteOnly
If supplied, the schema will be included in a request but not in a response
-.PARAMETER MinProperties
-If supplied, will restrict the minimun number of properties allowed in an schema.
-
-.PARAMETER MaxProperties
-If supplied, will restrict the maximum number of properties allowed in an schema.
-
.PARAMETER Array
If supplied, the schema will be treated as an array of objects.
@@ -2015,45 +2009,31 @@ function New-PodeOAComponentSchemaProperty {
[switch]
$XmlAttribute,
- [Parameter( ParameterSetName = 'Array')]
- [string]
- $XmlItemName,
-
- [Parameter( ParameterSetName = 'Array')]
- [switch]
- $XmlWrapped,
-
- [Parameter(ParameterSetName = 'Array')]
[object]
$Example,
- [Parameter(ParameterSetName = 'Array')]
[switch]
$Deprecated,
- [Parameter(ParameterSetName = 'Array')]
[switch]
$Required,
- [Parameter(ParameterSetName = 'Array')]
[switch]
$Nullable,
- [Parameter(ParameterSetName = 'Array')]
[switch]
$ReadOnly,
- [Parameter(ParameterSetName = 'Array')]
[switch]
$WriteOnly,
- [Parameter(ParameterSetName = 'Array')]
- [int]
- $MinProperties,
+ [Parameter( ParameterSetName = 'Array')]
+ [string]
+ $XmlItemName,
- [Parameter(ParameterSetName = 'Array')]
- [int]
- $MaxProperties,
+ [Parameter( ParameterSetName = 'Array')]
+ [switch]
+ $XmlWrapped,
[Parameter(Mandatory = $true, ParameterSetName = 'Array')]
[switch]
diff --git a/src/Public/OpenApi.ps1 b/src/Public/OpenApi.ps1
index 4a07a4d29..3c3940b32 100644
--- a/src/Public/OpenApi.ps1
+++ b/src/Public/OpenApi.ps1
@@ -395,21 +395,45 @@ function Add-PodeOAServerEndpoint {
$DefinitionTag
)
+
+ # If the DefinitionTag is empty, use the selected tag from Pode's OpenAPI context
if (Test-PodeIsEmpty -Value $DefinitionTag) {
$DefinitionTag = @($PodeContext.Server.OpenAPI.SelectedDefinitionTag)
}
+
+ # Loop through each tag to add the server object to the corresponding OpenAPI definition
foreach ($tag in $DefinitionTag) {
+ # If the 'servers' array for the tag doesn't exist, initialize it as an empty array
if (! $PodeContext.Server.OpenAPI.Definitions[$tag].servers) {
$PodeContext.Server.OpenAPI.Definitions[$tag].servers = @()
}
+
+ # Create an ordered hashtable representing the server object with the URL
$lUrl = [ordered]@{url = $Url }
+
+ # If a description is provided, add it to the server object
if ($Description) {
$lUrl.description = $Description
}
+ # If variables are provided, add them to the server object
if ($Variables) {
$lUrl.variables = $Variables
}
+
+ # Check if the URL is a local endpoint (not starting with 'http(s)://')
+ if ($lUrl.url -notmatch '^(?i)https?://') {
+ # Loop through existing server URLs in the definition
+ foreach ($srv in $PodeContext.Server.OpenAPI.Definitions[$tag].servers) {
+ # If there's already a local endpoint, throw an exception, as only one local endpoint is allowed per definition
+ # Both are defined as local OpenAPI endpoints, but only one local endpoint is allowed per API definition.
+ if ($srv.url -notmatch '^(?i)https?://') {
+ throw ($PodeLocale.LocalEndpointConflictExceptionMessage -f $Url, $srv.url)
+ }
+ }
+ }
+
+ # Add the new server object to the OpenAPI definition for the current tag
$PodeContext.Server.OpenAPI.Definitions[$tag].servers += $lUrl
}
}
@@ -563,7 +587,6 @@ An Array of strings representing the unique tag for the API specification.
This tag helps distinguish between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.
-
.EXAMPLE
Add-PodeRoute -PassThru | Add-PodeOAResponse -StatusCode 200 -Content @{ 'application/json' = (New-PodeOAIntProperty -Name 'userId' -Object) }
@@ -642,7 +665,6 @@ function Add-PodeOAResponse {
$Route = $pipelineValue
}
- $DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag
# override status code with default
if ($Default) {
$code = 'default'
@@ -653,7 +675,9 @@ function Add-PodeOAResponse {
# add the respones to the routes
foreach ($r in @($Route)) {
- foreach ($tag in $DefinitionTag) {
+ $oaDefinitionTag = Test-PodeRouteOADefinitionTag -Route $r -DefinitionTag $DefinitionTag
+
+ foreach ($tag in $oaDefinitionTag) {
if (! $r.OpenApi.Responses.$tag) {
$r.OpenApi.Responses.$tag = [ordered]@{}
}
@@ -735,7 +759,7 @@ function Remove-PodeOAResponse {
}
# remove the respones from the routes
foreach ($r in $Route) {
- if ($r.OpenApi.Responses.ContainsKey($code)) {
+ if ($r.OpenApi.Responses.Keys -Contains $code) {
$null = $r.OpenApi.Responses.Remove($code)
}
}
@@ -749,25 +773,37 @@ function Remove-PodeOAResponse {
<#
.SYNOPSIS
-Sets the definition of a request for a route.
+ Sets the OpenAPI request definition for a route.
.DESCRIPTION
-Sets the definition of a request for a route.
+ Configures the OpenAPI request properties for a specified route, including parameters and request body definition.
+ This function defines how the route should handle incoming requests in accordance with OpenAPI standards.
.PARAMETER Route
-The route to set a request definition, usually from -PassThru on Add-PodeRoute.
+ The route to set a request definition for. This is typically passed through from -PassThru on Add-PodeRoute.
.PARAMETER Parameters
-The Parameter definitions the request uses (from ConvertTo-PodeOAParameter).
+ Defines the parameters for the request, provided by ConvertTo-PodeOAParameter.
.PARAMETER RequestBody
-The Request Body definition the request uses (from New-PodeOARequestBody).
+ Specifies the body schema for the request, provided by New-PodeOARequestBody.
+
+.PARAMETER AllowNonStandardBody
+ Allows methods like DELETE and GET to include a request body, which is generally discouraged by RFC 7231.
+ This can be used to relax the default restriction and enable a body for HTTP methods that don’t typically support it.
.PARAMETER PassThru
-If supplied, the route passed in will be returned for further chaining.
+ If specified, returns the original route object for additional chaining after setting the request properties.
+
+.PARAMETER DefinitionTag
+ An Array of strings representing the unique tag for the API specification.
+ This tag helps distinguish between different versions or types of API specifications within the application.
+ You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.
.EXAMPLE
-Add-PodeRoute -PassThru | Set-PodeOARequest -RequestBody (New-PodeOARequestBody -Schema 'UserIdBody')
+ Add-PodeRoute -PassThru | Set-PodeOARequest -RequestBody (New-PodeOARequestBody -Schema 'UserIdBody') -AllowNonStandardBody
+
+ Sets the request body for a route and allows non-standard HTTP methods like DELETE to use a request body.
#>
function Set-PodeOARequest {
[CmdletBinding()]
@@ -785,7 +821,13 @@ function Set-PodeOARequest {
$RequestBody,
[switch]
- $PassThru
+ $PassThru,
+
+ [switch]
+ $AllowNonStandardBody,
+
+ [string[]]
+ $DefinitionTag
)
begin {
# Initialize an array to hold piped-in values
@@ -805,24 +847,29 @@ function Set-PodeOARequest {
foreach ($r in $Route) {
- if (($null -ne $Parameters) -and ($Parameters.Length -gt 0)) {
- $r.OpenApi.Parameters = @($Parameters)
- }
+ $oaDefinitionTag = Test-PodeRouteOADefinitionTag -Route $r -DefinitionTag $DefinitionTag
- if ($null -ne $RequestBody) {
- # Only 'POST', 'PUT', 'PATCH' can have a request body
- if (('POST', 'PUT', 'PATCH') -inotcontains $r.Method ) {
- # {0} operations cannot have a Request Body.
- throw ($PodeLocale.getRequestBodyNotAllowedExceptionMessage -f $r.Method)
+ foreach ($tag in $oaDefinitionTag) {
+ if (($null -ne $Parameters) -and ($Parameters.Length -gt 0)) {
+ $r.OpenApi.Parameters[$tag] = @($Parameters)
+ }
+
+ if ($null -ne $RequestBody) {
+ # Check if AllowNonStandardBody is used or if the method is typically allowed to have a body
+ if (! $AllowNonStandardBody -and ('POST', 'PUT', 'PATCH') -inotcontains $r.Method) {
+ #'{0}' operations cannot have a Request Body. Use -AllowNonStandardBody to override this restriction.
+ throw ($PodeLocale.getRequestBodyNotAllowedExceptionMessage -f $r.Method)
+ }
+ $r.OpenApi.RequestBody = $RequestBody
}
- $r.OpenApi.RequestBody = $RequestBody
- }
+ }
}
if ($PassThru) {
return $Route
}
+
}
}
@@ -1017,7 +1064,6 @@ message: any validation issue
$UserInfo = Test-PodeOAJsonSchemaCompliance -Json $UserInfo -SchemaReference 'UserIdSchema'}
#>
-
function Test-PodeOAJsonSchemaCompliance {
param (
[Parameter(Mandatory = $true)]
@@ -1032,7 +1078,7 @@ function Test-PodeOAJsonSchemaCompliance {
$DefinitionTag
)
if ($DefinitionTag) {
- if (! ($PodeContext.Server.OpenApi.Definitions.Keys -ccontains $DefinitionTag)) {
+ if (! ($PodeContext.Server.OpenApi.Definitions.Keys -icontains $DefinitionTag)) {
# DefinitionTag does not exist.
throw ($PodeLocale.definitionTagNotDefinedExceptionMessage -f $DefinitionTag)
}
@@ -1632,16 +1678,23 @@ function Set-PodeOARouteInfo {
$Route = $pipelineValue
}
- $DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag
+ $defaultTag = Test-PodeOADefinitionTag -Tag $DefinitionTag
foreach ($r in @($Route)) {
- if ((Compare-Object -ReferenceObject $r.OpenApi.DefinitionTag -DifferenceObject $DefinitionTag).Count -ne 0) {
- if ($r.OpenApi.IsDefTagConfigured ) {
- # Definition Tag for a Route cannot be changed.
- throw ($PodeLocale.definitionTagChangeNotAllowedExceptionMessage)
+ if ($DefinitionTag) {
+ if ((Compare-Object -ReferenceObject $r.OpenApi.DefinitionTag -DifferenceObject $DefinitionTag).Count -ne 0) {
+ if ($r.OpenApi.IsDefTagConfigured ) {
+ # Definition Tag for a Route cannot be changed.
+ throw ($PodeLocale.definitionTagChangeNotAllowedExceptionMessage)
+ }
+
+ $r.OpenApi.DefinitionTag = $defaultTag
+ $r.OpenApi.IsDefTagConfigured = $true
}
- else {
- $r.OpenApi.DefinitionTag = $DefinitionTag
+ }
+ else {
+ if (! $r.OpenApi.IsDefTagConfigured ) {
+ $r.OpenApi.DefinitionTag = $defaultTag
$r.OpenApi.IsDefTagConfigured = $true
}
}
@@ -1651,7 +1704,7 @@ function Set-PodeOARouteInfo {
# OperationID:$OperationId has to be unique and cannot be applied to an array
throw ($PodeLocale.operationIdMustBeUniqueForArrayExceptionMessage -f $OperationId)
}
- foreach ($tag in $DefinitionTag) {
+ foreach ($tag in $defaultTag) {
if ($PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.operationId -ccontains $OperationId) {
# OperationID:$OperationId has to be unique
throw ($PodeLocale.operationIdMustBeUniqueExceptionMessage -f $OperationId)
@@ -2711,16 +2764,17 @@ function Add-PodeOACallBack {
$Route = $pipelineValue
}
- $DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag
foreach ($r in @($Route)) {
- foreach ($tag in $DefinitionTag) {
+ $oaDefinitionTag = Test-PodeRouteOADefinitionTag -Route $r -DefinitionTag $DefinitionTag
+
+ foreach ($tag in $oaDefinitionTag) {
if ($Reference) {
Test-PodeOAComponentInternal -Field callbacks -DefinitionTag $tag -Name $Reference -PostValidation
if (!$Name) {
$Name = $Reference
}
- if (! $r.OpenApi.CallBacks.ContainsKey($tag)) {
+ if (! ($r.OpenApi.CallBacks.Keys -Contains $tag)) {
$r.OpenApi.CallBacks[$tag] = [ordered]@{}
}
$r.OpenApi.CallBacks[$tag].$Name = [ordered]@{
@@ -2728,7 +2782,7 @@ function Add-PodeOACallBack {
}
}
else {
- if (! $r.OpenApi.CallBacks.ContainsKey($tag)) {
+ if (! ($r.OpenApi.CallBacks.Keys -Contains $tag)) {
$r.OpenApi.CallBacks[$tag] = [ordered]@{}
}
$r.OpenApi.CallBacks[$tag].$Name = New-PodeOAComponentCallBackInternal -Params $PSBoundParameters -DefinitionTag $tag
@@ -2878,7 +2932,7 @@ function New-PodeOAResponse {
end {
if ($ResponseList) {
foreach ($tag in $DefinitionTag) {
- if (! $ResponseList.ContainsKey( $tag) ) {
+ if (! ($ResponseList.Keys -Contains $tag )) {
$ResponseList[$tag] = [ordered] @{}
}
$response[$tag].GetEnumerator() | ForEach-Object { $ResponseList[$tag][$_.Key] = $_.Value }
@@ -3313,7 +3367,7 @@ function Add-PodeOAExternalRoute {
# ensure the route has appropriate slashes
$Path = Update-PodeRouteSlash -Path $Path
- $OpenApiPath = ConvertTo-PodeOpenApiRoutePath -Path $Path
+ $OpenApiPath = ConvertTo-PodeOARoutePath -Path $Path
$Path = Resolve-PodePlaceholder -Path $Path
$extRoute = @{
Method = $Method.ToLower()
@@ -3321,9 +3375,9 @@ function Add-PodeOAExternalRoute {
Local = $false
OpenApi = @{
Path = $OpenApiPath
- Responses = $null
- Parameters = $null
- RequestBody = $null
+ Responses = [ordered]@{}
+ Parameters = [ordered]@{}
+ RequestBody = [ordered]@{}
callbacks = [ordered]@{}
Authentication = @()
Servers = $Servers
@@ -3483,8 +3537,8 @@ function Add-PodeOAWebhook {
NotPrepared = $true
OpenApi = @{
Responses = [ordered]@{}
- Parameters = $null
- RequestBody = $null
+ Parameters = [ordered]@{}
+ RequestBody = [ordered]@{}
callbacks = [ordered]@{}
Authentication = @()
DefinitionTag = $_definitionTag
@@ -3663,7 +3717,7 @@ function Test-PodeOADefinitionTag {
if ($Tag -and $Tag.Count -gt 0) {
foreach ($t in $Tag) {
- if (! ($PodeContext.Server.OpenApi.Definitions.Keys -ccontains $t)) {
+ if (! ($PodeContext.Server.OpenApi.Definitions.Keys -icontains $t)) {
# DefinitionTag does not exist.
throw ($PodeLocale.definitionTagNotDefinedExceptionMessage -f $t)
}
diff --git a/src/Public/Responses.ps1 b/src/Public/Responses.ps1
index 9aedf0fd6..9a9f82e3c 100644
--- a/src/Public/Responses.ps1
+++ b/src/Public/Responses.ps1
@@ -1,3 +1,5 @@
+using namespace Pode
+
<#
.SYNOPSIS
Attaches a file onto the Response for downloading.
@@ -216,7 +218,7 @@ function Write-PodeTextResponse {
else {
# convert string to bytes
if ($isStringValue) {
- $Bytes = ConvertFrom-PodeValueToByteArray -Value $Value
+ $Bytes = [System.Text.Encoding]::UTF8.GetBytes($Value)
}
# check if we only need a range of the bytes
@@ -272,23 +274,8 @@ function Write-PodeTextResponse {
# check if we need to compress the response
if ($PodeContext.Server.Web.Compression.Enabled -and ![string]::IsNullOrWhiteSpace($WebEvent.AcceptEncoding)) {
- try {
- $ms = [System.IO.MemoryStream]::new()
- $stream = Get-PodeCompressionStream -InputStream $ms -Encoding $WebEvent.AcceptEncoding -Mode Compress
- $stream.Write($Bytes, 0, $Bytes.Length)
- $stream.Close()
- $ms.Position = 0
- $Bytes = $ms.ToArray()
- }
- finally {
- if ($null -ne $stream) {
- $stream.Close()
- }
-
- if ($null -ne $ms) {
- $ms.Close()
- }
- }
+ # compress the bytes
+ $Bytes = [PodeHelpers]::CompressBytes($Bytes, $WebEvent.AcceptEncoding)
# set content encoding header
Set-PodeHeader -Name 'Content-Encoding' -Value $WebEvent.AcceptEncoding
diff --git a/src/Public/Routes.ps1 b/src/Public/Routes.ps1
index 9af545f77..9bc6db522 100644
--- a/src/Public/Routes.ps1
+++ b/src/Public/Routes.ps1
@@ -290,7 +290,7 @@ function Add-PodeRoute {
# ensure the route has appropriate slashes
$Path = Update-PodeRouteSlash -Path $Path
- $OpenApiPath = ConvertTo-PodeOpenApiRoutePath -Path $Path
+ $OpenApiPath = ConvertTo-PodeOARoutePath -Path $Path
$Path = Resolve-PodePlaceholder -Path $Path
# get endpoints from name
@@ -433,9 +433,9 @@ function Add-PodeRoute {
OpenApi = @{
Path = $OpenApiPath
Responses = $DefaultResponse
- Parameters = $null
- RequestBody = $null
- CallBacks = @{}
+ Parameters = [ordered]@{}
+ RequestBody = [ordered]@{}
+ CallBacks = [ordered]@{}
Authentication = @()
Servers = @()
DefinitionTag = $DefinitionTag
@@ -451,6 +451,12 @@ function Add-PodeRoute {
}
})
+
+ if ($PodeContext.Server.OpenAPI.Routes -notcontains $OpenApiPath ) {
+ $PodeContext.Server.OpenAPI.Routes += $OpenApiPath
+ }
+
+
if (![string]::IsNullOrWhiteSpace($Authentication)) {
Set-PodeOAAuth -Route $methodRoutes -Name $Authentication -AllowAnon:$AllowAnon
}
@@ -742,7 +748,7 @@ function Add-PodeStaticRoute {
# ensure the route has appropriate slashes
$Path = Update-PodeRouteSlash -Path $Path -Static
- $OpenApiPath = ConvertTo-PodeOpenApiRoutePath -Path $Path
+ $OpenApiPath = ConvertTo-PodeOARoutePath -Path $Path
$Path = Resolve-PodePlaceholder -Path $Path
# get endpoints from name
diff --git a/src/Public/Security.ps1 b/src/Public/Security.ps1
index ee5ab7947..f53fcb2ca 100644
--- a/src/Public/Security.ps1
+++ b/src/Public/Security.ps1
@@ -14,6 +14,9 @@ If supplied, the Strict-Transport-Security header will be set.
.PARAMETER XssBlock
If supplied, the X-XSS-Protection header will be set to blocking mode. (Default: Off)
+.PARAMETER CspReportOnly
+If supplied, the Content-Security-Policy header will be set as the Content-Security-Policy-Report-Only header.
+
.EXAMPLE
Set-PodeSecurity -Type Simple
@@ -32,7 +35,10 @@ function Set-PodeSecurity {
$UseHsts,
[switch]
- $XssBlock
+ $XssBlock,
+
+ [switch]
+ $CspReportOnly
)
# general headers
@@ -55,7 +61,7 @@ function Set-PodeSecurity {
Set-PodeSecurityCrossOrigin -Embed Require-Corp -Open Same-Origin -Resource Same-Origin
Set-PodeSecurityAccessControl -Origin '*' -Methods '*' -Headers '*' -Duration 7200
- Set-PodeSecurityContentSecurityPolicy -Default 'self' -XssBlock:$XssBlock
+ Set-PodeSecurityContentSecurityPolicy -Default 'self' -XssBlock:$XssBlock -ReportOnly:$CspReportOnly
# only add hsts if specifiec
if ($UseHsts) {
@@ -302,15 +308,42 @@ The values to use for the FormAction portion of the header.
.PARAMETER FrameAncestor
The values to use for the FrameAncestor portion of the header.
+.PARAMETER FencedFrame
+The values to use for the FencedFrame portion of the header.
+
+.PARAMETER Prefetch
+The values to use for the Prefetch portion of the header.
+
+.PARAMETER ScriptAttr
+The values to use for the ScriptAttr portion of the header.
+
+.PARAMETER ScriptElem
+The values to use for the ScriptElem portion of the header.
+
+.PARAMETER StyleAttr
+The values to use for the StyleAttr portion of the header.
+
+.PARAMETER StyleElem
+The values to use for the StyleElem portion of the header.
+
+.PARAMETER Worker
+The values to use for the Worker portion of the header.
+
.PARAMETER Sandbox
The value to use for the Sandbox portion of the header.
+.PARAMETER ReportUri
+The value to use for the ReportUri portion of the header.
+
.PARAMETER UpgradeInsecureRequests
If supplied, the header will have the upgrade-insecure-requests value added.
.PARAMETER XssBlock
If supplied, the X-XSS-Protection header will be set to blocking mode. (Default: Off)
+.PARAMETER ReportOnly
+If supplied, the header will be set as a report-only header.
+
.EXAMPLE
Set-PodeSecurityContentSecurityPolicy -Default 'self'
#>
@@ -373,6 +406,34 @@ function Set-PodeSecurityContentSecurityPolicy {
[string[]]
$FrameAncestor,
+ [Parameter()]
+ [string[]]
+ $FencedFrame,
+
+ [Parameter()]
+ [string[]]
+ $Prefetch,
+
+ [Parameter()]
+ [string[]]
+ $ScriptAttr,
+
+ [Parameter()]
+ [string[]]
+ $ScriptElem,
+
+ [Parameter()]
+ [string[]]
+ $StyleAttr,
+
+ [Parameter()]
+ [string[]]
+ $StyleElem,
+
+ [Parameter()]
+ [string[]]
+ $Worker,
+
[Parameter()]
[ValidateSet('', 'Allow-Downloads', 'Allow-Downloads-Without-User-Activation', 'Allow-Forms', 'Allow-Modals', 'Allow-Orientation-Lock',
'Allow-Pointer-Lock', 'Allow-Popups', 'Allow-Popups-To-Escape-Sandbox', 'Allow-Presentation', 'Allow-Same-Origin', 'Allow-Scripts',
@@ -380,11 +441,18 @@ function Set-PodeSecurityContentSecurityPolicy {
[string]
$Sandbox = 'None',
+ [Parameter()]
+ [string]
+ $ReportUri,
+
[switch]
$UpgradeInsecureRequests,
[switch]
- $XssBlock
+ $XssBlock,
+
+ [switch]
+ $ReportOnly
)
Set-PodeSecurityContentSecurityPolicyInternal -Params $PSBoundParameters
@@ -439,17 +507,43 @@ The values to add for the FormAction portion of the header.
.PARAMETER FrameAncestor
The values to add for the FrameAncestor portion of the header.
+.PARAMETER FencedFrame
+The values to add for the FencedFrame portion of the header.
+
+.PARAMETER Prefetch
+The values to add for the Prefetch portion of the header.
+
+.PARAMETER ScriptAttr
+The values to add for the ScriptAttr portion of the header.
+
+.PARAMETER ScriptElem
+The values to add for the ScriptElem portion of the header.
+
+.PARAMETER StyleAttr
+The values to add for the StyleAttr portion of the header.
+
+.PARAMETER StyleElem
+The values to add for the StyleElem portion of the header.
+
+.PARAMETER Worker
+The values to add for the Worker portion of the header.
+
.PARAMETER Sandbox
The value to use for the Sandbox portion of the header.
+.PARAMETER ReportUri
+The value to use for the ReportUri portion of the header.
+
.PARAMETER UpgradeInsecureRequests
If supplied, the header will have the upgrade-insecure-requests value added.
+.PARAMETER ReportOnly
+If supplied, the header will be set as a report-only header.
+
.EXAMPLE
Add-PodeSecurityContentSecurityPolicy -Default '*.twitter.com' -Image 'data'
#>
function Add-PodeSecurityContentSecurityPolicy {
- [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSPossibleIncorrectComparisonWithNull', '')]
[CmdletBinding()]
param(
[Parameter()]
@@ -508,6 +602,34 @@ function Add-PodeSecurityContentSecurityPolicy {
[string[]]
$FrameAncestor,
+ [Parameter()]
+ [string[]]
+ $FencedFrame,
+
+ [Parameter()]
+ [string[]]
+ $Prefetch,
+
+ [Parameter()]
+ [string[]]
+ $ScriptAttr,
+
+ [Parameter()]
+ [string[]]
+ $ScriptElem,
+
+ [Parameter()]
+ [string[]]
+ $StyleAttr,
+
+ [Parameter()]
+ [string[]]
+ $StyleElem,
+
+ [Parameter()]
+ [string[]]
+ $Worker,
+
[Parameter()]
[ValidateSet('', 'Allow-Downloads', 'Allow-Downloads-Without-User-Activation', 'Allow-Forms', 'Allow-Modals', 'Allow-Orientation-Lock',
'Allow-Pointer-Lock', 'Allow-Popups', 'Allow-Popups-To-Escape-Sandbox', 'Allow-Presentation', 'Allow-Same-Origin', 'Allow-Scripts',
@@ -515,8 +637,15 @@ function Add-PodeSecurityContentSecurityPolicy {
[string]
$Sandbox = 'None',
+ [Parameter()]
+ [string]
+ $ReportUri,
+
+ [switch]
+ $UpgradeInsecureRequests,
+
[switch]
- $UpgradeInsecureRequests
+ $ReportOnly
)
Set-PodeSecurityContentSecurityPolicyInternal -Params $PSBoundParameters -Append
diff --git a/src/Public/Tasks.ps1 b/src/Public/Tasks.ps1
index 26477d7c6..c0267f9be 100644
--- a/src/Public/Tasks.ps1
+++ b/src/Public/Tasks.ps1
@@ -486,95 +486,6 @@ function Wait-PodeTask {
}
}
-<#
-.SYNOPSIS
-Get all Task Processes.
-
-.DESCRIPTION
-Get all Task Processes, with support for filtering. These are the processes created when using Invoke-PodeTask.
-
-.PARAMETER Name
-An optional Name of the Task to filter by, can be one or more.
-
-.PARAMETER Id
-An optional ID of the Task process to filter by, can be one or more.
-
-.PARAMETER State
-An optional State of the Task process to filter by, can be one or more.
-
-.EXAMPLE
-Get-PodeTaskProcess
-
-.EXAMPLE
-Get-PodeTaskProcess -Name 'TaskName'
-
-.EXAMPLE
-Get-PodeTaskProcess -Id 'TaskId'
-
-.EXAMPLE
-Get-PodeTaskProcess -State 'Running'
-#>
-function Get-PodeTaskProcess {
- [CmdletBinding()]
- param(
- [Parameter()]
- [string[]]
- $Name,
-
- [Parameter()]
- [string[]]
- $Id,
-
- [Parameter()]
- [ValidateSet('All', 'Pending', 'Running', 'Completed', 'Failed')]
- [string[]]
- $State = 'All'
- )
-
- $processes = $PodeContext.Tasks.Processes.Values
-
- # filter processes by name
- if (($null -ne $Name) -and ($Name.Length -gt 0)) {
- $processes = @(foreach ($_name in $Name) {
- foreach ($process in $processes) {
- if ($process.Task -ine $_name) {
- continue
- }
-
- $process
- }
- })
- }
-
- # filter processes by id
- if (($null -ne $Id) -and ($Id.Length -gt 0)) {
- $processes = @(foreach ($_id in $Id) {
- foreach ($process in $processes) {
- if ($process.ID -ine $_id) {
- continue
- }
-
- $process
- }
- })
- }
-
- # filter processes by status
- if ($State -inotcontains 'All') {
- $processes = @(foreach ($process in $processes) {
- if ($State -inotcontains $process.State) {
- continue
- }
-
- $process
- })
- }
-
- # return processes
- return $processes
-}
-
-
<#
.SYNOPSIS
Get all Task Processes.
diff --git a/tests/integration/OpenApi.Tests.ps1 b/tests/integration/OpenApi.Tests.ps1
index 62cf921ad..a6e4a7e97 100644
--- a/tests/integration/OpenApi.Tests.ps1
+++ b/tests/integration/OpenApi.Tests.ps1
@@ -18,13 +18,8 @@ Describe 'OpenAPI integration tests' {
}
$PortV3 = 8080
$PortV3_1 = 8081
- $scriptPath = "$($PSScriptRoot)\..\..\examples\OpenApi-TuttiFrutti.ps1"
- if ($PSVersionTable.PsVersion -gt [version]'6.0') {
- Start-Process 'pwsh' -ArgumentList "-NoProfile -File `"$scriptPath`" -Quiet -PortV3 $PortV3 -PortV3_1 $PortV3_1 -DisableTermination" -NoNewWindow
- }
- else {
- Start-Process 'powershell' -ArgumentList "-NoProfile -File `"$scriptPath`" -Quiet -PortV3 $PortV3 -PortV3_1 $PortV3_1 -DisableTermination" -NoNewWindow
- }
+ $scriptPath = "$($PSScriptRoot)\..\..\examples\OpenApi-TuttiFrutti.ps1"
+ Start-Process (Get-Process -Id $PID).Path -ArgumentList "-NoProfile -File `"$scriptPath`" -Quiet -PortV3 $PortV3 -PortV3_1 $PortV3_1 -DisableTermination" -NoNewWindow
function Compare-StringRnLn {
param (
@@ -120,8 +115,8 @@ Describe 'OpenAPI integration tests' {
return $true
}
else {
- if($value1 -is [string] -and $value2 -is [string]){
- return Compare-StringRnLn $value1 $value2
+ if ($value1 -is [string] -and $value2 -is [string]) {
+ return Compare-StringRnLn $value1 $value2
}
# Check if the values are equal
return $value1 -eq $value2
diff --git a/tests/integration/specs/OpenApi-TuttiFrutti_3.0.3.json b/tests/integration/specs/OpenApi-TuttiFrutti_3.0.3.json
index 4edacfd8d..5f31e759c 100644
--- a/tests/integration/specs/OpenApi-TuttiFrutti_3.0.3.json
+++ b/tests/integration/specs/OpenApi-TuttiFrutti_3.0.3.json
@@ -52,6 +52,59 @@
],
"paths": {
"/pet": {
+ "post": {
+ "tags": [
+ "pet"
+ ],
+ "summary": "Add a new pet to the store",
+ "description": "Add a new pet to the store",
+ "operationId": "addPet",
+ "requestBody": {
+ "$ref": "#/components/requestBodies/PetBodySchema"
+ },
+ "security": [
+ {
+ "Login-OAuth2": [
+ "write"
+ ]
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful operation",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Pet"
+ }
+ },
+ "application/xml": {
+ "schema": {
+ "$ref": "#/components/schemas/Pet"
+ }
+ }
+ }
+ },
+ "405": {
+ "description": "Validation exception",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "result": {
+ "type": "string"
+ },
+ "message": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
"put": {
"tags": [
"pet"
@@ -66,12 +119,12 @@
"200": {
"description": "Successful operation",
"content": {
- "application/xml": {
+ "application/json": {
"schema": {
"$ref": "#/components/schemas/Pet"
}
},
- "application/json": {
+ "application/xml": {
"schema": {
"$ref": "#/components/schemas/Pet"
}
@@ -103,17 +156,68 @@
}
}
}
- },
+ }
+ },
+ "/petcallback": {
"post": {
"tags": [
"pet"
],
"summary": "Add a new pet to the store",
"description": "Add a new pet to the store",
- "operationId": "addPet",
+ "operationId": "addPetcallback",
"requestBody": {
"$ref": "#/components/requestBodies/PetBodySchema"
},
+ "callbacks": {
+ "test": {
+ "'{$request.body#/id}'": {
+ "post": {
+ "requestBody": {
+ "content": {
+ "\"*/*\"": {
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "Successful operation",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Pet"
+ }
+ }
+ },
+ "application/xml": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Pet"
+ }
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid ID supplied"
+ },
+ "404": {
+ "description": "Pet not found"
+ },
+ "default": {
+ "description": "Something is wrong"
+ }
+ }
+ }
+ }
+ }
+ },
"security": [
{
"Login-OAuth2": [
@@ -125,12 +229,12 @@
"200": {
"description": "Successful operation",
"content": {
- "application/xml": {
+ "application/json": {
"schema": {
"$ref": "#/components/schemas/Pet"
}
},
- "application/json": {
+ "application/xml": {
"schema": {
"$ref": "#/components/schemas/Pet"
}
@@ -158,1688 +262,20 @@
}
}
},
- "/user/{username}": {
- "put": {
- "tags": [
- "user"
- ],
- "summary": "Update user",
- "description": "This can only be done by the logged in user.",
- "operationId": "updateUser",
- "parameters": [
- {
- "description": " name that need to be updated.",
- "name": "username",
- "required": true,
- "schema": {
- "type": "string"
- },
- "in": "path"
- }
- ],
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/User"
- }
- },
- "application/xml": {
- "schema": {
- "$ref": "#/components/schemas/User"
- }
- },
- "application/x-www-form-urlencoded": {
- "schema": {
- "$ref": "#/components/schemas/User"
- }
- }
- },
- "required": true
- },
- "responses": {
- "200": {
- "$ref": "#/components/responses/UserOpSuccess"
- },
- "400": {
- "description": "Invalid username supplied"
- },
- "404": {
- "description": "User not found"
- },
- "405": {
- "description": "Invalid Input"
- }
- }
- },
- "delete": {
- "tags": [
- "user"
- ],
- "summary": "Delete user",
- "description": "This can only be done by the logged in user.",
- "operationId": "deleteUser",
- "parameters": [
- {
- "description": "The name that needs to be deleted.",
- "name": "username",
- "required": true,
- "schema": {
- "type": "string"
- },
- "in": "path"
- }
- ],
- "responses": {
- "200": {
- "description": "Successful operation"
- },
- "400": {
- "description": "Invalid username supplied"
- },
- "404": {
- "description": "User not found"
- }
- }
- },
- "get": {
- "tags": [
- "user"
- ],
- "summary": "Get user by user name",
- "description": "Get user by user name.",
- "operationId": "getUserByName",
- "parameters": [
- {
- "description": "The name that needs to be fetched. Use user1 for testing.",
- "name": "username",
- "required": true,
- "schema": {
- "type": "string"
- },
- "in": "path"
- }
- ],
- "responses": {
- "200": {
- "$ref": "#/components/responses/UserOpSuccess"
- },
- "400": {
- "description": "Invalid username supplied"
- },
- "404": {
- "description": "User not found"
- }
- }
- }
- },
- "/user_1/{username}": {
- "put": {
+ "/petcallbackReference": {
+ "post": {
"tags": [
- "user"
- ],
- "summary": "Update user",
- "description": "This can only be done by the logged in user.",
- "operationId": "updateUser_1",
- "parameters": [
- {
- "description": " name that need to be updated.",
- "name": "username",
- "required": true,
- "schema": {
- "type": "string"
- },
- "in": "path"
- }
+ "pet"
],
+ "summary": "Add a new pet to the store",
+ "description": "Add a new pet to the store",
+ "operationId": "petcallbackReference",
"requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/StructPart"
- }
- },
- "application/xml": {
- "schema": {
- "$ref": "#/components/schemas/StructPart"
- }
- },
- "application/x-www-form-urlencoded": {
- "schema": {
- "$ref": "#/components/schemas/StructPart"
- }
- }
- },
- "required": true
- },
- "responses": {
- "200": {
- "$ref": "#/components/responses/UserOpSuccess"
- },
- "400": {
- "description": "Invalid username supplied"
- },
- "404": {
- "description": "User not found"
- },
- "405": {
- "description": "Invalid Input"
- }
- }
- }
- },
- "/userLink/{username}": {
- "put": {
- "tags": [
- "user"
- ],
- "summary": "Update user",
- "description": "This can only be done by the logged in user.",
- "operationId": "updateUserLink",
- "parameters": [
- {
- "description": " name that need to be updated.",
- "name": "username",
- "required": true,
- "schema": {
- "type": "string"
- },
- "in": "path"
- }
- ],
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/User"
- }
- },
- "application/xml": {
- "schema": {
- "$ref": "#/components/schemas/User"
- }
- },
- "application/x-www-form-urlencoded": {
- "schema": {
- "$ref": "#/components/schemas/User"
- }
- }
- },
- "required": true
- },
- "responses": {
- "200": {
- "description": "OK",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/User"
- }
- }
- },
- "links": {
- "address": {
- "operationId": "getUserByName",
- "parameters": {
- "username": "$request.path.username"
- }
- }
- }
- },
- "400": {
- "description": "Invalid username supplied"
- },
- "404": {
- "description": "User not found"
- },
- "405": {
- "description": "Invalid Input"
- }
- }
- }
- },
- "/userLinkByRef/{username}": {
- "put": {
- "tags": [
- "user"
- ],
- "summary": "Update user",
- "description": "This can only be done by the logged in user.",
- "operationId": "updateUserLinkByRef",
- "parameters": [
- {
- "description": " name that need to be updated.",
- "name": "username",
- "required": true,
- "schema": {
- "type": "string"
- },
- "in": "path"
- }
- ],
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/User"
- }
- },
- "application/xml": {
- "schema": {
- "$ref": "#/components/schemas/User"
- }
- },
- "application/x-www-form-urlencoded": {
- "schema": {
- "$ref": "#/components/schemas/User"
- }
- }
- },
- "required": true
- },
- "responses": {
- "200": {
- "description": "OK",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/User"
- }
- }
- },
- "links": {
- "address2": {
- "$ref": "#/components/links/address"
- }
- }
- },
- "400": {
- "description": "Invalid username supplied"
- },
- "404": {
- "description": "User not found"
- },
- "405": {
- "description": "Invalid Input"
- }
- }
- }
- },
- "/api/v4/paet/{petId}": {
- "put": {
- "tags": [
- "pet"
- ],
- "summary": "Updates a pet in the store with form data",
- "operationId": "updatepaet",
- "parameters": [
- {
- "examples": {
- "user": {
- "summary": "User Example",
- "value": "http://foo.bar/examples/user-example.json"
- },
- "user1": {
- "summary": "User Example in XML",
- "value": "http://foo.bar/examples/user-example.xml"
- },
- "user2": {
- "summary": "User Example in Plain text",
- "value": "http://foo.bar/examples/user-example.txt"
- },
- "user3": {
- "summary": "User example in other forma",
- "value": "http://foo.bar/examples/user-example.whatever"
- }
- },
- "name": "petId",
- "in": "path",
- "required": true,
- "description": "ID of pet that needs to be updated",
- "schema": {
- "type": "string"
- }
- }
- ],
- "requestBody": {
- "content": {
- "application/x-www-form-urlencoded": {
- "schema": {
- "type": "object",
- "properties": {
- "name": {
- "type": "string",
- "description": "Updated name of the pet"
- },
- "status": {
- "type": "string",
- "description": "Updated status of the pet"
- }
- },
- "required": [
- "status"
- ]
- }
- }
- },
- "required": true
- },
- "responses": {
- "200": {
- "description": "Pet updated.",
- "content": {
- "application/json": {},
- "application/xml": {}
- }
- },
- "405": {
- "description": "Method Not Allowed",
- "content": {
- "application/json": {},
- "application/xml": {}
- }
- }
- }
- }
- },
- "/api/v4/paet2/{petId}": {
- "put": {
- "tags": [
- "pet"
- ],
- "summary": "Updates a pet in the store with form data",
- "operationId": "updatepaet2",
- "parameters": [
- {
- "description": "ID of pet that needs to be updated",
- "name": "petId",
- "required": true,
- "schema": {
- "type": "string"
- },
- "in": "path"
- }
- ],
- "requestBody": {
- "content": {
- "text/plain": {
- "schema": {
- "type": "array",
- "items": {
- "type": "string"
- }
- }
- }
- },
- "description": "user to add to the system"
- },
- "responses": {
- "200": {
- "description": "Pet updated.",
- "content": {
- "application/json": {},
- "application/xml": {}
- }
- },
- "405": {
- "description": "Method Not Allowed",
- "content": {
- "application/json": {},
- "application/xml": {}
- }
- }
- }
- }
- },
- "/api/v4/paet3/{petId}": {
- "put": {
- "tags": [
- "pet"
- ],
- "summary": "Updates a pet in the store with form data",
- "operationId": "updatepaet3",
- "parameters": [
- {
- "description": "ID of pet that needs to be updated",
- "name": "petId",
- "required": true,
- "schema": {
- "type": "string"
- },
- "in": "path"
- }
- ],
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/NewCat"
- },
- "examples": {
- "cat": {
- "summary": "An example of a cat",
- "value": {
- "color": "White",
- "name": "Fluffy",
- "petType": "Cat",
- "gender": "male",
- "breed": "Persian"
- }
- },
- "dog": {
- "summary": "An example of a dog with a cat's name",
- "value": {
- "color": "Black",
- "name": "Puma",
- "petType": "Dog",
- "gender": "Female",
- "breed": "Mixed"
- }
- },
- "frog-example": {
- "$ref": "#/components/examples/frog-example"
- }
- }
- }
- },
- "description": "user to add to the system"
- },
- "responses": {
- "200": {
- "description": "Pet updated.",
- "content": {
- "application/json": {},
- "application/xml": {}
- }
- },
- "4XX": {
- "description": "Method Not Allowed",
- "content": {
- "application/json": {},
- "application/xml": {}
- }
- }
- }
- }
- },
- "/api/v4/paet4/{petId}": {
- "put": {
- "tags": [
- "pet"
- ],
- "summary": "Updates a pet in the store with form data",
- "operationId": "updatepaet4",
- "parameters": [
- {
- "description": "ID of pet that needs to be updated",
- "name": "petId",
- "content": {
- "application/json": {
- "schema": {
- "type": "string"
- }
- }
- },
- "required": true,
- "in": "path"
- }
- ],
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/Pet"
- },
- "examples": {
- "cat": {
- "summary": "An example of a cat",
- "value": {
- "color": "White",
- "name": "Fluffy",
- "petType": "Cat",
- "gender": "male",
- "breed": "Persian"
- }
- },
- "dog": {
- "summary": "An example of a dog with a cat's name",
- "value": {
- "color": "Black",
- "name": "Puma",
- "petType": "Dog",
- "gender": "Female",
- "breed": "Mixed"
- }
- },
- "frog-example": {
- "$ref": "#/components/examples/frog-example"
- }
- }
- }
- },
- "description": "user to add to the system"
- },
- "responses": {
- "200": {
- "description": "Pet updated.",
- "content": {
- "application/xml": {},
- "application/json": {}
- }
- },
- "405": {
- "description": "Method Not Allowed",
- "content": {
- "application/json": {},
- "application/xml": {}
- }
- }
- }
- }
- },
- "/api/v4/pat/{petId}": {
- "put": {
- "tags": [
- "pet"
- ],
- "summary": "Updates a pet in the store with form data",
- "operationId": "updatePasdadaetWithForm",
- "parameters": [
- {
- "description": "ID of pet that needs to be updated",
- "name": "petId",
- "required": true,
- "schema": {
- "type": "string"
- },
- "in": "path"
- }
- ],
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/User"
- },
- "examples": {
- "user": {
- "summary": "User Example",
- "externalValue": "http://foo.bar/examples/user-example.json"
- }
- }
- },
- "application/xml": {
- "schema": {
- "$ref": "#/components/schemas/User"
- },
- "examples": {
- "user": {
- "summary": "User Example in XML",
- "externalValue": "http://foo.bar/examples/user-example.xml"
- }
- }
- },
- "text/plain": {
- "examples": {
- "user": {
- "summary": "User Example in Plain text",
- "externalValue": "http://foo.bar/examples/user-example.txt"
- }
- }
- },
- "\"*/*\"": {
- "examples": {
- "user": {
- "summary": "User example in other forma",
- "externalValue": "http://foo.bar/examples/user-example.whatever"
- }
- }
- }
- },
- "description": "user to add to the system"
- },
- "responses": {
- "200": {
- "description": "Pet updated.",
- "content": {
- "application/json": {},
- "application/xml": {}
- }
- },
- "405": {
- "description": "Method Not Allowed",
- "content": {
- "application/json": {},
- "application/xml": {}
- }
- }
- }
- }
- },
- "/pet/{petId}": {
- "delete": {
- "tags": [
- "pet"
- ],
- "summary": "Deletes a pet",
- "description": "Deletes a pet.",
- "operationId": "deletePet",
- "parameters": [
- {
- "$ref": "#/components/parameters/PetIdParam"
- }
- ],
- "responses": {
- "200": {
- "description": "Successful operation"
- },
- "400": {
- "description": "Invalid ID supplied"
- },
- "404": {
- "description": "Pet not found"
- }
- }
- },
- "get": {
- "tags": [
- "pet"
- ],
- "summary": "Find pet by ID",
- "description": "Returns a single pet.",
- "operationId": "getPetById",
- "parameters": [
- {
- "$ref": "#/components/parameters/PetIdParam"
- }
- ],
- "security": [
- {
- "Login-OAuth2": [
- "read"
- ]
- }
- ],
- "responses": {
- "200": {
- "description": "Successful operation",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/Pet"
- }
- },
- "application/xml": {
- "schema": {
- "$ref": "#/components/schemas/Pet"
- }
- }
- }
- },
- "400": {
- "description": "Invalid ID supplied"
- },
- "404": {
- "description": "Pet not found"
- }
- }
- },
- "post": {
- "tags": [
- "pet"
- ],
- "summary": "Updates a pet in the store",
- "description": "Updates a pet in the store with form data",
- "operationId": "updatePetWithForm",
- "parameters": [
- {
- "$ref": "#/components/parameters/PetIdParam"
- },
- {
- "description": "Name of pet that needs to be updated",
- "name": "name",
- "schema": {
- "type": "string"
- },
- "in": "query"
- },
- {
- "description": "Status of pet that needs to be updated",
- "name": "status",
- "schema": {
- "type": "string"
- },
- "in": "query"
- }
- ],
- "requestBody": {
- "content": {
- "multipart/form-data": {
- "schema": {
- "properties": {
- "file": {
- "type": "array",
- "items": {
- "type": "string",
- "format": "binary"
- }
- }
- }
- }
- }
- }
- },
- "security": [
- {
- "Login-OAuth2": [
- "write"
- ]
- }
- ],
- "responses": {
- "200": {
- "description": "Successful operation"
- },
- "400": {
- "description": "Invalid ID supplied"
- },
- "405": {
- "description": "Invalid Input"
- }
- }
- }
- },
- "/store/order/{orderId}": {
- "delete": {
- "tags": [
- "store"
- ],
- "summary": "Delete purchase order by ID",
- "description": "For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors.",
- "operationId": "deleteOrder",
- "parameters": [
- {
- "description": " ID of the order that needs to be deleted",
- "name": "orderId",
- "required": true,
- "schema": {
- "type": "integer",
- "format": "int64"
- },
- "in": "path"
- }
- ],
- "responses": {
- "200": {
- "description": "Successful operation"
- },
- "400": {
- "description": "Invalid ID supplied"
- },
- "404": {
- "description": "Order not found"
- }
- }
- },
- "get": {
- "tags": [
- "store"
- ],
- "summary": "Find purchase order by ID",
- "description": "For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions.",
- "operationId": "getOrderById",
- "parameters": [
- {
- "description": "ID of order that needs to be fetched",
- "name": "orderId",
- "required": true,
- "schema": {
- "type": "integer",
- "format": "int64"
- },
- "in": "path"
- }
- ],
- "servers": [
- {
- "description": "ext test server",
- "url": "http://ext.server.com/api/v12"
- },
- {
- "description": "ext test server 13",
- "url": "http://ext13.server.com/api/v12"
- },
- {
- "description": "ext test server 14",
- "url": "http://ext14.server.com/api/v12"
- },
- {
- "url": "/api/v3",
- "description": "default endpoint"
- }
- ],
- "responses": {
- "200": {
- "description": "Successful operation",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/Order"
- }
- },
- "application/xml": {
- "schema": {
- "$ref": "#/components/schemas/Order"
- }
- },
- "application/x-www-form-urlencoded": {
- "schema": {
- "$ref": "#/components/schemas/Order"
- }
- }
- }
- },
- "400": {
- "description": "Invalid ID supplied"
- },
- "404": {
- "description": "Order not found"
- }
- }
- }
- },
- "/pet/findByStatus": {
- "get": {
- "tags": [
- "pet"
- ],
- "summary": "Finds Pets by status",
- "description": "Multiple status values can be provided with comma separated strings",
- "operationId": "findPetsByStatus",
- "parameters": [
- {
- "description": "Status values that need to be considered for filter",
- "name": "status",
- "schema": {
- "type": "string",
- "default": "available",
- "enum": [
- "available",
- "pending",
- "sold"
- ]
- },
- "in": "query"
- }
- ],
- "security": [
- {},
- {
- "Login-OAuth2": [
- "read"
- ]
- }
- ],
- "responses": {
- "200": {
- "description": "Successful operation",
- "content": {
- "application/xml": {
- "schema": {
- "type": "array",
- "items": {
- "$ref": "#/components/schemas/Pet"
- }
- }
- },
- "application/json": {
- "schema": {
- "type": "array",
- "items": {
- "$ref": "#/components/schemas/Pet"
- }
- }
- }
- }
- },
- "400": {
- "description": "Invalid status value"
- }
- }
- }
- },
- "/pet/findByTag": {
- "get": {
- "tags": [
- "pet"
- ],
- "summary": "Finds Pets by tags",
- "description": "Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.",
- "operationId": "findPetsByTags",
- "parameters": [
- {
- "description": "Tags to filter by",
- "name": "tag",
- "explode": true,
- "schema": {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- "in": "query"
- }
- ],
- "security": [
- {
- "Login-OAuth2": [
- "read"
- ]
- },
- {
- "api_key": []
- }
- ],
- "responses": {
- "200": {
- "description": "Successful operation",
- "content": {
- "application/xml": {
- "schema": {
- "$ref": "#/components/schemas/Pet"
- }
- },
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/Pet"
- }
- }
- }
- },
- "400": {
- "description": "Invalid status value"
- },
- "default": {
- "description": "Unexpected error",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ErrorModel"
- }
- }
- }
- }
- }
- }
- },
- "/store/inventory": {
- "get": {
- "tags": [
- "store"
- ],
- "summary": "Returns pet inventories by status",
- "description": "Returns a map of status codes to quantities",
- "operationId": "getInventory",
- "responses": {
- "200": {
- "description": "Successful operation",
- "content": {
- "application/json": {
- "schema": {
- "type": "object",
- "properties": {
- "none": {
- "type": "string"
- }
- }
- }
- }
- }
- }
- }
- }
- },
- "/user/login": {
- "get": {
- "tags": [
- "user"
- ],
- "summary": "Logs user into the system.",
- "description": "Logs user into the system.",
- "operationId": "loginUser",
- "parameters": [
- {
- "description": "The user name for login",
- "name": "username",
- "schema": {
- "type": "string"
- },
- "in": "query"
- },
- {
- "description": "The password for login in clear text",
- "name": "password",
- "schema": {
- "type": "string",
- "format": "password"
- },
- "in": "query"
- }
- ],
- "responses": {
- "200": {
- "description": "Successful operation",
- "headers": {
- "X-Rate-Limit": {
- "$ref": "#/components/headers/X-Rate-Limit"
- },
- "X-Expires-After": {
- "$ref": "#/components/headers/X-Expires-After"
- }
- },
- "content": {
- "application/xml": {
- "schema": {
- "type": "string"
- }
- },
- "application/json": {
- "schema": {
- "type": "string"
- }
- }
- }
- },
- "400": {
- "description": "Invalid username/password supplied"
- }
- }
- }
- },
- "/user/logout": {
- "get": {
- "tags": [
- "user"
- ],
- "summary": "Logs out current logged in user session.",
- "description": "Logs out current logged in user session.",
- "operationId": "logoutUser",
- "responses": {
- "200": {
- "description": "Successful operation"
- }
- }
- }
- },
- "/peta/{id}": {
- "get": {
- "summary": "Find pets by ID",
- "description": "Returns pets based on ID",
- "operationId": "getPetsById",
- "parameters": [
- {
- "style": "simple",
- "name": "id",
- "in": "path",
- "required": true,
- "description": "ID of pet to use",
- "schema": {
- "type": "array",
- "items": {
- "type": "string"
- }
- }
- }
- ],
- "responses": {
- "200": {
- "description": "pet response",
- "content": {
- "\"*/*\"": {
- "schema": {
- "type": "array",
- "items": {
- "$ref": "#/components/schemas/Pet"
- }
- }
- }
- }
- },
- "default": {
- "description": "error payload",
- "content": {
- "text/html": {
- "schema": {
- "$ref": "#/components/schemas/ApiResponse"
- }
- }
- }
- }
- }
- }
- },
- "/pet/{petId}/uploadImage2": {
- "post": {
- "tags": [
- "pet"
- ],
- "summary": "Uploads an image",
- "description": "Updates a pet in the store with a new image",
- "operationId": "uploadFile2",
- "parameters": [
- {
- "description": "ID of pet that needs to be updated",
- "name": "petId",
- "required": true,
- "schema": {
- "type": "integer",
- "format": "int64"
- },
- "in": "path"
- },
- {
- "description": "Additional Metadata",
- "name": "additionalMetadata",
- "schema": {
- "type": "string"
- },
- "in": "query"
- }
- ],
- "requestBody": {
- "content": {
- "multipart/form-data": {
- "schema": {
- "type": "object",
- "properties": {
- "image": {
- "type": "string",
- "format": "binary"
- }
- }
- }
- }
- },
- "required": true
- },
- "responses": {
- "200": {
- "description": "A simple string response",
- "headers": {
- "X-Rate-Limit-Limit": {
- "description": "The number of allowed requests in the current period",
- "schema": {
- "type": "integer"
- }
- },
- "X-Rate-Limit-Remaining": {
- "description": "The number of remaining requests in the current period",
- "schema": {
- "type": "integer"
- }
- },
- "X-Rate-Limit-Reset": {
- "description": "The number of seconds left in the current period",
- "schema": {
- "type": "integer",
- "maximum": 3
- }
- }
- },
- "content": {
- "text/plain": {
- "schema": {
- "type": "string",
- "example": "whoa!"
- }
- }
- }
- }
- }
- }
- },
- "/pet/{petId}/uploadImageOctet": {
- "post": {
- "tags": [
- "pet"
- ],
- "summary": "Uploads an image",
- "description": "Updates a pet in the store with a new image",
- "operationId": "uploadFileOctet",
- "parameters": [
- {
- "description": "ID of pet that needs to be updated",
- "name": "petId",
- "required": true,
- "schema": {
- "type": "integer",
- "format": "int64"
- },
- "in": "path"
- },
- {
- "description": "Additional Metadata",
- "name": "additionalMetadata",
- "schema": {
- "type": "string"
- },
- "in": "query"
- }
- ],
- "requestBody": {
- "content": {
- "application/octet-stream": {
- "schema": {}
- }
- },
- "required": true
- },
- "responses": {
- "200": {
- "description": "Successful operation",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ApiResponse"
- }
- }
- }
- },
- "400": {
- "description": "Invalid ID supplied"
- },
- "405": {
- "description": "Invalid Input"
- }
- }
- }
- },
- "/pet/{petId}/uploadmultiImage": {
- "post": {
- "tags": [
- "pet"
- ],
- "summary": "Uploads an image",
- "description": "Updates a pet in the store with a new image",
- "operationId": "uploadFilemulti",
- "parameters": [
- {
- "description": "ID of pet that needs to be updated",
- "name": "petId",
- "required": true,
- "schema": {
- "type": "integer",
- "format": "int64"
- },
- "in": "path"
- },
- {
- "description": "Additional Metadata",
- "name": "additionalMetadata",
- "schema": {
- "type": "string"
- },
- "in": "query"
- }
- ],
- "requestBody": {
- "content": {
- "multipart/form-data": {
- "schema": {
- "type": "object",
- "properties": {
- "orderId": {
- "type": "integer"
- },
- "image": {
- "type": "string"
- }
- }
- }
- }
- },
- "required": true
- },
- "responses": {
- "200": {
- "description": "Successful operation",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ApiResponse"
- }
- }
- }
- },
- "400": {
- "description": "Invalid ID supplied"
- },
- "405": {
- "description": "Invalid Input"
- }
- }
- }
- },
- "/pet2/{petId}": {
- "post": {
- "tags": [
- "pet"
- ],
- "summary": "Updates a pet in the store",
- "description": "Updates a pet in the store with form data",
- "operationId": "updatePet2WithForm",
- "parameters": [
- {
- "$ref": "#/components/parameters/PetIdParam"
- },
- {
- "description": "Name of pet that needs to be updated",
- "name": "name",
- "schema": {
- "type": "string"
- },
- "in": "query"
- },
- {
- "description": "Status of pet that needs to be updated",
- "name": "status",
- "schema": {
- "type": "string"
- },
- "in": "query"
- }
- ],
- "requestBody": {
- "content": {
- "application/x-www-form-urlencoded": {
- "schema": {
- "type": "object",
- "properties": {
- "id": {
- "type": "string",
- "format": "uuid"
- },
- "address": {
- "type": "object",
- "properties": {}
- }
- }
- }
- }
- }
- },
- "security": [
- {
- "Login-OAuth2": [
- "write"
- ]
- }
- ],
- "responses": {
- "200": {
- "description": "Successful operation"
- },
- "400": {
- "description": "Invalid ID supplied"
- },
- "405": {
- "description": "Invalid Input"
- }
- }
- }
- },
- "/pet3/{petId}": {
- "post": {
- "tags": [
- "pet"
- ],
- "summary": "Updates a pet in the store",
- "description": "Updates a pet in the store with form data",
- "operationId": "updatePet3WithForm",
- "parameters": [
- {
- "$ref": "#/components/parameters/PetIdParam"
- },
- {
- "description": "Name of pet that needs to be updated",
- "name": "name",
- "schema": {
- "type": "string"
- },
- "in": "query"
- },
- {
- "description": "Status of pet that needs to be updated",
- "name": "status",
- "schema": {
- "type": "string"
- },
- "in": "query"
- }
- ],
- "requestBody": {
- "content": {
- "multipart/form-data": {
- "schema": {
- "type": "object",
- "properties": {
- "id": {
- "type": "string",
- "format": "uuid"
- },
- "address": {
- "type": "object",
- "properties": {}
- },
- "children": {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- "addresses": {
- "type": "array",
- "items": {
- "$ref": "#/components/schemas/Address"
- }
- }
- }
- }
- }
- }
- },
- "security": [
- {
- "Login-OAuth2": [
- "write"
- ]
- }
- ],
- "responses": {
- "200": {
- "description": "Successful operation"
- },
- "400": {
- "description": "Invalid ID supplied"
- },
- "405": {
- "description": "Invalid Input"
- }
- }
- }
- },
- "/pet4/{petId}": {
- "post": {
- "tags": [
- "pet"
- ],
- "summary": "Updates a pet in the store",
- "description": "Updates a pet in the store with form data",
- "operationId": "updatePet4WithForm",
- "parameters": [
- {
- "$ref": "#/components/parameters/PetIdParam"
- },
- {
- "description": "Name of pet that needs to be updated",
- "name": "name",
- "schema": {
- "type": "string"
- },
- "in": "query"
- },
- {
- "description": "Status of pet that needs to be updated",
- "name": "status",
- "schema": {
- "type": "string"
- },
- "in": "query"
- }
- ],
- "requestBody": {
- "content": {
- "multipart/form-data": {
- "encoding": {
- "historyMetadata": {
- "contentType": "application/xml; charset=utf-8"
- },
- "profileImage": {
- "contentType": "image/png, image/jpeg",
- "headers": {
- "X-Rate-Limit-Limit": {
- "description": "The number of allowed requests in the current period",
- "schema": {
- "enum": [
- 1,
- 2,
- 3
- ],
- "default": 3,
- "type": "integer",
- "maximum": 3
- }
- },
- "X-Rate-Limit-Reset": {
- "description": "The number of seconds left in the current period",
- "schema": {
- "type": "integer",
- "minimum": 2
- }
- }
- }
- }
- },
- "schema": {
- "type": "object",
- "properties": {
- "id": {
- "type": "string",
- "format": "uuid"
- },
- "address": {
- "type": "object",
- "properties": {}
- },
- "historyMetadata": {
- "type": "object",
- "description": "metadata in XML format",
- "properties": {}
- },
- "profileImage": {
- "type": "string",
- "format": "binary"
- }
- }
- }
- }
- }
- },
- "security": [
- {
- "Login-OAuth2": [
- "write"
- ]
- }
- ],
- "responses": {
- "200": {
- "description": "A simple string response",
- "content": {
- "text/plain": {
- "schema": {
- "type": "string"
- }
- }
- }
- },
- "400": {
- "description": "Invalid ID supplied"
- },
- "405": {
- "description": "Invalid Input"
- }
- }
- }
- },
- "/petcallback": {
- "post": {
- "tags": [
- "pet"
- ],
- "summary": "Add a new pet to the store",
- "description": "Add a new pet to the store",
- "operationId": "addPetcallback",
- "requestBody": {
- "$ref": "#/components/requestBodies/PetBodySchema"
+ "$ref": "#/components/requestBodies/PetBodySchema"
},
"callbacks": {
- "test": {
- "'{$request.body#/id}'": {
- "post": {
- "requestBody": {
- "content": {
- "\"*/*\"": {
- "schema": {
- "type": "string"
- }
- }
- }
- },
- "responses": {
- "200": {
- "description": "Successful operation",
- "content": {
- "application/xml": {
- "schema": {
- "type": "array",
- "items": {
- "$ref": "#/components/schemas/Pet"
- }
- }
- },
- "application/json": {
- "schema": {
- "type": "array",
- "items": {
- "$ref": "#/components/schemas/Pet"
- }
- }
- }
- }
- },
- "400": {
- "description": "Invalid ID supplied"
- },
- "404": {
- "description": "Pet not found"
- },
- "default": {
- "description": "Something is wrong"
- }
- }
- }
- }
+ "test1": {
+ "$ref": "#/components/callbacks/test"
}
},
"security": [
@@ -1853,12 +289,12 @@
"200": {
"description": "Successful operation",
"content": {
- "application/xml": {
+ "application/json": {
"schema": {
"$ref": "#/components/schemas/Pet"
}
},
- "application/json": {
+ "application/xml": {
"schema": {
"$ref": "#/components/schemas/Pet"
}
@@ -1886,26 +322,35 @@
}
}
},
- "/petcallbackReference": {
- "post": {
+ "/pet/findByStatus": {
+ "get": {
"tags": [
"pet"
],
- "summary": "Add a new pet to the store",
- "description": "Add a new pet to the store",
- "operationId": "petcallbackReference",
- "requestBody": {
- "$ref": "#/components/requestBodies/PetBodySchema"
- },
- "callbacks": {
- "test1": {
- "$ref": "#/components/callbacks/test"
+ "summary": "Finds Pets by status",
+ "description": "Multiple status values can be provided with comma separated strings",
+ "operationId": "findPetsByStatus",
+ "parameters": [
+ {
+ "description": "Status values that need to be considered for filter",
+ "schema": {
+ "type": "string",
+ "default": "available",
+ "enum": [
+ "available",
+ "pending",
+ "sold"
+ ]
+ },
+ "name": "status",
+ "in": "query"
}
- },
+ ],
"security": [
+ {},
{
"Login-OAuth2": [
- "write"
+ "read"
]
}
],
@@ -1913,29 +358,111 @@
"200": {
"description": "Successful operation",
"content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Pet"
+ }
+ }
+ },
"application/xml": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Pet"
+ }
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid status value"
+ }
+ }
+ }
+ },
+ "/pet/findByTag": {
+ "get": {
+ "tags": [
+ "pet"
+ ],
+ "summary": "Finds Pets by tags",
+ "description": "Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.",
+ "operationId": "findPetsByTags",
+ "parameters": [
+ {
+ "explode": true,
+ "description": "Tags to filter by",
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "name": "tag",
+ "in": "query"
+ }
+ ],
+ "security": [
+ {
+ "api_key": []
+ },
+ {
+ "Login-OAuth2": [
+ "read"
+ ]
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful operation",
+ "content": {
+ "application/json": {
"schema": {
"$ref": "#/components/schemas/Pet"
}
},
- "application/json": {
+ "application/xml": {
"schema": {
"$ref": "#/components/schemas/Pet"
}
}
}
},
- "405": {
- "description": "Validation exception",
+ "400": {
+ "description": "Invalid status value"
+ },
+ "default": {
+ "description": "Unexpected error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorModel"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/store/inventory": {
+ "get": {
+ "tags": [
+ "store"
+ ],
+ "summary": "Returns pet inventories by status",
+ "description": "Returns a map of status codes to quantities",
+ "operationId": "getInventory",
+ "responses": {
+ "200": {
+ "description": "Successful operation",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
- "result": {
- "type": "string"
- },
- "message": {
+ "none": {
"type": "string"
}
}
@@ -1957,17 +484,17 @@
"operationId": "placeOrder",
"requestBody": {
"content": {
- "application/json": {
+ "application/xml": {
"schema": {
"$ref": "#/components/schemas/Order"
}
},
- "application/xml": {
+ "application/x-www-form-urlencoded": {
"schema": {
"$ref": "#/components/schemas/Order"
}
},
- "application/x-www-form-urlencoded": {
+ "application/json": {
"schema": {
"$ref": "#/components/schemas/Order"
}
@@ -2007,17 +534,17 @@
"operationId": "createUser",
"requestBody": {
"content": {
- "application/json": {
+ "application/xml": {
"schema": {
"$ref": "#/components/schemas/User"
}
},
- "application/xml": {
+ "application/x-www-form-urlencoded": {
"schema": {
"$ref": "#/components/schemas/User"
}
},
- "application/x-www-form-urlencoded": {
+ "application/json": {
"schema": {
"$ref": "#/components/schemas/User"
}
@@ -2060,17 +587,17 @@
"operationId": "createUsersWithListInput",
"requestBody": {
"content": {
- "application/json": {
+ "application/xml": {
"schema": {
"$ref": "#/components/schemas/User"
}
},
- "application/xml": {
+ "application/x-www-form-urlencoded": {
"schema": {
"$ref": "#/components/schemas/User"
}
},
- "application/x-www-form-urlencoded": {
+ "application/json": {
"schema": {
"$ref": "#/components/schemas/User"
}
@@ -2088,9 +615,71 @@
}
}
},
- "/close": {
- "post": {
- "summary": "Shutdown the server",
+ "/user/login": {
+ "get": {
+ "tags": [
+ "user"
+ ],
+ "summary": "Logs user into the system.",
+ "description": "Logs user into the system.",
+ "operationId": "loginUser",
+ "parameters": [
+ {
+ "description": "The user name for login",
+ "schema": {
+ "type": "string"
+ },
+ "name": "username",
+ "in": "query"
+ },
+ {
+ "description": "The password for login in clear text",
+ "schema": {
+ "type": "string",
+ "format": "password"
+ },
+ "name": "password",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful operation",
+ "headers": {
+ "X-Rate-Limit": {
+ "$ref": "#/components/headers/X-Rate-Limit"
+ },
+ "X-Expires-After": {
+ "$ref": "#/components/headers/X-Expires-After"
+ }
+ },
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "string"
+ }
+ },
+ "application/xml": {
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid username/password supplied"
+ }
+ }
+ }
+ },
+ "/user/logout": {
+ "get": {
+ "tags": [
+ "user"
+ ],
+ "summary": "Logs out current logged in user session.",
+ "description": "Logs out current logged in user session.",
+ "operationId": "logoutUser",
"responses": {
"200": {
"description": "Successful operation"
@@ -2109,44 +698,44 @@
"parameters": [
{
"description": "ID of order that needs to be fetched",
- "name": "orderId",
"required": true,
"schema": {
"type": "integer",
"format": "int64"
},
+ "name": "orderId",
"in": "path"
}
],
"servers": [
{
- "description": "ext test server",
- "url": "http://ext.server.com/api/v12"
+ "url": "http://ext.server.com/api/v12",
+ "description": "ext test server"
},
{
- "description": "ext test server 13",
- "url": "http://ext13.server.com/api/v12"
+ "url": "http://ext13.server.com/api/v12",
+ "description": "ext test server 13"
},
{
- "description": "ext test server 14",
- "url": "http://ext14.server.com/api/v12"
+ "url": "http://ext14.server.com/api/v12",
+ "description": "ext test server 14"
}
],
"responses": {
"200": {
"description": "Successful operation",
"content": {
- "application/json": {
+ "application/xml": {
"schema": {
"$ref": "#/components/schemas/Order"
}
},
- "application/xml": {
+ "application/x-www-form-urlencoded": {
"schema": {
"$ref": "#/components/schemas/Order"
}
},
- "application/x-www-form-urlencoded": {
+ "application/json": {
"schema": {
"$ref": "#/components/schemas/Order"
}
@@ -2529,6 +1118,7 @@
"$ref": "#/components/schemas/Pet2"
},
{
+ "type": "object",
"properties": {
"huntingSkill": {
"type": "string",
@@ -2542,7 +1132,6 @@
]
}
},
- "type": "object",
"required": [
"huntingSkill"
]
@@ -2556,6 +1145,7 @@
"$ref": "#/components/schemas/Pet2"
},
{
+ "type": "object",
"properties": {
"packSize": {
"type": "integer",
@@ -2564,7 +1154,6 @@
"format": "int32"
}
},
- "type": "object",
"required": [
"packSize"
]
@@ -2578,12 +1167,12 @@
"$ref": "#/components/schemas/Pet"
},
{
+ "type": "object",
"properties": {
"rootCause": {
"type": "string"
}
},
- "type": "object",
"required": [
"rootCause"
]
@@ -2610,6 +1199,7 @@
"$ref": "#/components/schemas/Pet"
},
{
+ "type": "object",
"properties": {
"huntingSkill": {
"type": "string",
@@ -2621,8 +1211,7 @@
"aggressive"
]
}
- },
- "type": "object"
+ }
}
]
},
@@ -2716,12 +1305,12 @@
"parameters": {
"PetIdParam": {
"description": "ID of the pet",
- "name": "petId",
"required": true,
"schema": {
"type": "integer",
"format": "int64"
},
+ "name": "petId",
"in": "path"
}
},
@@ -2729,18 +1318,18 @@
"frog-example": {
"summary": "An example of a frog with a cat's name",
"value": {
- "color": "Lion",
"name": "Jaguar",
- "petType": "Panthera",
"gender": "Male",
- "breed": "Mantella Baroni"
+ "color": "Lion",
+ "breed": "Mantella Baroni",
+ "petType": "Panthera"
}
}
},
"requestBodies": {
"PetBodySchema": {
"content": {
- "application/json": {
+ "application/xml": {
"schema": {
"$ref": "#/components/schemas/Pets"
}
@@ -2750,7 +1339,7 @@
"$ref": "#/components/schemas/Pets"
}
},
- "application/xml": {
+ "application/json": {
"schema": {
"$ref": "#/components/schemas/Pets"
}
@@ -2799,7 +1388,7 @@
"200": {
"description": "Successful operation",
"content": {
- "application/xml": {
+ "application/json": {
"schema": {
"type": "array",
"items": {
@@ -2807,7 +1396,7 @@
}
}
},
- "application/json": {
+ "application/xml": {
"schema": {
"type": "array",
"items": {
@@ -2832,24 +1421,24 @@
}
},
"securitySchemes": {
- "Login": {
- "scheme": "basic",
- "type": "http"
- },
"LoginApiKey": {
"type": "apiKey",
"in": "header",
"name": "X-API-KEY"
},
+ "Login": {
+ "type": "http",
+ "scheme": "basic"
+ },
+ "Jwt": {
+ "type": "http",
+ "scheme": "bearer"
+ },
"api_key": {
"type": "apiKey",
"in": "header",
"name": "api_key"
},
- "Jwt": {
- "scheme": "bearer",
- "type": "http"
- },
"Login-OAuth2": {
"type": "oauth2",
"flows": {
diff --git a/tests/integration/specs/OpenApi-TuttiFrutti_3.1.0.json b/tests/integration/specs/OpenApi-TuttiFrutti_3.1.0.json
index c3a5d1de2..3519557a9 100644
--- a/tests/integration/specs/OpenApi-TuttiFrutti_3.1.0.json
+++ b/tests/integration/specs/OpenApi-TuttiFrutti_3.1.0.json
@@ -53,6 +53,65 @@
],
"paths": {
"/pet": {
+ "post": {
+ "tags": [
+ "pet"
+ ],
+ "summary": "Add a new pet to the store",
+ "description": "Add a new pet to the store",
+ "operationId": "addPet",
+ "requestBody": {
+ "$ref": "#/components/requestBodies/PetBodySchema"
+ },
+ "security": [
+ {
+ "Login-OAuth2": [
+ "write"
+ ]
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful operation",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Pet"
+ }
+ },
+ "application/xml": {
+ "schema": {
+ "$ref": "#/components/schemas/Pet"
+ }
+ }
+ }
+ },
+ "405": {
+ "description": "Validation exception",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": [
+ "object"
+ ],
+ "properties": {
+ "result": {
+ "type": [
+ "string"
+ ]
+ },
+ "message": {
+ "type": [
+ "string"
+ ]
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
"put": {
"tags": [
"pet"
@@ -67,12 +126,12 @@
"200": {
"description": "Successful operation",
"content": {
- "application/xml": {
+ "application/json": {
"schema": {
"$ref": "#/components/schemas/Pet"
}
},
- "application/json": {
+ "application/xml": {
"schema": {
"$ref": "#/components/schemas/Pet"
}
@@ -110,17 +169,70 @@
}
}
}
- },
+ }
+ },
+ "/petcallback": {
"post": {
"tags": [
"pet"
],
"summary": "Add a new pet to the store",
"description": "Add a new pet to the store",
- "operationId": "addPet",
+ "operationId": "addPetcallback",
"requestBody": {
"$ref": "#/components/requestBodies/PetBodySchema"
},
+ "callbacks": {
+ "test": {
+ "'{$request.body#/id}'": {
+ "post": {
+ "requestBody": {
+ "content": {
+ "\"*/*\"": {
+ "schema": {
+ "type": [
+ "string"
+ ]
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "Successful operation",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Pet"
+ }
+ }
+ },
+ "application/xml": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Pet"
+ }
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid ID supplied"
+ },
+ "404": {
+ "description": "Pet not found"
+ },
+ "default": {
+ "description": "Something is wrong"
+ }
+ }
+ }
+ }
+ }
+ },
"security": [
{
"Login-OAuth2": [
@@ -132,12 +244,12 @@
"200": {
"description": "Successful operation",
"content": {
- "application/xml": {
+ "application/json": {
"schema": {
"$ref": "#/components/schemas/Pet"
}
},
- "application/json": {
+ "application/xml": {
"schema": {
"$ref": "#/components/schemas/Pet"
}
@@ -171,1391 +283,101 @@
}
}
},
- "/user/{username}": {
- "put": {
+ "/petcallbackReference": {
+ "post": {
"tags": [
- "user"
- ],
- "summary": "Update user",
- "description": "This can only be done by the logged in user.",
- "operationId": "updateUser",
- "parameters": [
- {
- "description": " name that need to be updated.",
- "name": "username",
- "required": true,
- "schema": {
- "type": "string"
- },
- "in": "path"
- }
+ "pet"
],
+ "summary": "Add a new pet to the store",
+ "description": "Add a new pet to the store",
+ "operationId": "petcallbackReference",
"requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/User"
- }
- },
- "application/xml": {
- "schema": {
- "$ref": "#/components/schemas/User"
- }
- },
- "application/x-www-form-urlencoded": {
- "schema": {
- "$ref": "#/components/schemas/User"
- }
- }
- },
- "required": true
+ "$ref": "#/components/requestBodies/PetBodySchema"
},
- "responses": {
- "200": {
- "$ref": "#/components/responses/UserOpSuccess"
- },
- "400": {
- "description": "Invalid username supplied"
- },
- "404": {
- "description": "User not found"
- },
- "405": {
- "description": "Invalid Input"
+ "callbacks": {
+ "test1": {
+ "$ref": "#/components/callbacks/test"
}
- }
- },
- "delete": {
- "tags": [
- "user"
- ],
- "summary": "Delete user",
- "description": "This can only be done by the logged in user.",
- "operationId": "deleteUser",
- "parameters": [
+ },
+ "security": [
{
- "description": "The name that needs to be deleted.",
- "name": "username",
- "required": true,
- "schema": {
- "type": "string"
- },
- "in": "path"
+ "Login-OAuth2": [
+ "write"
+ ]
}
],
"responses": {
"200": {
- "description": "Successful operation"
- },
- "400": {
- "description": "Invalid username supplied"
+ "description": "Successful operation",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Pet"
+ }
+ },
+ "application/xml": {
+ "schema": {
+ "$ref": "#/components/schemas/Pet"
+ }
+ }
+ }
},
- "404": {
- "description": "User not found"
+ "405": {
+ "description": "Validation exception",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": [
+ "object"
+ ],
+ "properties": {
+ "result": {
+ "type": [
+ "string"
+ ]
+ },
+ "message": {
+ "type": [
+ "string"
+ ]
+ }
+ }
+ }
+ }
+ }
}
}
- },
+ }
+ },
+ "/pet/findByStatus": {
"get": {
"tags": [
- "user"
+ "pet"
],
- "summary": "Get user by user name",
- "description": "Get user by user name.",
- "operationId": "getUserByName",
+ "summary": "Finds Pets by status",
+ "description": "Multiple status values can be provided with comma separated strings",
+ "operationId": "findPetsByStatus",
"parameters": [
{
- "description": "The name that needs to be fetched. Use user1 for testing.",
- "name": "username",
- "required": true,
- "schema": {
- "type": "string"
- },
- "in": "path"
- }
- ],
- "responses": {
- "200": {
- "$ref": "#/components/responses/UserOpSuccess"
- },
- "400": {
- "description": "Invalid username supplied"
- },
- "404": {
- "description": "User not found"
- }
- }
- }
- },
- "/user_1/{username}": {
- "put": {
- "tags": [
- "user"
- ],
- "summary": "Update user",
- "description": "This can only be done by the logged in user.",
- "operationId": "updateUser_1",
- "parameters": [
- {
- "description": " name that need to be updated.",
- "name": "username",
- "required": true,
- "schema": {
- "type": "string"
- },
- "in": "path"
- }
- ],
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/StructPart"
- }
- },
- "application/xml": {
- "schema": {
- "$ref": "#/components/schemas/StructPart"
- }
- },
- "application/x-www-form-urlencoded": {
- "schema": {
- "$ref": "#/components/schemas/StructPart"
- }
- }
- },
- "required": true
- },
- "responses": {
- "200": {
- "$ref": "#/components/responses/UserOpSuccess"
- },
- "400": {
- "description": "Invalid username supplied"
- },
- "404": {
- "description": "User not found"
- },
- "405": {
- "description": "Invalid Input"
- }
- }
- }
- },
- "/userLink/{username}": {
- "put": {
- "tags": [
- "user"
- ],
- "summary": "Update user",
- "description": "This can only be done by the logged in user.",
- "operationId": "updateUserLink",
- "parameters": [
- {
- "description": " name that need to be updated.",
- "name": "username",
- "required": true,
- "schema": {
- "type": "string"
- },
- "in": "path"
- }
- ],
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/User"
- }
- },
- "application/xml": {
- "schema": {
- "$ref": "#/components/schemas/User"
- }
- },
- "application/x-www-form-urlencoded": {
- "schema": {
- "$ref": "#/components/schemas/User"
- }
- }
- },
- "required": true
- },
- "responses": {
- "200": {
- "description": "OK",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/User"
- }
- }
- },
- "links": {
- "address": {
- "operationId": "getUserByName",
- "parameters": {
- "username": "$request.path.username"
- }
- }
- }
- },
- "400": {
- "description": "Invalid username supplied"
- },
- "404": {
- "description": "User not found"
- },
- "405": {
- "description": "Invalid Input"
- }
- }
- }
- },
- "/userLinkByRef/{username}": {
- "put": {
- "tags": [
- "user"
- ],
- "summary": "Update user",
- "description": "This can only be done by the logged in user.",
- "operationId": "updateUserLinkByRef",
- "parameters": [
- {
- "description": " name that need to be updated.",
- "name": "username",
- "required": true,
- "schema": {
- "type": "string"
- },
- "in": "path"
- }
- ],
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/User"
- }
- },
- "application/xml": {
- "schema": {
- "$ref": "#/components/schemas/User"
- }
- },
- "application/x-www-form-urlencoded": {
- "schema": {
- "$ref": "#/components/schemas/User"
- }
- }
- },
- "required": true
- },
- "responses": {
- "200": {
- "description": "OK",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/User"
- }
- }
- },
- "links": {
- "address2": {
- "$ref": "#/components/links/address"
- }
- }
- },
- "400": {
- "description": "Invalid username supplied"
- },
- "404": {
- "description": "User not found"
- },
- "405": {
- "description": "Invalid Input"
- }
- }
- }
- },
- "/pet/{petId}": {
- "delete": {
- "tags": [
- "pet"
- ],
- "summary": "Deletes a pet",
- "description": "Deletes a pet.",
- "operationId": "deletePet",
- "parameters": [
- {
- "$ref": "#/components/parameters/PetIdParam"
- }
- ],
- "responses": {
- "200": {
- "description": "Successful operation"
- },
- "400": {
- "description": "Invalid ID supplied"
- },
- "404": {
- "description": "Pet not found"
- }
- }
- },
- "get": {
- "tags": [
- "pet"
- ],
- "summary": "Find pet by ID",
- "description": "Returns a single pet.",
- "operationId": "getPetById",
- "parameters": [
- {
- "$ref": "#/components/parameters/PetIdParam"
- }
- ],
- "security": [
- {
- "Login-OAuth2": [
- "read"
- ]
- }
- ],
- "responses": {
- "200": {
- "description": "Successful operation",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/Pet"
- }
- },
- "application/xml": {
- "schema": {
- "$ref": "#/components/schemas/Pet"
- }
- }
- }
- },
- "400": {
- "description": "Invalid ID supplied"
- },
- "404": {
- "description": "Pet not found"
- }
- }
- },
- "post": {
- "tags": [
- "pet"
- ],
- "summary": "Updates a pet in the store",
- "description": "Updates a pet in the store with form data",
- "operationId": "updatePetWithForm",
- "parameters": [
- {
- "$ref": "#/components/parameters/PetIdParam"
- },
- {
- "description": "Name of pet that needs to be updated",
- "name": "name",
- "schema": {
- "type": "string"
- },
- "in": "query"
- },
- {
- "description": "Status of pet that needs to be updated",
- "name": "status",
- "schema": {
- "type": "string"
- },
- "in": "query"
- }
- ],
- "requestBody": {
- "content": {
- "multipart/form-data": {
- "schema": {
- "properties": {
- "file": {
- "type": "array",
- "items": {
- "type": [
- "string"
- ],
- "format": "binary"
- }
- }
- }
- }
- }
- }
- },
- "security": [
- {
- "Login-OAuth2": [
- "write"
- ]
- }
- ],
- "responses": {
- "200": {
- "description": "Successful operation"
- },
- "400": {
- "description": "Invalid ID supplied"
- },
- "405": {
- "description": "Invalid Input"
- }
- }
- }
- },
- "/store/order/{orderId}": {
- "delete": {
- "tags": [
- "store"
- ],
- "summary": "Delete purchase order by ID",
- "description": "For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors.",
- "operationId": "deleteOrder",
- "parameters": [
- {
- "description": " ID of the order that needs to be deleted",
- "name": "orderId",
- "required": true,
- "schema": {
- "type": "integer",
- "format": "int64"
- },
- "in": "path"
- }
- ],
- "responses": {
- "200": {
- "description": "Successful operation"
- },
- "400": {
- "description": "Invalid ID supplied"
- },
- "404": {
- "description": "Order not found"
- }
- }
- },
- "get": {
- "tags": [
- "store"
- ],
- "summary": "Find purchase order by ID",
- "description": "For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions.",
- "operationId": "getOrderById",
- "parameters": [
- {
- "description": "ID of order that needs to be fetched",
- "name": "orderId",
- "required": true,
- "schema": {
- "type": "integer",
- "format": "int64"
- },
- "in": "path"
- }
- ],
- "servers": [
- {
- "description": "ext test server",
- "url": "http://ext.server.com/api/v12"
- },
- {
- "description": "ext test server 13",
- "url": "http://ext13.server.com/api/v12"
- },
- {
- "description": "ext test server 14",
- "url": "http://ext14.server.com/api/v12"
- },
- {
- "url": "/api/v3",
- "description": "default endpoint"
- }
- ],
- "responses": {
- "200": {
- "description": "Successful operation",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/Order"
- }
- },
- "application/xml": {
- "schema": {
- "$ref": "#/components/schemas/Order"
- }
- },
- "application/x-www-form-urlencoded": {
- "schema": {
- "$ref": "#/components/schemas/Order"
- }
- }
- }
- },
- "400": {
- "description": "Invalid ID supplied"
- },
- "404": {
- "description": "Order not found"
- }
- }
- }
- },
- "/pet/findByStatus": {
- "get": {
- "tags": [
- "pet"
- ],
- "summary": "Finds Pets by status",
- "description": "Multiple status values can be provided with comma separated strings",
- "operationId": "findPetsByStatus",
- "parameters": [
- {
- "description": "Status values that need to be considered for filter",
- "name": "status",
- "schema": {
- "type": "string",
- "default": "available",
- "enum": [
- "available",
- "pending",
- "sold"
- ]
- },
- "in": "query"
- }
- ],
- "security": [
- {},
- {
- "Login-OAuth2": [
- "read"
- ]
- }
- ],
- "responses": {
- "200": {
- "description": "Successful operation",
- "content": {
- "application/xml": {
- "schema": {
- "type": "array",
- "items": {
- "$ref": "#/components/schemas/Pet"
- }
- }
- },
- "application/json": {
- "schema": {
- "type": "array",
- "items": {
- "$ref": "#/components/schemas/Pet"
- }
- }
- }
- }
- },
- "400": {
- "description": "Invalid status value"
- }
- }
- }
- },
- "/pet/findByTag": {
- "get": {
- "tags": [
- "pet"
- ],
- "summary": "Finds Pets by tags",
- "description": "Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.",
- "operationId": "findPetsByTags",
- "parameters": [
- {
- "description": "Tags to filter by",
- "name": "tag",
- "explode": true,
- "schema": {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- "in": "query"
- }
- ],
- "security": [
- {
- "Login-OAuth2": [
- "read"
- ]
- },
- {
- "api_key": []
- }
- ],
- "responses": {
- "200": {
- "description": "Successful operation",
- "content": {
- "application/xml": {
- "schema": {
- "$ref": "#/components/schemas/Pet"
- }
- },
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/Pet"
- }
- }
- }
- },
- "400": {
- "description": "Invalid status value"
- },
- "default": {
- "description": "Unexpected error",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ErrorModel"
- }
- }
- }
- }
- }
- }
- },
- "/store/inventory": {
- "get": {
- "tags": [
- "store"
- ],
- "summary": "Returns pet inventories by status",
- "description": "Returns a map of status codes to quantities",
- "operationId": "getInventory",
- "responses": {
- "200": {
- "description": "Successful operation",
- "content": {
- "application/json": {
- "schema": {
- "type": [
- "object"
- ],
- "properties": {
- "none": {
- "type": [
- "string"
- ]
- }
- }
- }
- }
- }
- }
- }
- }
- },
- "/user/login": {
- "get": {
- "tags": [
- "user"
- ],
- "summary": "Logs user into the system.",
- "description": "Logs user into the system.",
- "operationId": "loginUser",
- "parameters": [
- {
- "description": "The user name for login",
- "name": "username",
- "schema": {
- "type": "string"
- },
- "in": "query"
- },
- {
- "description": "The password for login in clear text",
- "name": "password",
- "schema": {
- "type": "string",
- "format": "password"
- },
- "in": "query"
- }
- ],
- "responses": {
- "200": {
- "description": "Successful operation",
- "headers": {
- "X-Rate-Limit": {
- "$ref": "#/components/headers/X-Rate-Limit"
- },
- "X-Expires-After": {
- "$ref": "#/components/headers/X-Expires-After"
- }
- },
- "content": {
- "application/xml": {
- "schema": {
- "type": "string"
- }
- },
- "application/json": {
- "schema": {
- "type": "string"
- }
- }
- }
- },
- "400": {
- "description": "Invalid username/password supplied"
- }
- }
- }
- },
- "/user/logout": {
- "get": {
- "tags": [
- "user"
- ],
- "summary": "Logs out current logged in user session.",
- "description": "Logs out current logged in user session.",
- "operationId": "logoutUser",
- "responses": {
- "200": {
- "description": "Successful operation"
- }
- }
- }
- },
- "/peta/{id}": {
- "get": {
- "summary": "Find pets by ID",
- "description": "Returns pets based on ID",
- "operationId": "getPetsById",
- "parameters": [
- {
- "style": "simple",
- "name": "id",
- "in": "path",
- "required": true,
- "description": "ID of pet to use",
- "schema": {
- "type": "array",
- "items": {
- "type": "string"
- }
- }
- }
- ],
- "responses": {
- "200": {
- "description": "pet response",
- "content": {
- "\"*/*\"": {
- "schema": {
- "type": "array",
- "items": {
- "$ref": "#/components/schemas/Pet"
- }
- }
- }
- }
- },
- "default": {
- "description": "error payload",
- "content": {
- "text/html": {
- "schema": {
- "$ref": "#/components/schemas/ApiResponse"
- }
- }
- }
- }
- }
- }
- },
- "/pet/{petId}/uploadImage2": {
- "post": {
- "tags": [
- "pet"
- ],
- "summary": "Uploads an image",
- "description": "Updates a pet in the store with a new image",
- "operationId": "uploadFile2",
- "parameters": [
- {
- "description": "ID of pet that needs to be updated",
- "name": "petId",
- "required": true,
- "schema": {
- "type": "integer",
- "format": "int64"
- },
- "in": "path"
- },
- {
- "description": "Additional Metadata",
- "name": "additionalMetadata",
- "schema": {
- "type": "string"
- },
- "in": "query"
- }
- ],
- "requestBody": {
- "content": {
- "multipart/form-data": {
- "schema": {
- "type": [
- "object"
- ],
- "properties": {
- "image": {
- "type": [
- "string"
- ],
- "format": "binary"
- }
- }
- }
- }
- },
- "required": true
- },
- "responses": {
- "200": {
- "description": "A simple string response",
- "headers": {
- "X-Rate-Limit-Limit": {
- "description": "The number of allowed requests in the current period",
- "schema": {
- "type": "integer"
- }
- },
- "X-Rate-Limit-Remaining": {
- "description": "The number of remaining requests in the current period",
- "schema": {
- "type": "integer"
- }
- },
- "X-Rate-Limit-Reset": {
- "description": "The number of seconds left in the current period",
- "schema": {
- "type": "integer",
- "maximum": 3
- }
- }
- },
- "content": {
- "text/plain": {
- "schema": {
- "type": [
- "string"
- ],
- "examples": [
- "whoa!"
- ]
- }
- }
- }
- }
- }
- }
- },
- "/pet/{petId}/uploadImageOctet": {
- "post": {
- "tags": [
- "pet"
- ],
- "summary": "Uploads an image",
- "description": "Updates a pet in the store with a new image",
- "operationId": "uploadFileOctet",
- "parameters": [
- {
- "description": "ID of pet that needs to be updated",
- "name": "petId",
- "required": true,
- "schema": {
- "type": "integer",
- "format": "int64"
- },
- "in": "path"
- },
- {
- "description": "Additional Metadata",
- "name": "additionalMetadata",
- "schema": {
- "type": "string"
- },
- "in": "query"
- }
- ],
- "requestBody": {
- "content": {
- "application/octet-stream": {
- "schema": {}
- }
- },
- "required": true
- },
- "responses": {
- "200": {
- "description": "Successful operation",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ApiResponse"
- }
- }
- }
- },
- "400": {
- "description": "Invalid ID supplied"
- },
- "405": {
- "description": "Invalid Input"
- }
- }
- }
- },
- "/pet/{petId}/uploadmultiImage": {
- "post": {
- "tags": [
- "pet"
- ],
- "summary": "Uploads an image",
- "description": "Updates a pet in the store with a new image",
- "operationId": "uploadFilemulti",
- "parameters": [
- {
- "description": "ID of pet that needs to be updated",
- "name": "petId",
- "required": true,
- "schema": {
- "type": "integer",
- "format": "int64"
- },
- "in": "path"
- },
- {
- "description": "Additional Metadata",
- "name": "additionalMetadata",
- "schema": {
- "type": "string"
- },
- "in": "query"
- }
- ],
- "requestBody": {
- "content": {
- "multipart/form-data": {
- "schema": {
- "type": [
- "object"
- ],
- "properties": {
- "orderId": {
- "type": [
- "integer"
- ]
- },
- "image": {
- "type": [
- "string"
- ]
- }
- }
- }
- }
- },
- "required": true
- },
- "responses": {
- "200": {
- "description": "Successful operation",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ApiResponse"
- }
- }
- }
- },
- "400": {
- "description": "Invalid ID supplied"
- },
- "405": {
- "description": "Invalid Input"
- }
- }
- }
- },
- "/pet2/{petId}": {
- "post": {
- "tags": [
- "pet"
- ],
- "summary": "Updates a pet in the store",
- "description": "Updates a pet in the store with form data",
- "operationId": "updatePet2WithForm",
- "parameters": [
- {
- "$ref": "#/components/parameters/PetIdParam"
- },
- {
- "description": "Name of pet that needs to be updated",
- "name": "name",
- "schema": {
- "type": "string"
- },
- "in": "query"
- },
- {
- "description": "Status of pet that needs to be updated",
- "name": "status",
- "schema": {
- "type": "string"
- },
- "in": "query"
- }
- ],
- "requestBody": {
- "content": {
- "application/x-www-form-urlencoded": {
- "schema": {
- "type": [
- "object"
- ],
- "properties": {
- "id": {
- "type": [
- "string"
- ],
- "format": "uuid"
- },
- "address": {
- "type": [
- "object"
- ],
- "properties": {}
- }
- }
- }
- }
- }
- },
- "security": [
- {
- "Login-OAuth2": [
- "write"
- ]
- }
- ],
- "responses": {
- "200": {
- "description": "Successful operation"
- },
- "400": {
- "description": "Invalid ID supplied"
- },
- "405": {
- "description": "Invalid Input"
- }
- }
- }
- },
- "/pet3/{petId}": {
- "post": {
- "tags": [
- "pet"
- ],
- "summary": "Updates a pet in the store",
- "description": "Updates a pet in the store with form data",
- "operationId": "updatePet3WithForm",
- "parameters": [
- {
- "$ref": "#/components/parameters/PetIdParam"
- },
- {
- "description": "Name of pet that needs to be updated",
- "name": "name",
- "schema": {
- "type": "string"
- },
- "in": "query"
- },
- {
- "description": "Status of pet that needs to be updated",
- "name": "status",
- "schema": {
- "type": "string"
- },
- "in": "query"
- }
- ],
- "requestBody": {
- "content": {
- "multipart/form-data": {
- "schema": {
- "type": [
- "object"
- ],
- "properties": {
- "id": {
- "type": [
- "string"
- ],
- "format": "uuid"
- },
- "address": {
- "type": [
- "object"
- ],
- "properties": {}
- },
- "children": {
- "type": "array",
- "items": {
- "type": [
- "string"
- ]
- }
- },
- "addresses": {
- "type": "array",
- "items": {
- "$ref": "#/components/schemas/Address"
- }
- }
- }
- }
- }
- }
- },
- "security": [
- {
- "Login-OAuth2": [
- "write"
- ]
- }
- ],
- "responses": {
- "200": {
- "description": "Successful operation"
- },
- "400": {
- "description": "Invalid ID supplied"
- },
- "405": {
- "description": "Invalid Input"
- }
- }
- }
- },
- "/pet4/{petId}": {
- "post": {
- "tags": [
- "pet"
- ],
- "summary": "Updates a pet in the store",
- "description": "Updates a pet in the store with form data",
- "operationId": "updatePet4WithForm",
- "parameters": [
- {
- "$ref": "#/components/parameters/PetIdParam"
- },
- {
- "description": "Name of pet that needs to be updated",
- "name": "name",
- "schema": {
- "type": "string"
- },
- "in": "query"
- },
- {
- "description": "Status of pet that needs to be updated",
- "name": "status",
- "schema": {
- "type": "string"
- },
- "in": "query"
- }
- ],
- "requestBody": {
- "content": {
- "multipart/form-data": {
- "encoding": {
- "historyMetadata": {
- "contentType": "application/xml; charset=utf-8"
- },
- "profileImage": {
- "contentType": "image/png, image/jpeg",
- "headers": {
- "X-Rate-Limit-Limit": {
- "description": "The number of allowed requests in the current period",
- "schema": {
- "enum": [
- 1,
- 2,
- 3
- ],
- "default": 3,
- "type": "integer",
- "maximum": 3
- }
- },
- "X-Rate-Limit-Reset": {
- "description": "The number of seconds left in the current period",
- "schema": {
- "type": "integer",
- "minimum": 2
- }
- }
- }
- }
- },
- "schema": {
- "type": [
- "object"
- ],
- "properties": {
- "id": {
- "type": [
- "string"
- ],
- "format": "uuid"
- },
- "address": {
- "type": [
- "object"
- ],
- "properties": {}
- },
- "historyMetadata": {
- "type": [
- "object"
- ],
- "description": "metadata in XML format",
- "properties": {}
- },
- "profileImage": {
- "type": [
- "string"
- ],
- "format": "binary"
- }
- }
- }
- }
- }
- },
- "security": [
- {
- "Login-OAuth2": [
- "write"
- ]
- }
- ],
- "responses": {
- "200": {
- "description": "A simple string response",
- "content": {
- "text/plain": {
- "schema": {
- "type": [
- "string"
- ]
- }
- }
- }
- },
- "400": {
- "description": "Invalid ID supplied"
- },
- "405": {
- "description": "Invalid Input"
- }
- }
- }
- },
- "/petcallback": {
- "post": {
- "tags": [
- "pet"
- ],
- "summary": "Add a new pet to the store",
- "description": "Add a new pet to the store",
- "operationId": "addPetcallback",
- "requestBody": {
- "$ref": "#/components/requestBodies/PetBodySchema"
- },
- "callbacks": {
- "test": {
- "'{$request.body#/id}'": {
- "post": {
- "requestBody": {
- "content": {
- "\"*/*\"": {
- "schema": {
- "type": [
- "string"
- ]
- }
- }
- }
- },
- "responses": {
- "200": {
- "description": "Successful operation",
- "content": {
- "application/xml": {
- "schema": {
- "type": "array",
- "items": {
- "$ref": "#/components/schemas/Pet"
- }
- }
- },
- "application/json": {
- "schema": {
- "type": "array",
- "items": {
- "$ref": "#/components/schemas/Pet"
- }
- }
- }
- }
- },
- "400": {
- "description": "Invalid ID supplied"
- },
- "404": {
- "description": "Pet not found"
- },
- "default": {
- "description": "Something is wrong"
- }
- }
- }
- }
+ "description": "Status values that need to be considered for filter",
+ "schema": {
+ "type": "string",
+ "default": "available",
+ "enum": [
+ "available",
+ "pending",
+ "sold"
+ ]
+ },
+ "name": "status",
+ "in": "query"
}
- },
+ ],
"security": [
+ {},
{
"Login-OAuth2": [
- "write"
+ "read"
]
}
],
@@ -1563,65 +385,59 @@
"200": {
"description": "Successful operation",
"content": {
- "application/xml": {
- "schema": {
- "$ref": "#/components/schemas/Pet"
- }
- },
"application/json": {
"schema": {
- "$ref": "#/components/schemas/Pet"
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Pet"
+ }
}
- }
- }
- },
- "405": {
- "description": "Validation exception",
- "content": {
- "application/json": {
+ },
+ "application/xml": {
"schema": {
- "type": [
- "object"
- ],
- "properties": {
- "result": {
- "type": [
- "string"
- ]
- },
- "message": {
- "type": [
- "string"
- ]
- }
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Pet"
}
}
}
}
+ },
+ "400": {
+ "description": "Invalid status value"
}
}
}
},
- "/petcallbackReference": {
- "post": {
+ "/pet/findByTag": {
+ "get": {
"tags": [
"pet"
],
- "summary": "Add a new pet to the store",
- "description": "Add a new pet to the store",
- "operationId": "petcallbackReference",
- "requestBody": {
- "$ref": "#/components/requestBodies/PetBodySchema"
- },
- "callbacks": {
- "test1": {
- "$ref": "#/components/callbacks/test"
+ "summary": "Finds Pets by tags",
+ "description": "Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.",
+ "operationId": "findPetsByTags",
+ "parameters": [
+ {
+ "explode": true,
+ "description": "Tags to filter by",
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "name": "tag",
+ "in": "query"
}
- },
+ ],
"security": [
+ {
+ "api_key": []
+ },
{
"Login-OAuth2": [
- "write"
+ "read"
]
}
],
@@ -1629,20 +445,45 @@
"200": {
"description": "Successful operation",
"content": {
- "application/xml": {
+ "application/json": {
"schema": {
"$ref": "#/components/schemas/Pet"
}
},
- "application/json": {
+ "application/xml": {
"schema": {
"$ref": "#/components/schemas/Pet"
}
}
}
},
- "405": {
- "description": "Validation exception",
+ "400": {
+ "description": "Invalid status value"
+ },
+ "default": {
+ "description": "Unexpected error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorModel"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/store/inventory": {
+ "get": {
+ "tags": [
+ "store"
+ ],
+ "summary": "Returns pet inventories by status",
+ "description": "Returns a map of status codes to quantities",
+ "operationId": "getInventory",
+ "responses": {
+ "200": {
+ "description": "Successful operation",
"content": {
"application/json": {
"schema": {
@@ -1650,12 +491,7 @@
"object"
],
"properties": {
- "result": {
- "type": [
- "string"
- ]
- },
- "message": {
+ "none": {
"type": [
"string"
]
@@ -1679,17 +515,17 @@
"operationId": "placeOrder",
"requestBody": {
"content": {
- "application/json": {
+ "application/xml": {
"schema": {
"$ref": "#/components/schemas/Order"
}
},
- "application/xml": {
+ "application/x-www-form-urlencoded": {
"schema": {
"$ref": "#/components/schemas/Order"
}
},
- "application/x-www-form-urlencoded": {
+ "application/json": {
"schema": {
"$ref": "#/components/schemas/Order"
}
@@ -1729,17 +565,17 @@
"operationId": "createUser",
"requestBody": {
"content": {
- "application/json": {
+ "application/xml": {
"schema": {
"$ref": "#/components/schemas/User"
}
},
- "application/xml": {
+ "application/x-www-form-urlencoded": {
"schema": {
"$ref": "#/components/schemas/User"
}
},
- "application/x-www-form-urlencoded": {
+ "application/json": {
"schema": {
"$ref": "#/components/schemas/User"
}
@@ -1788,17 +624,17 @@
"operationId": "createUsersWithListInput",
"requestBody": {
"content": {
- "application/json": {
+ "application/xml": {
"schema": {
"$ref": "#/components/schemas/User"
}
},
- "application/xml": {
+ "application/x-www-form-urlencoded": {
"schema": {
"$ref": "#/components/schemas/User"
}
},
- "application/x-www-form-urlencoded": {
+ "application/json": {
"schema": {
"$ref": "#/components/schemas/User"
}
@@ -1816,9 +652,71 @@
}
}
},
- "/close": {
- "post": {
- "summary": "Shutdown the server",
+ "/user/login": {
+ "get": {
+ "tags": [
+ "user"
+ ],
+ "summary": "Logs user into the system.",
+ "description": "Logs user into the system.",
+ "operationId": "loginUser",
+ "parameters": [
+ {
+ "description": "The user name for login",
+ "schema": {
+ "type": "string"
+ },
+ "name": "username",
+ "in": "query"
+ },
+ {
+ "description": "The password for login in clear text",
+ "schema": {
+ "type": "string",
+ "format": "password"
+ },
+ "name": "password",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful operation",
+ "headers": {
+ "X-Rate-Limit": {
+ "$ref": "#/components/headers/X-Rate-Limit"
+ },
+ "X-Expires-After": {
+ "$ref": "#/components/headers/X-Expires-After"
+ }
+ },
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "string"
+ }
+ },
+ "application/xml": {
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid username/password supplied"
+ }
+ }
+ }
+ },
+ "/user/logout": {
+ "get": {
+ "tags": [
+ "user"
+ ],
+ "summary": "Logs out current logged in user session.",
+ "description": "Logs out current logged in user session.",
+ "operationId": "logoutUser",
"responses": {
"200": {
"description": "Successful operation"
@@ -1837,44 +735,44 @@
"parameters": [
{
"description": "ID of order that needs to be fetched",
- "name": "orderId",
"required": true,
"schema": {
"type": "integer",
"format": "int64"
},
+ "name": "orderId",
"in": "path"
}
],
"servers": [
{
- "description": "ext test server",
- "url": "http://ext.server.com/api/v12"
+ "url": "http://ext.server.com/api/v12",
+ "description": "ext test server"
},
{
- "description": "ext test server 13",
- "url": "http://ext13.server.com/api/v12"
+ "url": "http://ext13.server.com/api/v12",
+ "description": "ext test server 13"
},
{
- "description": "ext test server 14",
- "url": "http://ext14.server.com/api/v12"
+ "url": "http://ext14.server.com/api/v12",
+ "description": "ext test server 14"
}
],
"responses": {
"200": {
"description": "Successful operation",
"content": {
- "application/json": {
+ "application/xml": {
"schema": {
"$ref": "#/components/schemas/Order"
}
},
- "application/xml": {
+ "application/x-www-form-urlencoded": {
"schema": {
"$ref": "#/components/schemas/Order"
}
},
- "application/x-www-form-urlencoded": {
+ "application/json": {
"schema": {
"$ref": "#/components/schemas/Order"
}
@@ -2420,6 +1318,7 @@
"$ref": "#/components/schemas/Pet2"
},
{
+ "type": "object",
"properties": {
"huntingSkill": {
"type": [
@@ -2435,7 +1334,6 @@
]
}
},
- "type": "object",
"required": [
"huntingSkill"
]
@@ -2449,6 +1347,7 @@
"$ref": "#/components/schemas/Pet2"
},
{
+ "type": "object",
"properties": {
"packSize": {
"type": [
@@ -2459,7 +1358,6 @@
"format": "int32"
}
},
- "type": "object",
"required": [
"packSize"
]
@@ -2473,6 +1371,7 @@
"$ref": "#/components/schemas/Pet"
},
{
+ "type": "object",
"properties": {
"rootCause": {
"type": [
@@ -2480,7 +1379,6 @@
]
}
},
- "type": "object",
"required": [
"rootCause"
]
@@ -2511,6 +1409,7 @@
"$ref": "#/components/schemas/Pet"
},
{
+ "type": "object",
"properties": {
"huntingSkill": {
"type": [
@@ -2524,8 +1423,7 @@
"aggressive"
]
}
- },
- "type": "object"
+ }
}
]
},
@@ -2641,19 +1539,19 @@
"parameters": {
"PetIdParam": {
"description": "ID of the pet",
- "name": "petId",
"required": true,
"schema": {
"type": "integer",
"format": "int64"
},
+ "name": "petId",
"in": "path"
}
},
"requestBodies": {
"PetBodySchema": {
"content": {
- "application/json": {
+ "application/xml": {
"schema": {
"$ref": "#/components/schemas/Pets"
}
@@ -2663,7 +1561,7 @@
"$ref": "#/components/schemas/Pets"
}
},
- "application/xml": {
+ "application/json": {
"schema": {
"$ref": "#/components/schemas/Pets"
}
@@ -2718,7 +1616,7 @@
"200": {
"description": "Successful operation",
"content": {
- "application/xml": {
+ "application/json": {
"schema": {
"type": "array",
"items": {
@@ -2726,7 +1624,7 @@
}
}
},
- "application/json": {
+ "application/xml": {
"schema": {
"type": "array",
"items": {
@@ -2762,12 +1660,12 @@
"parameters": [
{
"description": "ID of pet to return",
- "name": "petId",
"required": true,
"schema": {
"type": "integer",
"format": "int64"
},
+ "name": "petId",
"in": "path"
}
],
@@ -2775,12 +1673,12 @@
"200": {
"description": "Successful operation",
"content": {
- "application/xml": {
+ "application/json": {
"schema": {
"$ref": "#/components/schemas/Pet"
}
},
- "application/json": {
+ "application/xml": {
"schema": {
"$ref": "#/components/schemas/Pet"
}
@@ -2801,24 +1699,24 @@
}
},
"securitySchemes": {
- "Login": {
- "scheme": "basic",
- "type": "http"
- },
"LoginApiKey": {
"type": "apiKey",
"in": "header",
"name": "X-API-KEY"
},
+ "Login": {
+ "type": "http",
+ "scheme": "basic"
+ },
+ "Jwt": {
+ "type": "http",
+ "scheme": "bearer"
+ },
"api_key": {
"type": "apiKey",
"in": "header",
"name": "api_key"
},
- "Jwt": {
- "scheme": "bearer",
- "type": "http"
- },
"Login-OAuth2": {
"type": "oauth2",
"flows": {
diff --git a/tests/unit/Helpers.Tests.ps1 b/tests/unit/Helpers.Tests.ps1
index 76c2c716a..4c67439b8 100644
--- a/tests/unit/Helpers.Tests.ps1
+++ b/tests/unit/Helpers.Tests.ps1
@@ -1310,7 +1310,7 @@ Describe 'Out-PodeHost' {
}
It 'Writes an Array to the Host by pipeline' {
- @('France','Rick',21 ,'male') | Out-PodeHost
+ @('France', 'Rick', 21 , 'male') | Out-PodeHost
Assert-MockCalled Out-Default -Scope It -Times 1
}
}
@@ -1441,12 +1441,15 @@ Describe 'ConvertFrom-PodeHeaderQValue' {
Describe 'Get-PodeAcceptEncoding' {
BeforeEach {
+ Mock New-PodeRequestException { return [System.Net.Http.HttpRequestException]::new() }
+
$PodeContext = @{
Server = @{
Web = @{ Compression = @{ Enabled = $true } }
Compression = @{ Encodings = @('gzip', 'deflate', 'x-gzip') }
}
- } }
+ }
+ }
It 'Returns empty for no encoding' {
Get-PodeAcceptEncoding -AcceptEncoding '' | Should -Be ''
@@ -1500,11 +1503,13 @@ Describe 'Get-PodeAcceptEncoding' {
It 'Errors when no encoding matches, and identity disabled' {
$PodeContext.Server.Web.Compression.Enabled = $true
{ Get-PodeAcceptEncoding -AcceptEncoding 'br,identity;q=0' -ThrowError } | Should -Throw -ExceptionType 'System.Net.Http.HttpRequestException'
+ Assert-MockCalled New-PodeRequestException -Scope It -Times 1
}
It 'Errors when no encoding matches, and wildcard disabled' {
$PodeContext.Server.Web.Compression.Enabled = $true
{ Get-PodeAcceptEncoding -AcceptEncoding 'br,*;q=0' -ThrowError } | Should -Throw -ExceptionType 'System.Net.Http.HttpRequestException'
+ Assert-MockCalled New-PodeRequestException -Scope It -Times 1
}
It 'Returns empty if identity is allowed, but wildcard disabled' {
@@ -1514,9 +1519,13 @@ Describe 'Get-PodeAcceptEncoding' {
}
Describe 'Get-PodeTransferEncoding' {
- $PodeContext = @{
- Server = @{
- Compression = @{ Encodings = @('gzip', 'deflate', 'x-gzip') }
+ BeforeEach {
+ Mock New-PodeRequestException { return [System.Net.Http.HttpRequestException]::new() }
+
+ $PodeContext = @{
+ Server = @{
+ Compression = @{ Encodings = @('gzip', 'deflate', 'x-gzip') }
+ }
}
}
@@ -1543,6 +1552,7 @@ Describe 'Get-PodeTransferEncoding' {
It 'Errors when no encoding matches' {
{ Get-PodeTransferEncoding -TransferEncoding 'compress,chunked' -ThrowError } | Should -Throw -ExceptionType 'System.Net.Http.HttpRequestException'
+ Assert-MockCalled New-PodeRequestException -Scope It -Times 1
}
}
@@ -1756,6 +1766,51 @@ Describe 'ConvertTo-PodeYamlInternal Tests' {
}
}
+
+ # Test case for a hashtable containing a key named 'Count'
+ Context "When a hashtable contains a 'Count' key" {
+
+ It 'Should convert the hashtable to YAML without error' {
+ # Arrange
+ $hashtable = @{
+ Name = 'Sample'
+ Count = 10
+ }
+
+ # Act
+ $result = { ConvertTo-PodeYamlInternal -InputObject $hashtable -NoNewLine } | Should -Not -Throw
+
+ # Assert
+ $yaml = ConvertTo-PodeYamlInternal -InputObject $hashtable -NoNewLine
+
+ # Check if YAML conversion includes both 'Name' and 'Count' keys in the YAML output
+ $yaml | Should -Match 'Name: Sample'
+ $yaml | Should -Match 'Count: 10'
+ }
+ }
+
+ # Test case for a PSCustomObject containing a key named 'Count'
+ Context "When a PSCustomObject contains a 'Count' property" {
+
+ It 'Should convert the PSCustomObject to YAML without error' {
+ # Arrange
+ $object = [pscustomobject]@{
+ Name = 'Sample'
+ Count = 20
+ }
+
+ # Act
+ $result = { ConvertTo-PodeYamlInternal -InputObject $object -NoNewLine } | Should -Not -Throw
+
+ # Assert
+ $yaml = ConvertTo-PodeYamlInternal -InputObject $object -NoNewLine
+
+ # Check if YAML conversion includes both 'Name' and 'Count' properties in the YAML output
+ $yaml | Should -Match 'Name: Sample'
+ $yaml | Should -Match 'Count: 20'
+ }
+ }
+
Context 'Error handling' {
It 'Returns empty string for null input' {
$result = ConvertTo-PodeYamlInternal -InputObject $null
diff --git a/tests/unit/OpenApi.Tests.ps1 b/tests/unit/OpenApi.Tests.ps1
index 4f4a26bbf..b62b29b89 100644
--- a/tests/unit/OpenApi.Tests.ps1
+++ b/tests/unit/OpenApi.Tests.ps1
@@ -1821,7 +1821,7 @@ Describe 'OpenApi' {
}
It 'ArrayNoSwitchesUniqueItems' {
- $result = New-PodeOASchemaProperty -Name 'testSchema' -Description 'Test for New-PodeOASchemaProperty' -Reference 'Cat' -MinProperties 1 -MaxProperties 2 `
+ $result = New-PodeOASchemaProperty -Name 'testSchema' -Description 'Test for New-PodeOASchemaProperty' -Reference 'Cat' `
-Example 'Example for New-PodeOASchemaProperty' -Array -MinItems 2 -MaxItems 4 -UniqueItems
$result | Should -Not -BeNullOrEmpty
#$result.Count | Should -Be 1
@@ -1829,8 +1829,6 @@ Describe 'OpenApi' {
$result.name | Should -Be 'testSchema'
$result.schema | Should -Be 'Cat'
$result.description | Should -Be 'Test for New-PodeOASchemaProperty'
- $result.minProperties | Should -Be 1
- $result.maxProperties | Should -Be 2
$result['example'] | Should -Be 'Example for New-PodeOASchemaProperty'
$result.array | Should -BeTrue
$result.uniqueItems | Should -BeTrue
@@ -1839,7 +1837,7 @@ Describe 'OpenApi' {
}
It 'ArrayDeprecatedUniqueItems' {
- $result = New-PodeOASchemaProperty -Name 'testSchema' -Description 'Test for New-PodeOASchemaProperty' -Reference 'Cat' -MinProperties 1 -MaxProperties 2 `
+ $result = New-PodeOASchemaProperty -Name 'testSchema' -Description 'Test for New-PodeOASchemaProperty' -Reference 'Cat' `
-Example 'Example for New-PodeOASchemaProperty' -Deprecated -Array -MinItems 2 -MaxItems 4 -UniqueItems
$result | Should -Not -BeNullOrEmpty
#$result.Count | Should -Be 1
@@ -1847,8 +1845,6 @@ Describe 'OpenApi' {
$result.name | Should -Be 'testSchema'
$result.schema | Should -Be 'Cat'
$result.description | Should -Be 'Test for New-PodeOASchemaProperty'
- $result.minProperties | Should -Be 1
- $result.maxProperties | Should -Be 2
$result['example'] | Should -Be 'Example for New-PodeOASchemaProperty'
$result.deprecated | Should -Be $true
$result.array | Should -BeTrue
@@ -1857,7 +1853,7 @@ Describe 'OpenApi' {
$result.maxItems | Should -BeTrue
}
It 'ArrayNullableUniqueItems' {
- $result = New-PodeOASchemaProperty -Name 'testSchema' -Description 'Test for New-PodeOASchemaProperty' -Reference 'Cat' -MinProperties 1 -MaxProperties 2 `
+ $result = New-PodeOASchemaProperty -Name 'testSchema' -Description 'Test for New-PodeOASchemaProperty' -Reference 'Cat' `
-Example 'Example for New-PodeOASchemaProperty' -Nullable -Array -MinItems 2 -MaxItems 4 -UniqueItems
$result | Should -Not -BeNullOrEmpty
#$result.Count | Should -Be 2
@@ -1865,8 +1861,6 @@ Describe 'OpenApi' {
$result.name | Should -Be 'testSchema'
$result.schema | Should -Be 'Cat'
$result.description | Should -Be 'Test for New-PodeOASchemaProperty'
- $result.minProperties | Should -Be 1
- $result.maxProperties | Should -Be 2
$result['example'] | Should -Be 'Example for New-PodeOASchemaProperty'
$result['nullable'] | Should -Be $true
$result.array | Should -BeTrue
@@ -1875,7 +1869,7 @@ Describe 'OpenApi' {
$result.maxItems | Should -BeTrue
}
It 'ArrayWriteOnlyUniqueItems' {
- $result = New-PodeOASchemaProperty -Name 'testSchema' -Description 'Test for New-PodeOASchemaProperty' -Reference 'Cat' -MinProperties 1 -MaxProperties 2 `
+ $result = New-PodeOASchemaProperty -Name 'testSchema' -Description 'Test for New-PodeOASchemaProperty' -Reference 'Cat' `
-Example 'Example for New-PodeOASchemaProperty' -WriteOnly -Array -MinItems 2 -MaxItems 4 -UniqueItems
$result | Should -Not -BeNullOrEmpty
#$result.Count | Should -Be 2
@@ -1883,8 +1877,6 @@ Describe 'OpenApi' {
$result.name | Should -Be 'testSchema'
$result.schema | Should -Be 'Cat'
$result.description | Should -Be 'Test for New-PodeOASchemaProperty'
- $result.minProperties | Should -Be 1
- $result.maxProperties | Should -Be 2
$result['example'] | Should -Be 'Example for New-PodeOASchemaProperty'
$result['writeOnly'] | Should -Be $true
$result.array | Should -BeTrue
@@ -1893,7 +1885,7 @@ Describe 'OpenApi' {
$result.maxItems | Should -BeTrue
}
It 'ArrayReadOnlyUniqueItems' {
- $result = New-PodeOASchemaProperty -Name 'testSchema' -Description 'Test for New-PodeOASchemaProperty' -Reference 'Cat' -MinProperties 1 -MaxProperties 2 `
+ $result = New-PodeOASchemaProperty -Name 'testSchema' -Description 'Test for New-PodeOASchemaProperty' -Reference 'Cat' `
-Example 'Example for New-PodeOASchemaProperty' -ReadOnly -Array -MinItems 2 -MaxItems 4 -UniqueItems
$result | Should -Not -BeNullOrEmpty
#$result.Count | Should -Be 2
@@ -1901,8 +1893,6 @@ Describe 'OpenApi' {
$result.name | Should -Be 'testSchema'
$result.schema | Should -Be 'Cat'
$result.description | Should -Be 'Test for New-PodeOASchemaProperty'
- $result.minProperties | Should -Be 1
- $result.maxProperties | Should -Be 2
$result['example'] | Should -Be 'Example for New-PodeOASchemaProperty'
$result['readOnly'] | Should -Be $true
$result.array | Should -BeTrue
@@ -1912,7 +1902,7 @@ Describe 'OpenApi' {
}
It 'ArrayNoSwitches' {
- $result = New-PodeOASchemaProperty -Name 'testSchema' -Description 'Test for New-PodeOASchemaProperty' -Reference 'Cat' -MinProperties 1 -MaxProperties 2 `
+ $result = New-PodeOASchemaProperty -Name 'testSchema' -Description 'Test for New-PodeOASchemaProperty' -Reference 'Cat' `
-Example 'Example for New-PodeOASchemaProperty' -Array -MinItems 2 -MaxItems 4
$result | Should -Not -BeNullOrEmpty
#$result.Count | Should -Be 1
@@ -1920,8 +1910,6 @@ Describe 'OpenApi' {
$result.name | Should -Be 'testSchema'
$result.schema | Should -Be 'Cat'
$result.description | Should -Be 'Test for New-PodeOASchemaProperty'
- $result.minProperties | Should -Be 1
- $result.maxProperties | Should -Be 2
$result['example'] | Should -Be 'Example for New-PodeOASchemaProperty'
$result.array | Should -BeTrue
$result.minItems | Should -BeTrue
@@ -1929,7 +1917,7 @@ Describe 'OpenApi' {
}
It 'ArrayDeprecated' {
- $result = New-PodeOASchemaProperty -Name 'testSchema' -Description 'Test for New-PodeOASchemaProperty' -Reference 'Cat' -MinProperties 1 -MaxProperties 2 `
+ $result = New-PodeOASchemaProperty -Name 'testSchema' -Description 'Test for New-PodeOASchemaProperty' -Reference 'Cat' `
-Example 'Example for New-PodeOASchemaProperty' -Deprecated -Array -MinItems 2 -MaxItems 4
$result | Should -Not -BeNullOrEmpty
#$result.Count | Should -Be 1
@@ -1937,8 +1925,6 @@ Describe 'OpenApi' {
$result.name | Should -Be 'testSchema'
$result.schema | Should -Be 'Cat'
$result.description | Should -Be 'Test for New-PodeOASchemaProperty'
- $result.minProperties | Should -Be 1
- $result.maxProperties | Should -Be 2
$result['example'] | Should -Be 'Example for New-PodeOASchemaProperty'
$result.deprecated | Should -Be $true
$result.array | Should -BeTrue
@@ -1946,7 +1932,7 @@ Describe 'OpenApi' {
$result.maxItems | Should -BeTrue
}
It 'ArrayNullable' {
- $result = New-PodeOASchemaProperty -Name 'testSchema' -Description 'Test for New-PodeOASchemaProperty' -Reference 'Cat' -MinProperties 1 -MaxProperties 2 `
+ $result = New-PodeOASchemaProperty -Name 'testSchema' -Description 'Test for New-PodeOASchemaProperty' -Reference 'Cat' `
-Example 'Example for New-PodeOASchemaProperty' -Nullable -Array -MinItems 2 -MaxItems 4
$result | Should -Not -BeNullOrEmpty
#$result.Count | Should -Be 2
@@ -1954,8 +1940,6 @@ Describe 'OpenApi' {
$result.name | Should -Be 'testSchema'
$result.schema | Should -Be 'Cat'
$result.description | Should -Be 'Test for New-PodeOASchemaProperty'
- $result.minProperties | Should -Be 1
- $result.maxProperties | Should -Be 2
$result['example'] | Should -Be 'Example for New-PodeOASchemaProperty'
$result['nullable'] | Should -Be $true
$result.array | Should -BeTrue
@@ -1963,7 +1947,7 @@ Describe 'OpenApi' {
$result.maxItems | Should -BeTrue
}
It 'ArrayWriteOnly' {
- $result = New-PodeOASchemaProperty -Name 'testSchema' -Description 'Test for New-PodeOASchemaProperty' -Reference 'Cat' -MinProperties 1 -MaxProperties 2 `
+ $result = New-PodeOASchemaProperty -Name 'testSchema' -Description 'Test for New-PodeOASchemaProperty' -Reference 'Cat' `
-Example 'Example for New-PodeOASchemaProperty' -WriteOnly -Array -MinItems 2 -MaxItems 4
$result | Should -Not -BeNullOrEmpty
#$result.Count | Should -Be 2
@@ -1971,8 +1955,6 @@ Describe 'OpenApi' {
$result.name | Should -Be 'testSchema'
$result.schema | Should -Be 'Cat'
$result.description | Should -Be 'Test for New-PodeOASchemaProperty'
- $result.minProperties | Should -Be 1
- $result.maxProperties | Should -Be 2
$result['example'] | Should -Be 'Example for New-PodeOASchemaProperty'
$result['writeOnly'] | Should -Be $true
$result.array | Should -BeTrue
@@ -1980,7 +1962,7 @@ Describe 'OpenApi' {
$result.maxItems | Should -BeTrue
}
It 'ArrayReadOnly' {
- $result = New-PodeOASchemaProperty -Name 'testSchema' -Description 'Test for New-PodeOASchemaProperty' -Reference 'Cat' -MinProperties 1 -MaxProperties 2 `
+ $result = New-PodeOASchemaProperty -Name 'testSchema' -Description 'Test for New-PodeOASchemaProperty' -Reference 'Cat' `
-Example 'Example for New-PodeOASchemaProperty' -ReadOnly -Array -MinItems 2 -MaxItems 4
$result | Should -Not -BeNullOrEmpty
#$result.Count | Should -Be 2
@@ -1988,8 +1970,6 @@ Describe 'OpenApi' {
$result.name | Should -Be 'testSchema'
$result.schema | Should -Be 'Cat'
$result.description | Should -Be 'Test for New-PodeOASchemaProperty'
- $result.minProperties | Should -Be 1
- $result.maxProperties | Should -Be 2
$result['example'] | Should -Be 'Example for New-PodeOASchemaProperty'
$result['readOnly'] | Should -Be $true
$result.array | Should -BeTrue
@@ -2189,12 +2169,13 @@ Describe 'OpenApi' {
$Route = @{
OpenApi = @{
Path = '/test'
- Responses = @{
+ Responses = [ordered]@{
'200' = @{ description = 'OK' }
'default' = @{ description = 'Internal server error' }
}
- Parameters = $null
- RequestBody = $null
+ Parameters = [ordered]@{}
+ RequestBody = [ordered]@{}
+ callbacks = [ordered]@{}
Authentication = @()
DefinitionTag = @('Default')
IsDefTagConfigured = $false
@@ -3180,7 +3161,12 @@ Describe 'OpenApi' {
It 'Sets Parameters on the route if provided' {
$route = @{
Method = 'GET'
- OpenApi = @{}
+ OpenApi = @{
+ Responses = [ordered]@{}
+ Parameters = [ordered]@{}
+ RequestBody = [ordered]@{}
+ callbacks = [ordered]@{}
+ }
}
$parameters = @(
@{ Name = 'param1'; In = 'query' }
@@ -3188,7 +3174,7 @@ Describe 'OpenApi' {
Set-PodeOARequest -Route $route -Parameters $parameters
- $route.OpenApi.Parameters | Should -BeExactly $parameters
+ $route.OpenApi.Parameters['Default'] | Should -BeExactly $parameters
}
It 'Sets RequestBody on the route if method is POST' {
@@ -3215,6 +3201,18 @@ Describe 'OpenApi' {
} | Should -Throw -ExpectedMessage ($PodeLocale.getRequestBodyNotAllowedExceptionMessage -f 'GET')
}
+ It 'Allows a RequestBody on non-standard methods with AllowNonStandardBody' {
+ $route = @{
+ Method = 'DELETE'
+ OpenApi = @{}
+ }
+ $requestBody = @{ Content = 'application/json' }
+
+ Set-PodeOARequest -Route $route -RequestBody $requestBody -AllowNonStandardBody
+
+ $route.OpenApi.RequestBody | Should -BeExactly $requestBody
+ }
+
It 'Returns the route when PassThru is used' {
$route = @{
Method = 'POST'
@@ -3236,8 +3234,103 @@ Describe 'OpenApi' {
$route.OpenApi.RequestBody | Should -BeNullOrEmpty
}
+
+ It 'Sets Parameters with DefinitionTag if provided' {
+ $route = @{
+ Method = 'GET'
+ OpenApi = @{
+ Responses = [ordered]@{}
+ Parameters = [ordered]@{}
+ RequestBody = [ordered]@{}
+ callbacks = [ordered]@{}
+ }
+ }
+ $parameters = @(
+ @{ Name = 'param1'; In = 'query' }
+ )
+
+ $definitionTag = 'v1'
+ $PodeContext.Server.OpenAPI.Definitions[ $definitionTag] = Get-PodeOABaseObject
+
+ Set-PodeOARequest -Route $route -Parameters $parameters -DefinitionTag $definitionTag
+
+ $route.OpenApi.Parameters[$definitionTag] | Should -BeExactly $parameters
+ }
+
+ It 'Defaults Parameters to an empty array if not provided' {
+ $route = @{
+ Method = 'GET'
+ OpenApi = @{
+ Responses = [ordered]@{}
+ Parameters = [ordered]@{}
+ RequestBody = [ordered]@{}
+ callbacks = [ordered]@{}
+ }
+ }
+
+ Set-PodeOARequest -Route $route
+
+ $route.OpenApi.Parameters['Default'] | Should -BeNullOrEmpty
+ }
}
+ Describe 'Add-PodeOAServerEndpoint' {
+ # Mocking Pode related context and functions
+ BeforeAll {
+
+
+ function Test-PodeIsEmpty {
+ param ($Value)
+ return -not $Value
+ }
+ }
+
+ Context 'When adding a server with URL and description' {
+ It 'Should add the server to the OpenAPI definition' {
+ Add-PodeOAServerEndpoint -Url 'https://myserver.io/api' -Description 'My test server'
+
+ $servers = $PodeContext.Server.OpenAPI.Definitions['default'].servers
+ $servers | Should -HaveCount 1
+ $servers[0].url | Should -Be 'https://myserver.io/api'
+ $servers[0].description | Should -Be 'My test server'
+ }
+ }
+
+ Context 'When adding a server with variables' {
+ It 'Should add the server with variables to the OpenAPI definition' {
+ $variables = [ordered]@{
+ username = [ordered]@{
+ default = 'demo'
+ description = 'assigned by provider'
+ }
+ port = [ordered]@{
+ default = 8443
+ }
+ basePath = [ordered]@{
+ default = 'v2'
+ }
+ }
+
+ Add-PodeOAServerEndpoint -Url 'https://{username}.server.com:{port}/{basePath}' -Variables $variables
+
+ $servers = $PodeContext.Server.OpenAPI.Definitions['default'].servers
+ $servers | Should -HaveCount 1
+ $servers[0].url | Should -Be 'https://{username}.server.com:{port}/{basePath}'
+ $servers[0].variables | Should -Be $variables
+ }
+ }
+
+ Context 'When adding multiple local endpoints' {
+ It 'Should throw an error when multiple local URLs are defined' {
+ Add-PodeOAServerEndpoint -Url '/api' -Description 'Local endpoint 1'
+
+ { Add-PodeOAServerEndpoint -Url '/api/v2' -Description 'Local endpoint 2' } |
+ Should -Throw "Both '/api/v2' and '/api' are defined as local OpenAPI endpoints, but only one local endpoint is allowed per API definition."
+ }
+ }
+
+
+ }
Context 'Pet Object example' {
diff --git a/tests/unit/PrivateOpenApi.Tests.ps1 b/tests/unit/PrivateOpenApi.Tests.ps1
index 4a6b807fe..ec822fcab 100644
--- a/tests/unit/PrivateOpenApi.Tests.ps1
+++ b/tests/unit/PrivateOpenApi.Tests.ps1
@@ -419,4 +419,24 @@ Describe 'PrivateOpenApi' {
}
+ Describe 'ConvertTo-PodeOARoutePath' {
+
+ It 'should convert the path "/v4.2/:potato" to OpenAPI format' {
+ $result = ConvertTo-PodeOARoutePath -Path '/v4.2/:potato'
+ $result | Should -BeExactly '/v4.2/{potato}'
+ }
+
+ It 'should convert the path "/:potato" to OpenAPI format' {
+ $result = ConvertTo-PodeOARoutePath -Path '/:potato'
+ $result | Should -BeExactly '/{potato}'
+ }
+
+ It 'should convert the path "/stores/order/:orderId/invoice" to OpenAPI format' {
+ $result = ConvertTo-PodeOARoutePath -Path '/stores/order/:orderId/invoice'
+ $result | Should -BeExactly '/stores/order/{orderId}/invoice'
+ }
+
+
+ }
+
}
\ No newline at end of file
diff --git a/tests/unit/_.Tests.ps1 b/tests/unit/_.Tests.ps1
index 61e8aad23..74a8cea04 100644
--- a/tests/unit/_.Tests.ps1
+++ b/tests/unit/_.Tests.ps1
@@ -1,4 +1,5 @@
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')]
+[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')]
param()
BeforeDiscovery {
@@ -7,7 +8,7 @@ BeforeDiscovery {
# List of directories to exclude
$excludeDirs = @('scripts', 'views', 'static', 'public', 'assets', 'timers', 'modules',
- 'Authentication', 'certs', 'logs', 'relative', 'routes', 'issues')
+ 'Authentication', 'certs', 'logs', 'relative', 'routes', 'issues','auth')
# Convert exlusion list into single regex pattern for directory matching
$dirSeparator = [IO.Path]::DirectorySeparatorChar
@@ -176,4 +177,60 @@ Describe 'Examples Script Headers' {
}
}
-}
\ No newline at end of file
+}
+
+
+Describe 'Check for Duplicate Function Definitions' {
+ BeforeAll { $path = $PSCommandPath
+ $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/'
+ # Retrieve all function definitions from the module files
+ $functionNames = @{}
+ $duplicatedFunctionNames = @{}
+ $moduleFiles = Get-ChildItem -Path $src -Recurse -Include '*.ps1', '*.psm1'
+
+ foreach ($file in $moduleFiles) {
+ $content = Get-Content -Path $file.FullName
+ $lineNumber = 0
+ foreach ($line in $content) {
+ # Increment line number for accurate tracking
+ $lineNumber++
+ # Match function definitions (e.g., "function MyFunction {")
+ if ($line -match 'function\s+([^\s{]+)\s*{') {
+ $functionName = $Matches[1]
+
+ # Check if function name already exists
+ if (! $functionNames.ContainsKey($functionName)) {
+ $functionNames[$functionName] = @{
+ FunctionName = $functionName
+ FilePath = @( )
+ LineNumber = @( )
+ }
+ }
+ else {
+ # Add to duplicated function names if not already tracked
+ $duplicatedFunctionNames[$functionName] = $functionNames[$functionName]
+ }
+ # Update the function details
+ $functionNames[$functionName].LineNumber += $lineNumber
+ $functionNames[$functionName].FilePath += $file.FullName
+ }
+ }
+ }
+
+ # Additional information in case of failure
+ if ($duplicatedFunctionNames.Count -gt 0) {
+ Write-host 'The following functions have multiple definitions:'
+ foreach ($key in $duplicatedFunctionNames.Keys) {
+ Write-host "Function: $($key)"
+ for ($i = 0; $i -lt $duplicatedFunctionNames[$key].LineNumber.Count ; $i++) {
+ Write-host " - File: $($duplicatedFunctionNames[$key].FilePath[$i]), Line: $($duplicatedFunctionNames[$key].LineNumber[$i])"
+ }
+ }
+ }
+ }
+
+ It 'should not have duplicate function definitions' {
+ # Assert no duplicate function definitions
+ $duplicatedFunctionNames.Count | Should -Be 0
+ }
+}