/* 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 . 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 services=new Dictionary(); 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 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;} } }