Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for Custom and default Favicons in Pode Endpoints #1509

Open
wants to merge 8 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions docs/Tutorials/Endpoints/Basics.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,3 +188,39 @@ To set this property, include it in `server.psd1` configuration file as shown be
}
}
```

## Favicons

Pode allows you to customize or disable the favicon for HTTP/HTTPS endpoints. By default, Pode serves a built-in `favicon.ico`, but you can override this behavior using the `-Favicon` and `-NoFavicon` parameters.

- **`-Favicon` (byte[])**: Allows you to specify a custom favicon as a byte array.
- **`-NoFavicon` (switch)**: Disables the favicon, preventing browsers from requesting it.

### **Favicon Format and Specifications**

Favicons are typically stored in the `.ico` format, which is a container that can hold multiple image sizes and color depths. This ensures compatibility with different browsers and devices. Some modern browsers also support `.png` and `.svg` favicons.

For more details on favicon formats and specifications, refer to the [Favicon specification](https://en.wikipedia.org/wiki/Favicon) and [RFC 5988](https://datatracker.ietf.org/doc/html/rfc5988).

### **Favicon Size Recommendations**

Favicons should include multiple resolutions for optimal display across different devices. Recommended sizes include:

- **16x16** → Used in browser tabs, bookmarks, and address bars.
- **32x32** → Used in browser tabs on higher-resolution displays.
- **48x48** → Used by some older browsers and web applications.
- **64x64+** → Generally not used by browsers but can be helpful for scalability in web apps.
- **256x256** → Mainly for **Windows app icons** (not typically used as a favicon in browsers).

### **Usage Example**

```powershell
# Load a custom favicon from file
$iconBytes = [System.IO.File]::ReadAllBytes("C:\path\to\custom.ico")
Add-PodeEndpoint -Address localhost -Port 8080 -Protocol Http -Favicon $iconBytes

