diff --git a/CREDITS.MD b/CREDITS.MD new file mode 100644 index 0000000..b7d28cc --- /dev/null +++ b/CREDITS.MD @@ -0,0 +1,5 @@ +# Credits + +* Original socket adapter code is mostly taken from [korylprince/printer-manager-cups](https://github.com/korylprince/printer-manager-cups) +([MIT](https://github.com/korylprince/printer-manager-cups/blob/v1.0.9/LICENSE) licensed): +[conn.go](https://github.com/korylprince/printer-manager-cups/blob/v1.0.9/cups/conn.go) diff --git a/adapter-http.go b/adapter-http.go new file mode 100644 index 0000000..131c8d4 --- /dev/null +++ b/adapter-http.go @@ -0,0 +1,118 @@ +package ipp + +import ( + "bytes" + "crypto/tls" + "fmt" + "io" + "net" + "net/http" + "strconv" +) + +type HttpAdapter struct { + host string + port int + username string + password string + useTLS bool + client *http.Client +} + +func NewHttpAdapter(host string, port int, username, password string, useTLS bool) *HttpAdapter { + httpClient := http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + }, + } + + return &HttpAdapter{ + host: host, + port: port, + username: username, + password: password, + useTLS: useTLS, + client: &httpClient, + } +} + +func (h *HttpAdapter) SendRequest(url string, req *Request, additionalResponseData io.Writer) (*Response, error) { + payload, err := req.Encode() + if err != nil { + return nil, err + } + + var body io.Reader + size := len(payload) + + if req.File != nil && req.FileSize != -1 { + size += req.FileSize + + body = io.MultiReader(bytes.NewBuffer(payload), req.File) + } else { + body = bytes.NewBuffer(payload) + } + + httpReq, err := http.NewRequest("POST", url, body) + if err != nil { + return nil, err + } + + httpReq.Header.Set("Content-Length", strconv.Itoa(size)) + httpReq.Header.Set("Content-Type", ContentTypeIPP) + + if h.username != "" && h.password != "" { + httpReq.SetBasicAuth(h.username, h.password) + } + + httpResp, err := h.client.Do(httpReq) + if err != nil { + return nil, err + } + defer httpResp.Body.Close() + + if httpResp.StatusCode != 200 { + return nil, HTTPError{ + Code: httpResp.StatusCode, + } + } + + resp, err := NewResponseDecoder(httpResp.Body).Decode(additionalResponseData) + if err != nil { + return nil, err + } + + err = resp.CheckForErrors() + return resp, err +} + +func (h *HttpAdapter) GetHttpUri(namespace string, object interface{}) string { + proto := "http" + if h.useTLS { + proto = "https" + } + + uri := fmt.Sprintf("%s://%s:%d", proto, h.host, h.port) + + if namespace != "" { + uri = fmt.Sprintf("%s/%s", uri, namespace) + } + + if object != nil { + uri = fmt.Sprintf("%s/%v", uri, object) + } + + return uri +} + +func (h *HttpAdapter) TestConnection() error { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", h.host, h.port)) + if err != nil { + return err + } + conn.Close() + + return nil +} diff --git a/adapter-socket.go b/adapter-socket.go new file mode 100644 index 0000000..a5806a8 --- /dev/null +++ b/adapter-socket.go @@ -0,0 +1,196 @@ +package ipp + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "net" + "net/http" + "os" + "os/user" + "strconv" +) + +var socketNotFoundError = errors.New("unable to locate CUPS socket") +var certNotFoundError = errors.New("unable to locate CUPS certificate") + +var ( + DefaultSocketSearchPaths = []string{"/var/run/cupsd", "/var/run/cups/cups.sock", "/run/cups/cups.sock"} + DefaultCertSearchPaths = []string{"/etc/cups/certs/0", "/run/cups/certs/0"} +) + +const defaultRequestRetryLimit = 3 + +type SocketAdapter struct { + host string + useTLS bool + SocketSearchPaths []string + CertSearchPaths []string + requestRetryLimit int +} + +func NewSocketAdapter(host string, useTLS bool) *SocketAdapter { + return &SocketAdapter{ + host: host, + useTLS: useTLS, + SocketSearchPaths: DefaultSocketSearchPaths, + CertSearchPaths: DefaultCertSearchPaths, + requestRetryLimit: defaultRequestRetryLimit, + } +} + +//DoRequest performs the given IPP request to the given URL, returning the IPP response or an error if one occurred +func (h *SocketAdapter) SendRequest(url string, r *Request, _ io.Writer) (*Response, error) { + // set user field + user, err := user.Current() + if err != nil { + return nil, fmt.Errorf("unable to lookup current user: %v", err) + } + r.OperationAttributes[AttributeRequestingUserName] = user.Username + + for i := 0; i < h.requestRetryLimit; i++ { + // encode request + payload, err := r.Encode() + if err != nil { + return nil, fmt.Errorf("unable to encode IPP request: %v", err) + } + + req, err := http.NewRequest("POST", url, bytes.NewBuffer(payload)) + if err != nil { + return nil, fmt.Errorf("unable to create HTTP request: %v", err) + } + + sock, err := h.GetSocket() + if err != nil { + return nil, err + } + + // if cert isn't found, do a request to generate it + cert, err := h.GetCert() + if err != nil && err != certNotFoundError { + return nil, err + } + + req.Header.Set("Content-Length", strconv.Itoa(len(payload))) + req.Header.Set("Content-Type", ContentTypeIPP) + req.Header.Set("Authorization", fmt.Sprintf("Local %s", cert)) + + unixClient := http.Client{ + Transport: &http.Transport{ + DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { + return net.Dial("unix", sock) + }, + }, + } + + // send request + resp, err := unixClient.Do(req) + if err != nil { + return nil, fmt.Errorf("unable to perform HTTP request: %v", err) + } + + if resp.StatusCode == http.StatusUnauthorized { + // retry with newly generated cert + resp.Body.Close() + continue + } + + if resp.StatusCode != http.StatusOK { + resp.Body.Close() + return nil, fmt.Errorf("server did not return Status OK: %d", resp.StatusCode) + } + + // buffer response to avoid read issues + buf := new(bytes.Buffer) + if _, err := io.Copy(buf, resp.Body); err != nil { + resp.Body.Close() + return nil, fmt.Errorf("unable to buffer response: %v", err) + } + + resp.Body.Close() + + // decode reply + ippResp, err := NewResponseDecoder(bytes.NewReader(buf.Bytes())).Decode(nil) + if err != nil { + return nil, fmt.Errorf("unable to decode IPP response: %v", err) + } + + if err = ippResp.CheckForErrors(); err != nil { + return nil, fmt.Errorf("received error IPP response: %v", err) + } + + return ippResp, nil + } + + return nil, errors.New("request retry limit exceeded") +} + +//GetSocket returns the path to the cupsd socket by searching SocketSearchPaths +func (h *SocketAdapter) GetSocket() (string, error) { + for _, path := range h.SocketSearchPaths { + fi, err := os.Stat(path) + if err != nil { + if os.IsNotExist(err) { + continue + } else if os.IsPermission(err) { + return "", errors.New("unable to access socket: Access denied") + } + return "", fmt.Errorf("unable to access socket: %v", err) + } + + if fi.Mode()&os.ModeSocket != 0 { + return path, nil + } + } + + return "", socketNotFoundError +} + +//GetCert returns the current CUPs authentication certificate by searching CertSearchPaths +func (h *SocketAdapter) GetCert() (string, error) { + for _, path := range h.CertSearchPaths { + f, err := os.Open(path) + if err != nil { + if os.IsNotExist(err) { + continue + } else if os.IsPermission(err) { + return "", errors.New("unable to access certificate: Access denied") + } + return "", fmt.Errorf("unable to access certificate: %v", err) + } + defer f.Close() + + buf := new(bytes.Buffer) + if _, err := io.Copy(buf, f); err != nil { + return "", fmt.Errorf("unable to access certificate: %v", err) + } + return buf.String(), nil + } + + return "", certNotFoundError +} + +func (h *SocketAdapter) GetHttpUri(namespace string, object interface{}) string { + proto := "http" + if h.useTLS { + proto = "https" + } + + uri := fmt.Sprintf("%s://%s", proto, h.host) + + if namespace != "" { + uri = fmt.Sprintf("%s/%s", uri, namespace) + } + + if object != nil { + uri = fmt.Sprintf("%s/%v", uri, object) + } + + return uri +} + +func (h *SocketAdapter) TestConnection() error { + return nil +} diff --git a/adapter.go b/adapter.go new file mode 100644 index 0000000..15ad2d0 --- /dev/null +++ b/adapter.go @@ -0,0 +1,9 @@ +package ipp + +import "io" + +type Adapter interface { + SendRequest(url string, req *Request, additionalResponseData io.Writer) (*Response, error) + GetHttpUri(namespace string, object interface{}) string + TestConnection() error +} diff --git a/cups-client.go b/cups-client.go index 3cd4007..322bd64 100644 --- a/cups-client.go +++ b/cups-client.go @@ -10,12 +10,18 @@ type CUPSClient struct { *IPPClient } -// NewCUPSClient creates a new cups ipp client +// NewCUPSClient creates a new cups ipp client (used HttpAdapter internally) func NewCUPSClient(host string, port int, username, password string, useTLS bool) *CUPSClient { ippClient := NewIPPClient(host, port, username, password, useTLS) return &CUPSClient{ippClient} } +// NewCUPSClient creates a new cups ipp client with given Adapter +func NewCUPSClientWithAdapter(username string, adapter Adapter) *CUPSClient { + ippClient := NewIPPClientWithAdapter(username, adapter) + return &CUPSClient{ippClient} +} + // GetDevices returns a map of device uris and printer attributes func (c *CUPSClient) GetDevices() (map[string]Attributes, error) { req := NewRequest(OperationCupsGetDevices, 1) diff --git a/ipp-client.go b/ipp-client.go index 51885c3..76bb27a 100644 --- a/ipp-client.go +++ b/ipp-client.go @@ -1,16 +1,11 @@ package ipp import ( - "bytes" - "crypto/tls" "errors" "fmt" "io" - "net" - "net/http" "os" "path" - "strconv" ) // Document wraps an io.Reader with more information, needed for encoding @@ -23,45 +18,30 @@ type Document struct { // IPPClient implements a generic ipp client type IPPClient struct { - host string - port int username string - password string - useTLS bool - - client *http.Client + adapter Adapter } -// NewIPPClient creates a new generic ipp client +// NewIPPClient creates a new generic ipp client (used HttpAdapter internally) func NewIPPClient(host string, port int, username, password string, useTLS bool) *IPPClient { - httpClient := http.Client{ - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: true, - }, - }, - } + adapter := NewHttpAdapter(host, port, username, password, useTLS) - return &IPPClient{host, port, username, password, useTLS, &httpClient} -} - -func (c *IPPClient) getHttpUri(namespace string, object interface{}) string { - proto := "http" - if c.useTLS { - proto = "https" - } - - uri := fmt.Sprintf("%s://%s:%d", proto, c.host, c.port) - - if namespace != "" { - uri = fmt.Sprintf("%s/%s", uri, namespace) + return &IPPClient{ + username: username, + adapter: adapter, } +} - if object != nil { - uri = fmt.Sprintf("%s/%v", uri, object) +// NewIPPClientWithAdapter creates a new generic ipp client with given Adapter +func NewIPPClientWithAdapter(username string, adapter Adapter) *IPPClient { + return &IPPClient{ + username: username, + adapter: adapter, } +} - return uri +func (c *IPPClient) getHttpUri(namespace string, object interface{}) string { + return c.adapter.GetHttpUri(namespace, object) } func (c *IPPClient) getPrinterUri(printer string) string { @@ -78,53 +58,7 @@ func (c *IPPClient) getClassUri(printer string) string { // SendRequest sends a request to a remote uri end returns the response func (c *IPPClient) SendRequest(url string, req *Request, additionalResponseData io.Writer) (*Response, error) { - payload, err := req.Encode() - if err != nil { - return nil, err - } - - var body io.Reader - size := len(payload) - - if req.File != nil && req.FileSize != -1 { - size += req.FileSize - - body = io.MultiReader(bytes.NewBuffer(payload), req.File) - } else { - body = bytes.NewBuffer(payload) - } - - httpReq, err := http.NewRequest("POST", url, body) - if err != nil { - return nil, err - } - - httpReq.Header.Set("Content-Length", strconv.Itoa(size)) - httpReq.Header.Set("Content-Type", ContentTypeIPP) - - if c.username != "" && c.password != "" { - httpReq.SetBasicAuth(c.username, c.password) - } - - httpResp, err := c.client.Do(httpReq) - if err != nil { - return nil, err - } - defer httpResp.Body.Close() - - if httpResp.StatusCode != 200 { - return nil, HTTPError{ - Code: httpResp.StatusCode, - } - } - - resp, err := NewResponseDecoder(httpResp.Body).Decode(additionalResponseData) - if err != nil { - return nil, err - } - - err = resp.CheckForErrors() - return resp, err + return c.adapter.SendRequest(url, req, additionalResponseData) } // PrintDocuments prints one or more documents using a Create-Job operation followed by one or more Send-Document operation(s). custom job settings can be specified via the jobAttributes parameter @@ -391,11 +325,5 @@ func (c *IPPClient) HoldJobUntil(jobID int, holdUntil string) error { // TestConnection tests if a tcp connection to the remote server is possible func (c *IPPClient) TestConnection() error { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", c.host, c.port)) - if err != nil { - return err - } - conn.Close() - - return nil + return c.adapter.TestConnection() }