initial commit

This commit is contained in:
tumillanino
2025-11-12 23:59:00 +11:00
commit 991cca06bf
274 changed files with 70484 additions and 0 deletions

5
pkg/ipp/CREDITS.MD Normal file
View File

@@ -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)

201
pkg/ipp/LICENSE Normal file
View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

132
pkg/ipp/adapter-http.go Normal file
View File

@@ -0,0 +1,132 @@
package ipp
import (
"bytes"
"crypto/tls"
"fmt"
"io"
"net"
"net/http"
"strconv"
"time"
)
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{
Timeout: 0,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
ResponseHeaderTimeout: 90 * time.Second,
IdleConnTimeout: 120 * time.Second,
},
}
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
}
size := len(payload)
var body io.Reader
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,
}
}
// buffer response to avoid read issues
buf := new(bytes.Buffer)
if httpResp.ContentLength > 0 {
buf.Grow(int(httpResp.ContentLength))
}
if _, err := io.Copy(buf, httpResp.Body); err != nil {
return nil, fmt.Errorf("unable to buffer response: %w", err)
}
ippResp, err := NewResponseDecoder(buf).Decode(additionalResponseData)
if err != nil {
return nil, err
}
if err = ippResp.CheckForErrors(); err != nil {
return nil, fmt.Errorf("received error IPP response: %w", err)
}
return ippResp, nil
}
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
}

9
pkg/ipp/adapter.go Normal file
View File

@@ -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
}

528
pkg/ipp/attribute.go Normal file
View File

@@ -0,0 +1,528 @@
package ipp
import (
"encoding/binary"
"fmt"
"io"
)
const (
sizeInteger = int16(4)
sizeBoolean = int16(1)
)
// AttributeEncoder encodes attribute to a io.Writer
type AttributeEncoder struct {
writer io.Writer
}
// NewAttributeEncoder returns a new encoder that writes to w
func NewAttributeEncoder(w io.Writer) *AttributeEncoder {
return &AttributeEncoder{w}
}
// Encode encodes a attribute and its value to a io.Writer
// the tag is determined by the AttributeTagMapping map
func (e *AttributeEncoder) Encode(attribute string, value interface{}) error {
tag, ok := AttributeTagMapping[attribute]
if !ok {
return fmt.Errorf("cannot get tag of attribute %s", attribute)
}
switch v := value.(type) {
case int:
if tag != TagInteger && tag != TagEnum {
return fmt.Errorf("tag for attribute %s does not match with value type", attribute)
}
if err := e.encodeTag(tag); err != nil {
return err
}
if err := e.encodeString(attribute); err != nil {
return err
}
if err := e.encodeInteger(int32(v)); err != nil {
return err
}
case int16:
if tag != TagInteger && tag != TagEnum {
return fmt.Errorf("tag for attribute %s does not match with value type", attribute)
}
if err := e.encodeTag(tag); err != nil {
return err
}
if err := e.encodeString(attribute); err != nil {
return err
}
if err := e.encodeInteger(int32(v)); err != nil {
return err
}
case int8:
if tag != TagInteger && tag != TagEnum {
return fmt.Errorf("tag for attribute %s does not match with value type", attribute)
}
if err := e.encodeTag(tag); err != nil {
return err
}
if err := e.encodeString(attribute); err != nil {
return err
}
if err := e.encodeInteger(int32(v)); err != nil {
return err
}
case int32:
if tag != TagInteger && tag != TagEnum {
return fmt.Errorf("tag for attribute %s does not match with value type", attribute)
}
if err := e.encodeTag(tag); err != nil {
return err
}
if err := e.encodeString(attribute); err != nil {
return err
}
if err := e.encodeInteger(v); err != nil {
return err
}
case int64:
if tag != TagInteger && tag != TagEnum {
return fmt.Errorf("tag for attribute %s does not match with value type", attribute)
}
if err := e.encodeTag(tag); err != nil {
return err
}
if err := e.encodeString(attribute); err != nil {
return err
}
if err := e.encodeInteger(int32(v)); err != nil {
return err
}
case []int:
if tag != TagInteger && tag != TagEnum {
return fmt.Errorf("tag for attribute %s does not match with value type", attribute)
}
for index, val := range v {
if err := e.encodeTag(tag); err != nil {
return err
}
if index == 0 {
if err := e.encodeString(attribute); err != nil {
return err
}
} else {
if err := e.writeNullByte(); err != nil {
return err
}
}
if err := e.encodeInteger(int32(val)); err != nil {
return err
}
}
case []int16:
if tag != TagInteger && tag != TagEnum {
return fmt.Errorf("tag for attribute %s does not match with value type", attribute)
}
for index, val := range v {
if err := e.encodeTag(tag); err != nil {
return err
}
if index == 0 {
if err := e.encodeString(attribute); err != nil {
return err
}
} else {
if err := e.writeNullByte(); err != nil {
return err
}
}
if err := e.encodeInteger(int32(val)); err != nil {
return err
}
}
case []int8:
if tag != TagInteger && tag != TagEnum {
return fmt.Errorf("tag for attribute %s does not match with value type", attribute)
}
for index, val := range v {
if err := e.encodeTag(tag); err != nil {
return err
}
if index == 0 {
if err := e.encodeString(attribute); err != nil {
return err
}
} else {
if err := e.writeNullByte(); err != nil {
return err
}
}
if err := e.encodeInteger(int32(val)); err != nil {
return err
}
}
case []int32:
if tag != TagInteger && tag != TagEnum {
return fmt.Errorf("tag for attribute %s does not match with value type", attribute)
}
for index, val := range v {
if err := e.encodeTag(tag); err != nil {
return err
}
if index == 0 {
if err := e.encodeString(attribute); err != nil {
return err
}
} else {
if err := e.writeNullByte(); err != nil {
return err
}
}
if err := e.encodeInteger(val); err != nil {
return err
}
}
case []int64:
if tag != TagInteger && tag != TagEnum {
return fmt.Errorf("tag for attribute %s does not match with value type", attribute)
}
for index, val := range v {
if err := e.encodeTag(tag); err != nil {
return err
}
if index == 0 {
if err := e.encodeString(attribute); err != nil {
return err
}
} else {
if err := e.writeNullByte(); err != nil {
return err
}
}
if err := e.encodeInteger(int32(val)); err != nil {
return err
}
}
case bool:
if tag != TagBoolean {
return fmt.Errorf("tag for attribute %s does not match with value type", attribute)
}
if err := e.encodeTag(tag); err != nil {
return err
}
if err := e.encodeString(attribute); err != nil {
return err
}
if err := e.encodeBoolean(v); err != nil {
return err
}
case []bool:
if tag != TagBoolean {
return fmt.Errorf("tag for attribute %s does not match with value type", attribute)
}
for index, val := range v {
if err := e.encodeTag(tag); err != nil {
return err
}
if index == 0 {
if err := e.encodeString(attribute); err != nil {
return err
}
} else {
if err := e.writeNullByte(); err != nil {
return err
}
}
if err := e.encodeBoolean(val); err != nil {
return err
}
}
case string:
if err := e.encodeTag(tag); err != nil {
return err
}
if err := e.encodeString(attribute); err != nil {
return err
}
if err := e.encodeString(v); err != nil {
return err
}
case []string:
for index, val := range v {
if err := e.encodeTag(tag); err != nil {
return err
}
if index == 0 {
if err := e.encodeString(attribute); err != nil {
return err
}
} else {
if err := e.writeNullByte(); err != nil {
return err
}
}
if err := e.encodeString(val); err != nil {
return err
}
}
default:
return fmt.Errorf("type %T is not supported", value)
}
return nil
}
func (e *AttributeEncoder) encodeString(s string) error {
if err := binary.Write(e.writer, binary.BigEndian, int16(len(s))); err != nil {
return err
}
_, err := e.writer.Write([]byte(s))
return err
}
func (e *AttributeEncoder) encodeInteger(i int32) error {
if err := binary.Write(e.writer, binary.BigEndian, sizeInteger); err != nil {
return err
}
return binary.Write(e.writer, binary.BigEndian, i)
}
func (e *AttributeEncoder) encodeBoolean(b bool) error {
if err := binary.Write(e.writer, binary.BigEndian, sizeBoolean); err != nil {
return err
}
return binary.Write(e.writer, binary.BigEndian, b)
}
func (e *AttributeEncoder) encodeTag(t int8) error {
return binary.Write(e.writer, binary.BigEndian, t)
}
func (e *AttributeEncoder) writeNullByte() error {
return binary.Write(e.writer, binary.BigEndian, int16(0))
}
// Attribute defines an ipp attribute
type Attribute struct {
Tag int8
Name string
Value interface{}
}
// Resolution defines the resolution attribute
type Resolution struct {
Height int32
Width int32
Depth int8
}
// AttributeDecoder reads and decodes ipp from an input stream
type AttributeDecoder struct {
reader io.Reader
}
// NewAttributeDecoder returns a new decoder that reads from r
func NewAttributeDecoder(r io.Reader) *AttributeDecoder {
return &AttributeDecoder{r}
}
// Decode reads the next ipp attribute into a attribute struct. the type is identified by a tag passed as an argument
func (d *AttributeDecoder) Decode(tag int8) (*Attribute, error) {
attr := Attribute{Tag: tag}
name, err := d.decodeString()
if err != nil {
return nil, err
}
attr.Name = name
switch attr.Tag {
case TagEnum, TagInteger:
val, err := d.decodeInteger()
if err != nil {
return nil, err
}
attr.Value = val
case TagBoolean:
val, err := d.decodeBool()
if err != nil {
return nil, err
}
attr.Value = val
case TagDate:
val, err := d.decodeDate()
if err != nil {
return nil, err
}
attr.Value = val
case TagRange:
val, err := d.decodeRange()
if err != nil {
return nil, err
}
attr.Value = val
case TagResolution:
val, err := d.decodeResolution()
if err != nil {
return nil, err
}
attr.Value = val
default:
val, err := d.decodeString()
if err != nil {
return nil, err
}
attr.Value = val
}
return &attr, nil
}
func (d *AttributeDecoder) decodeBool() (b bool, err error) {
if _, err = d.readValueLength(); err != nil {
return
}
if err = binary.Read(d.reader, binary.BigEndian, &b); err != nil {
return
}
return
}
func (d *AttributeDecoder) decodeInteger() (i int, err error) {
if _, err = d.readValueLength(); err != nil {
return
}
var reti int32
if err = binary.Read(d.reader, binary.BigEndian, &reti); err != nil {
return
}
return int(reti), nil
}
func (d *AttributeDecoder) decodeString() (string, error) {
length, err := d.readValueLength()
if err != nil {
return "", err
}
if length == 0 {
return "", nil
}
bs := make([]byte, length)
if _, err := d.reader.Read(bs); err != nil {
return "", nil
}
return string(bs), nil
}
func (d *AttributeDecoder) decodeDate() ([]int, error) {
length, err := d.readValueLength()
if err != nil {
return nil, err
}
is := make([]int, length)
var ti int8
for i := int16(0); i < length; i++ {
if err = binary.Read(d.reader, binary.BigEndian, &ti); err != nil {
return nil, err
}
is[i] = int(ti)
}
return is, nil
}
func (d *AttributeDecoder) decodeRange() ([]int32, error) {
length, err := d.readValueLength()
if err != nil {
return nil, err
}
// initialize range element count (c) and range slice (r)
c := length / 4
r := make([]int32, c)
for i := int16(0); i < c; i++ {
var ti int32
if err = binary.Read(d.reader, binary.BigEndian, &ti); err != nil {
return nil, err
}
r[i] = ti
}
return r, nil
}
func (d *AttributeDecoder) decodeResolution() (res Resolution, err error) {
_, err = d.readValueLength()
if err != nil {
return
}
if err = binary.Read(d.reader, binary.BigEndian, &res.Height); err != nil {
return
}
if err = binary.Read(d.reader, binary.BigEndian, &res.Width); err != nil {
return
}
if err = binary.Read(d.reader, binary.BigEndian, &res.Depth); err != nil {
return
}
return
}
func (d *AttributeDecoder) readValueLength() (length int16, err error) {
err = binary.Read(d.reader, binary.BigEndian, &length)
return
}

