415 lines
15 KiB
C#
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");
|
|
}
|
|
} |