Files
Print_server/Inspectron.Epson/EpsonPrinterDiscovery.cs
2026-01-13 09:06:47 +01:00

415 lines
15 KiB
C#

using System.Net;
using System.Net.Sockets;
using System.Text;
namespace Inspectron.Epson;
/// <summary>
/// Service Location Protocol (SLP) Discovery for Epson Printers
/// Implements SLPv2 (RFC 2608) for printer discovery on port 427
/// </summary>
public class EpsonPrinterDiscovery
{
private const int SLP_PORT = 427;
private const int DISCOVER_TIMEOUT_MS = 5000;
private const string SLP_MULTICAST_ADDRESS = "239.255.255.253"; // SLP multicast group
// SLP Message Function IDs (SLPv2)
private const byte FUNC_SRVRQST = 1; // Service Request
private const byte FUNC_SRVRPLY = 2; // Service Reply
private const byte FUNC_DAADVERT = 8; // Directory Agent Advertisement
private const byte FUNC_ATTRRQST = 6; // Attribute Request
private const byte FUNC_ATTRRPLY = 7; // Attribute Reply
private static ushort _nextXid = 1;
/// <summary>
/// Discovers printers using SLP (Service Location Protocol)
/// </summary>
/// <param name="serviceType">Service type to search for (default: "service:printer")</param>
/// <param name="scope">Scope to search in (default: "DEFAULT")</param>
/// <param name="timeoutMs">Timeout in milliseconds</param>
/// <returns>List of discovered printers</returns>
public static List<DiscoveredPrinter> DiscoverPrinters(
string serviceType = "service:printer",
string scope = "DEFAULT",
int timeoutMs = DISCOVER_TIMEOUT_MS)
{
var discoveredPrinters = new List<DiscoveredPrinter>();
var printerUrls = new HashSet<string>();
using (var udpClient = new UdpClient())
{
try
{
// Bind to local port
udpClient.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
udpClient.Client.Bind(new IPEndPoint(IPAddress.Any, 0));
// Join multicast group
var multicastAddress = IPAddress.Parse(SLP_MULTICAST_ADDRESS);
udpClient.JoinMulticastGroup(multicastAddress);
// Set receive timeout
udpClient.Client.ReceiveTimeout = timeoutMs;
// Build and send Service Request
var xid = _nextXid++;
byte[] serviceRequest = BuildServiceRequest(serviceType, scope, xid);
var multicastEndpoint = new IPEndPoint(multicastAddress, SLP_PORT);
Console.WriteLine($"[SLP Discovery] Sending Service Request to {multicastEndpoint}...");
Console.WriteLine($"[SLP Discovery] Service Type: {serviceType}, Scope: {scope}");
int bytesSent = udpClient.Send(serviceRequest, serviceRequest.Length, multicastEndpoint);
Console.WriteLine($"[SLP Discovery] Sent {bytesSent} bytes");
// Wait for responses
var startTime = DateTime.Now;
int receiveCount = 0;
while ((DateTime.Now - startTime).TotalMilliseconds < timeoutMs)
{
try
{
IPEndPoint remoteEndpoint = null;
byte[] responseData = udpClient.Receive(ref remoteEndpoint);
receiveCount++;
Console.WriteLine($"[SLP Discovery] Received {responseData.Length} bytes from {remoteEndpoint.Address}");
// Parse SLP response
if (IsValidSlpResponse(responseData, xid))
{
var printers = ParseServiceReply(responseData, remoteEndpoint.Address.ToString());
foreach (var printer in printers)
{
if (!printerUrls.Contains(printer.ServiceUrl))
{
discoveredPrinters.Add(printer);
printerUrls.Add(printer.ServiceUrl);
Console.WriteLine($"[SLP Discovery] ✓ Found printer: {printer.ServiceUrl}");
}
}
}
}
catch (SocketException ex) when (ex.SocketErrorCode == SocketError.TimedOut)
{
// Timeout - no more responses
break;
}
}
Console.WriteLine($"[SLP Discovery] Discovery complete. Found {discoveredPrinters.Count} printer(s)");
// Leave multicast group
udpClient.DropMulticastGroup(multicastAddress);
}
catch (Exception ex)
{
Console.WriteLine($"[SLP Discovery] Error: {ex.Message}");
}
}
return discoveredPrinters;
}
/// <summary>
/// Discovers printers asynchronously
/// </summary>
public static async Task<List<DiscoveredPrinter>> DiscoverPrintersAsync(
string serviceType = "service:printer",
string scope = "DEFAULT",
int timeoutMs = DISCOVER_TIMEOUT_MS)
{
return await Task.Run(() => DiscoverPrinters(serviceType, scope, timeoutMs));
}
/// <summary>
/// Builds an SLP Service Request packet (SLPv2 - RFC 2608)
/// </summary>
private static byte[] BuildServiceRequest(string serviceType, string scope, ushort xid)
{
using (var ms = new System.IO.MemoryStream())
using (var writer = new System.IO.BinaryWriter(ms))
{
// SLP Header (14 bytes for SLPv2)
writer.Write((byte)2); // Version (SLPv2)
writer.Write(FUNC_SRVRQST); // Function: Service Request
// Length placeholder (will be updated at end)
long lengthPosition = ms.Position;
writer.Write((byte)0);
writer.Write((ushort)0); // Length (3 bytes, big-endian)
// Flags (2 bytes)
ushort flags = 0x2000; // Request Multicast flag
writer.Write((byte)((flags >> 8) & 0xFF));
writer.Write((byte)(flags & 0xFF));
// Next Extension Offset (3 bytes) - 0 for no extensions
writer.Write((byte)0);
writer.Write((ushort)0);
// XID (Transaction ID) - 2 bytes
writer.Write((byte)((xid >> 8) & 0xFF));
writer.Write((byte)(xid & 0xFF));
// Language Tag (2 + length bytes) - "en"
string langTag = "en";
writer.Write((ushort)langTag.Length);
writer.Write(Encoding.ASCII.GetBytes(langTag));
// --- Service Request Body ---
// Previous Responder List (2 bytes for count)
writer.Write((ushort)0); // No previous responders
// Service Type (2 + length bytes)
writer.Write((ushort)serviceType.Length);
writer.Write(Encoding.ASCII.GetBytes(serviceType));
// Scope List (2 + length bytes)
writer.Write((ushort)scope.Length);
writer.Write(Encoding.ASCII.GetBytes(scope));
// Predicate (2 + length bytes) - Empty for "all"
writer.Write((ushort)0); // No predicate (match all)
// SLP SPI String (2 + length bytes) - Empty (no authentication)
writer.Write((ushort)0);
// Update length field
byte[] packet = ms.ToArray();
int totalLength = packet.Length;
// Write length in big-endian format (3 bytes starting at position 2)
packet[2] = (byte)((totalLength >> 16) & 0xFF);
packet[3] = (byte)((totalLength >> 8) & 0xFF);
packet[4] = (byte)(totalLength & 0xFF);
return packet;
}
}
/// <summary>
/// Validates if response is a proper SLP Service Reply
/// </summary>
private static bool IsValidSlpResponse(byte[] response, ushort expectedXid)
{
if (response == null || response.Length < 14)
return false;
// Check version (must be 2)
if (response[0] != 2)
return false;
// Check function (should be Service Reply = 2)
if (response[1] != FUNC_SRVRPLY)
return false;
// Check XID matches
ushort xid = (ushort)((response[12] << 8) | response[13]);
if (xid != expectedXid)
return false;
return true;
}
/// <summary>
/// Parses SLP Service Reply packet
/// </summary>
private static List<DiscoveredPrinter> ParseServiceReply(byte[] response, string sourceIp)
{
var printers = new List<DiscoveredPrinter>();
try
{
int offset = 14; // Skip SLP header
// Read Language Tag
ushort langTagLen = ReadUInt16(response, ref offset);
string langTag = ReadString(response, ref offset, langTagLen);
// Read Error Code (2 bytes)
ushort errorCode = ReadUInt16(response, ref offset);
if (errorCode != 0)
{
Console.WriteLine($"[SLP Parser] Error code in response: {errorCode}");
return printers;
}
// Read URL Entry Count
ushort urlCount = ReadUInt16(response, ref offset);
Console.WriteLine($"[SLP Parser] Found {urlCount} URL(s) in response");
// Parse each URL entry
for (int i = 0; i < urlCount; i++)
{
try
{
var printer = new DiscoveredPrinter
{
DiscoveredAt = DateTime.Now,
RawResponse = response
};
// Reserved byte
offset++;
// Lifetime (2 bytes)
printer.Lifetime = ReadUInt16(response, ref offset);
// URL Length and URL
ushort urlLen = ReadUInt16(response, ref offset);
printer.ServiceUrl = ReadString(response, ref offset, urlLen);
// Parse URL to extract IP and port
ParseServiceUrl(printer.ServiceUrl, printer);
// Auth Block Count (1 byte)
byte authBlockCount = response[offset++];
// Skip auth blocks if present
for (int j = 0; j < authBlockCount; j++)
{
// Skip authentication blocks (not typically used)
ushort blockLen = ReadUInt16(response, ref offset);
offset += blockLen;
}
printers.Add(printer);
}
catch (Exception ex)
{
Console.WriteLine($"[SLP Parser] Error parsing URL entry {i}: {ex.Message}");
}
}
}
catch (Exception ex)
{
Console.WriteLine($"[SLP Parser] Error parsing response: {ex.Message}");
}
return printers;
}
/// <summary>
/// Parses a service URL to extract IP address and port
/// Examples:
/// service:printer:ipp://192.168.1.100:631/printers/lp
/// service:printer:lpr://192.168.1.100/queue
/// </summary>
private static void ParseServiceUrl(string serviceUrl, DiscoveredPrinter printer)
{
try
{
// Extract service type
if (serviceUrl.StartsWith("service:"))
{
var parts = serviceUrl.Split(new[] { ':' }, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length >= 2)
{
printer.ServiceType = parts[1]; // e.g., "printer"
}
}
// Extract IP and port from URL
// Pattern: protocol://ip:port/path
var protocolIndex = serviceUrl.IndexOf("://");
if (protocolIndex > 0)
{
var afterProtocol = serviceUrl.Substring(protocolIndex + 3);
var slashIndex = afterProtocol.IndexOf('/');
var hostPort = slashIndex > 0 ?
afterProtocol.Substring(0, slashIndex) :
afterProtocol;
// Check for port
var colonIndex = hostPort.LastIndexOf(':');
if (colonIndex > 0 && colonIndex < hostPort.Length - 1)
{
printer.IPAddress = hostPort.Substring(0, colonIndex);
if (int.TryParse(hostPort.Substring(colonIndex + 1), out int port))
{
printer.Port = port;
}
}
else
{
printer.IPAddress = hostPort;
// Default ports based on protocol
if (serviceUrl.Contains("ipp://"))
printer.Port = 631;
else if (serviceUrl.Contains("lpr://"))
printer.Port = 515;
else
printer.Port = 9100; // Default ESC/POS
}
}
}
catch (Exception ex)
{
Console.WriteLine($"[SLP Parser] Error parsing service URL: {ex.Message}");
}
}
/// <summary>
/// Reads a 16-bit unsigned integer in big-endian format
/// </summary>
private static ushort ReadUInt16(byte[] data, ref int offset)
{
if (offset + 2 > data.Length)
throw new InvalidOperationException("Not enough data to read UInt16");
ushort value = (ushort)((data[offset] << 8) | data[offset + 1]);
offset += 2;
return value;
}
/// <summary>
/// Reads a string of specified length
/// </summary>
private static string ReadString(byte[] data, ref int offset, int length)
{
if (offset + length > data.Length)
throw new InvalidOperationException("Not enough data to read string");
string value = Encoding.UTF8.GetString(data, offset, length);
offset += length;
return value;
}
/// <summary>
/// Prints hex dump of byte array for debugging
/// </summary>
public static void PrintHexDump(byte[] data, int bytesPerLine = 16)
{
Console.WriteLine("\n=== SLP Hex Dump ===");
for (int i = 0; i < data.Length; i += bytesPerLine)
{
Console.Write($"{i:X4}: ");
for (int j = 0; j < bytesPerLine; j++)
{
if (i + j < data.Length)
Console.Write($"{data[i + j]:X2} ");
else
Console.Write(" ");
}
Console.Write(" | ");
for (int j = 0; j < bytesPerLine && i + j < data.Length; j++)
{
byte b = data[i + j];
char c = (b >= 32 && b < 127) ? (char)b : '.';
Console.Write(c);
}
Console.WriteLine();
}
Console.WriteLine("===================\n");
}
}