449
pkg/ipp/constants.go Normal file
View File

@@ -0,0 +1,449 @@
package ipp
// ipp status codes
const (
StatusCupsInvalid int16 = -1
StatusOk int16 = 0x0000
StatusOkIgnoredOrSubstituted int16 = 0x0001
StatusOkConflicting int16 = 0x0002
StatusOkIgnoredSubscriptions int16 = 0x0003
StatusOkIgnoredNotifications int16 = 0x0004
StatusOkTooManyEvents int16 = 0x0005
StatusOkButCancelSubscription int16 = 0x0006
StatusOkEventsComplete int16 = 0x0007
StatusRedirectionOtherSite int16 = 0x0200
StatusCupsSeeOther int16 = 0x0280
StatusErrorBadRequest int16 = 0x0400
StatusErrorForbidden int16 = 0x0401
StatusErrorNotAuthenticated int16 = 0x0402
StatusErrorNotAuthorized int16 = 0x0403
StatusErrorNotPossible int16 = 0x0404
StatusErrorTimeout int16 = 0x0405
StatusErrorNotFound int16 = 0x0406
StatusErrorGone int16 = 0x0407
StatusErrorRequestEntity int16 = 0x0408
StatusErrorRequestValue int16 = 0x0409
StatusErrorDocumentFormatNotSupported int16 = 0x040a
StatusErrorAttributesOrValues int16 = 0x040b
StatusErrorUriScheme int16 = 0x040c
StatusErrorCharset int16 = 0x040d
StatusErrorConflicting int16 = 0x040e
StatusErrorCompressionError int16 = 0x040f
StatusErrorDocumentFormatError int16 = 0x0410
StatusErrorDocumentAccess int16 = 0x0411
StatusErrorAttributesNotSettable int16 = 0x0412
StatusErrorIgnoredAllSubscriptions int16 = 0x0413
StatusErrorTooManySubscriptions int16 = 0x0414
StatusErrorIgnoredAllNotifications int16 = 0x0415
StatusErrorPrintSupportFileNotFound int16 = 0x0416
StatusErrorDocumentPassword int16 = 0x0417
StatusErrorDocumentPermission int16 = 0x0418
StatusErrorDocumentSecurity int16 = 0x0419
StatusErrorDocumentUnprintable int16 = 0x041a
StatusErrorAccountInfoNeeded int16 = 0x041b
StatusErrorAccountClosed int16 = 0x041c
StatusErrorAccountLimitReached int16 = 0x041d
StatusErrorAccountAuthorizationFailed int16 = 0x041e
StatusErrorNotFetchable int16 = 0x041f
StatusErrorCupsAccountInfoNeeded int16 = 0x049C
StatusErrorCupsAccountClosed int16 = 0x049d
StatusErrorCupsAccountLimitReached int16 = 0x049e
StatusErrorCupsAccountAuthorizationFailed int16 = 0x049f
StatusErrorInternal int16 = 0x0500
StatusErrorOperationNotSupported int16 = 0x0501
StatusErrorServiceUnavailable int16 = 0x0502
StatusErrorVersionNotSupported int16 = 0x0503
StatusErrorDevice int16 = 0x0504
StatusErrorTemporary int16 = 0x0505
StatusErrorNotAcceptingJobs int16 = 0x0506
StatusErrorBusy int16 = 0x0507
StatusErrorJobCanceled int16 = 0x0508
StatusErrorMultipleJobsNotSupported int16 = 0x0509
StatusErrorPrinterIsDeactivated int16 = 0x050a
StatusErrorTooManyJobs int16 = 0x050b
StatusErrorTooManyDocuments int16 = 0x050c
StatusErrorCupsAuthenticationCanceled int16 = 0x1000
StatusErrorCupsPki int16 = 0x1001
StatusErrorCupsUpgradeRequired int16 = 0x1002
)
// ipp operations
const (
OperationCupsInvalid int16 = -0x0001
OperationCupsNone int16 = 0x0000
OperationPrintJob int16 = 0x0002
OperationPrintUri int16 = 0x0003
OperationValidateJob int16 = 0x0004
OperationCreateJob int16 = 0x0005
OperationSendDocument int16 = 0x0006
OperationSendUri int16 = 0x0007
OperationCancelJob int16 = 0x0008
OperationGetJobAttributes int16 = 0x0009
OperationGetJobs int16 = 0x000a
OperationGetPrinterAttributes int16 = 0x000b
OperationHoldJob int16 = 0x000c
OperationReleaseJob int16 = 0x000d
OperationRestartJob int16 = 0x000e
OperationPausePrinter int16 = 0x0010
OperationResumePrinter int16 = 0x0011
OperationPurgeJobs int16 = 0x0012
OperationSetPrinterAttributes int16 = 0x0013
OperationSetJobAttributes int16 = 0x0014
OperationGetPrinterSupportedValues int16 = 0x0015
OperationCreatePrinterSubscriptions int16 = 0x0016
OperationCreateJobSubscriptions int16 = 0x0017
OperationGetSubscriptionAttributes int16 = 0x0018
OperationGetSubscriptions int16 = 0x0019
OperationRenewSubscription int16 = 0x001a
OperationCancelSubscription int16 = 0x001b
OperationGetNotifications int16 = 0x001c
OperationSendNotifications int16 = 0x001d
OperationGetResourceAttributes int16 = 0x001e
OperationGetResourceData int16 = 0x001f
OperationGetResources int16 = 0x0020
OperationGetPrintSupportFiles int16 = 0x0021
OperationEnablePrinter int16 = 0x0022
OperationDisablePrinter int16 = 0x0023
OperationPausePrinterAfterCurrentJob int16 = 0x0024
OperationHoldNewJobs int16 = 0x0025
OperationReleaseHeldNewJobs int16 = 0x0026
OperationDeactivatePrinter int16 = 0x0027
OperationActivatePrinter int16 = 0x0028
OperationRestartPrinter int16 = 0x0029
OperationShutdownPrinter int16 = 0x002a
OperationStartupPrinter int16 = 0x002b
OperationReprocessJob int16 = 0x002c
OperationCancelCurrentJob int16 = 0x002d
OperationSuspendCurrentJob int16 = 0x002e
OperationResumeJob int16 = 0x002f
OperationOperationPromoteJob int16 = 0x0030
OperationScheduleJobAfter int16 = 0x0031
OperationCancelDocument int16 = 0x0033
OperationGetDocumentAttributes int16 = 0x0034
OperationGetDocuments int16 = 0x0035
OperationDeleteDocument int16 = 0x0036
OperationSetDocumentAttributes int16 = 0x0037
OperationCancelJobs int16 = 0x0038
OperationCancelMyJobs int16 = 0x0039
OperationResubmitJob int16 = 0x003a
OperationCloseJob int16 = 0x003b
OperationIdentifyPrinter int16 = 0x003c
OperationValidateDocument int16 = 0x003d
OperationAddDocumentImages int16 = 0x003e
OperationAcknowledgeDocument int16 = 0x003f
OperationAcknowledgeIdentifyPrinter int16 = 0x0040
OperationAcknowledgeJob int16 = 0x0041
OperationFetchDocument int16 = 0x0042
OperationFetchJob int16 = 0x0043
OperationGetOutputDeviceAttributes int16 = 0x0044
OperationUpdateActiveJobs int16 = 0x0045
OperationDeregisterOutputDevice int16 = 0x0046
OperationUpdateDocumentStatus int16 = 0x0047
OperationUpdateJobStatus int16 = 0x0048
OperationUpdateOutputDeviceAttributes int16 = 0x0049
OperationGetNextDocumentData int16 = 0x004a
OperationAllocatePrinterResources int16 = 0x004b
OperationCreatePrinter int16 = 0x004c
OperationDeallocatePrinterResources int16 = 0x004d
OperationDeletePrinter int16 = 0x004e
OperationGetPrinters int16 = 0x004f
OperationShutdownOnePrinter int16 = 0x0050
OperationStartupOnePrinter int16 = 0x0051
OperationCancelResource int16 = 0x0052
OperationCreateResource int16 = 0x0053
OperationInstallResource int16 = 0x0054
OperationSendResourceData int16 = 0x0055
OperationSetResourceAttributes int16 = 0x0056
OperationCreateResourceSubscriptions int16 = 0x0057
OperationCreateSystemSubscriptions int16 = 0x0058
OperationDisableAllPrinters int16 = 0x0059
OperationEnableAllPrinters int16 = 0x005a
OperationGetSystemAttributes int16 = 0x005b
OperationGetSystemSupportedValues int16 = 0x005c
OperationPauseAllPrinters int16 = 0x005d
OperationPauseAllPrintersAfterCurrentJob int16 = 0x005e
OperationRegisterOutputDevice int16 = 0x005f
OperationRestartSystem int16 = 0x0060
OperationResumeAllPrinters int16 = 0x0061
OperationSetSystemAttributes int16 = 0x0062
OperationShutdownAllPrinter int16 = 0x0063
OperationStartupAllPrinters int16 = 0x0064
OperationPrivate int16 = 0x4000
OperationCupsGetDefault int16 = 0x4001
OperationCupsGetPrinters int16 = 0x4002
OperationCupsAddModifyPrinter int16 = 0x4003
OperationCupsDeletePrinter int16 = 0x4004
OperationCupsGetClasses int16 = 0x4005
OperationCupsAddModifyClass int16 = 0x4006
OperationCupsDeleteClass int16 = 0x4007
OperationCupsAcceptJobs int16 = 0x4008
OperationCupsRejectJobs int16 = 0x4009
OperationCupsSetDefault int16 = 0x400a
OperationCupsGetDevices int16 = 0x400b
OperationCupsGetPPDs int16 = 0x400c
OperationCupsMoveJob int16 = 0x400d
OperationCupsAuthenticateJob int16 = 0x400e
OperationCupsGetPpd int16 = 0x400f
OperationCupsGetDocument int16 = 0x4027
OperationCupsCreateLocalPrinter int16 = 0x4028
)
// ipp tags
const (
TagCupsInvalid int8 = -1
TagZero int8 = 0x00
TagOperation int8 = 0x01
TagJob int8 = 0x02
TagEnd int8 = 0x03
TagPrinter int8 = 0x04
TagUnsupportedGroup int8 = 0x05
TagSubscription int8 = 0x06
TagEventNotification int8 = 0x07
TagResource int8 = 0x08
TagDocument int8 = 0x09
TagSystem int8 = 0x0a
TagUnsupportedValue int8 = 0x10
TagDefault int8 = 0x11
TagUnknown int8 = 0x12
TagNoValue int8 = 0x13
TagNotSettable int8 = 0x15
TagDeleteAttr int8 = 0x16
TagAdminDefine int8 = 0x17
TagInteger int8 = 0x21
TagBoolean int8 = 0x22
TagEnum int8 = 0x23
TagString int8 = 0x30
TagDate int8 = 0x31
TagResolution int8 = 0x32
TagRange int8 = 0x33
TagBeginCollection int8 = 0x34
TagTextLang int8 = 0x35
TagNameLang int8 = 0x36
TagEndCollection int8 = 0x37
TagText int8 = 0x41
TagName int8 = 0x42
TagReservedString int8 = 0x43
TagKeyword int8 = 0x44
TagUri int8 = 0x45
TagUriScheme int8 = 0x46
TagCharset int8 = 0x47
TagLanguage int8 = 0x48
TagMimeType int8 = 0x49
TagMemberName int8 = 0x4a
TagExtension int8 = 0x7f
)
// job states
const (
JobStatePending int8 = 0x03
JobStateHeld int8 = 0x04
JobStateProcessing int8 = 0x05
JobStateStopped int8 = 0x06
JobStateCanceled int8 = 0x07
JobStateAborted int8 = 0x08
JobStateCompleted int8 = 0x09
)
// document states
const (
DocumentStatePending int8 = 0x03
DocumentStateProcessing int8 = 0x05
DocumentStateCanceled int8 = 0x07
DocumentStateAborted int8 = 0x08
DocumentStateCompleted int8 = 0x08
)
// printer states
const (
PrinterStateIdle int8 = 0x0003
PrinterStateProcessing int8 = 0x0004
PrinterStateStopped int8 = 0x0005
)
// job state filter
const (
JobStateFilterNotCompleted = "not-completed"
JobStateFilterCompleted = "completed"
JobStateFilterAll = "all"
)
// error policies
const (
ErrorPolicyRetryJob = "retry-job"
ErrorPolicyAbortJob = "abort-job"
ErrorPolicyRetryCurrentJob = "retry-current-job"
ErrorPolicyStopPrinter = "stop-printer"
)
// ipp defaults
const (
CharsetLanguage = "en-US"
Charset = "utf-8"
ProtocolVersionMajor = int8(2)
ProtocolVersionMinor = int8(0)
DefaultJobPriority = 50
)
// useful mime types for ipp
const (
MimeTypePostscript = "application/postscript"
MimeTypeOctetStream = "application/octet-stream"
)
// ipp content types
const (
ContentTypeIPP = "application/ipp"
)
// known ipp attributes
const (
AttributeCopies = "copies"
AttributeDocumentFormat = "document-format"
AttributeDocumentName = "document-name"
AttributeJobID = "job-id"
AttributeJobName = "job-name"
AttributeJobPriority = "job-priority"
AttributeJobURI = "job-uri"
AttributeLastDocument = "last-document"
AttributeMyJobs = "my-jobs"
AttributePPDName = "ppd-name"
AttributePPDMakeAndModel = "ppd-make-and-model"
AttributePrinterIsShared = "printer-is-shared"
AttributePrinterIsTemporary = "printer-is-temporary"
AttributePrinterURI = "printer-uri"
AttributePurgeJobs = "purge-jobs"
AttributeRequestedAttributes = "requested-attributes"
AttributeRequestingUserName = "requesting-user-name"
AttributeWhichJobs = "which-jobs"
AttributeFirstJobID = "first-job-id"
AttributeLimit = "limit"
AttributeStatusMessage = "status-message"
AttributeCharset = "attributes-charset"
AttributeNaturalLanguage = "attributes-natural-language"
AttributeDeviceURI = "device-uri"
AttributeHoldJobUntil = "job-hold-until"
AttributePrinterErrorPolicy = "printer-error-policy"
AttributePrinterInfo = "printer-info"
AttributePrinterLocation = "printer-location"
AttributePrinterName = "printer-name"
AttributePrinterStateReasons = "printer-state-reasons"
AttributeJobPrinterURI = "job-printer-uri"
AttributeMemberURIs = "member-uris"
AttributeDocumentNumber = "document-number"
AttributeDocumentState = "document-state"
AttributeFinishings = "finishings"
AttributeJobHoldUntil = "hold-job-until"
AttributeJobSheets = "job-sheets"
AttributeJobState = "job-state"
AttributeJobStateReason = "job-state-reason"
AttributeMedia = "media"
AttributeSides = "sides"
AttributeNumberUp = "number-up"
AttributeOrientationRequested = "orientation-requested"
AttributePrintQuality = "print-quality"
AttributePrinterIsAcceptingJobs = "printer-is-accepting-jobs"
AttributePrinterResolution = "printer-resolution"
AttributePrinterState = "printer-state"
AttributeMemberNames = "member-names"
AttributePrinterType = "printer-type"
AttributePrinterMakeAndModel = "printer-make-and-model"
AttributePrinterStateMessage = "printer-state-message"
AttributePrinterUriSupported = "printer-uri-supported"
AttributeJobMediaProgress = "job-media-progress"
AttributeJobKilobyteOctets = "job-k-octets"
AttributeNumberOfDocuments = "number-of-documents"
AttributeJobOriginatingUserName = "job-originating-user-name"
AttributeOutputOrder = "outputorder"
AttributeJobStateReasons = "job-state-reasons"
AttributeJobStateMessage = "job-state-message"
AttributeJobPrinterStateReasons = "job-printer-state-reasons"
AttributeJobPrinterStateMessage = "job-printer-state-message"
AttributeJobImpressionsCompleted = "job-impressions-completed"
AttributePrintScaling = "print-scaling"
)
// Default attributes
var (
DefaultClassAttributes = []string{AttributePrinterName, AttributeMemberNames}
DefaultPrinterAttributes = []string{
AttributePrinterName, AttributePrinterType, AttributePrinterLocation, AttributePrinterInfo,
AttributePrinterMakeAndModel, AttributePrinterState, AttributePrinterStateMessage, AttributePrinterStateReasons,
AttributePrinterUriSupported, AttributeDeviceURI, AttributePrinterIsShared,
}
DefaultJobAttributes = []string{
AttributeJobID, AttributeJobName, AttributePrinterURI, AttributeJobState, AttributeJobStateReason,
AttributeJobHoldUntil, AttributeJobMediaProgress, AttributeJobKilobyteOctets, AttributeNumberOfDocuments, AttributeCopies,
AttributeJobOriginatingUserName,
}
)
// Attribute to tag mapping
var (
AttributeTagMapping = map[string]int8{
AttributeCharset: TagCharset,
AttributeNaturalLanguage: TagLanguage,
AttributeCopies: TagInteger,
AttributeDeviceURI: TagUri,
AttributeDocumentFormat: TagMimeType,
AttributeDocumentName: TagName,
AttributeDocumentNumber: TagInteger,
AttributeDocumentState: TagEnum,
AttributeFinishings: TagEnum,
AttributeJobHoldUntil: TagKeyword,
AttributeHoldJobUntil: TagKeyword,
AttributeJobID: TagInteger,
AttributeJobName: TagName,
AttributeJobPrinterURI: TagUri,
AttributeJobPriority: TagInteger,
AttributeJobSheets: TagName,
AttributeJobState: TagEnum,
AttributeJobStateReason: TagKeyword,
AttributeJobURI: TagUri,
AttributeLastDocument: TagBoolean,
AttributeMedia: TagKeyword,
AttributeSides: TagKeyword,
AttributeMemberURIs: TagUri,
AttributeMyJobs: TagBoolean,
AttributeNumberUp: TagInteger,
AttributeOrientationRequested: TagEnum,
AttributePPDName: TagName,
AttributePPDMakeAndModel: TagText,
AttributeNumberOfDocuments: TagInteger,
AttributePrintQuality: TagEnum,
AttributePrinterErrorPolicy: TagName,
AttributePrinterInfo: TagText,
AttributePrinterIsAcceptingJobs: TagBoolean,
AttributePrinterIsShared: TagBoolean,
AttributePrinterIsTemporary: TagBoolean,
AttributePrinterName: TagName,
AttributePrinterLocation: TagText,
AttributePrinterResolution: TagResolution,
AttributePrinterState: TagEnum,
AttributePrinterStateReasons: TagKeyword,
AttributePrinterURI: TagUri,
AttributePurgeJobs: TagBoolean,
AttributeRequestedAttributes: TagKeyword,
AttributeRequestingUserName: TagName,
AttributeWhichJobs: TagKeyword,
AttributeFirstJobID: TagInteger,
AttributeStatusMessage: TagText,
AttributeLimit: TagInteger,
AttributeOutputOrder: TagName,
AttributeJobStateReasons: TagString,
AttributeJobStateMessage: TagString,
AttributeJobPrinterStateReasons: TagString,
AttributeJobPrinterStateMessage: TagString,
AttributeJobImpressionsCompleted: TagInteger,
AttributePrintScaling: TagKeyword,
// IPP Subscription/Notification attributes (added for dankdots)
"notify-events": TagKeyword,
"notify-pull-method": TagKeyword,
"notify-lease-duration": TagInteger,
"notify-subscription-id": TagInteger,
"notify-subscription-ids": TagInteger,
"notify-sequence-numbers": TagInteger,
"notify-wait": TagBoolean,
"notify-recipient-uri": TagUri,
}
)

