mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-04-04 04:42:05 -04:00
Add a new "Add by Address" flow in the printer settings that allows users to manually add printers by IP address or hostname, enabling printing to devices not visible via mDNS/Avahi discovery (e.g., printers behind Tailscale subnet routers, VPNs, or across network boundaries). Go backend: - New cups.testConnection IPC method that probes remote printers via IPP Get-Printer-Attributes with /ipp/print then / fallback - Input validation with host sanitization and protocol allowlist - Auth-aware probing (HTTP 401/403 reported as reachable) - lpadmin CLI fallback for CreatePrinter/DeletePrinter when cups-pk-helper polkit authorization fails QML frontend: - "Add by Address" toggle alongside existing device discovery - Manual entry form with host, port, protocol fields - Test Connection button with loading state and result display - Smart PPD auto-selection by probed makeModel with driverless fallback - All strings use I18n.tr() with translator context Includes 20+ unit tests covering validation, probe delegation, TLS flag propagation, auth error detection, and handler routing.
177 lines
4.8 KiB
Go
177 lines
4.8 KiB
Go
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)
|
|
}
|