209 lines
8.5 KiB
C#
209 lines
8.5 KiB
C#
|
/*
|
|||
|
A simple udp broadcast library
|
|||
|
Copyright (C) 2024 Mike Nolan
|
|||
|
|
|||
|
This program is free software: you can redistribute it and/or modify
|
|||
|
it under the terms of the GNU General Public License as published by
|
|||
|
the Free Software Foundation, either version 3 of the License, or
|
|||
|
(at your option) any later version.
|
|||
|
|
|||
|
This program is distributed in the hope that it will be useful,
|
|||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|||
|
GNU General Public License for more details.
|
|||
|
|
|||
|
You should have received a copy of the GNU General Public License
|
|||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
|
|
|||
|
I am reachable at tesses@tesses.net
|
|||
|
*/
|
|||
|
using System;
|
|||
|
using System.Threading;
|
|||
|
using System.Collections.Generic;
|
|||
|
using System.Threading.Tasks;
|
|||
|
using System.Net.Sockets;
|
|||
|
using System.Net;
|
|||
|
using System.Text;
|
|||
|
using System.Runtime.CompilerServices;
|
|||
|
using System.Linq;
|
|||
|
|
|||
|
namespace Tesses.Broadcast
|
|||
|
{
|
|||
|
public class BroadcastServerRequestContext
|
|||
|
{
|
|||
|
public string ServiceUrl {get;set;}="";
|
|||
|
|
|||
|
public string DeviceName {get;set;}="";
|
|||
|
|
|||
|
public string ServiceName {get;set;}="";
|
|||
|
|
|||
|
public bool ToSend {get;set;}=false;
|
|||
|
}
|
|||
|
public delegate Task BroadcastHandleRequestAsync(BroadcastServerRequestContext ctx);
|
|||
|
public sealed class BroadcastServer
|
|||
|
{
|
|||
|
BroadcastHandleRequestAsync handler;
|
|||
|
int port;
|
|||
|
public BroadcastServer(BroadcastHandleRequestAsync handler, int port)
|
|||
|
{
|
|||
|
this.handler = handler;
|
|||
|
this.port = port;
|
|||
|
}
|
|||
|
|
|||
|
public async Task ListenAsync(CancellationToken token=default)
|
|||
|
{
|
|||
|
using(UdpClient udpClient=new UdpClient()){
|
|||
|
udpClient.Client.Bind(new IPEndPoint(IPAddress.Any,port));
|
|||
|
udpClient.EnableBroadcast=true;
|
|||
|
|
|||
|
while(!token.IsCancellationRequested)
|
|||
|
{
|
|||
|
var res=await udpClient.ReceiveAsync();
|
|||
|
if(res.Buffer.StartsWith(BroadcastClient.SIGReq))
|
|||
|
{
|
|||
|
int serviceNameLen = (res.Buffer[BroadcastClient.SIGReq.Length] << 8) | res.Buffer[BroadcastClient.SIGReq.Length+1];
|
|||
|
string serviceName=Encoding.UTF8.GetString(res.Buffer,BroadcastClient.SIGReq.Length+2,serviceNameLen);
|
|||
|
BroadcastServerRequestContext ctx=new BroadcastServerRequestContext();
|
|||
|
ctx.ServiceName = serviceName;
|
|||
|
await handler(ctx);
|
|||
|
|
|||
|
if(ctx.ToSend)
|
|||
|
{
|
|||
|
byte[] deviceNameData = Encoding.UTF8.GetBytes(ctx.DeviceName);
|
|||
|
byte[] serviceUrlData = Encoding.UTF8.GetBytes(ctx.ServiceUrl);
|
|||
|
byte[] response = new byte[BroadcastClient.SIGResp.Length+4+deviceNameData.Length+serviceUrlData.Length];
|
|||
|
Array.Copy(BroadcastClient.SIGResp,response,BroadcastClient.SIGResp.Length);
|
|||
|
response[BroadcastClient.SIGResp.Length] = (byte)((deviceNameData.Length>>8) & 0xFF);
|
|||
|
response[BroadcastClient.SIGResp.Length+1] = (byte)(deviceNameData.Length & 0xFF);
|
|||
|
Array.Copy(deviceNameData,0,response,BroadcastClient.SIGResp.Length+2,deviceNameData.Length);
|
|||
|
response[BroadcastClient.SIGResp.Length+2+deviceNameData.Length] = (byte)((serviceUrlData.Length >> 8) & 0xFF);
|
|||
|
response[BroadcastClient.SIGResp.Length+3+deviceNameData.Length] = (byte)(serviceUrlData.Length & 0xFF);
|
|||
|
Array.Copy(serviceUrlData,0,response,BroadcastClient.SIGResp.Length+4+deviceNameData.Length,serviceUrlData.Length);
|
|||
|
await udpClient.SendAsync(response,response.Length,res.RemoteEndPoint);
|
|||
|
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
public sealed class BroadcastServerBuilder
|
|||
|
{
|
|||
|
string deviceName=$"Unknown {Guid.NewGuid()}";
|
|||
|
int port=6942;
|
|||
|
|
|||
|
Dictionary<string,string> services=new Dictionary<string, string>();
|
|||
|
public BroadcastServerBuilder WithPort(int port)
|
|||
|
{
|
|||
|
this.port=port;
|
|||
|
return this;
|
|||
|
}
|
|||
|
public BroadcastServerBuilder WithDeviceName(string name)
|
|||
|
{
|
|||
|
deviceName=name;
|
|||
|
return this;
|
|||
|
}
|
|||
|
public BroadcastServerBuilder WithService(string serviceName, string serviceUrl)
|
|||
|
{
|
|||
|
services.Add(serviceName,serviceUrl);
|
|||
|
return this;
|
|||
|
}
|
|||
|
public BroadcastServer Build()
|
|||
|
{
|
|||
|
return new BroadcastServer(Hdlr,port);
|
|||
|
}
|
|||
|
|
|||
|
private async Task Hdlr(BroadcastServerRequestContext ctx)
|
|||
|
{
|
|||
|
if(services.ContainsKey(ctx.ServiceName))
|
|||
|
{
|
|||
|
ctx.ServiceUrl = services[ctx.ServiceName];
|
|||
|
ctx.DeviceName=deviceName;
|
|||
|
ctx.ToSend=true;
|
|||
|
}
|
|||
|
await Task.CompletedTask;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
public static class BroadcastClient
|
|||
|
{
|
|||
|
internal static bool StartsWith(this byte[] haystack, byte[] needle)
|
|||
|
{
|
|||
|
if(haystack.Length < needle.Length) return false;
|
|||
|
for(int i = 0;i<needle.Length;i++)
|
|||
|
{
|
|||
|
if(haystack[i] != needle[i]) return false;
|
|||
|
}
|
|||
|
return true;
|
|||
|
}
|
|||
|
internal static byte[] SIGReq = Encoding.UTF8.GetBytes("TessesBcReq");
|
|||
|
internal static byte[] SIGResp = Encoding.UTF8.GetBytes("TessesBcResp");
|
|||
|
public static async IAsyncEnumerable<BroadcastResponse> ScanAsync(string serviceName,int port=6942, [EnumeratorCancellation] CancellationToken token=default)
|
|||
|
{
|
|||
|
using(UdpClient client = new UdpClient())
|
|||
|
{
|
|||
|
token.Register(()=>client.Dispose());
|
|||
|
client.Client.Bind(new IPEndPoint(IPAddress.Any,0));
|
|||
|
client.EnableBroadcast=true;
|
|||
|
|
|||
|
byte[] name = Encoding.UTF8.GetBytes(serviceName);
|
|||
|
|
|||
|
byte[] datagram = new byte[SIGReq.Length+2+name.Length];
|
|||
|
|
|||
|
Array.Copy(SIGReq,datagram,SIGReq.Length);
|
|||
|
datagram[SIGReq.Length] = (byte)((name.Length >> 8) & 0xFF);
|
|||
|
datagram[SIGReq.Length+1]=(byte)(name.Length & 0xFF);
|
|||
|
Array.Copy(name,0,datagram,SIGReq.Length+2,name.Length);
|
|||
|
|
|||
|
|
|||
|
await client.SendAsync(datagram,datagram.Length,"255.255.255.255",port);
|
|||
|
while(!token.IsCancellationRequested)
|
|||
|
{
|
|||
|
|
|||
|
UdpReceiveResult r;
|
|||
|
try{
|
|||
|
r=await client.ReceiveAsync();
|
|||
|
}catch(System.Net.Sockets.SocketException ex)
|
|||
|
{
|
|||
|
_=ex;
|
|||
|
yield break;
|
|||
|
}
|
|||
|
var recvBuffer=r.Buffer;
|
|||
|
if(recvBuffer.StartsWith(SIGResp))
|
|||
|
{
|
|||
|
int deviceNameLen = (recvBuffer[SIGResp.Length] << 8) | recvBuffer[SIGResp.Length+1];
|
|||
|
string deviceName = Encoding.UTF8.GetString(recvBuffer,SIGResp.Length+2,deviceNameLen);
|
|||
|
int serviceUrlLen = (recvBuffer[SIGResp.Length+2+deviceNameLen] << 8) | recvBuffer[SIGResp.Length+3+deviceNameLen];
|
|||
|
string serviceUrl = Encoding.UTF8.GetString(recvBuffer,SIGResp.Length+4+deviceNameLen,serviceUrlLen);
|
|||
|
Fix127(ref serviceUrl,r.RemoteEndPoint);
|
|||
|
yield return new BroadcastResponse(){ServiceUrl=serviceUrl,DeviceName = deviceName, Endpoint = r.RemoteEndPoint};
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
private static void Fix127(ref string serviceUrl, IPEndPoint remoteEndPoint)
|
|||
|
{
|
|||
|
if(Uri.TryCreate(serviceUrl,UriKind.Absolute,out var uri))
|
|||
|
{
|
|||
|
if(uri.Host.StartsWith("127.") || uri.Host.StartsWith("localhost"))
|
|||
|
{
|
|||
|
serviceUrl=$"{uri.Scheme}://{remoteEndPoint.Address}:{uri.Port}{uri.PathAndQuery}";
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
public class BroadcastResponse
|
|||
|
{
|
|||
|
public string ServiceUrl {get;set;}="";
|
|||
|
|
|||
|
public string DeviceName {get;set;}="";
|
|||
|
|
|||
|
public IPEndPoint Endpoint {get;set;}
|
|||
|
}
|
|||
|
}
|