322
pkg/ipp/cups-client.go Normal file
View File

@@ -0,0 +1,322 @@
package ipp
import (
"bytes"
"strings"
)
// CUPSClient implements a ipp client with specific cups operations
type CUPSClient struct {
*IPPClient
}
// 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}
}
// NewCUPSClientWithAdapter 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)
resp, err := c.SendRequest(c.adapter.GetHttpUri("", nil), req, nil)
if err != nil {
return nil, err
}
printerNameMap := make(map[string]Attributes)
for _, printerAttributes := range resp.PrinterAttributes {
printerNameMap[printerAttributes[AttributeDeviceURI][0].Value.(string)] = printerAttributes
}
return printerNameMap, nil
}
// MoveJob moves a job to a other printer
func (c *CUPSClient) MoveJob(jobID int, destPrinter string) error {
req := NewRequest(OperationCupsMoveJob, 1)
req.OperationAttributes[AttributeJobURI] = c.getJobUri(jobID)
req.PrinterAttributes[AttributeJobPrinterURI] = c.getPrinterUri(destPrinter)
_, err := c.SendRequest(c.adapter.GetHttpUri("jobs", ""), req, nil)
return err
}
// MoveAllJob moves all job from a printer to a other printer
func (c *CUPSClient) MoveAllJob(srcPrinter, destPrinter string) error {
req := NewRequest(OperationCupsMoveJob, 1)
req.OperationAttributes[AttributePrinterURI] = c.getPrinterUri(srcPrinter)
req.PrinterAttributes[AttributeJobPrinterURI] = c.getPrinterUri(destPrinter)
_, err := c.SendRequest(c.adapter.GetHttpUri("jobs", ""), req, nil)
return err
}
// GetPPDs returns a map of ppd names and attributes
func (c *CUPSClient) GetPPDs() (map[string]Attributes, error) {
req := NewRequest(OperationCupsGetPPDs, 1)
resp, err := c.SendRequest(c.adapter.GetHttpUri("", nil), req, nil)
if err != nil {
return nil, err
}
ppdNameMap := make(map[string]Attributes)
for _, printerAttributes := range resp.PrinterAttributes {
ppdNameMap[printerAttributes[AttributePPDName][0].Value.(string)] = printerAttributes
}
return ppdNameMap, nil
}
// AcceptJobs lets a printer accept jobs again
func (c *CUPSClient) AcceptJobs(printer string) error {
req := NewRequest(OperationCupsAcceptJobs, 1)
req.OperationAttributes[AttributePrinterURI] = c.getPrinterUri(printer)
_, err := c.SendRequest(c.adapter.GetHttpUri("admin", ""), req, nil)
return err
}
// RejectJobs does not let a printer accept jobs
func (c *CUPSClient) RejectJobs(printer string) error {
req := NewRequest(OperationCupsRejectJobs, 1)
req.OperationAttributes[AttributePrinterURI] = c.getPrinterUri(printer)
_, err := c.SendRequest(c.adapter.GetHttpUri("admin", ""), req, nil)
return err
}
// AddPrinterToClass adds a printer to a class, if the class does not exists it will be crated
func (c *CUPSClient) AddPrinterToClass(class, printer string) error {
attributes, err := c.GetPrinterAttributes(class, []string{AttributeMemberURIs})
if err != nil && !IsNotExistsError(err) {
return err
}
memberURIList := make([]string, 0)
if !IsNotExistsError(err) {
for _, member := range attributes[AttributeMemberURIs] {
memberString := strings.Split(member.Value.(string), "/")
printerName := memberString[len(memberString)-1]
if printerName == printer {
return nil
}
memberURIList = append(memberURIList, member.Value.(string))
}
}
memberURIList = append(memberURIList, c.getPrinterUri(printer))
req := NewRequest(OperationCupsAddModifyClass, 1)
req.OperationAttributes[AttributePrinterURI] = c.getClassUri(class)
req.PrinterAttributes[AttributeMemberURIs] = memberURIList
_, err = c.SendRequest(c.adapter.GetHttpUri("admin", ""), req, nil)
return err
}
// DeletePrinterFromClass removes a printer from a class, if a class has no more printer it will be deleted
func (c *CUPSClient) DeletePrinterFromClass(class, printer string) error {
attributes, err := c.GetPrinterAttributes(class, []string{AttributeMemberURIs})
if err != nil {
return err
}
memberURIList := make([]string, 0)
for _, member := range attributes[AttributeMemberURIs] {
memberString := strings.Split(member.Value.(string), "/")
printerName := memberString[len(memberString)-1]
if printerName != printer {
memberURIList = append(memberURIList, member.Value.(string))
}
}
if len(memberURIList) == 0 {
return c.DeleteClass(class)
}
req := NewRequest(OperationCupsAddModifyClass, 1)
req.OperationAttributes[AttributePrinterURI] = c.getClassUri(class)
req.PrinterAttributes[AttributeMemberURIs] = memberURIList
_, err = c.SendRequest(c.adapter.GetHttpUri("admin", ""), req, nil)
return err
}
// DeleteClass deletes a class
func (c *CUPSClient) DeleteClass(class string) error {
req := NewRequest(OperationCupsDeleteClass, 1)
req.OperationAttributes[AttributePrinterURI] = c.getClassUri(class)
_, err := c.SendRequest(c.adapter.GetHttpUri("admin", ""), req, nil)
return err
}
// CreatePrinter creates a new printer
func (c *CUPSClient) CreatePrinter(name, deviceURI, ppd string, shared bool, errorPolicy string, information, location string) error {
req := NewRequest(OperationCupsAddModifyPrinter, 1)
req.OperationAttributes[AttributePrinterURI] = c.getPrinterUri(name)
req.OperationAttributes[AttributePPDName] = ppd
req.OperationAttributes[AttributePrinterIsShared] = shared
req.PrinterAttributes[AttributePrinterStateReasons] = "none"
req.PrinterAttributes[AttributeDeviceURI] = deviceURI
req.PrinterAttributes[AttributePrinterInfo] = information
req.PrinterAttributes[AttributePrinterLocation] = location
req.PrinterAttributes[AttributePrinterErrorPolicy] = errorPolicy
_, err := c.SendRequest(c.adapter.GetHttpUri("admin", ""), req, nil)
return err
}
// SetPrinterPPD sets the ppd for a printer
func (c *CUPSClient) SetPrinterPPD(printer, ppd string) error {
req := NewRequest(OperationCupsAddModifyPrinter, 1)
req.OperationAttributes[AttributePrinterURI] = c.getPrinterUri(printer)
req.OperationAttributes[AttributePPDName] = ppd
_, err := c.SendRequest(c.adapter.GetHttpUri("admin", ""), req, nil)
return err
}
// SetPrinterDeviceURI sets the device uri for a printer
func (c *CUPSClient) SetPrinterDeviceURI(printer, deviceURI string) error {
req := NewRequest(OperationCupsAddModifyPrinter, 1)
req.OperationAttributes[AttributePrinterURI] = c.getPrinterUri(printer)
req.PrinterAttributes[AttributeDeviceURI] = deviceURI
_, err := c.SendRequest(c.adapter.GetHttpUri("admin", ""), req, nil)
return err
}
// SetPrinterIsShared shares or unshares a printer in the network
func (c *CUPSClient) SetPrinterIsShared(printer string, shared bool) error {
req := NewRequest(OperationCupsAddModifyPrinter, 1)
req.OperationAttributes[AttributePrinterURI] = c.getPrinterUri(printer)
req.OperationAttributes[AttributePrinterIsShared] = shared
_, err := c.SendRequest(c.adapter.GetHttpUri("admin", ""), req, nil)
return err
}
// SetPrinterErrorPolicy sets the error policy for a printer
func (c *CUPSClient) SetPrinterErrorPolicy(printer string, errorPolicy string) error {
req := NewRequest(OperationCupsAddModifyPrinter, 1)
req.OperationAttributes[AttributePrinterURI] = c.getPrinterUri(printer)
req.PrinterAttributes[AttributePrinterErrorPolicy] = errorPolicy
_, err := c.SendRequest(c.adapter.GetHttpUri("admin", ""), req, nil)
return err
}
// SetPrinterInformation sets general printer information
func (c *CUPSClient) SetPrinterInformation(printer, information string) error {
req := NewRequest(OperationCupsAddModifyPrinter, 1)
req.OperationAttributes[AttributePrinterURI] = c.getPrinterUri(printer)
req.PrinterAttributes[AttributePrinterInfo] = information
_, err := c.SendRequest(c.adapter.GetHttpUri("admin", ""), req, nil)
return err
}
// SetPrinterLocation sets the printer location
func (c *CUPSClient) SetPrinterLocation(printer, location string) error {
req := NewRequest(OperationCupsAddModifyPrinter, 1)
req.OperationAttributes[AttributePrinterURI] = c.getPrinterUri(printer)
req.PrinterAttributes[AttributePrinterLocation] = location
_, err := c.SendRequest(c.adapter.GetHttpUri("admin", ""), req, nil)
return err
}
// DeletePrinter deletes a printer
func (c *CUPSClient) DeletePrinter(printer string) error {
req := NewRequest(OperationCupsDeletePrinter, 1)
req.OperationAttributes[AttributePrinterURI] = c.getPrinterUri(printer)
_, err := c.SendRequest(c.adapter.GetHttpUri("admin", ""), req, nil)
return err
}
// GetPrinters returns a map of printer names and attributes
func (c *CUPSClient) GetPrinters(attributes []string) (map[string]Attributes, error) {
req := NewRequest(OperationCupsGetPrinters, 1)
if attributes == nil {
req.OperationAttributes[AttributeRequestedAttributes] = DefaultPrinterAttributes
} else {
req.OperationAttributes[AttributeRequestedAttributes] = append(attributes, AttributePrinterName)
}
resp, err := c.SendRequest(c.adapter.GetHttpUri("", nil), req, nil)
if err != nil {
return nil, err
}
printerNameMap := make(map[string]Attributes)
for _, printerAttributes := range resp.PrinterAttributes {
printerNameMap[printerAttributes[AttributePrinterName][0].Value.(string)] = printerAttributes
}
return printerNameMap, nil
}
// GetClasses returns a map of class names and attributes
func (c *CUPSClient) GetClasses(attributes []string) (map[string]Attributes, error) {
req := NewRequest(OperationCupsGetClasses, 1)
if attributes == nil {
req.OperationAttributes[AttributeRequestedAttributes] = DefaultClassAttributes
} else {
req.OperationAttributes[AttributeRequestedAttributes] = append(attributes, AttributePrinterName)
}
resp, err := c.SendRequest(c.adapter.GetHttpUri("", nil), req, nil)
if err != nil {
return nil, err
}
printerNameMap := make(map[string]Attributes)
for _, printerAttributes := range resp.PrinterAttributes {
printerNameMap[printerAttributes[AttributePrinterName][0].Value.(string)] = printerAttributes
}
return printerNameMap, nil
}
// PrintTestPage prints a test page of type application/vnd.cups-pdf-banner
func (c *CUPSClient) PrintTestPage(printer string) (int, error) {
testPage := new(bytes.Buffer)
testPage.WriteString("#PDF-BANNER\n")
testPage.WriteString("Template default-testpage.pdf\n")
testPage.WriteString("Show printer-name printer-info printer-location printer-make-and-model printer-driver-name")
testPage.WriteString("printer-driver-version paper-size imageable-area job-id options time-at-creation")
testPage.WriteString("time-at-processing\n\n")
return c.PrintDocuments([]Document{
{
Document: testPage,
Name: "Test Page",
Size: testPage.Len(),
MimeType: MimeTypePostscript,
},
}, printer, map[string]interface{}{
AttributeJobName: "Test Page",
})
}

