Skip to content

Commit

Permalink
Merge pull request #1267 from Badgerati/Issue-1251
Browse files Browse the repository at this point in the history
Add DualMode support for Endpoints to listen on IPv4 and IPv6 at the same time
  • Loading branch information
Badgerati authored Mar 27, 2024
2 parents f68042c + b7377c5 commit dd9d544
Show file tree
Hide file tree
Showing 30 changed files with 422 additions and 189 deletions.
30 changes: 30 additions & 0 deletions Pode.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.002.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{41F81369-8680-4BC5-BA16-C7891D245717}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Pode", "src\Listener\Pode.csproj", "{772D5C9F-1B25-46A7-8977-412A5F7F77D1}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{772D5C9F-1B25-46A7-8977-412A5F7F77D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{772D5C9F-1B25-46A7-8977-412A5F7F77D1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{772D5C9F-1B25-46A7-8977-412A5F7F77D1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{772D5C9F-1B25-46A7-8977-412A5F7F77D1}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{772D5C9F-1B25-46A7-8977-412A5F7F77D1} = {41F81369-8680-4BC5-BA16-C7891D245717}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {F24001DC-2986-4305-B1B5-8E73BCDF1A77}
EndGlobalSection
EndGlobal
98 changes: 66 additions & 32 deletions docs/Tutorials/Endpoints/Basics.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,55 @@
# Basics

Endpoints in Pode are used to bind your server to specific IPs, Hostnames and ports, over specific protocols (such as HTTP or HTTPS). Endpoints can have unique names, so you can bind Routes to certain endpoints only.
Endpoints in Pode are used to bind your server to specific IPs, Hostnames, and ports over specific protocols (such as HTTP or HTTPS). Endpoints can have unique names, so you can bind Routes to certain endpoints only.

!!! tip
Endpoints support both IPv4 and IPv6 addresses.

## Usage

To add new endpoints to your server, you can use [`Add-PodeEndpoint`](../../../Functions/Core/Add-PodeEndpoint). A quick and simple example is the following, which will bind your server to `http://localhost:8080`:
To add new endpoints to your server you use [`Add-PodeEndpoint`](../../../Functions/Core/Add-PodeEndpoint). A quick and simple example is the following, which will bind your server to `http://localhost:8080`:

```powershell
Start-PodeServer {
Add-PodeEndpoint -Address localhost -Port 8080 -Protocol Http
}
```

The `-Address` can be local or private IP address. The `-Port` is any valid port number, and the `-Protocol` defines which protocol the endpoint will use: HTTP, HTTPS, SMTP, TCP, WS and WSS.
The `-Address` can be a local or a private IP address. The `-Port` is any valid port number, and the `-Protocol` defines which protocol the endpoint will use: HTTP, HTTPS, SMTP, TCP, WS, or WSS.

You can also supply an optional unique `-Name` to your endpoint. This name will allow you to bind routes to certain endpoints; so if you have endpoint A and B, and you bind some route to endpoint A, then it won't be accessible over endpoint B.

```powershell
Add-PodeEndpoint -Address localhost -Port 8080 -Protocol Http -Name EndpointA
Add-PodeEndpoint -Address localhost -Port 8080 -Protocol Http -Name EndpointB
Add-PodeRoute -Method Get -Path '/page-a' -EndpointName EndpointA -ScriptBlock {
# logic
}
Add-PodeRoute -Method Get -Path '/page-b' -EndpointName EndpointB -ScriptBlock {
# logic
}
```

## Dual Mode

When you create an Endpoint in Pode, it will listen on either just the IPv4 or IPv6 address that you supplied - for localhost, this will be `127.0.0.1`, and for "*", this will be `0.0.0.0`, unless the IPv6 equivalents of `::1` or `::` were directly supplied.

This means if you have the following endpoint to listen on "everything", then it will only really be everything for just IPv4:

```powershell
Add-PodeEndpoint -Address * -Port 8080 -Protocol Http
```

However, you can pass the `-DualMode` switch on [`Add-PodeEndpoint`](../../../Functions/Core/Add-PodeEndpoint) and this will tell Pode to listen on both IPv4 and IPv6 instead:

```powershell
Add-PodeEndpoint -Address * -Port 8080 -Protocol Http -DualMode
```

This will work for any IPv4 address, including localhost, if the underlying OS supports IPv6. For IPv6 addresses, only those that can be converted back to an equivalent IPv4 address will work - it will still listen on the supplied IPv6 address, there just won't be an IPv4 that Pode could also listen on.

## Hostnames

You can specify a `-Hostname` for an endpoint, in doing so you can only access routes via the specified hostname. Using a hostname will allow you to have multiple endpoints all using the same IP/Port, but with different hostnames.
Expand All @@ -32,7 +66,7 @@ To bind a hostname to a specific IP you can use `-Address`:
Add-PodeEndpoint -Address 127.0.0.2 -Hostname example.pode.com -Port 8080 -Protocol Http
```

or, lookup the hostnames IP from host file or DNS:
or, lookup the hostname's IP from the host file or DNS:

```powershell
Add-PodeEndpoint -Hostname example.pode.com -Port 8080 -Protocol Http -LookupHostname
Expand All @@ -49,24 +83,24 @@ Add-PodeEndpoint -Address 127.0.0.3 -Hostname two.pode.com -Port 8080 -Protocol

If you add an HTTPS or WSS endpoint, then you'll be required to also supply certificate details. To configure a certificate you can use one of the following parameters:

| Name | Description |
| ---- | ----------- |
| Certificate | The path to a `.pfx` or `.cer` certificate |
| CertificatePassword | The password for the above `.pfx` certificate |
| CertificateThumbprint | The thumbprint of a certificate to find (Windows only) |
| CertificateName | The subject name of a certificate to find (Windows only) |
| CertificateStoreName | The name of the certificate store (Default: My) (Windows only) |
| CertificateStoreLocation | The location of the certificate store (Default: CurrentUser) (Windows only) |
| X509Certificate | A raw X509Certificate object |
| SelfSigned | If supplied, Pode will automatically generate a self-signed certificate as an X509Certificate object |
| Name | Description |
| ------------------------ | ---------------------------------------------------------------------------------------------------- |
| Certificate | The path to a `.pfx` or `.cer` certificate |
| CertificatePassword | The password for the above `.pfx` certificate |
| CertificateThumbprint | The thumbprint of a certificate to find (Windows only) |
| CertificateName | The subject name of a certificate to find (Windows only) |
| CertificateStoreName | The name of the certificate store (Default: My) (Windows only) |
| CertificateStoreLocation | The location of the certificate store (Default: CurrentUser) (Windows only) |
| X509Certificate | A raw X509Certificate object |
| SelfSigned | If supplied, Pode will automatically generate a self-signed certificate as an X509Certificate object |

The below example will create an endpoint using a `.pfx` certificate:

```powershell
Add-PodeEndpoint -Address * -Port 8443 -Protocol Https -Certificate './certs/example.pfx' -CertificatePassword 'hunter2'
```

Whereas the following will instead create an X509Certificate, and pass that to the endpoint instead:
However, the following will instead create an X509Certificate, and pass that to the endpoint instead:

```powershell
$cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new('./certs/example.cer')
Expand All @@ -81,10 +115,10 @@ Add-PodeEndpoint -Address * -Port 8443 -Protocol Https -SelfSigned

### SSL Protocols

By default Pode will use the SSL3 or TLS12 protocols - or just TLS12 if on MacOS. You can override this default in one of two ways:
By default, Pode will use the SSL3 or TLS12 protocols - or just TLS12 if on MacOS. You can override this default in one of two ways:

1. Update the global default in Pode's configuration file, as [described here](../../Certificates#ssl-protocols).
2. Specify specific SSL Protocols to use per Endpoints using the `-SslProtocol` parameter on [`Add-PodeEndpoint`](../../../Functions/Core/Add-PodeEndpoint).
2. Specify specific SSL Protocols to use per Endpoint using the `-SslProtocol` parameter on [`Add-PodeEndpoint`](../../../Functions/Core/Add-PodeEndpoint).

## Endpoint Names

Expand Down Expand Up @@ -124,19 +158,19 @@ Get-PodeEndpoint -Name Admin, User

The following is the structure of the Endpoint object internally, as well as the object that is returned from [`Get-PodeEndpoint`](../../../Functions/Core/Get-PodeEndpoint):

| Name | Type | Description |
| ---- | ---- | ----------- |
| Name | string | The name of the Endpoint, if a name was supplied |
| Description | string | A description of the Endpoint, usually used for OpenAPI |
| Address | IPAddress | The IP address that will be used for the Endpoint |
| RawAddress | string | The address/host and port of the Endpoint |
| Port | int | The port the Endpoint will use |
| IsIPAddress | bool | Whether or not the listener will bind using Hostname or IP address |
| Hostname | string | The hostname of the Endpoint |
| FriendlyName | string | A user friendly hostname to use when generating internal URLs |
| Url | string | The full base URL of the Endpoint |
| Ssl.Enabled | bool | Whether or not this Endpoint uses SSL |
| Name | Type | Description |
| ------------- | ------------ | ------------------------------------------------------------------------------- |
| Name | string | The name of the Endpoint, if a name was supplied |
| Description | string | A description of the Endpoint, usually used for OpenAPI |
| Address | IPAddress | The IP address that will be used for the Endpoint |
| RawAddress | string | The address/host and port of the Endpoint |
| Port | int | The port the Endpoint will use |
| IsIPAddress | bool | Whether or not the listener will bind using Hostname or IP address |
| Hostname | string | The hostname of the Endpoint |
| FriendlyName | string | A user friendly hostname to use when generating internal URLs |
| Url | string | The full base URL of the Endpoint |
| Ssl.Enabled | bool | Whether or not this Endpoint uses SSL |
| Ssl.Protocols | SslProtocols | An aggregated integer which specifies the SSL protocols this endpoints supports |
| Protocol | string | The protocol of the Endpoint. Such as: HTTP, HTTPS, WS, etc. |
| Type | string | The type of the Endpoint. Such as: HTTP, WS, SMTP, TCP |
| Certificate | hashtable | Details about the certificate that will be used for SSL Endpoints |
| Protocol | string | The protocol of the Endpoint. Such as: HTTP, HTTPS, WS, etc. |
| Type | string | The type of the Endpoint. Such as: HTTP, WS, SMTP, TCP |
| Certificate | hashtable | Details about the certificate that will be used for SSL Endpoints |
2 changes: 1 addition & 1 deletion examples/rest-api.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop
# create a server, and start listening on port 8086
Start-PodeServer {

Add-PodeEndpoint -Address * -Port 8086 -Protocol Http
Add-PodeEndpoint -Address 'localhost' -Port 8086 -Protocol Http -DualMode

# request logging
New-PodeLoggingMethod -Terminal -Batch 10 -BatchTimeout 10 | Enable-PodeRequestLogging
Expand Down
11 changes: 8 additions & 3 deletions src/Listener/PodeContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,15 @@ public class PodeContext : PodeProtocol, IDisposable
public PodeResponse Response { get; private set; }
public PodeListener Listener { get; private set; }
public Socket Socket { get; private set; }
public PodeSocket PodeSocket { get; private set;}
public PodeSocket PodeSocket { get; private set; }
public DateTime Timestamp { get; private set; }
public Hashtable Data { get; private set; }

public string EndpointName
{
get => PodeSocket.Name;
}

private object _lockable = new object();

private PodeContextState _state;
Expand Down Expand Up @@ -281,7 +286,7 @@ public async void Receive()
SetContextType();
EndReceive(close);
}
catch (OperationCanceledException) {}
catch (OperationCanceledException) { }
}
catch (Exception ex)
{
Expand Down Expand Up @@ -436,7 +441,7 @@ public void Dispose(bool force)
Response.Dispose();
}
}
catch {}
catch { }

// if keep-alive, or awaiting body, setup for re-receive
if ((_awaitingBody || (IsKeepAlive && !IsErrored && !IsTimeout && !Response.SseEnabled)) && !force)
Expand Down
78 changes: 78 additions & 0 deletions src/Listener/PodeEndpoint.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using System;
using System.Net;
using System.Net.Sockets;

namespace Pode
{
public class PodeEndpoint : IDisposable
{
public IPAddress IPAddress { get; private set; }
public int Port { get; private set; }
public IPEndPoint Endpoint { get; private set; }
public Socket Socket { get; private set; }
public PodeSocket PodeSocket { get; private set; }
public bool DualMode { get; private set; }
public bool IsDisposed { get; private set; }

public int ReceiveTimeout
{
get => Socket.ReceiveTimeout;
set => Socket.ReceiveTimeout = value;
}

public PodeEndpoint(PodeSocket socket, IPAddress ipAddress, int port, bool dualMode)
{
IsDisposed = false;
PodeSocket = socket;
IPAddress = ipAddress;
Port = port;
Endpoint = new IPEndPoint(ipAddress, port);

Socket = new Socket(Endpoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp)
{
ReceiveTimeout = 100,
NoDelay = true
};

if (dualMode && Endpoint.AddressFamily == AddressFamily.InterNetworkV6)
{
DualMode = true;
Socket.DualMode = true;
Socket.SetSocketOption(SocketOptionLevel.IPv6, SocketOptionName.IPv6Only, false);
}
else
{
DualMode = false;
}
}

public void Listen()
{
Socket.Bind(Endpoint);
Socket.Listen(int.MaxValue);
}

public bool AcceptAsync(SocketAsyncEventArgs args)
{
if (IsDisposed)
{
throw new ObjectDisposedException("PodeEndpoint disposed");
}

return Socket.AcceptAsync(args);
}

public void Dispose()
{
IsDisposed = true;
PodeSocket.CloseSocket(Socket);
Socket = default(Socket);
}

public new bool Equals(object obj)
{
var _endpoint = (PodeEndpoint)obj;
return Endpoint.ToString() == _endpoint.Endpoint.ToString() && Port == _endpoint.Port;
}
}
}
2 changes: 1 addition & 1 deletion src/Listener/PodeFileWatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public class PodeFileWatcher
private RecoveringFileSystemWatcher FileWatcher;

public string Name { get; private set; }
public ISet<PodeFileWatcherChangeType> EventsRegistered {get; private set; }
public ISet<PodeFileWatcherChangeType> EventsRegistered { get; private set; }

public PodeFileWatcher(string name, string path, bool includeSubdirectories, int internalBufferSize, NotifyFilters notifyFilters)
{
Expand Down
6 changes: 4 additions & 2 deletions src/Listener/PodeListener.cs
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,8 @@ public void AddSseConnection(PodeServerEvent sse)

public void SendSseEvent(string name, string[] groups, string[] clientIds, string eventType, string data, string id = null)
{
Task.Factory.StartNew(() => {
Task.Factory.StartNew(() =>
{
if (!ServerEvents.ContainsKey(name))
{
return;
Expand Down Expand Up @@ -165,7 +166,8 @@ public void SendSseEvent(string name, string[] groups, string[] clientIds, strin

public void CloseSseConnection(string name, string[] groups, string[] clientIds)
{
Task.Factory.StartNew(() => {
Task.Factory.StartNew(() =>
{
if (!ServerEvents.ContainsKey(name))
{
return;
Expand Down
2 changes: 1 addition & 1 deletion src/Listener/PodeSmtpRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,7 @@ private void ParseBoundary()
var contentType = $"{headers["Content-Type"]}";
var contentEncoding = $"{headers["Content-Transfer-Encoding"]}";

// get the boundary
// get the boundary
var body = ParseBody(boundaryBody, Boundary);
var bodyBytes = ConvertBodyEncoding(body, contentEncoding);

Expand Down
Loading

0 comments on commit dd9d544

Please sign in to comment.