Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
32 changes: 32 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: CI

on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]

jobs:
build-test-pack:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 10.0.x

- name: Restore
run: dotnet restore OpenPort.Net.sln

- name: Build
run: dotnet build OpenPort.Net.sln --configuration Release --no-restore

- name: Test
run: dotnet test OpenPort.Net.sln --configuration Release --no-build --verbosity normal

- name: Pack
run: dotnet pack OpenPort.Net/OpenPort.Net.csproj --configuration Release --no-build --output artifacts
51 changes: 51 additions & 0 deletions OpenPort.Net.sln
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,66 @@
Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenPort.Net", "OpenPort.Net\OpenPort.Net.csproj", "{1FBFE6D8-7A0D-4967-B073-29EA63B5186F}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenPort.Net.Tests", "tests\OpenPort.Net.Tests\OpenPort.Net.Tests.csproj", "{B95309B1-ED32-4197-ABAF-39A5DF0FF45F}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{5D20AA90-6969-D8BD-9DCD-8634F4692FDA}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenPort.Net.Sample", "samples\OpenPort.Net.Sample\OpenPort.Net.Sample.csproj", "{098F1BA7-20E9-47BF-BBBA-AD8F7C92D363}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{1FBFE6D8-7A0D-4967-B073-29EA63B5186F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1FBFE6D8-7A0D-4967-B073-29EA63B5186F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1FBFE6D8-7A0D-4967-B073-29EA63B5186F}.Debug|x64.ActiveCfg = Debug|Any CPU
{1FBFE6D8-7A0D-4967-B073-29EA63B5186F}.Debug|x64.Build.0 = Debug|Any CPU
{1FBFE6D8-7A0D-4967-B073-29EA63B5186F}.Debug|x86.ActiveCfg = Debug|Any CPU
{1FBFE6D8-7A0D-4967-B073-29EA63B5186F}.Debug|x86.Build.0 = Debug|Any CPU
{1FBFE6D8-7A0D-4967-B073-29EA63B5186F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1FBFE6D8-7A0D-4967-B073-29EA63B5186F}.Release|Any CPU.Build.0 = Release|Any CPU
{1FBFE6D8-7A0D-4967-B073-29EA63B5186F}.Release|x64.ActiveCfg = Release|Any CPU
{1FBFE6D8-7A0D-4967-B073-29EA63B5186F}.Release|x64.Build.0 = Release|Any CPU
{1FBFE6D8-7A0D-4967-B073-29EA63B5186F}.Release|x86.ActiveCfg = Release|Any CPU
{1FBFE6D8-7A0D-4967-B073-29EA63B5186F}.Release|x86.Build.0 = Release|Any CPU
{B95309B1-ED32-4197-ABAF-39A5DF0FF45F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B95309B1-ED32-4197-ABAF-39A5DF0FF45F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B95309B1-ED32-4197-ABAF-39A5DF0FF45F}.Debug|x64.ActiveCfg = Debug|Any CPU
{B95309B1-ED32-4197-ABAF-39A5DF0FF45F}.Debug|x64.Build.0 = Debug|Any CPU
{B95309B1-ED32-4197-ABAF-39A5DF0FF45F}.Debug|x86.ActiveCfg = Debug|Any CPU
{B95309B1-ED32-4197-ABAF-39A5DF0FF45F}.Debug|x86.Build.0 = Debug|Any CPU
{B95309B1-ED32-4197-ABAF-39A5DF0FF45F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B95309B1-ED32-4197-ABAF-39A5DF0FF45F}.Release|Any CPU.Build.0 = Release|Any CPU
{B95309B1-ED32-4197-ABAF-39A5DF0FF45F}.Release|x64.ActiveCfg = Release|Any CPU
{B95309B1-ED32-4197-ABAF-39A5DF0FF45F}.Release|x64.Build.0 = Release|Any CPU
{B95309B1-ED32-4197-ABAF-39A5DF0FF45F}.Release|x86.ActiveCfg = Release|Any CPU
{B95309B1-ED32-4197-ABAF-39A5DF0FF45F}.Release|x86.Build.0 = Release|Any CPU
{098F1BA7-20E9-47BF-BBBA-AD8F7C92D363}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{098F1BA7-20E9-47BF-BBBA-AD8F7C92D363}.Debug|Any CPU.Build.0 = Debug|Any CPU
{098F1BA7-20E9-47BF-BBBA-AD8F7C92D363}.Debug|x64.ActiveCfg = Debug|Any CPU
{098F1BA7-20E9-47BF-BBBA-AD8F7C92D363}.Debug|x64.Build.0 = Debug|Any CPU
{098F1BA7-20E9-47BF-BBBA-AD8F7C92D363}.Debug|x86.ActiveCfg = Debug|Any CPU
{098F1BA7-20E9-47BF-BBBA-AD8F7C92D363}.Debug|x86.Build.0 = Debug|Any CPU
{098F1BA7-20E9-47BF-BBBA-AD8F7C92D363}.Release|Any CPU.ActiveCfg = Release|Any CPU
{098F1BA7-20E9-47BF-BBBA-AD8F7C92D363}.Release|Any CPU.Build.0 = Release|Any CPU
{098F1BA7-20E9-47BF-BBBA-AD8F7C92D363}.Release|x64.ActiveCfg = Release|Any CPU
{098F1BA7-20E9-47BF-BBBA-AD8F7C92D363}.Release|x64.Build.0 = Release|Any CPU
{098F1BA7-20E9-47BF-BBBA-AD8F7C92D363}.Release|x86.ActiveCfg = Release|Any CPU
{098F1BA7-20E9-47BF-BBBA-AD8F7C92D363}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{B95309B1-ED32-4197-ABAF-39A5DF0FF45F} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
{098F1BA7-20E9-47BF-BBBA-AD8F7C92D363} = {5D20AA90-6969-D8BD-9DCD-8634F4692FDA}
EndGlobalSection
EndGlobal
9 changes: 9 additions & 0 deletions OpenPort.Net/Discovery/GatewayDiscovery.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System.Net;
using OpenPort.Net.Internal;

