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.
488 lines
14 KiB
Go
488 lines
14 KiB
Go
package cups
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net"
|
|
|
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/params"
|
|
)
|
|
|
|
type CUPSEvent struct {
|
|
Type string `json:"type"`
|
|
Data CUPSState `json:"data"`
|
|
}
|
|
|
|
type TestPageResult struct {
|
|
Success bool `json:"success"`
|
|
JobID int `json:"jobId"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
|
|
switch req.Method {
|
|
case "cups.subscribe":
|
|
handleSubscribe(conn, req, manager)
|
|
case "cups.getPrinters":
|
|
handleGetPrinters(conn, req, manager)
|
|
case "cups.getJobs":
|
|
handleGetJobs(conn, req, manager)
|
|
case "cups.pausePrinter":
|
|
handlePausePrinter(conn, req, manager)
|
|
case "cups.resumePrinter":
|
|
handleResumePrinter(conn, req, manager)
|
|
case "cups.cancelJob":
|
|
handleCancelJob(conn, req, manager)
|
|
case "cups.purgeJobs":
|
|
handlePurgeJobs(conn, req, manager)
|
|
case "cups.getDevices":
|
|
handleGetDevices(conn, req, manager)
|
|
case "cups.getPPDs":
|
|
handleGetPPDs(conn, req, manager)
|
|
case "cups.getClasses":
|
|
handleGetClasses(conn, req, manager)
|
|
case "cups.createPrinter":
|
|
handleCreatePrinter(conn, req, manager)
|
|
case "cups.deletePrinter":
|
|
handleDeletePrinter(conn, req, manager)
|
|
case "cups.acceptJobs":
|
|
handleAcceptJobs(conn, req, manager)
|
|
case "cups.rejectJobs":
|
|
handleRejectJobs(conn, req, manager)
|
|
case "cups.setPrinterShared":
|
|
handleSetPrinterShared(conn, req, manager)
|
|
case "cups.setPrinterLocation":
|
|
handleSetPrinterLocation(conn, req, manager)
|
|
case "cups.setPrinterInfo":
|
|
handleSetPrinterInfo(conn, req, manager)
|
|
case "cups.moveJob":
|
|
handleMoveJob(conn, req, manager)
|
|
case "cups.printTestPage":
|
|
handlePrintTestPage(conn, req, manager)
|
|
case "cups.addPrinterToClass":
|
|
handleAddPrinterToClass(conn, req, manager)
|
|
case "cups.removePrinterFromClass":
|
|
handleRemovePrinterFromClass(conn, req, manager)
|
|
case "cups.deleteClass":
|
|
handleDeleteClass(conn, req, manager)
|
|
case "cups.restartJob":
|
|
handleRestartJob(conn, req, manager)
|
|
case "cups.holdJob":
|
|
handleHoldJob(conn, req, manager)
|
|
case "cups.testConnection":
|
|
handleTestConnection(conn, req, manager)
|
|
default:
|
|
models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method))
|
|
}
|
|
}
|
|
|
|
func handleGetPrinters(conn net.Conn, req models.Request, manager *Manager) {
|
|
printers, err := manager.GetPrinters()
|
|
if err != nil {
|
|
models.RespondError(conn, req.ID, err.Error())
|
|
return
|
|
}
|
|
models.Respond(conn, req.ID, printers)
|
|
}
|
|
|
|
func handleGetJobs(conn net.Conn, req models.Request, manager *Manager) {
|
|
printerName, err := params.String(req.Params, "printerName")
|
|
if err != nil {
|
|
models.RespondError(conn, req.ID, err.Error())
|
|
return
|
|
}
|
|
|
|
jobs, err := manager.GetJobs(printerName, "not-completed")
|
|
if err != nil {
|
|
models.RespondError(conn, req.ID, err.Error())
|
|
return
|
|
}
|
|
models.Respond(conn, req.ID, jobs)
|
|
}
|
|
|
|
func handlePausePrinter(conn net.Conn, req models.Request, manager *Manager) {
|
|
printerName, err := params.String(req.Params, "printerName")
|
|
if err != nil {
|
|
models.RespondError(conn, req.ID, err.Error())
|
|
return
|
|
}
|
|
|
|
if err := manager.PausePrinter(printerName); err != nil {
|
|
models.RespondError(conn, req.ID, err.Error())
|
|
return
|
|
}
|
|
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "paused"})
|
|
}
|
|
|
|
func handleResumePrinter(conn net.Conn, req models.Request, manager *Manager) {
|
|
printerName, err := params.String(req.Params, "printerName")
|
|
if err != nil {
|
|
models.RespondError(conn, req.ID, err.Error())
|
|
return
|
|
}
|
|
|
|
if err := manager.ResumePrinter(printerName); err != nil {
|
|
models.RespondError(conn, req.ID, err.Error())
|
|
return
|
|
}
|
|
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "resumed"})
|
|
}
|
|
|
|
func handleCancelJob(conn net.Conn, req models.Request, manager *Manager) {
|
|
jobID, err := params.Int(req.Params, "jobID")
|
|
if err != nil {
|
|
models.RespondError(conn, req.ID, err.Error())
|
|
return
|
|
}
|
|
|
|
if err := manager.CancelJob(jobID); err != nil {
|
|
models.RespondError(conn, req.ID, err.Error())
|
|
return
|
|
}
|
|
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "job canceled"})
|
|
}
|
|
|
|
func handlePurgeJobs(conn net.Conn, req models.Request, manager *Manager) {
|
|
printerName, err := params.String(req.Params, "printerName")
|
|
if err != nil {
|
|
models.RespondError(conn, req.ID, err.Error())
|
|
return
|
|
}
|
|
|
|
if err := manager.PurgeJobs(printerName); err != nil {
|
|
models.RespondError(conn, req.ID, err.Error())
|
|
return
|
|
}
|
|
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "jobs canceled"})
|
|
}
|
|
|
|
func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) {
|
|
clientID := fmt.Sprintf("client-%p", conn)
|
|
stateChan := manager.Subscribe(clientID)
|
|
defer manager.Unsubscribe(clientID)
|
|
|
|
initialState := manager.GetState()
|
|
event := CUPSEvent{
|
|
Type: "state_changed",
|
|
Data: initialState,
|
|
}
|
|
|
|
if err := json.NewEncoder(conn).Encode(models.Response[CUPSEvent]{
|
|
ID: req.ID,
|
|
Result: &event,
|
|
}); err != nil {
|
|
return
|
|
}
|
|
|
|
for state := range stateChan {
|
|
event := CUPSEvent{
|
|
Type: "state_changed",
|
|
Data: state,
|
|
}
|
|
if err := json.NewEncoder(conn).Encode(models.Response[CUPSEvent]{
|
|
Result: &event,
|
|
}); err != nil {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func handleGetDevices(conn net.Conn, req models.Request, manager *Manager) {
|
|
devices, err := manager.GetDevices()
|
|
if err != nil {
|
|
models.RespondError(conn, req.ID, err.Error())
|
|
return
|
|
}
|
|
models.Respond(conn, req.ID, devices)
|
|
}
|
|
|
|
func handleGetPPDs(conn net.Conn, req models.Request, manager *Manager) {
|
|
ppds, err := manager.GetPPDs()
|
|
if err != nil {
|
|
models.RespondError(conn, req.ID, err.Error())
|
|
return
|
|
}
|
|
models.Respond(conn, req.ID, ppds)
|
|
}
|
|
|
|
func handleGetClasses(conn net.Conn, req models.Request, manager *Manager) {
|
|
classes, err := manager.GetClasses()
|
|
if err != nil {
|
|
models.RespondError(conn, req.ID, err.Error())
|
|
return
|
|
}
|
|
models.Respond(conn, req.ID, classes)
|
|
}
|
|
|
|
func handleCreatePrinter(conn net.Conn, req models.Request, manager *Manager) {
|
|
name, err := params.StringNonEmpty(req.Params, "name")
|
|
if err != nil {
|
|
models.RespondError(conn, req.ID, err.Error())
|
|
return
|
|
}
|
|
|
|
deviceURI, err := params.StringNonEmpty(req.Params, "deviceURI")
|
|
if err != nil {
|
|
models.RespondError(conn, req.ID, err.Error())
|
|
return
|
|
}
|
|
|
|
ppd, err := params.StringNonEmpty(req.Params, "ppd")
|
|
if err != nil {
|
|
models.RespondError(conn, req.ID, err.Error())
|
|
return
|
|
}
|
|
|
|
shared := params.BoolOpt(req.Params, "shared", false)
|
|
errorPolicy := params.StringOpt(req.Params, "errorPolicy", "")
|
|
information := params.StringOpt(req.Params, "information", "")
|
|
location := params.StringOpt(req.Params, "location", "")
|
|
|
|
if err := manager.CreatePrinter(name, deviceURI, ppd, shared, errorPolicy, information, location); err != nil {
|
|
models.RespondError(conn, req.ID, err.Error())
|
|
return
|
|
}
|
|
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "printer created"})
|
|
}
|
|
|
|
func handleDeletePrinter(conn net.Conn, req models.Request, manager *Manager) {
|
|
printerName, err := params.StringNonEmpty(req.Params, "printerName")
|
|
if err != nil {
|
|
models.RespondError(conn, req.ID, err.Error())
|
|
return
|
|
}
|
|
|
|
if err := manager.DeletePrinter(printerName); err != nil {
|
|
models.RespondError(conn, req.ID, err.Error())
|
|
return
|
|
}
|
|
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "printer deleted"})
|
|
}
|
|
|
|
func handleAcceptJobs(conn net.Conn, req models.Request, manager *Manager) {
|
|
printerName, err := params.StringNonEmpty(req.Params, "printerName")
|
|
if err != nil {
|
|
models.RespondError(conn, req.ID, err.Error())
|
|
return
|
|
}
|
|
|
|
if err := manager.AcceptJobs(printerName); err != nil {
|
|
models.RespondError(conn, req.ID, err.Error())
|
|
return
|
|
}
|
|
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "accepting jobs"})
|
|
}
|
|
|
|
func handleRejectJobs(conn net.Conn, req models.Request, manager *Manager) {
|
|
printerName, err := params.StringNonEmpty(req.Params, "printerName")
|
|
if err != nil {
|
|
models.RespondError(conn, req.ID, err.Error())
|
|
return
|
|
}
|
|
|
|
if err := manager.RejectJobs(printerName); err != nil {
|
|
models.RespondError(conn, req.ID, err.Error())
|
|
return
|
|
}
|
|
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "rejecting jobs"})
|
|
}
|
|
|
|
func handleSetPrinterShared(conn net.Conn, req models.Request, manager *Manager) {
|
|
printerName, err := params.StringNonEmpty(req.Params, "printerName")
|
|
if err != nil {
|
|
models.RespondError(conn, req.ID, err.Error())
|
|
return
|
|
}
|
|
|
|
shared, err := params.Bool(req.Params, "shared")
|
|
if err != nil {
|
|
models.RespondError(conn, req.ID, err.Error())
|
|
return
|
|
}
|
|
|
|
if err := manager.SetPrinterShared(printerName, shared); err != nil {
|
|
models.RespondError(conn, req.ID, err.Error())
|
|
return
|
|
}
|
|
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "sharing updated"})
|
|
}
|
|
|
|
func handleSetPrinterLocation(conn net.Conn, req models.Request, manager *Manager) {
|
|
printerName, err := params.StringNonEmpty(req.Params, "printerName")
|
|
if err != nil {
|
|
models.RespondError(conn, req.ID, err.Error())
|
|
return
|
|
}
|
|
|
|
location, err := params.String(req.Params, "location")
|
|
if err != nil {
|
|
models.RespondError(conn, req.ID, err.Error())
|
|
return
|
|
}
|
|
|
|
if err := manager.SetPrinterLocation(printerName, location); err != nil {
|
|
models.RespondError(conn, req.ID, err.Error())
|
|
return
|
|
}
|
|
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "location updated"})
|
|
}
|
|
|
|
func handleSetPrinterInfo(conn net.Conn, req models.Request, manager *Manager) {
|
|
printerName, err := params.StringNonEmpty(req.Params, "printerName")
|
|
if err != nil {
|
|
models.RespondError(conn, req.ID, err.Error())
|
|
return
|
|
}
|
|
|
|
info, err := params.String(req.Params, "info")
|
|
if err != nil {
|
|
models.RespondError(conn, req.ID, err.Error())
|
|
return
|
|
}
|
|
|
|
if err := manager.SetPrinterInfo(printerName, info); err != nil {
|
|
models.RespondError(conn, req.ID, err.Error())
|
|
return
|
|
}
|
|
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "info updated"})
|
|
}
|
|
|
|
func handleMoveJob(conn net.Conn, req models.Request, manager *Manager) {
|
|
jobID, err := params.Int(req.Params, "jobID")
|
|
if err != nil {
|
|
models.RespondError(conn, req.ID, err.Error())
|
|
return
|
|
}
|
|
|
|
destPrinter, err := params.StringNonEmpty(req.Params, "destPrinter")
|
|
if err != nil {
|
|
models.RespondError(conn, req.ID, err.Error())
|
|
return
|
|
}
|
|
|
|
if err := manager.MoveJob(jobID, destPrinter); err != nil {
|
|
models.RespondError(conn, req.ID, err.Error())
|
|
return
|
|
}
|
|
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "job moved"})
|
|
}
|
|
|
|
func handlePrintTestPage(conn net.Conn, req models.Request, manager *Manager) {
|
|
printerName, err := params.StringNonEmpty(req.Params, "printerName")
|
|
if err != nil {
|
|
models.RespondError(conn, req.ID, err.Error())
|
|
return
|
|
}
|
|
|
|
jobID, err := manager.PrintTestPage(printerName)
|
|
if err != nil {
|
|
models.RespondError(conn, req.ID, err.Error())
|
|
return
|
|
}
|
|
models.Respond(conn, req.ID, TestPageResult{Success: true, JobID: jobID, Message: "test page queued"})
|
|
}
|
|
|
|
func handleAddPrinterToClass(conn net.Conn, req models.Request, manager *Manager) {
|
|
className, err := params.StringNonEmpty(req.Params, "className")
|
|
if err != nil {
|
|
models.RespondError(conn, req.ID, err.Error())
|
|
return
|
|
}
|
|
|
|
printerName, err := params.StringNonEmpty(req.Params, "printerName")
|
|
if err != nil {
|
|
models.RespondError(conn, req.ID, err.Error())
|
|
return
|
|
}
|
|
|
|
if err := manager.AddPrinterToClass(className, printerName); err != nil {
|
|
models.RespondError(conn, req.ID, err.Error())
|
|
return
|
|
}
|
|
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "printer added to class"})
|
|
}
|
|
|
|
func handleRemovePrinterFromClass(conn net.Conn, req models.Request, manager *Manager) {
|
|
className, err := params.StringNonEmpty(req.Params, "className")
|
|
if err != nil {
|
|
models.RespondError(conn, req.ID, err.Error())
|
|
return
|
|
}
|
|
|
|
printerName, err := params.StringNonEmpty(req.Params, "printerName")
|
|
if err != nil {
|
|
models.RespondError(conn, req.ID, err.Error())
|
|
return
|
|
}
|
|
|
|
if err := manager.RemovePrinterFromClass(className, printerName); err != nil {
|
|
models.RespondError(conn, req.ID, err.Error())
|
|
return
|
|
}
|
|
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "printer removed from class"})
|
|
}
|
|
|
|
func handleDeleteClass(conn net.Conn, req models.Request, manager *Manager) {
|
|
className, err := params.StringNonEmpty(req.Params, "className")
|
|
if err != nil {
|
|
models.RespondError(conn, req.ID, err.Error())
|
|
return
|
|
}
|
|
|
|
if err := manager.DeleteClass(className); err != nil {
|
|
models.RespondError(conn, req.ID, err.Error())
|
|
return
|
|
}
|
|
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "class deleted"})
|
|
}
|
|
|
|
func handleRestartJob(conn net.Conn, req models.Request, manager *Manager) {
|
|
jobID, err := params.Int(req.Params, "jobID")
|
|
if err != nil {
|
|
models.RespondError(conn, req.ID, err.Error())
|
|
return
|
|
}
|
|
|
|
if err := manager.RestartJob(jobID); err != nil {
|
|
models.RespondError(conn, req.ID, err.Error())
|
|
return
|
|
}
|
|
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "job restarted"})
|
|
}
|
|
|
|
func handleHoldJob(conn net.Conn, req models.Request, manager *Manager) {
|
|
jobID, err := params.Int(req.Params, "jobID")
|
|
if err != nil {
|
|
models.RespondError(conn, req.ID, err.Error())
|
|
return
|
|
}
|
|
|
|
holdUntil := params.StringOpt(req.Params, "holdUntil", "indefinite")
|
|
|
|
if err := manager.HoldJob(jobID, holdUntil); err != nil {
|
|
models.RespondError(conn, req.ID, err.Error())
|
|
return
|
|
}
|
|
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "job held"})
|
|
}
|
|
|
|
func handleTestConnection(conn net.Conn, req models.Request, manager *Manager) {
|
|
host, err := params.StringNonEmpty(req.Params, "host")
|
|
if err != nil {
|
|
models.RespondError(conn, req.ID, err.Error())
|
|
return
|
|
}
|
|
|
|
port := params.IntOpt(req.Params, "port", 631)
|
|
protocol := params.StringOpt(req.Params, "protocol", "ipp")
|
|
|
|
result, err := manager.TestRemotePrinter(host, port, protocol)
|
|
if err != nil {
|
|
models.RespondError(conn, req.ID, err.Error())
|
|
return
|
|
}
|
|
|
|
models.Respond(conn, req.ID, result)
|
|
}
|