tesses.broadcast/Tesses.Broadcast/Class1.cs

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;}
}
}