namespace OpenPort.Net.Discovery;

internal sealed class GatewayDiscovery
{
public IPAddress? DiscoverGatewayAddress() => NetworkUtils.GetDefaultGatewayAddress();
}
105 changes: 105 additions & 0 deletions OpenPort.Net/Discovery/SsdpDiscovery.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
using System.Net;
using System.Net.Sockets;
using System.Text;

namespace OpenPort.Net.Discovery;

internal sealed class SsdpDiscovery
{
private static readonly IPEndPoint MulticastEndPoint = new(IPAddress.Parse("239.255.255.250"), 1900);
private readonly TimeSpan _timeout;

public SsdpDiscovery(TimeSpan timeout)
{
_timeout = timeout;
}

public async Task<IReadOnlyList<Uri>> DiscoverInternetGatewayDevicesAsync(CancellationToken cancellationToken)
{
var searchTargets = new[]
{
"urn:schemas-upnp-org:device:InternetGatewayDevice:1",
"urn:schemas-upnp-org:service:WANIPConnection:1",
"urn:schemas-upnp-org:service:WANIPConnection:2",
"urn:schemas-upnp-org:service:WANPPPConnection:1"
};

var locations = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

using var udpClient = new UdpClient(AddressFamily.InterNetwork);
udpClient.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);

foreach (var searchTarget in searchTargets)
{
var request = BuildSearchRequest(searchTarget);
var bytes = Encoding.ASCII.GetBytes(request);
await udpClient.SendAsync(bytes, bytes.Length, MulticastEndPoint).ConfigureAwait(false);
}

using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
timeout.CancelAfter(_timeout);

while (!timeout.IsCancellationRequested)
{
try
{
var response = await ReceiveAsync(udpClient, timeout.Token).ConfigureAwait(false);
if (response is null)
{
break;
}

var text = Encoding.ASCII.GetString(response.Value.Buffer);
var location = ParseHeader(text, "LOCATION");
if (Uri.TryCreate(location, UriKind.Absolute, out var uri))
{
locations.Add(uri.AbsoluteUri);
}
}
catch (OperationCanceledException) when (timeout.IsCancellationRequested)
{
break;
}
}

return locations.Select(location => new Uri(location)).ToList();
}

private static string BuildSearchRequest(string searchTarget) =>
"M-SEARCH * HTTP/1.1\r\n" +
"HOST: 239.255.255.250:1900\r\n" +
"MAN: \"ssdp:discover\"\r\n" +
"MX: 2\r\n" +
$"ST: {searchTarget}\r\n\r\n";

private static string? ParseHeader(string response, string name)
{
foreach (var line in response.Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries))
{
var separator = line.IndexOf(':');
if (separator <= 0)
{
continue;
}

if (string.Equals(line[..separator].Trim(), name, StringComparison.OrdinalIgnoreCase))
{
return line[(separator + 1)..].Trim();
}
}

return null;
}

