Skip to content

Commit

Permalink
Added adapter implementation including Socket Adapter
Browse files Browse the repository at this point in the history
  • Loading branch information
dennisoehme committed Feb 26, 2021
1 parent 57266b8 commit af51cfe
Show file tree
Hide file tree
Showing 6 changed files with 352 additions and 90 deletions.
5 changes: 5 additions & 0 deletions CREDITS.MD
Original file line number Diff line number Diff line change
@@ -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)
118 changes: 118 additions & 0 deletions adapter-http.go
Original file line number Diff line number Diff line change
@@ -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
}
196 changes: 196 additions & 0 deletions adapter-socket.go
Original file line number Diff line number Diff line change
@@ -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
}
9 changes: 9 additions & 0 deletions adapter.go
Original file line number Diff line number Diff line change
@@ -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
}
8 changes: 7 additions & 1 deletion cups-client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading

0 comments on commit af51cfe

Please sign in to comment.