31
pkg/ipp/error.go Normal file
View File

@@ -0,0 +1,31 @@
package ipp
import "fmt"
// IsNotExistsError checks a given error whether a printer or class does not exist
func IsNotExistsError(err error) bool {
if err == nil {
return false
}
return err.Error() == "The printer or class does not exist."
}
// IPPError used for non ok ipp status codes
type IPPError struct {
Status int16
Message string
}
func (e IPPError) Error() string {
return fmt.Sprintf("ipp status: %d, message: %s", e.Status, e.Message)
}
// HTTPError used for non 200 http codes
type HTTPError struct {
Code int
}
func (e HTTPError) Error() string {
return fmt.Sprintf("got http code %d", e.Code)
}

329
pkg/ipp/ipp-client.go Normal file
View File

@@ -0,0 +1,329 @@
package ipp
import (
"errors"
"fmt"
"io"
"os"
"path"
)
// Document wraps an io.Reader with more information, needed for encoding
type Document struct {
Document io.Reader
Size int
Name string
MimeType string
}
// IPPClient implements a generic ipp client
type IPPClient struct {
username string
adapter Adapter
}
// NewIPPClient creates a new generic ipp client (used HttpAdapter internally)
func NewIPPClient(host string, port int, username, password string, useTLS bool) *IPPClient {
adapter := NewHttpAdapter(host, port, username, password, useTLS)
return &IPPClient{
username: username,
adapter: adapter,
}
}
// NewIPPClientWithAdapter creates a new generic ipp client with given Adapter
func NewIPPClientWithAdapter(username string, adapter Adapter) *IPPClient {
return &IPPClient{
username: username,
adapter: adapter,
}
}
func (c *IPPClient) getPrinterUri(printer string) string {
return fmt.Sprintf("ipp://localhost/printers/%s", printer)
}
func (c *IPPClient) getJobUri(jobID int) string {
return fmt.Sprintf("ipp://localhost/jobs/%d", jobID)
}
func (c *IPPClient) getClassUri(printer string) string {
return fmt.Sprintf("ipp://localhost/classes/%s", printer)
}
// 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) {
if _, ok := req.OperationAttributes[AttributeRequestingUserName]; !ok {
req.OperationAttributes[AttributeRequestingUserName] = c.username
}
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
func (c *IPPClient) PrintDocuments(docs []Document, printer string, jobAttributes map[string]interface{}) (int, error) {
printerURI := c.getPrinterUri(printer)
req := NewRequest(OperationCreateJob, 1)
req.OperationAttributes[AttributePrinterURI] = printerURI
req.OperationAttributes[AttributeRequestingUserName] = c.username
// set defaults for some attributes, may get overwritten
req.OperationAttributes[AttributeJobName] = docs[0].Name
req.OperationAttributes[AttributeCopies] = 1
req.OperationAttributes[AttributeJobPriority] = DefaultJobPriority
for key, value := range jobAttributes {
req.JobAttributes[key] = value
}
resp, err := c.SendRequest(c.adapter.GetHttpUri("printers", printer), req, nil)
if err != nil {
return -1, err
}
if len(resp.JobAttributes) == 0 {
return 0, errors.New("server doesn't returned a job id")
}
jobID := resp.JobAttributes[0][AttributeJobID][0].Value.(int)
documentCount := len(docs) - 1
for docID, doc := range docs {
req = NewRequest(OperationSendDocument, 2)
req.OperationAttributes[AttributePrinterURI] = printerURI
req.OperationAttributes[AttributeRequestingUserName] = c.username
req.OperationAttributes[AttributeJobID] = jobID
req.OperationAttributes[AttributeDocumentName] = doc.Name
req.OperationAttributes[AttributeDocumentFormat] = doc.MimeType
req.OperationAttributes[AttributeLastDocument] = docID == documentCount
req.File = doc.Document
req.FileSize = doc.Size
_, err = c.SendRequest(c.adapter.GetHttpUri("printers", printer), req, nil)
if err != nil {
return -1, err
}
}
return jobID, nil
}
// PrintJob prints a document using a Print-Job operation. custom job settings can be specified via the jobAttributes parameter
func (c *IPPClient) PrintJob(doc Document, printer string, jobAttributes map[string]interface{}) (int, error) {
printerURI := c.getPrinterUri(printer)
req := NewRequest(OperationPrintJob, 1)
req.OperationAttributes[AttributePrinterURI] = printerURI
req.OperationAttributes[AttributeRequestingUserName] = c.username
req.OperationAttributes[AttributeJobName] = doc.Name
req.OperationAttributes[AttributeDocumentFormat] = doc.MimeType
// set defaults for some attributes, may get overwritten
req.OperationAttributes[AttributeCopies] = 1
req.OperationAttributes[AttributeJobPriority] = DefaultJobPriority
for key, value := range jobAttributes {
req.JobAttributes[key] = value
}
req.File = doc.Document
req.FileSize = doc.Size
resp, err := c.SendRequest(c.adapter.GetHttpUri("printers", printer), req, nil)
if err != nil {
return -1, err
}
if len(resp.JobAttributes) == 0 {
return 0, errors.New("server doesn't returned a job id")
}
jobID := resp.JobAttributes[0][AttributeJobID][0].Value.(int)
return jobID, nil
}
// PrintFile prints a local file on the file system. custom job settings can be specified via the jobAttributes parameter
func (c *IPPClient) PrintFile(filePath, printer string, jobAttributes map[string]interface{}) (int, error) {
fileStats, err := os.Stat(filePath)
if os.IsNotExist(err) {
return -1, err
}
fileName := path.Base(filePath)
document, err := os.Open(filePath)
if err != nil {
return 0, err
}
defer document.Close()
jobAttributes[AttributeJobName] = fileName
return c.PrintDocuments([]Document{
{
Document: document,
Name: fileName,
Size: int(fileStats.Size()),
MimeType: MimeTypeOctetStream,
},
}, printer, jobAttributes)
}
// GetPrinterAttributes returns the requested attributes for the specified printer, if attributes is nil the default attributes will be used
func (c *IPPClient) GetPrinterAttributes(printer string, attributes []string) (Attributes, error) {
req := NewRequest(OperationGetPrinterAttributes, 1)
req.OperationAttributes[AttributePrinterURI] = c.getPrinterUri(printer)
req.OperationAttributes[AttributeRequestingUserName] = c.username
if attributes == nil {
req.OperationAttributes[AttributeRequestedAttributes] = DefaultPrinterAttributes
} else {
req.OperationAttributes[AttributeRequestedAttributes] = attributes
}
resp, err := c.SendRequest(c.adapter.GetHttpUri("printers", printer), req, nil)
if err != nil {
return nil, err
}
if len(resp.PrinterAttributes) == 0 {
return nil, errors.New("server doesn't return any printer attributes")
}
return resp.PrinterAttributes[0], nil
}
// ResumePrinter resumes a printer
func (c *IPPClient) ResumePrinter(printer string) error {
req := NewRequest(OperationResumePrinter, 1)
req.OperationAttributes[AttributePrinterURI] = c.getPrinterUri(printer)
_, err := c.SendRequest(c.adapter.GetHttpUri("admin", ""), req, nil)
return err
}
// PausePrinter pauses a printer
func (c *IPPClient) PausePrinter(printer string) error {
req := NewRequest(OperationPausePrinter, 1)
req.OperationAttributes[AttributePrinterURI] = c.getPrinterUri(printer)
_, err := c.SendRequest(c.adapter.GetHttpUri("admin", ""), req, nil)
return err
}
// GetJobAttributes returns the requested attributes for the specified job, if attributes is nil the default job will be used
func (c *IPPClient) GetJobAttributes(jobID int, attributes []string) (Attributes, error) {
req := NewRequest(OperationGetJobAttributes, 1)
req.OperationAttributes[AttributeJobURI] = c.getJobUri(jobID)
if attributes == nil {
req.OperationAttributes[AttributeRequestedAttributes] = DefaultJobAttributes
} else {
req.OperationAttributes[AttributeRequestedAttributes] = attributes
}
resp, err := c.SendRequest(c.adapter.GetHttpUri("jobs", jobID), req, nil)
if err != nil {
return nil, err
}
if len(resp.JobAttributes) == 0 {
return nil, errors.New("server doesn't return any job attributes")
}
return resp.JobAttributes[0], nil
}
// GetJobs returns jobs from a printer or class
func (c *IPPClient) GetJobs(printer, class string, whichJobs string, myJobs bool, firstJobId, limit int, attributes []string) (map[int]Attributes, error) {
req := NewRequest(OperationGetJobs, 1)
req.OperationAttributes[AttributeWhichJobs] = whichJobs
req.OperationAttributes[AttributeMyJobs] = myJobs
if printer != "" {
req.OperationAttributes[AttributePrinterURI] = c.getPrinterUri(printer)
} else if class != "" {
req.OperationAttributes[AttributePrinterURI] = c.getClassUri(printer)
} else {
req.OperationAttributes[AttributePrinterURI] = "ipp://localhost/"
}
if firstJobId > 0 {
req.OperationAttributes[AttributeFirstJobID] = firstJobId
}
if limit > 0 {
req.OperationAttributes[AttributeLimit] = limit
}
if myJobs {
req.OperationAttributes[AttributeRequestingUserName] = c.username
}
if attributes == nil {
req.OperationAttributes[AttributeRequestedAttributes] = DefaultJobAttributes
} else {
req.OperationAttributes[AttributeRequestedAttributes] = append(attributes, AttributeJobID)
}
resp, err := c.SendRequest(c.adapter.GetHttpUri("", nil), req, nil)
if err != nil {
return nil, err
}
jobIDMap := make(map[int]Attributes)
for _, jobAttributes := range resp.JobAttributes {
jobIDMap[jobAttributes[AttributeJobID][0].Value.(int)] = jobAttributes
}
return jobIDMap, nil
}
// CancelJob cancels a job. if purge is true, the job will also be removed
func (c *IPPClient) CancelJob(jobID int, purge bool) error {
req := NewRequest(OperationCancelJob, 1)
req.OperationAttributes[AttributeJobURI] = c.getJobUri(jobID)
req.OperationAttributes[AttributePurgeJobs] = purge
_, err := c.SendRequest(c.adapter.GetHttpUri("jobs", ""), req, nil)
return err
}
// CancelAllJob cancels all jobs for a specified printer. if purge is true, the jobs will also be removed
func (c *IPPClient) CancelAllJob(printer string, purge bool) error {
req := NewRequest(OperationCancelJobs, 1)
req.OperationAttributes[AttributePrinterURI] = c.getPrinterUri(printer)
req.OperationAttributes[AttributePurgeJobs] = purge
_, err := c.SendRequest(c.adapter.GetHttpUri("admin", ""), req, nil)
return err
}
// RestartJob restarts a job
func (c *IPPClient) RestartJob(jobID int) error {
req := NewRequest(OperationRestartJob, 1)
req.OperationAttributes[AttributeJobURI] = c.getJobUri(jobID)
_, err := c.SendRequest(c.adapter.GetHttpUri("jobs", ""), req, nil)
return err
}
// HoldJobUntil holds a job
func (c *IPPClient) HoldJobUntil(jobID int, holdUntil string) error {
req := NewRequest(OperationRestartJob, 1)
req.OperationAttributes[AttributeJobURI] = c.getJobUri(jobID)
req.JobAttributes[AttributeHoldJobUntil] = holdUntil
_, err := c.SendRequest(c.adapter.GetHttpUri("jobs", ""), req, nil)
return err
}
// TestConnection tests if a tcp connection to the remote server is possible
func (c *IPPClient) TestConnection() error {
return c.adapter.TestConnection()
}