private static async Task<UdpReceiveResult?> ReceiveAsync(UdpClient udpClient, CancellationToken cancellationToken)
{
#if NET8_0_OR_GREATER
return await udpClient.ReceiveAsync(cancellationToken).ConfigureAwait(false);
#else
var receiveTask = udpClient.ReceiveAsync();
var completed = await Task.WhenAny(receiveTask, Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken)).ConfigureAwait(false);
cancellationToken.ThrowIfCancellationRequested();
return completed == receiveTask ? receiveTask.Result : null;
#endif
}
}
7 changes: 7 additions & 0 deletions OpenPort.Net/Internal/CompilerCompatibility.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#if NETSTANDARD2_1
namespace System.Runtime.CompilerServices;

internal static class IsExternalInit
{
}
#endif
130 changes: 130 additions & 0 deletions OpenPort.Net/Internal/NatPmpMessage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
using System.Net;
using OpenPort.Net.Models;

namespace OpenPort.Net.Internal;

internal static class NatPmpMessage
{
public const byte Version = 0;
public const byte ExternalAddressOpcode = 0;
public const byte UdpMapOpcode = 1;
public const byte TcpMapOpcode = 2;
public const byte ResponseOffset = 128;

public static byte[] CreateExternalAddressRequest() => [Version, ExternalAddressOpcode];

public static byte[] CreateMapRequest(PortProtocol protocol, int internalPort, int externalPort, uint lifetimeSeconds)
{
var request = new byte[12];
request[0] = Version;
request[1] = ToMapOpcode(protocol);
NetworkUtils.WriteUInt16BigEndian(request, 4, internalPort);
NetworkUtils.WriteUInt16BigEndian(request, 6, externalPort);
NetworkUtils.WriteUInt32BigEndian(request, 8, lifetimeSeconds);
return request;
}

public static bool TryParseExternalAddressResponse(
byte[] response,
out ushort resultCode,
out IPAddress? externalAddress)
{
resultCode = 0;
externalAddress = null;

if (response.Length < 8 || response[0] != Version || response[1] != ExternalAddressOpcode + ResponseOffset)
{
return false;
}

resultCode = NetworkUtils.ReadUInt16BigEndian(response, 2);
if (resultCode != 0)
{
return true;
}

if (response.Length < 12)
{
return false;
}

externalAddress = new IPAddress(response.Skip(8).Take(4).ToArray());
return true;
}

public static bool TryParseMapResponse(
byte[] response,
PortProtocol protocol,
out NatPmpMapResponse mapResponse)
{
mapResponse = default;
var opcode = ToMapOpcode(protocol);

if (response.Length < 8 || response[0] != Version || response[1] != opcode + ResponseOffset)
{
return false;
}

var resultCode = NetworkUtils.ReadUInt16BigEndian(response, 2);
if (resultCode != 0)
{
mapResponse = new NatPmpMapResponse(resultCode, 0, 0, 0);
return true;
}

if (response.Length < 16)
{
return false;
}

mapResponse = new NatPmpMapResponse(
resultCode,
NetworkUtils.ReadUInt16BigEndian(response, 8),
NetworkUtils.ReadUInt16BigEndian(response, 10),
NetworkUtils.ReadUInt32BigEndian(response, 12));
return true;
}

public static OpenPortStatus MapResultCode(ushort code) =>
code switch
{
0 => OpenPortStatus.Success,
1 => OpenPortStatus.NotSupported,
2 => OpenPortStatus.Unauthorized,
3 => OpenPortStatus.Failed,
4 => OpenPortStatus.NoResources,
5 => OpenPortStatus.NotSupported,
_ => OpenPortStatus.Failed
};

public static string GetResultName(ushort code) =>
code switch
{
0 => "Success",
1 => "UnsupportedVersion",
2 => "NotAuthorized",
3 => "NetworkFailure",
4 => "OutOfResources",
5 => "UnsupportedOpcode",
_ => "Unknown"
};

private static byte ToMapOpcode(PortProtocol protocol) =>
protocol == PortProtocol.Udp ? UdpMapOpcode : TcpMapOpcode;
}

internal readonly struct NatPmpMapResponse
{
public NatPmpMapResponse(ushort resultCode, int internalPort, int externalPort, uint lifetimeSeconds)
{
ResultCode = resultCode;
InternalPort = internalPort;
ExternalPort = externalPort;
LifetimeSeconds = lifetimeSeconds;
}

public ushort ResultCode { get; }
public int InternalPort { get; }
public int ExternalPort { get; }
public uint LifetimeSeconds { get; }
}
Loading
Loading