package cups import ( "errors" "fmt" "net" "strings" "time" "github.com/AvengeMedia/DankMaterialShell/core/pkg/ipp" ) var validProtocols = map[string]bool{ "ipp": true, "ipps": true, "lpd": true, "socket": true, } func validateTestConnectionParams(host string, port int, protocol string) error { if host == "" { return errors.New("host is required") } if strings.ContainsAny(host, " \t\n\r/\\") { return errors.New("host contains invalid characters") } if port < 1 || port > 65535 { return errors.New("port must be between 1 and 65535") } if protocol != "" && !validProtocols[protocol] { return errors.New("protocol must be one of: ipp, ipps, lpd, socket") } return nil } const probeTimeout = 10 * time.Second func probeRemotePrinter(host string, port int, useTLS bool) (*RemotePrinterInfo, error) { addr := net.JoinHostPort(host, fmt.Sprintf("%d", port)) // Fast fail: TCP reachability check conn, err := net.DialTimeout("tcp", addr, probeTimeout) if err != nil { return &RemotePrinterInfo{ Reachable: false, Error: fmt.Sprintf("cannot reach %s: %s", addr, err.Error()), }, nil } conn.Close() // Create a temporary IPP client pointing at the remote host. // The TCP dial above provides fast-fail for unreachable hosts. // The IPP adapter's ResponseHeaderTimeout (90s) bounds stalling servers. client := ipp.NewIPPClient(host, port, "", "", useTLS) // Try /ipp/print first (modern driverless printers), then / (legacy) info, err := probeIPPEndpoint(client, host, port, useTLS, "/ipp/print") if err != nil { // If we got an auth error, the printer exists but requires credentials. // Report it as reachable with the URI that triggered the auth challenge. if isAuthError(err) { proto := "ipp" if useTLS { proto = "ipps" } return &RemotePrinterInfo{ Reachable: true, URI: fmt.Sprintf("%s://%s:%d/ipp/print", proto, host, port), Info: "authentication required", }, nil } info, err = probeIPPEndpoint(client, host, port, useTLS, "/") } if err != nil { if isAuthError(err) { proto := "ipp" if useTLS { proto = "ipps" } return &RemotePrinterInfo{ Reachable: true, URI: fmt.Sprintf("%s://%s:%d/", proto, host, port), Info: "authentication required", }, nil } // TCP reachable but not an IPP printer return &RemotePrinterInfo{ Reachable: true, Error: fmt.Sprintf("host is reachable but does not appear to be an IPP printer: %s", err.Error()), }, nil } return info, nil } func probeIPPEndpoint(client *ipp.IPPClient, host string, port int, useTLS bool, resourcePath string) (*RemotePrinterInfo, error) { proto := "ipp" if useTLS { proto = "ipps" } printerURI := fmt.Sprintf("%s://%s:%d%s", proto, host, port, resourcePath) httpProto := "http" if useTLS { httpProto = "https" } httpURL := fmt.Sprintf("%s://%s:%d%s", httpProto, host, port, resourcePath) req := ipp.NewRequest(ipp.OperationGetPrinterAttributes, 1) req.OperationAttributes[ipp.AttributePrinterURI] = printerURI req.OperationAttributes[ipp.AttributeRequestedAttributes] = []string{ ipp.AttributePrinterName, ipp.AttributePrinterMakeAndModel, ipp.AttributePrinterState, ipp.AttributePrinterInfo, ipp.AttributePrinterUriSupported, } resp, err := client.SendRequest(httpURL, req, nil) if err != nil { return nil, err } if len(resp.PrinterAttributes) == 0 { return nil, errors.New("no printer attributes returned") } attrs := resp.PrinterAttributes[0] return &RemotePrinterInfo{ Reachable: true, MakeModel: getStringAttr(attrs, ipp.AttributePrinterMakeAndModel), Name: getStringAttr(attrs, ipp.AttributePrinterName), Info: getStringAttr(attrs, ipp.AttributePrinterInfo), State: parsePrinterState(attrs), URI: printerURI, }, nil } // TestRemotePrinter validates inputs and probes a remote printer via IPP. // For lpd/socket protocols, only TCP reachability is tested. func (m *Manager) TestRemotePrinter(host string, port int, protocol string) (*RemotePrinterInfo, error) { if protocol == "" { protocol = "ipp" } if err := validateTestConnectionParams(host, port, protocol); err != nil { return nil, err } // For non-IPP protocols, only check TCP reachability if protocol == "lpd" || protocol == "socket" { addr := net.JoinHostPort(host, fmt.Sprintf("%d", port)) conn, err := net.DialTimeout("tcp", addr, probeTimeout) if err != nil { return &RemotePrinterInfo{ Reachable: false, Error: fmt.Sprintf("cannot reach %s: %s", addr, err.Error()), }, nil } conn.Close() return &RemotePrinterInfo{ Reachable: true, URI: fmt.Sprintf("%s://%s:%d", protocol, host, port), }, nil } useTLS := protocol == "ipps" probeFn := m.probeRemoteFn if probeFn == nil { probeFn = probeRemotePrinter } return probeFn(host, port, useTLS) }