299
pkg/ipp/request.go Normal file
View File

@@ -0,0 +1,299 @@
package ipp
import (
"bytes"
"encoding/binary"
"io"
)
// Request defines a ipp request
type Request struct {
ProtocolVersionMajor int8
ProtocolVersionMinor int8
Operation int16
RequestId int32
OperationAttributes map[string]interface{}
JobAttributes map[string]interface{}
PrinterAttributes map[string]interface{}
SubscriptionAttributes map[string]interface{} // Added for subscription operations
File io.Reader
FileSize int
}
// NewRequest creates a new ipp request
func NewRequest(op int16, reqID int32) *Request {
return &Request{
ProtocolVersionMajor: ProtocolVersionMajor,
ProtocolVersionMinor: ProtocolVersionMinor,
Operation: op,
RequestId: reqID,
OperationAttributes: make(map[string]interface{}),
JobAttributes: make(map[string]interface{}),
PrinterAttributes: make(map[string]interface{}),
SubscriptionAttributes: make(map[string]interface{}),
File: nil,
FileSize: -1,
}
}
// Encode encodes the request to a byte slice
func (r *Request) Encode() ([]byte, error) {
buf := new(bytes.Buffer)
enc := NewAttributeEncoder(buf)
if err := binary.Write(buf, binary.BigEndian, r.ProtocolVersionMajor); err != nil {
return nil, err
}
if err := binary.Write(buf, binary.BigEndian, r.ProtocolVersionMinor); err != nil {
return nil, err
}
if err := binary.Write(buf, binary.BigEndian, r.Operation); err != nil {
return nil, err
}
if err := binary.Write(buf, binary.BigEndian, r.RequestId); err != nil {
return nil, err
}
if err := binary.Write(buf, binary.BigEndian, TagOperation); err != nil {
return nil, err
}
if r.OperationAttributes == nil {
r.OperationAttributes = make(map[string]interface{}, 2)
}
if _, found := r.OperationAttributes[AttributeCharset]; !found {
r.OperationAttributes[AttributeCharset] = Charset
}
if _, found := r.OperationAttributes[AttributeNaturalLanguage]; !found {
r.OperationAttributes[AttributeNaturalLanguage] = CharsetLanguage
}
if err := r.encodeOperationAttributes(enc); err != nil {
return nil, err
}
if len(r.JobAttributes) > 0 {
if err := binary.Write(buf, binary.BigEndian, TagJob); err != nil {
return nil, err
}
for attr, value := range r.JobAttributes {
if err := enc.Encode(attr, value); err != nil {
return nil, err
}
}
}
if len(r.PrinterAttributes) > 0 {
if err := binary.Write(buf, binary.BigEndian, TagPrinter); err != nil {
return nil, err
}
for attr, value := range r.PrinterAttributes {
if err := enc.Encode(attr, value); err != nil {
return nil, err
}
}
}
if len(r.SubscriptionAttributes) > 0 {
if err := binary.Write(buf, binary.BigEndian, TagSubscription); err != nil {
return nil, err
}
if err := r.encodeSubscriptionAttributes(enc); err != nil {
return nil, err
}
}
if err := binary.Write(buf, binary.BigEndian, TagEnd); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func (r *Request) encodeOperationAttributes(enc *AttributeEncoder) error {
ordered := []string{
AttributeCharset,
AttributeNaturalLanguage,
AttributePrinterURI,
AttributeJobID,
}
for _, attr := range ordered {
if value, ok := r.OperationAttributes[attr]; ok {
delete(r.OperationAttributes, attr)
if err := enc.Encode(attr, value); err != nil {
return err
}
}
}
for attr, value := range r.OperationAttributes {
if err := enc.Encode(attr, value); err != nil {
return err
}
}
return nil
}
func (r *Request) encodeSubscriptionAttributes(enc *AttributeEncoder) error {
// Encode subscription attributes in proper order
// notify-pull-method and notify-lease-duration must come before notify-events
ordered := []string{
"notify-pull-method",
"notify-lease-duration",
"notify-events",
}
for _, attr := range ordered {
if value, ok := r.SubscriptionAttributes[attr]; ok {
delete(r.SubscriptionAttributes, attr)
if err := enc.Encode(attr, value); err != nil {
return err
}
}
}
// Encode any remaining subscription attributes
for attr, value := range r.SubscriptionAttributes {
if err := enc.Encode(attr, value); err != nil {
return err
}
}
return nil
}
// RequestDecoder reads and decodes a request from a stream
type RequestDecoder struct {
reader io.Reader
}
// NewRequestDecoder returns a new decoder that reads from r
func NewRequestDecoder(r io.Reader) *RequestDecoder {
return &RequestDecoder{
reader: r,
}
}
// Decode decodes a ipp request into a request struct. additional data will be written to an io.Writer if data is not nil
func (d *RequestDecoder) Decode(data io.Writer) (*Request, error) {
req := new(Request)
if err := binary.Read(d.reader, binary.BigEndian, &req.ProtocolVersionMajor); err != nil {
return nil, err
}
if err := binary.Read(d.reader, binary.BigEndian, &req.ProtocolVersionMinor); err != nil {
return nil, err
}
if err := binary.Read(d.reader, binary.BigEndian, &req.Operation); err != nil {
return nil, err
}
if err := binary.Read(d.reader, binary.BigEndian, &req.RequestId); err != nil {
return nil, err
}
startByteSlice := make([]byte, 1)
tag := TagCupsInvalid
previousAttributeName := ""
tagSet := false
attribDecoder := NewAttributeDecoder(d.reader)
// decode attribute buffer
for {
if _, err := d.reader.Read(startByteSlice); err != nil {
// when we read from a stream, we may get an EOF if we want to read the end tag
// all data should be read and we can ignore the error
if err == io.EOF {
break
}
return nil, err
}
startByte := int8(startByteSlice[0])
// check if attributes are completed
if startByte == TagEnd {
break
}
if startByte == TagOperation {
if req.OperationAttributes == nil {
req.OperationAttributes = make(map[string]interface{})
}
tag = TagOperation
tagSet = true
}
if startByte == TagJob {
if req.JobAttributes == nil {
req.JobAttributes = make(map[string]interface{})
}
tag = TagJob
tagSet = true
}
if startByte == TagPrinter {
if req.PrinterAttributes == nil {
req.PrinterAttributes = make(map[string]interface{})
}
tag = TagPrinter
tagSet = true
}
if tagSet {
if _, err := d.reader.Read(startByteSlice); err != nil {
return nil, err
}
startByte = int8(startByteSlice[0])
}
attrib, err := attribDecoder.Decode(startByte)
if err != nil {
return nil, err
}
if attrib.Name != "" {
appendAttributeToRequest(req, tag, attrib.Name, attrib.Value)
previousAttributeName = attrib.Name
} else {
appendAttributeToRequest(req, tag, previousAttributeName, attrib.Value)
}
tagSet = false
}
if data != nil {
if _, err := io.Copy(data, d.reader); err != nil {
return nil, err
}
}
return req, nil
}
func appendAttributeToRequest(req *Request, tag int8, name string, value interface{}) {
switch tag {
case TagOperation:
req.OperationAttributes[name] = value
case TagPrinter:
req.PrinterAttributes[name] = value
case TagJob:
req.JobAttributes[name] = value
}
}

383
pkg/ipp/response.go Normal file
View File

@@ -0,0 +1,383 @@
package ipp
import (
"bytes"
"encoding/binary"
"io"
)
// Attributes is a wrapper for a set of attributes
type Attributes map[string][]Attribute
// Response defines a ipp response
type Response struct {
ProtocolVersionMajor int8
ProtocolVersionMinor int8
StatusCode int16
RequestId int32
OperationAttributes Attributes
PrinterAttributes []Attributes
JobAttributes []Attributes
SubscriptionAttributes []Attributes // Added for subscription responses
}
// CheckForErrors checks the status code and returns a error if it is not zero. it also returns the status message if provided by the server
func (r *Response) CheckForErrors() error {
if r.StatusCode != StatusOk {
err := IPPError{
Status: r.StatusCode,
Message: "no status message returned",
}
if len(r.OperationAttributes["status-message"]) > 0 {
err.Message = r.OperationAttributes["status-message"][0].Value.(string)
}
return err
}
return nil
}
// NewResponse creates a new ipp response
func NewResponse(statusCode int16, reqID int32) *Response {
return &Response{
ProtocolVersionMajor: ProtocolVersionMajor,
ProtocolVersionMinor: ProtocolVersionMinor,
StatusCode: statusCode,
RequestId: reqID,
OperationAttributes: make(Attributes),
PrinterAttributes: make([]Attributes, 0),
JobAttributes: make([]Attributes, 0),
}
}
// Encode encodes the response to a byte slice
func (r *Response) Encode() ([]byte, error) {
buf := new(bytes.Buffer)
enc := NewAttributeEncoder(buf)
if err := binary.Write(buf, binary.BigEndian, r.ProtocolVersionMajor); err != nil {
return nil, err
}
if err := binary.Write(buf, binary.BigEndian, r.ProtocolVersionMinor); err != nil {
return nil, err
}
if err := binary.Write(buf, binary.BigEndian, r.StatusCode); err != nil {
return nil, err
}
if err := binary.Write(buf, binary.BigEndian, r.RequestId); err != nil {
return nil, err
}
if err := binary.Write(buf, binary.BigEndian, TagOperation); err != nil {
return nil, err
}
if r.OperationAttributes == nil {
r.OperationAttributes = make(Attributes, 0)
}
if _, found := r.OperationAttributes[AttributeCharset]; !found {
r.OperationAttributes[AttributeCharset] = []Attribute{
{
Value: Charset,
},
}
}
if _, found := r.OperationAttributes[AttributeNaturalLanguage]; !found {
r.OperationAttributes[AttributeNaturalLanguage] = []Attribute{
{
Value: CharsetLanguage,
},
}
}
if err := r.encodeOperationAttributes(enc); err != nil {
return nil, err
}
if len(r.PrinterAttributes) > 0 {
for _, printerAttr := range r.PrinterAttributes {
if err := binary.Write(buf, binary.BigEndian, TagPrinter); err != nil {
return nil, err
}
for name, attr := range printerAttr {
if len(attr) == 0 {
continue
}
values := make([]interface{}, len(attr))
for i, v := range attr {
values[i] = v.Value
}
if len(values) == 1 {
if err := enc.Encode(name, values[0]); err != nil {
return nil, err
}
} else {
if err := enc.Encode(name, values); err != nil {
return nil, err
}
}
}
}
}
if len(r.JobAttributes) > 0 {
for _, jobAttr := range r.JobAttributes {
if err := binary.Write(buf, binary.BigEndian, TagJob); err != nil {
return nil, err
}
for name, attr := range jobAttr {
if len(attr) == 0 {
continue
}
values := make([]interface{}, len(attr))
for i, v := range attr {
values[i] = v.Value
}
if len(values) == 1 {
if err := enc.Encode(name, values[0]); err != nil {
return nil, err
}
} else {
if err := enc.Encode(name, values); err != nil {
return nil, err
}
}
}
}
}
if err := binary.Write(buf, binary.BigEndian, TagEnd); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func (r *Response) encodeOperationAttributes(enc *AttributeEncoder) error {
ordered := []string{
AttributeCharset,
AttributeNaturalLanguage,
AttributePrinterURI,
AttributeJobID,
}
for _, name := range ordered {
if attr, ok := r.OperationAttributes[name]; ok {
delete(r.OperationAttributes, name)
if err := encodeOperationAttribute(enc, name, attr); err != nil {
return err
}
}
}
for name, attr := range r.OperationAttributes {
if err := encodeOperationAttribute(enc, name, attr); err != nil {
return err
}
}
return nil
}
func encodeOperationAttribute(enc *AttributeEncoder, name string, attr []Attribute) error {
if len(attr) == 0 {
return nil
}
values := make([]interface{}, len(attr))
for i, v := range attr {
values[i] = v.Value
}
if len(values) == 1 {
return enc.Encode(name, values[0])
}
return enc.Encode(name, values)
}
// ResponseDecoder reads and decodes a response from a stream
type ResponseDecoder struct {
reader io.Reader
}
// NewResponseDecoder returns a new decoder that reads from r
func NewResponseDecoder(r io.Reader) *ResponseDecoder {
return &ResponseDecoder{
reader: r,
}
}
// Decode decodes a ipp response into a response struct. additional data will be written to an io.Writer if data is not nil
func (d *ResponseDecoder) Decode(data io.Writer) (*Response, error) {
/*
1 byte: Protocol Major Version - b
1 byte: Protocol Minor Version - b
2 byte: Status ID - h
4 byte: Request ID - i
1 byte: Operation Attribute Byte (\0x01)
N times: Attributes
1 byte: Attribute End Byte (\0x03)
*/
resp := new(Response)
// wrap the reader so we have more functionality
// reader := bufio.NewReader(d.reader)
if err := binary.Read(d.reader, binary.BigEndian, &resp.ProtocolVersionMajor); err != nil {
return nil, err
}
if err := binary.Read(d.reader, binary.BigEndian, &resp.ProtocolVersionMinor); err != nil {
return nil, err
}
if err := binary.Read(d.reader, binary.BigEndian, &resp.StatusCode); err != nil {
return nil, err
}
if err := binary.Read(d.reader, binary.BigEndian, &resp.RequestId); err != nil {
return nil, err
}
startByteSlice := make([]byte, 1)
tag := TagCupsInvalid
previousAttributeName := ""
tempAttributes := make(Attributes)
tagSet := false
attribDecoder := NewAttributeDecoder(d.reader)
// decode attribute buffer
for {
if _, err := d.reader.Read(startByteSlice); err != nil {
// when we read from a stream, we may get an EOF if we want to read the end tag
// all data should be read and we can ignore the error
if err == io.EOF {
break
}
return nil, err
}
startByte := int8(startByteSlice[0])
// check if attributes are completed
if startByte == TagEnd {
break
}
if startByte == TagOperation {
if len(tempAttributes) > 0 && tag != TagCupsInvalid {
appendAttributeToResponse(resp, tag, tempAttributes)
tempAttributes = make(Attributes)
}
tag = TagOperation
tagSet = true
}
if startByte == TagJob {
if len(tempAttributes) > 0 && tag != TagCupsInvalid {
appendAttributeToResponse(resp, tag, tempAttributes)
tempAttributes = make(Attributes)
}
tag = TagJob
tagSet = true
}
if startByte == TagPrinter {
if len(tempAttributes) > 0 && tag != TagCupsInvalid {
appendAttributeToResponse(resp, tag, tempAttributes)
tempAttributes = make(Attributes)
}
tag = TagPrinter
tagSet = true
}
if startByte == TagSubscription {
if len(tempAttributes) > 0 && tag != TagCupsInvalid {
appendAttributeToResponse(resp, tag, tempAttributes)
tempAttributes = make(Attributes)
}
tag = TagSubscription
tagSet = true
}
if startByte == TagEventNotification {
if len(tempAttributes) > 0 && tag != TagCupsInvalid {
appendAttributeToResponse(resp, tag, tempAttributes)
tempAttributes = make(Attributes)
}
tag = TagEventNotification
tagSet = true
}
if tagSet {
if _, err := d.reader.Read(startByteSlice); err != nil {
return nil, err
}
startByte = int8(startByteSlice[0])
}
attrib, err := attribDecoder.Decode(startByte)
if err != nil {
return nil, err
}
if attrib.Name != "" {
tempAttributes[attrib.Name] = append(tempAttributes[attrib.Name], *attrib)
previousAttributeName = attrib.Name
} else {
tempAttributes[previousAttributeName] = append(tempAttributes[previousAttributeName], *attrib)
}
tagSet = false
}
if len(tempAttributes) > 0 && tag != TagCupsInvalid {
appendAttributeToResponse(resp, tag, tempAttributes)
}
if data != nil {
if _, err := io.Copy(data, d.reader); err != nil {
return nil, err
}
}
return resp, nil
}
func appendAttributeToResponse(resp *Response, tag int8, attr map[string][]Attribute) {
switch tag {
case TagOperation:
resp.OperationAttributes = attr
case TagPrinter:
resp.PrinterAttributes = append(resp.PrinterAttributes, attr)
case TagJob:
resp.JobAttributes = append(resp.JobAttributes, attr)
case TagSubscription, TagEventNotification:
// Both subscription and event notification attributes go to SubscriptionAttributes
resp.SubscriptionAttributes = append(resp.SubscriptionAttributes, attr)
}
}

28
pkg/ipp/utils.go Normal file
View File

@@ -0,0 +1,28 @@
package ipp
import (
"fmt"
"os"
"path"
)
// ParseControlFile reads and decodes a cups control file into a response
func ParseControlFile(jobID int, spoolDirectory string) (*Response, error) {
if spoolDirectory == "" {
spoolDirectory = "/var/spool/cups"
}
controlFilePath := path.Join(spoolDirectory, fmt.Sprintf("c%d", jobID))
if _, err := os.Stat(controlFilePath); err != nil {
return nil, err
}
controlFile, err := os.Open(controlFilePath)
if err != nil {
return nil, err
}
defer controlFile.Close()
return NewResponseDecoder(controlFile).Decode(nil)
}