# Disable favicon for an endpoint
Add-PodeEndpoint -Address localhost -Port 8080 -Protocol Http -NoFavicon
```

Using a favicon enhances the user experience by providing a recognizable site icon in browser tabs and bookmarks.
Binary file added src/Misc/favicon.ico
Binary file not shown.
87 changes: 47 additions & 40 deletions src/Private/PodeServer.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -210,59 +210,66 @@ function Start-PodeWebServer {
if ($Request.IsAborted) {
throw $Request.Error
}

# if we have an sse clientId, verify it and then set details in WebEvent
if ($WebEvent.Request.HasSseClientId) {
if (!(Test-PodeSseClientIdValid)) {
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 [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 = @{
Name = $WebEvent.Request.SseClientName
Group = $WebEvent.Request.SseClientGroup
ClientId = $WebEvent.Request.SseClientId
LastEventId = $null
IsLocal = $false
}

# deal with favicon if available
if ($WebEvent.Path -eq '/favicon.ico' -and ($null -ne $PodeContext.Server.Endpoints[$context.EndpointName].Favicon)) {
# Write the file content as the HTTP response
Write-PodeTextResponse -Bytes $PodeContext.Server.Endpoints[$context.EndpointName].Favicon -ContentType 'image/png' -StatusCode 200
}
else {
# if we have an sse clientId, verify it and then set details in WebEvent
if ($WebEvent.Request.HasSseClientId) {
if (!(Test-PodeSseClientIdValid)) {
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 [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)
}

# invoke global and route middleware
if ((Invoke-PodeMiddleware -Middleware $PodeContext.Server.Middleware -Route $WebEvent.Path)) {
# has the request been aborted
if ($Request.IsAborted) {
throw $Request.Error
$WebEvent.Sse = @{
Name = $WebEvent.Request.SseClientName
Group = $WebEvent.Request.SseClientGroup
ClientId = $WebEvent.Request.SseClientId
LastEventId = $null
IsLocal = $false
}
}

if ((Invoke-PodeMiddleware -Middleware $WebEvent.Route.Middleware)) {
# invoke global and route middleware
if ((Invoke-PodeMiddleware -Middleware $PodeContext.Server.Middleware -Route $WebEvent.Path)) {
# has the request been aborted
if ($Request.IsAborted) {
throw $Request.Error
}

# invoke the route
if ($null -ne $WebEvent.StaticContent) {
$fileBrowser = $WebEvent.Route.FileBrowser
if ($WebEvent.StaticContent.IsDownload) {
Write-PodeAttachmentResponseInternal -Path $WebEvent.StaticContent.Source -FileBrowser:$fileBrowser
if ((Invoke-PodeMiddleware -Middleware $WebEvent.Route.Middleware)) {
# has the request been aborted
if ($Request.IsAborted) {
throw $Request.Error
}
elseif ($WebEvent.StaticContent.RedirectToDefault) {
$file = [System.IO.Path]::GetFileName($WebEvent.StaticContent.Source)
Move-PodeResponseUrl -Url "$($WebEvent.Path)/$($file)"

# invoke the route
if ($null -ne $WebEvent.StaticContent) {
$fileBrowser = $WebEvent.Route.FileBrowser
if ($WebEvent.StaticContent.IsDownload) {
Write-PodeAttachmentResponseInternal -Path $WebEvent.StaticContent.Source -FileBrowser:$fileBrowser
}
elseif ($WebEvent.StaticContent.RedirectToDefault) {
$file = [System.IO.Path]::GetFileName($WebEvent.StaticContent.Source)
Move-PodeResponseUrl -Url "$($WebEvent.Path)/$($file)"
}
else {
$cachable = $WebEvent.StaticContent.IsCachable
Write-PodeFileResponseInternal -Path $WebEvent.StaticContent.Source -MaxAge $PodeContext.Server.Web.Static.Cache.MaxAge `
-Cache:$cachable -FileBrowser:$fileBrowser
}
}
else {
$cachable = $WebEvent.StaticContent.IsCachable
Write-PodeFileResponseInternal -Path $WebEvent.StaticContent.Source -MaxAge $PodeContext.Server.Web.Static.Cache.MaxAge `
-Cache:$cachable -FileBrowser:$fileBrowser
elseif ($null -ne $WebEvent.Route.Logic) {
$null = Invoke-PodeScriptBlock -ScriptBlock $WebEvent.Route.Logic -Arguments $WebEvent.Route.Arguments `
-UsingVariables $WebEvent.Route.UsingVariables -Scoped -Splat
}
}
elseif ($null -ne $WebEvent.Route.Logic) {
$null = Invoke-PodeScriptBlock -ScriptBlock $WebEvent.Route.Logic -Arguments $WebEvent.Route.Arguments `
-UsingVariables $WebEvent.Route.UsingVariables -Scoped -Splat
}
}
}
}
Expand Down
59 changes: 48 additions & 11 deletions src/Public/Endpoint.ps1
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

<#
.SYNOPSIS
Bind an endpoint to listen for incoming Requests.
Expand All @@ -19,13 +18,13 @@ An optional hostname for the endpoint, specifying a hostname restricts access to
The protocol of the supplied endpoint.

.PARAMETER Certificate
The path to a certificate that can be use to enable HTTPS
The path to a certificate that can be used to enable HTTPS.

.PARAMETER CertificatePassword
The password for the certificate file referenced in Certificate
The password for the certificate file referenced in Certificate.

.PARAMETER CertificateKey
A key file to be paired with a PEM certificate file referenced in Certificate
A key file to be paired with a PEM certificate file referenced in Certificate.

.PARAMETER CertificateThumbprint
A certificate thumbprint to bind onto HTTPS endpoints (Windows).
Expand All @@ -34,13 +33,13 @@ A certificate thumbprint to bind onto HTTPS endpoints (Windows).
A certificate subject name to bind onto HTTPS endpoints (Windows).

.PARAMETER CertificateStoreName
The name of a certifcate store where a certificate can be found (Default: My) (Windows).
The name of a certificate store where a certificate can be found (Default: My) (Windows).

.PARAMETER CertificateStoreLocation
The location of a certifcate store where a certificate can be found (Default: CurrentUser) (Windows).
The location of a certificate store where a certificate can be found (Default: CurrentUser) (Windows).

.PARAMETER X509Certificate
The raw X509 certificate that can be use to enable HTTPS
The raw X509 certificate that can be used to enable HTTPS.

.PARAMETER TlsMode
The TLS mode to use on secure connections, options are Implicit or Explicit (SMTP only) (Default: Implicit).
Expand All @@ -58,16 +57,16 @@ A quick description of the Endpoint - normally used in OpenAPI.
An optional Acknowledge message to send to clients when they first connect, for TCP and SMTP endpoints only.

.PARAMETER SslProtocol
One or more optional SSL Protocols this endpoints supports. (Default: SSL3/TLS12 - Just TLS12 on MacOS).
One or more optional SSL Protocols this endpoint supports. (Default: SSL3/TLS12 - Just TLS12 on MacOS).

.PARAMETER CRLFMessageEnd
If supplied, TCP endpoints will expect incoming data to end with CRLF.

.PARAMETER Force
Ignore Adminstrator checks for non-localhost endpoints.
Ignore Administrator checks for non-localhost endpoints.

.PARAMETER SelfSigned
Create and bind a self-signed certifcate for HTTPS endpoints.
Create and bind a self-signed certificate for HTTPS endpoints.

.PARAMETER AllowClientCertificate
Allow for client certificates to be sent on requests.
Expand All @@ -85,6 +84,12 @@ For IPv6, this will only work if the IPv6 address can convert to a valid IPv4 ad
.PARAMETER Default
If supplied, this endpoint will be the default one used for internally generating URLs.

.PARAMETER Favicon
A byte array representing a custom favicon for HTTP/HTTPS endpoints.

.PARAMETER DefaultFavicon
If supplied, enable the default Pode favicon for HTTP/HTTPS endpoints.

.EXAMPLE
Add-PodeEndpoint -Address localhost -Port 8090 -Protocol Http

Expand Down Expand Up @@ -208,7 +213,13 @@ function Add-PodeEndpoint {
$DualMode,

[switch]
$Default
$Default,

[byte[]]
$Favicon,

[switch]
$DefaultFavicon
)

# error if serverless
Expand Down Expand Up @@ -292,6 +303,30 @@ function Add-PodeEndpoint {
throw ($PodeLocale.crlfMessageEndCheckOnlySupportedOnTcpEndpointsExceptionMessage)
}

# Check if both -Favicon and -DefaultFavicon are provided, which is not allowed.
# If both are set, throw an exception with a relevant message.
if (($null -ne $Favicon) -and $DefaultFavicon) {
throw ($Podelocale.parametersMutuallyExclusiveExceptionMessage -f '-Favicon', '-DefaultFavicon')
}

# If no -Favicon is provided, the protocol is either HTTP or HTTPS, and -DefaultFavicon is enabled,
# set the default favicon from the Pode module's miscellaneous path.
if ( ($null -eq $Favicon) -and (@('Http', 'Https') -icontains $Protocol) -and $DefaultFavicon) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We wouldn't need to check ($null -eq $Favicon) at this point, as we know it will be null/empty if $DefaultFavicon is true.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why? I can call add-podeEndpoint with both -Favicon adn -DefaultFavicon.
I can create an new parameterset but start to become complicated with all these parameters

# Retrieve the root path of the Pode module.
$podeRoot = Get-PodeModuleMiscPath

# Check if running in PowerShell Core to determine the correct method for reading binary data.
if (Test-PodeIsPSCore) {
# In PowerShell Core, use -AsByteStream to read the file as a byte array.
$Favicon = (Get-Content -Path ([System.IO.Path]::Combine($podeRoot, 'favicon.ico')) -Raw -AsByteStream)
}
else {
# In Windows PowerShell, use -Encoding byte to read the file as a byte array.
$Favicon = (Get-Content -Path ([System.IO.Path]::Combine($podeRoot, 'favicon.ico')) -Raw -Encoding byte)
}
}


# new endpoint object
$obj = @{
Name = $Name
Expand Down Expand Up @@ -324,8 +359,10 @@ function Add-PodeEndpoint {
Acknowledge = $Acknowledge
CRLFMessageEnd = $CRLFMessageEnd
}
Favicon = $Favicon
}


# set ssl protocols
if (!(Test-PodeIsEmpty $SslProtocol)) {
$obj.Ssl.Protocols = (ConvertTo-PodeSslProtocol -Protocol $SslProtocol)
Expand Down