File ipp-usb-0.9.30.obscpio of Package ipp-usb
07070100000000000041ED00000000000000000000000267D72F5D00000000000000000000000000000000000000000000001700000000ipp-usb-0.9.30/.github07070100000001000041ED00000000000000000000000267D72F5D00000000000000000000000000000000000000000000002100000000ipp-usb-0.9.30/.github/workflows07070100000002000081A400000000000000000000000167D72F5D000004DB000000000000000000000000000000000000003100000000ipp-usb-0.9.30/.github/workflows/auto-update.ymlname: Push new tag update to stable branch
on:
schedule:
- cron: '9 7 * * *'
workflow_dispatch:
inputs:
workflow_choice:
description: "Choose YAML to update"
required: true
default: "both"
type: choice
options:
- snapcraft
- rockcraft
- both
jobs:
update-yamls:
runs-on: ubuntu-latest
steps:
- name: Checkout this repo
uses: actions/checkout@v3
- name: Run desktop-snaps action (Snapcraft)
if: ${{ github.event_name == 'schedule' github.event.inputs.workflow_choice == 'snapcraft' github.event.inputs.workflow_choice == 'both' }}
uses: ubuntu/desktop-snaps@stable
with:
token: ${{ secrets.GITHUB_TOKEN }}
repo: ${{ github.repository }}
yaml-path: 'snap/snapcraft.yaml'
- name: Run desktop-snaps action (Rockcraft)
if: ${{ github.event_name == 'schedule' github.event.inputs.workflow_choice == 'rockcraft' github.event.inputs.workflow_choice == 'both' }}
uses: ubuntu/desktop-snaps@stable
with:
token: ${{ secrets.GITHUB_TOKEN }}
repo: ${{ github.repository }}
yaml-path: 'rock/rockcraft.yaml'
07070100000003000081A400000000000000000000000167D72F5D00001026000000000000000000000000000000000000003600000000ipp-usb-0.9.30/.github/workflows/registry-actions.ymlname: Pack and Publish OCI Image to Docker Registry and GitHub Packages
on:
push:
branches:
- main
- master
workflow_dispatch:
inputs:
workflow_choice:
description: "Choose Release Channel"
required: true
default: "edge"
type: choice
options:
- edge
- stable
- both
workflow_run:
workflows: ["Push new tag update to stable branch"]
types:
- completed
jobs:
build-rock:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Pack with Rockcraft
uses: canonical/craft-actions/rockcraft-pack@main
id: rockcraft
with:
path: rock
- name: Upload Rock Artifact
uses: actions/upload-artifact@v4
with:
name: ipp-usb-rock
path: ${{ steps.rockcraft.outputs.rock }}
publish-rock:
needs: build-rock
if: github.ref_name == 'main'|| github.ref_name == 'master'
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Download Rock Artifact
uses: actions/download-artifact@v4
with:
name: ipp-usb-rock
- name: Install Dependencies
run: |
sudo snap install rockcraft --classic
sudo snap install docker
sudo snap install yq
- name: Ensure Docker Daemon is Running
run: |
sudo systemctl start docker
sudo systemctl enable docker
sudo systemctl is-active --quiet docker || sudo systemctl start docker
#- name: Log in to Docker Hub
# uses: docker/login-action@v3.2.0
# with:
# username: ${{ secrets.DOCKER_USERNAME }}
# password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log in to GitHub Packages
uses: docker/login-action@v3.2.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and Push Docker Image (Edge & Latest Channel)
if: github.event.inputs.workflow_choice == 'edge' || github.event.inputs.workflow_choice == 'both' || github.event_name == 'push' || github.event_name == 'workflow_run'
env:
USERNAME: ${{ secrets.DOCKER_USERNAME }}
run: |
IMAGE="$(yq '.name' rock/rockcraft.yaml)"
VERSION="$(yq '.version' rock/rockcraft.yaml)"
ROCK="$(ls *.rock | tail -n 1)"
# Push to Docker Hub
sudo rockcraft.skopeo --insecure-policy copy oci-archive:"${ROCK}" docker-daemon:"${USERNAME}/${IMAGE}:${VERSION}-edge"
docker push ${USERNAME}/${IMAGE}:${VERSION}-edge
docker tag ${USERNAME}/${IMAGE}:${VERSION}-edge ${USERNAME}/${IMAGE}:latest
docker push ${USERNAME}/${IMAGE}:latest
# Push to GitHub Packages
GITHUB_IMAGE="ghcr.io/${{ github.repository_owner }}/${IMAGE}"
docker tag ${USERNAME}/${IMAGE}:${VERSION}-edge ${GITHUB_IMAGE}:${VERSION}-edge
docker push ${GITHUB_IMAGE}:${VERSION}-edge
docker tag ${GITHUB_IMAGE}:${VERSION}-edge ${GITHUB_IMAGE}:latest
docker push ${GITHUB_IMAGE}:latest
- name: Build and Push Docker Image (Stable Channel)
if: github.event.inputs.workflow_choice == 'stable' || github.event.inputs.workflow_choice == 'both'
env:
USERNAME: ${{ secrets.DOCKER_USERNAME }}
run: |
IMAGE="$(yq '.name' rock/rockcraft.yaml)"
VERSION="$(yq '.version' rock/rockcraft.yaml)"
ROCK="$(ls *.rock | tail -n 1)"
# Push to Docker Hub
sudo rockcraft.skopeo --insecure-policy copy oci-archive:"${ROCK}" docker-daemon:"${USERNAME}/${IMAGE}:${VERSION}-stable"
docker push ${USERNAME}/${IMAGE}:${VERSION}-stable
# Push to GitHub Packages
GITHUB_IMAGE="ghcr.io/${{ github.repository_owner }}/${IMAGE}"
docker tag ${USERNAME}/${IMAGE}:${VERSION}-stable ${GITHUB_IMAGE}:${VERSION}-stable
docker push ${GITHUB_IMAGE}:${VERSION}-stable
07070100000004000081A400000000000000000000000167D72F5D00000021000000000000000000000000000000000000001A00000000ipp-usb-0.9.30/.gitignoreipp-usb
tags
*.swp
*.orig
*.rock
07070100000005000081A400000000000000000000000167D72F5D0000052F000000000000000000000000000000000000001700000000ipp-usb-0.9.30/LICENSEBSD 2-Clause License
Copyright (c) 2020, Alexander Pevzner
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
07070100000006000081A400000000000000000000000167D72F5D00000321000000000000000000000000000000000000001800000000ipp-usb-0.9.30/MakefileMANDIR = /usr/share/man/
QUIRKSDIR = /usr/share/ipp-usb/quirks
MANPAGE = ipp-usb.8
# Merge DESTDIR and PREFIX
PREFIX := $(abspath $(DESTDIR)/$(PREFIX))
ifeq ($(PREFIX),/)
PREFIX :=
endif
all:
-gotags -R . > tags
go build -ldflags "-s -w" -tags nethttpomithttp2 -mod=vendor
man: $(MANPAGE)
$(MANPAGE): $(MANPAGE).md
ronn --roff --manual=$@ $<
install: all
install -s -D -t $(PREFIX)/sbin ipp-usb
install -m 644 -D -t $(PREFIX)/lib/udev/rules.d systemd-udev/*.rules
install -m 644 -D -t $(PREFIX)/lib/systemd/system systemd-udev/*.service
install -m 644 -D -t $(PREFIX)/etc/ipp-usb ipp-usb.conf
mkdir -p $(PREFIX)/$(MANDIR)/man8
gzip <$(MANPAGE) > $(PREFIX)$(MANDIR)/man8/$(MANPAGE).gz
install -m 644 -D -t $(PREFIX)/$(QUIRKSDIR) ipp-usb-quirks/*
test:
go test -mod=vendor
07070100000007000081A400000000000000000000000167D72F5D0000399E000000000000000000000000000000000000001900000000ipp-usb-0.9.30/README.md# ipp-usb

[](https://goreportcard.com/badge/github.com/OpenPrinting/ipp-usb)
## Introduction
[IPP-over-USB](https://www.usb.org/document-library/ipp-protocol-10)
allows using the IPP protocol, normally designed for network printers,
to be used with USB printers as well.
The idea behind this standard is simple: It allows to send HTTP
requests to the device via a USB connection, so enabling IPP, eSCL
(AirScan) and web console on devices without Ethernet or WiFi
connections.
Unfortunately, the naive implementation, which simply relays a TCP
connection to USB, does not work. It happens because closing the TCP
connection on the client side has a useful side effect of discarding
all data sent to this connection from the server side, but it does not
happen with USB connections. In the case of USB, all data not received
by the client will remain in the USB buffers, and the next time the
client connects to the device, it will receive unexpected data, left
from the previous abnormally completed request.
Actually, it is an obvious flaw in the IPP-over-USB standard, but we
have to live with it.
So the implementation, once the HTTP request is sent, must read the
entire HTTP response, which means that the implementation must
understand the HTTP protocol, and effectively implement a HTTP reverse
proxy, backed by the IPP-over-USB connection to the device.
And this is what the **ipp-usb** program actually does.
## Features in detail
* Implements HTTP proxy, backed by USB connection to IPP-over-USB device
* Full support of IPP printing, eSCL scanning, and web admin interface
* DNS-SD advertising for all supported services
* DNS-SD parameters for IPP based on IPP get-printer-attributes query
* DNS-SD parameters for eSCL based on parsing GET /eSCL/ScannerCapabilities response
* TCP port allocation for device is bound to particular device (combination of
VendorID, ProductID and device serial number), so if the user has multiple
devices, they will receive the same TCP port when connected. This allocation
is persisted on a disk
* Automatic DNS-SD name conflict resolution. The finally chosen device's
network name is persisted on a disk
* Can be started by **UDEV** or run in standalone mode
* Can share printer to other computers on a network, or use the loopback interface only
* Can generate very detailed logs for possible troubleshooting
## Under the hood
Though looks simple, ipp-usb does many non obvious things under the hood
* Client-side HTTP connections are completely decoupled from printer-side HTTP-over-USB connections
* HTTP requests are sanitized, missed headers are added
* HTTP protocol upgraded from 1.0 to 1.1, if needed
* Attempts to upgrade HTTP connection to winsock, if unwisely made by web console, are
prohibited, because it can steal USB connection for a long time
* Client HTTP requests are fairly balanced between all available 2-3 USB connections,
regardless of number and persistence of client connections
* Dropping connection by client properly handled in all cases, even in a middle of sending.
In a worst case, printer may receive truncated document, but HTTP transaction will always be
performed correctly
## Memory footprint
Being written on Go, ipp-usb has a large executable size. However, its
memory consumption is not very high. When single device is connected,
ipp-usb RSS is similar or even slightly less in comparison to ippusbxd.
And because ipp-usb handles all devices in a single process, it uses noticeably
less memory that ippusbxd, when serving 2 or more devices.
## External dependencies
This program has very few external dependencies, namely:
* `libusb` for USB access
* `libavahi-common` and `libavahi-client` for DNS-SD
* Running Avahi daemon
## Binary packages
Binary packages available for the following Linux distros:
* **Debian** (10)
* **Fedora** (29, 30, 31 and 32)
* **openSUSE** (Tumbleweed)
* **Ubuntu** (18.04, 19.04, 19.10 and 20.04)
**Linux Mint** users may use Ubuntu packages:
* Linux Mint 18.x - use packages for Ubuntu 16.04
* Linux Mint 19.x - use packages for Ubuntu 18.04
Follow this link for downloads: https://download.opensuse.org/repositories/home:/pzz/
## Not only Linux
We are glad to announce that `ipp-usb` was recently included into the
FreeBSD ports: https://www.freshports.org/print/ipp-usb/
Hope, NetBSD/OpenBSD support will be added as well, so technology
becomes not Linux-only, but UNIX-wide.
## The ipp-usb Snap
ipp-usb is also available as a Snap in the Snap Store: https://snapcraft.io/ipp-usb
Before you install the Snap, uninstall any already existing
installation of ipp-usb.
Simply install it via any GUI client for the Snap Store (Like "Ubuntu
Software") or via command line:
sudo snap install --edge ipp-usb
Now you can connect and disconnect IPP-over-USB devices and ipp-usb
gets started by the Snap whenever needed. Also devices which are
already connected during boot, start, or update of the Snap are
considered.
You can also use
ipp-usb status
to check the status of the running ipp-usb daemon (supported device
must be connected for the ipp-usb daemon to be running, accesses only
the ipp-usb daemon of the Snap) and
ipp-usb check
to scan the USB for the presence of potentially supported USB devices
(7/1/4 interface protocol). This command requires access to the raw
USB and therefore on many systems root privileges are required.
The Snap is automatically updated when further development on ipp-usb
happens.
The configuration file is here:
/var/snap/ipp-usb/common/etc/ipp-usb.conf
You can edit it and afterwards restart the Snap to use the changed
configuration.
Incompatibilities of particular devices are handled by workarounds
defined in the quirk files. You find them here:
/var/snap/ipp-usb/common/quirks
You can add your own quirk files (but if they solve your problem,
please report an issue here, with your quirk file attached, so that
others with the same problem will get helped, too).
For quick tests you can also edit the existing files, but they will
get replaced (and so your changes lost) on the next update of the
Snap, as we are changing them on any report of further device
incompatibilities.
The log file is here
/var/snap/ipp-usb/common/var/log
and device state files (to assure that each device appears on the same
port and with the same DNS-SD service name) are here:
/var/snap/ipp-usb/common/var/dev
You can also build the Snap locally. This is useful when
* You want to modify ipp-usb
* You want to learn about snapping Go projects
* You want to learn about how to use UDEV from within a Snap (note that a Snap cannot install UDEV rules into the system)
To do so, run from the main directory of this source repository
snapcraft snap
and then install the resulting Snap with
sudo snap install --dangerous ipp-usb*.snap
An installed Snap from the Snap Store will get overwritten/replaced by your Snap.
Some technical notes about this Snap:
Snapping the Go project with one Go library taken from upstream (and
not from Ubuntu Core) was rather straight-forward. Only observation
was that the Go plugin seems not to do "make install". So I had to use
an "override-build" to manually install the auxiliary files
(ipp-usb.conf, quirk files). I also have adapted the auxiliary file
and state directories in paths.go in the "override-build" scriptlet.
The real challenge of this Snap was to trigger ipp-usb on the
appearing (and also the presence) of IPP-over-USB devices.
In the classic installation of ipp-usb (via "make install" or RPM/DEB
package installation) a UDEV rules file and a systemd service file (in
systemd-udev/) are installed, so that the system automatically triggers
the launch of ipp-usb when an appropriate device is connected or
already present. A Snap is not able to do so. It cannot install any
files into the system. It can only bring its own, static file system
and create files only in its own state directory. These locations are
not scanned for UDEV rules.
So the Snap must discover the devices without its own UDEV rules, but
it still can use UDEV. The trick is to do a generic monitoring of UDEV
events and filtering out the USB devices with IPP-over-USB interface
(7/1/4). If such a device appears, we trigger and ipp-usb launch. We
also check on startup of the Snap whether there is such a device
already and if so, we also trigger an ipp-usb launch.
ipp-usb is run, as in the classic installation, with "udev"
argument. This way it stops by itself when there is no device any more
(and we do not need to observe the disappearal events of the devices)
and it is assured that only one single instance of ipp-usb is running.
To do this with low coding effort I use the UDEV command line tool
udevadm in a shell script (snap/local/run-ipp-usb). Once it runs in
"monitor" mode to observe the UDEV events. Then we parse the output
lines to only consider the ones for a device appearing and run
"udevadm info -q property" on each device path, to get the properties
and filter the 7/1/4 interface. In the beginning we use "udevadm
trigger" to find the already passed appearal event of a device which
is already present. So the shell script is an auxiliary daemon to
start ipp-usb when needed.
## The ipp-usb Rock
### Install from GitHub Container Registry
#### Prerequisites
1. **Docker Installed**: Ensure Docker is installed on your system. You can download it from the [official Docker website](https://www.docker.com/get-started).
```sh
sudo snap install docker
```
#### Step-by-Step Guide
You can pull the `ipp-usb` Docker image from the GitHub Container Registry.
**From GitHub Container Registry** <br>
To pull the image from the GitHub Container Registry, run the following command:
```sh
sudo docker pull ghcr.io/openprinting/ipp-usb:latest
```
To run the container after pulling the image, use:
```sh
sudo docker run -d --network host \
-v /dev/bus/usb:/dev/bus/usb:ro \
--device-cgroup-rule='c 189:* rmw' \
--name ipp-usb \
ghcr.io/openprinting/ipp-usb:latest
```
- `--network host`: Uses the host network, ensuring IPP-over-USB and Avahi service discovery work correctly.
- `-v /dev/bus/usb:/dev/bus/usb:ro`: Grants the container read-only access to USB devices.
- `--device-cgroup-rule='c 189:* rmw'`: Grants the container permission to manage USB devices (189:* covers USB device nodes).
To check the logs of `ipp-usb`, run:
```sh
sudo docker logs -f ipp-usb
```
### Building and Running `ipp-usb` Locally
#### Prerequisites
**Docker Installed**: Ensure Docker is installed on your system. You can download it from the [official Docker website](https://www.docker.com/get-started) or from the Snap Store:
```sh
sudo snap install docker
```
**Rockcraft**: Rockcraft should be installed. You can install Rockcraft using the following command:
```sh
sudo snap install rockcraft --classic
```
#### Step-by-Step Guide
**Build the `ipp-usb` Rock Image**
The first step is to build the Rock from the `rockcraft.yaml`. This image will contain all the configurations and dependencies required to run `ipp-usb`.
Navigate to the directory containing `rockcraft.yaml`, then run:
```sh
rockcraft pack -v
```
**Compile to Docker Image**
Once the `.rock` file is built, compile a Docker image from it using:
```sh
sudo rockcraft.skopeo --insecure-policy copy oci-archive:<rock_image_name> docker-daemon:ipp-usb:latest
```
**Run the `ipp-usb` Docker Container**
```sh
sudo docker run -d --network host \
-v /dev/bus/usb:/dev/bus/usb:ro \
--device-cgroup-rule='c 189:* rmw' \
--name ipp-usb \
ipp-usb:latest
```
### Accessing the Container Shell
To enter the running `ipp-usb` container and access a shell inside it, use:
```sh
sudo docker exec -it ipp-usb bash
```
This allows you to inspect logs, debug issues, or manually run commands inside the container.
### Configuration
The `ipp-usb` container uses a configuration file located at:
```
/etc/ipp-usb.conf
```
To customize the configuration, mount a modified config file:
```sh
sudo docker run -d --network host \
-v /dev/bus/usb:/dev/bus/usb:ro \
--device-cgroup-rule='c 189:* rmw' \
-v /path/to/custom/ipp-usb.conf:/etc/ipp-usb.conf:ro \
--name ipp-usb-container \
ghcr.io/openprinting/ipp-usb:latest
```
## Installation from source
You will need to install the following packages (exact name depends
of your Linux distro):
* libusb development files
* libavahi-client and libavahi-common development files
* gcc
* Go compiler
* pkg-config
* git, make and so on
Building is really simple:
git clone https://github.com/OpenPrinting/ipp-usb.git
cd ipp-usb
make
Then you may `make install` or just try to run `./ipp-usb` directly from
the build directory
## Avahi Notes (exposing printer to localhost)
IPP-over-USB normally exposes printer to localhost only, hence it
requires DNS-SD announces to work for localhost.
This requires Avahi 0.8.0 or newer. Older Avahi versions do not
support announcing to localhost.
Some Linux distros (for example recent Ubuntu and Fedora versions)
have their Avahi patched to support localhost, others (for example
Debian) not.
To determine if your Avahi supports localhost, run the following
command in one terminal session:
```
avahi-publish -s test _test._tcp 1234
```
And simultaneously the following command in another terminal session
on the same machine:
```
avahi-browse _test._tcp -r
```
If you see localhost in the avahi-browse output, like this:
```
= lo IPv4 test _test._tcp local
hostname = [localhost]
address = [127.0.0.1]
port = [1234]
txt = []
```
your Avahi is OK. Otherwise, update or patching is required.
So users of distros that ship a too old Avahi and without the patch
have three possibilities:
1. Update Avahi to 0.8.0 or newer
2. Apply the patch by themself, rebuild and reinstall avahi-daemon
3. Configure `ipp-usb` to run on all network interfaces, not only on loopback
If you decide to apply the patch, get it as `avahi/avahi-localhost.patch`
in this package or [download it here](https://raw.githubusercontent.com/OpenPrinting/ipp-usb/master/avahi/avahi-localhost.patch).
The third method is simple to do, just replace `interface = loopback`
with `interface = all` in the `ipp-usb.conf` file, but this has the
disadvantage of exposing your local USB-connected printer to the
entire local network, which can be an unwanted side effect, especially
in a big corporative network.
07070100000008000081A400000000000000000000000167D72F5D000007FC000000000000000000000000000000000000001E00000000ipp-usb-0.9.30/addpdl_test.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
*
* Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
* See LICENSE for license terms and conditions
*
* (*DNSSdTxtRecord) AddPDL() test
*/
package main
import (
"testing"
)
var testDataAddPDL = []struct{ in, out string }{
{
"application/pdf",
"application/pdf",
},
{
"application/octet-stream," +
"application/pdf,image/tiff,image/jpeg,image/urf," +
"application/postscript,application/vnd.hp-PCL," +
"application/vnd.hp-PCLXL,application/vnd.xpsdocument," +
"image/pwg-raster",
"application/octet-stream," +
"application/pdf,image/tiff,image/jpeg,image/urf," +
"application/postscript,application/vnd.hp-PCL," +
"application/vnd.hp-PCLXL,application/vnd.xpsdocument," +
"image/pwg-raster",
},
{
"application/vnd.hp-PCL,application/vnd.hp-PCLXL," +
"application/postscript,application/msword," +
"application/pdf,image/jpeg,image/urf," +
"image/pwg-raster," +
"application/PCLm," +
"application/vnd.openxmlformats-officedocument.wordprocessingml.document," +
"application/vnd.ms-excel," +
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet," +
"application/vnd.ms-powerpoint," +
"application/vnd.openxmlformats-officedocument.presentationml.presentation," +
"application/octet-stream",
"application/vnd.hp-PCL,application/vnd.hp-PCLXL," +
"application/postscript,application/msword," +
"application/pdf,image/jpeg,image/urf," +
"image/pwg-raster,application/PCLm," +
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
},
}
// Test .INI reader
func TestAddPDL(t *testing.T) {
for i, data := range testDataAddPDL {
var txt DNSSdTxtRecord
txt.AddPDL("pdl", data.in)
if len(txt) != 1 {
t.Errorf("test %d: unexpected (%d) number of TXT elements added",
i+1, len(txt))
return
}
if txt[0].Value != data.out {
t.Errorf("test %d: expected %q, got %q",
i+1, data.out, txt[0].Value)
}
}
}
07070100000009000081A400000000000000000000000167D72F5D00001F96000000000000000000000000000000000000001700000000ipp-usb-0.9.30/auth.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
*
* Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
* See LICENSE for license terms and conditions
*
* Authentication
*/
package main
import (
"errors"
"fmt"
"net"
"net/http"
"os/user"
"runtime"
"strconv"
"strings"
"sync"
"time"
)
// AuthUIDRule represents a single rule for client authentication
// based on client UID
type AuthUIDRule struct {
Name string // @name means group, * means any
Allowed AuthOps // Allowed operations
}
// IsUser tells if rule is a user rule
func (rule *AuthUIDRule) IsUser() bool {
return !rule.IsGroup()
}
// IsGroup tells if rule is a group rule
func (rule *AuthUIDRule) IsGroup() bool {
return strings.HasPrefix(rule.Name, "@")
}
// MatchUser matches rule against user name
func (rule *AuthUIDRule) MatchUser(name string) AuthOps {
if rule.IsGroup() {
return 0
}
if rule.Name == "*" || rule.Name == name {
return rule.Allowed
}
return 0
}
// MatchGroup matches rule against group name
func (rule *AuthUIDRule) MatchGroup(name string) AuthOps {
if !rule.IsGroup() {
return 0
}
ruleName := rule.Name[1:] // Strip leading '@'
if ruleName == "*" || ruleName == name {
return rule.Allowed
}
return 0
}
// AuthOps is bitmask of allowed operations
type AuthOps int
// AuthOps values
const (
AuthOpsConfig AuthOps = 1 << iota // Configuration web console
AuthOpsFax // Faxing
AuthOpsPrint // Printing
AuthOpsScan // Scanning
// All and None of above
AuthOpsAll = AuthOpsConfig | AuthOpsFax | AuthOpsPrint |
AuthOpsScan
AuthOpsNone AuthOps = 0
)
// String returns string representation of AuthOps flags, for debugging.
func (ops AuthOps) String() string {
if ops == 0 {
return "none"
}
s := []string{}
if ops&AuthOpsConfig != 0 {
s = append(s, "config")
}
if ops&AuthOpsFax != 0 {
s = append(s, "fax")
}
if ops&AuthOpsPrint != 0 {
s = append(s, "print")
}
if ops&AuthOpsScan != 0 {
s = append(s, "scan")
}
return strings.Join(s, ",")
}
// AuthUIDinfo is the resolved and cached UID info, for matching
type AuthUIDinfo struct {
UsrNames []string // User (numerical and symbolic) names
GrpNames []string // Group names (numerical and symbolic)
expires time.Time // Expiration time, for caching
}
// authUIDinfoCache contains authUIDinfo cache, indexed by UID
var (
authUIDinfoCache = make(map[int]*AuthUIDinfo)
authUIDinfoCacheLock sync.Mutex
)
// authUIDinfoCacheTTL is the expiration timeout for authUIDinfoCache
const authUIDinfoCacheTTL = 2 * time.Second
// AuthUIDinfoLookup performs AuthUIDinfo lookup by UID.
func AuthUIDinfoLookup(uid int) (*AuthUIDinfo, error) {
// UID is not known. Use "*" user/group names, as promised
// by documentation
if uid == -1 {
info := &AuthUIDinfo{
UsrNames: []string{"*"},
GrpNames: []string{"*"},
expires: time.Now().Add(authUIDinfoCacheTTL),
}
return info, nil
}
// Lookup authUIDinfoCache
authUIDinfoCacheLock.Lock()
info := authUIDinfoCache[uid]
authUIDinfoCacheLock.Unlock()
if info != nil && info.expires.After(time.Now()) {
return info, nil
}
// Resolve user names for matching
// Also populates grpIDs with numeric group IDs
usrNames := []string{strconv.Itoa(uid)}
grpIDs := []string{}
usr, err := user.LookupId(usrNames[0])
if err != nil {
return nil, err
}
usrNames = append(usrNames, usr.Username)
grpIDs = append(grpIDs, usr.Gid)
grpids, err := usr.GroupIds()
if err != nil {
return nil, err
}
grpIDs = append(grpIDs, grpids...)
// Resolve group IDs to names
grpNames := append([]string{}, grpIDs...)
for _, gid := range grpIDs {
grp, err := user.LookupGroupId(gid)
if err != nil {
return nil, err
}
grpNames = append(grpNames, grp.Name)
}
// Update cache
info = &AuthUIDinfo{
UsrNames: usrNames,
GrpNames: grpNames,
expires: time.Now().Add(authUIDinfoCacheTTL),
}
authUIDinfoCacheLock.Lock()
authUIDinfoCache[uid] = info
authUIDinfoCacheLock.Unlock()
// Return the answer
return info, nil
}
// AuthUID returns operations allowed to client with given UID
// uid == -1 indicates that UID is not available (i.e., external
// connection)
func AuthUID(info *AuthUIDinfo) AuthOps {
// Everything is allowed if authentication is not configured
if Conf.ConfAuthUID == nil {
return AuthOpsAll
}
// Apply rules
allowed := AuthOpsNone
for _, rule := range Conf.ConfAuthUID {
if rule.IsUser() {
for _, usr := range info.UsrNames {
allowed |= rule.MatchUser(usr)
}
} else {
for _, grp := range info.GrpNames {
allowed |= rule.MatchGroup(grp)
}
}
}
return allowed
}
// authUIDrequiresUID tells if UID authentication really requires UID.
// UID is not required, if either authentication is not configured, or
// there is no rules with non-wildcard UID.
func authUIDrequiresUID() bool {
for _, rule := range Conf.ConfAuthUID {
if rule.Name != "*" && rule.Name != "@*" {
return true
}
}
return false
}
// AuthHTTPRequest performs authentication for the incoming
// HTTP request
//
// On success, status is http.StatusOK and err is nil.
// Otherwise, status is appropriate for HTTP error response,
// and err explains the reason
func AuthHTTPRequest(log *Logger,
client, server *net.TCPAddr,
rq *http.Request) (status int, err error) {
// Guess the operation by URL
post := rq.Method == "POST"
ops := AuthOpsConfig // The default
switch {
case post && strings.HasPrefix(rq.URL.Path, "/ipp/print"):
ops = AuthOpsPrint
case post && strings.HasPrefix(rq.URL.Path, "/ipp/faxout"):
ops = AuthOpsFax
case strings.HasPrefix(rq.URL.Path, "/eSCL"):
ops = AuthOpsScan
}
log.Debug(' ', "auth: operation requested: %s (HTTP %s %s)",
ops, rq.Method, rq.URL)
// Check if client and server addresses are both local
addrs, err := net.InterfaceAddrs()
if err != nil {
err = fmt.Errorf("can't get local IP addresses: %s", err)
log.Error('!', "auth: %s", err)
return http.StatusInternalServerError, err
}
clientIsLocal := client.IP.IsLoopback()
serverIsLocal := server.IP.IsLoopback()
for _, addr := range addrs {
if clientIsLocal && serverIsLocal {
// Both addresses known to be local,
// we don't need to continue
break
}
if ip, ok := addr.(*net.IPNet); ok {
if client.IP.Equal(ip.IP) {
clientIsLocal = true
}
if server.IP.Equal(ip.IP) {
serverIsLocal = true
}
}
}
log.Debug(' ', "auth: address check:")
log.Debug(' ', " client-addr %s local=%v", client.IP, clientIsLocal)
log.Debug(' ', " server-addr %s local=%v", server.IP, serverIsLocal)
// Do we need UID?
uid := -1
reason := ""
switch {
case !clientIsLocal || !serverIsLocal:
reason = "non-local connection"
case !TCPClientUIDSupported():
reason = fmt.Sprintf("UID auth not supported on %s",
runtime.GOOS)
case !authUIDrequiresUID():
reason = "auth rules don't use UID"
}
// Obtain UID, if we really need it
if reason == "" {
uid, err = TCPClientUID(client, server)
if err != nil {
err = fmt.Errorf("can't get client UID: %s",
err)
log.Error('!', "auth: %s", err)
return http.StatusInternalServerError, err
}
log.Debug(' ', "auth: client UID=%d", uid)
} else {
log.Debug(' ', "auth: client UID=%d (%s)", uid, reason)
}
// Lookup UID info
info, err := AuthUIDinfoLookup(uid)
if err != nil {
err = fmt.Errorf("can't resolve UID %d: %s", uid, err)
log.Error('!', "auth: %s", err)
return 0, err
}
log.Debug(' ', "auth: UID %d resolved:", uid)
log.Debug(' ', " user names: %s", strings.Join(info.UsrNames, ","))
log.Debug(' ', " group names: %s", strings.Join(info.GrpNames, ","))
// Authenticate
allowed := AuthUID(info)
log.Debug(' ', "auth: allowed operations: %s", allowed)
if ops&allowed != AuthOpsNone {
log.Debug(' ', "auth: access granted")
return http.StatusOK, nil
}
err = errors.New("Operation not allowed. See ipp-usb.conf for details")
log.Error('!', "auth: %s", err)
return http.StatusForbidden, err
}
0707010000000A000041ED00000000000000000000000267D72F5D00000000000000000000000000000000000000000000001500000000ipp-usb-0.9.30/avahi0707010000000B000081A400000000000000000000000167D72F5D00000B3A000000000000000000000000000000000000002B00000000ipp-usb-0.9.30/avahi/avahi-localhost.patchdiff --git a/avahi-core/iface-linux.c b/avahi-core/iface-linux.c
index c6c5f77..e116c7b 100644
--- a/avahi-core/iface-linux.c
+++ b/avahi-core/iface-linux.c
@@ -104,8 +104,8 @@ static void netlink_callback(AvahiNetlink *nl, struct nlmsghdr *n, void* userdat
hw->flags_ok =
(ifinfomsg->ifi_flags & IFF_UP) &&
(!m->server->config.use_iff_running || (ifinfomsg->ifi_flags & IFF_RUNNING)) &&
- !(ifinfomsg->ifi_flags & IFF_LOOPBACK) &&
- (ifinfomsg->ifi_flags & IFF_MULTICAST) &&
+ ((ifinfomsg->ifi_flags & IFF_LOOPBACK) ||
+ (ifinfomsg->ifi_flags & IFF_MULTICAST)) &&
(m->server->config.allow_point_to_point || !(ifinfomsg->ifi_flags & IFF_POINTOPOINT));
/* Handle interface attributes */
diff --git a/avahi-core/iface-pfroute.c b/avahi-core/iface-pfroute.c
index 9a2e953..27c3443 100644
--- a/avahi-core/iface-pfroute.c
+++ b/avahi-core/iface-pfroute.c
@@ -80,8 +80,8 @@ static void rtm_info(struct rt_msghdr *rtm, AvahiInterfaceMonitor *m)
hw->flags_ok =
(ifm->ifm_flags & IFF_UP) &&
(!m->server->config.use_iff_running || (ifm->ifm_flags & IFF_RUNNING)) &&
- !(ifm->ifm_flags & IFF_LOOPBACK) &&
- (ifm->ifm_flags & IFF_MULTICAST) &&
+ ((ifm->ifm_flags & IFF_LOOPBACK) ||
+ (ifm->ifm_flags & IFF_MULTICAST)) &&
(m->server->config.allow_point_to_point || !(ifm->ifm_flags & IFF_POINTOPOINT));
avahi_free(hw->name);
@@ -427,8 +427,8 @@ static void if_add_interface(struct lifreq *lifreq, AvahiInterfaceMonitor *m, in
hw->flags_ok =
(flags & IFF_UP) &&
(!m->server->config.use_iff_running || (flags & IFF_RUNNING)) &&
- !(flags & IFF_LOOPBACK) &&
- (flags & IFF_MULTICAST) &&
+ ((flags & IFF_LOOPBACK) ||
+ (flags & IFF_MULTICAST)) &&
(m->server->config.allow_point_to_point || !(flags & IFF_POINTOPOINT));
hw->name = avahi_strdup(lifreq->lifr_name);
hw->mtu = mtu;
diff --git a/avahi-core/resolve-service.c b/avahi-core/resolve-service.c
index 3377a50..3311b6b 100644
--- a/avahi-core/resolve-service.c
+++ b/avahi-core/resolve-service.c
@@ -24,6 +24,7 @@
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
+#include <net/if.h>
#include <avahi-common/domain.h>
#include <avahi-common/timeval.h>
@@ -129,7 +130,7 @@ static void finish(AvahiSServiceResolver *r, AvahiResolverEvent event) {
r->service_name,
r->service_type,
r->domain_name,
- r->srv_record->data.srv.name,
+ (r->interface == if_nametoindex("lo")) ? "localhost" : r->srv_record->data.srv.name,
r->address_record ? &a : NULL,
r->srv_record->data.srv.port,
r->txt_record ? r->txt_record->data.txt.string_list : NULL,
0707010000000C000081A400000000000000000000000167D72F5D000014BC000000000000000000000000000000000000001700000000ipp-usb-0.9.30/conf.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
*
* Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
* See LICENSE for license terms and conditions
*
* Program configuration
*/
package main
import (
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"unicode"
)
const (
// ConfFileName defines a name of ipp-usb configuration file
ConfFileName = "ipp-usb.conf"
)
// Configuration represents a program configuration
type Configuration struct {
HTTPMinPort int // Starting port number for HTTP to bind to
HTTPMaxPort int // Ending port number for HTTP to bind to
DNSSdEnable bool // Enable DNS-SD advertising
LoopbackOnly bool // Use only loopback interface
IPV6Enable bool // Enable IPv6 advertising
ConfAuthUID []*AuthUIDRule // [auth uid], parsed
LogDevice LogLevel // Per-device LogLevel mask
LogMain LogLevel // Main log LogLevel mask
LogConsole LogLevel // Console LogLevel mask
LogMaxFileSize int64 // Maximum log file size
LogMaxBackupFiles uint // Count of files preserved during rotation
LogAllPrinterAttrs bool // Get *all* printer attrs, for logging
ColorConsole bool // Enable ANSI colors on console
Quirks QuirksSet // Device quirks
}
// Conf contains a global instance of program configuration
var Conf = Configuration{
HTTPMinPort: 60000,
HTTPMaxPort: 65535,
DNSSdEnable: true,
LoopbackOnly: true,
IPV6Enable: true,
ConfAuthUID: nil,
LogDevice: LogDebug,
LogMain: LogDebug,
LogConsole: LogDebug,
LogMaxFileSize: 256 * 1024,
LogMaxBackupFiles: 5,
LogAllPrinterAttrs: false,
ColorConsole: true,
}
// ConfLoad loads the program configuration
func ConfLoad() error {
// Obtain path to executable directory
exepath, err := os.Executable()
if err != nil {
return fmt.Errorf("conf: %s", err)
}
exepath = filepath.Dir(exepath)
// Build list of configuration files
files := []string{
filepath.Join(PathConfDir, ConfFileName),
filepath.Join(exepath, ConfFileName),
}
// Load file by file
for _, file := range files {
err = confLoadInternal(file)
if err != nil {
return err
}
}
// Load quirks
quirksDirs := []string{
PathQuirksDir,
PathConfQuirksDir,
filepath.Join(exepath, "ipp-usb-quirks"),
}
if err == nil {
Conf.Quirks, err = LoadQuirksSet(quirksDirs...)
}
return err
}
// Load the program configuration -- internal version
func confLoadInternal(path string) error {
// Open configuration file
ini, err := OpenIniFile(path)
if err != nil {
if os.IsNotExist(err) {
err = nil
}
return err
}
defer ini.Close()
// Extract options
for err == nil {
var rec *IniRecord
rec, err = ini.Next()
if err != nil {
break
}
switch {
case confMatchName(rec.Section, "network"):
switch {
case confMatchName(rec.Key, "http-min-port"):
err = rec.LoadIPPort(&Conf.HTTPMinPort)
case confMatchName(rec.Key, "http-max-port"):
err = rec.LoadIPPort(&Conf.HTTPMaxPort)
case confMatchName(rec.Key, "dns-sd"):
err = rec.LoadNamedBool(&Conf.DNSSdEnable, "disable", "enable")
case confMatchName(rec.Key, "interface"):
err = rec.LoadNamedBool(&Conf.LoopbackOnly, "all", "loopback")
case confMatchName(rec.Key, "ipv6"):
err = rec.LoadNamedBool(&Conf.IPV6Enable, "disable", "enable")
}
case confMatchName(rec.Section, "auth uid"):
err = rec.LoadAuthUIDRules(&Conf.ConfAuthUID)
case confMatchName(rec.Section, "logging"):
switch {
case confMatchName(rec.Key, "device-log"):
err = rec.LoadLogLevel(&Conf.LogDevice)
case confMatchName(rec.Key, "main-log"):
err = rec.LoadLogLevel(&Conf.LogMain)
case confMatchName(rec.Key, "console-log"):
err = rec.LoadLogLevel(&Conf.LogConsole)
case confMatchName(rec.Key, "console-color"):
err = rec.LoadNamedBool(&Conf.ColorConsole, "disable", "enable")
case confMatchName(rec.Key, "max-file-size"):
err = rec.LoadSize(&Conf.LogMaxFileSize)
case confMatchName(rec.Key, "max-backup-files"):
err = rec.LoadUint(&Conf.LogMaxBackupFiles)
case confMatchName(rec.Key, "get-all-printer-attrs"):
err = rec.LoadBool(&Conf.LogAllPrinterAttrs)
}
}
}
if err != nil && err != io.EOF {
return err
}
// Validate configuration
if Conf.HTTPMinPort >= Conf.HTTPMaxPort {
return errors.New("http-min-port must be less that http-max-port")
}
return nil
}
// confMatchName tells if section or key name matches
// the pattern
// - match is case-insensitive
// - difference in amount of free space is ignored
// - leading and trailing space is ignored
func confMatchName(name, pattern string) bool {
name = strings.TrimSpace(name)
pattern = strings.TrimSpace(pattern)
for name != "" && pattern != "" {
c1 := rune(name[0])
c2 := rune(pattern[0])
switch {
case unicode.IsSpace(c1):
if !unicode.IsSpace(c2) {
return false
}
name = strings.TrimSpace(name)
pattern = strings.TrimSpace(pattern)
case c1 == c2:
name = name[1:]
pattern = pattern[1:]
default:
return false
}
}
return true
}
0707010000000D000081A400000000000000000000000167D72F5D0000030E000000000000000000000000000000000000001800000000ipp-usb-0.9.30/const.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
*
* Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
* See LICENSE for license terms and conditions
*
* Configuration constants
*/
package main
import (
"time"
)
const (
// DevInitTimeout specifies how much time to wait for
// device initialization
DevInitTimeout = 5 * time.Second
// DevShutdownTimeout specifies how much time to wait for
// device graceful shutdown
DevShutdownTimeout = 5 * time.Second
// DevInitRetryInterval specifies the retry interval for
// failed device initialization
DevInitRetryInterval = 2 * time.Second
// DNSSdRetryInterval specifies the retry interval in a case
// of failed DNS-SD operation
DNSSdRetryInterval = 2 * time.Second
)
0707010000000E000081A400000000000000000000000167D72F5D00000B57000000000000000000000000000000000000001B00000000ipp-usb-0.9.30/ctrlsock.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
*
* Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
* See LICENSE for license terms and conditions
*
* Control socket handler
*
* ipp-usb runs a HTTP server on a top of the unix domain control
* socket.
*
* Currently it is only used to obtain a per-device status from the
* running daemon. Using HTTP here sounds as overkill, but taking
* in account that it costs us virtually nothing and this mechanism
* is well-extendable, this is a good choice
*/
package main
import (
"log"
"net"
"net/http"
"os"
"syscall"
)
var (
// CtrlsockAddr contains control socket address in
// a form of the net.UnixAddr structure
CtrlsockAddr = &net.UnixAddr{Name: PathControlSocket, Net: "unix"}
// ctrlsockServer is a HTTP server that runs on a top of
// the status socket
ctrlsockServer = http.Server{
Handler: http.HandlerFunc(ctrlsockHandler),
ErrorLog: log.New(Log.LineWriter(LogError, '!'), "", 0),
}
)
// ctrlsockHandler handles HTTP requests that come over the
// control socket
func ctrlsockHandler(w http.ResponseWriter, r *http.Request) {
Log.Debug(' ', "ctrlsock: %s %s", r.Method, r.URL)
// Catch panics to log
defer func() {
v := recover()
if v != nil {
Log.Panic(v)
}
}()
// Check request method
if r.Method != "GET" {
http.Error(w, r.Method+": method not supported",
http.StatusMethodNotAllowed)
return
}
// Check request path
if r.URL.Path != "/status" {
http.Error(w, "Not found", http.StatusNotFound)
return
}
// Handle the request
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
httpNoCache(w)
w.WriteHeader(http.StatusOK)
w.Write(StatusFormat())
}
// CtrlsockStart starts control socket server
func CtrlsockStart() error {
Log.Debug(' ', "ctrlsock: listening at %q", PathControlSocket)
// Listen the socket
os.Remove(PathControlSocket)
listener, err := net.ListenUnix("unix", CtrlsockAddr)
if err != nil {
return err
}
// Make socket accessible to everybody. Error is ignores,
// it's not a reason to abort ipp-usb
os.Chmod(PathControlSocket, 0777)
// Start HTTP server on a top of the listening socket
go func() {
ctrlsockServer.Serve(listener)
}()
return nil
}
// CtrlsockStop stops the control socket server
func CtrlsockStop() {
Log.Debug(' ', "ctrlsock: shutdown")
ctrlsockServer.Close()
}
// CtrlsockDial connects to the control socket of the running
// ipp-usb daemon
func CtrlsockDial() (net.Conn, error) {
conn, err := net.DialUnix("unix", nil, CtrlsockAddr)
if err == nil {
return conn, err
}
if neterr, ok := err.(*net.OpError); ok {
if syserr, ok := neterr.Err.(*os.SyscallError); ok {
switch syserr.Err {
case syscall.ECONNREFUSED, syscall.ENOENT:
err = ErrNoIppUsb
case syscall.EACCES, syscall.EPERM:
err = ErrAccess
}
}
}
return conn, err
}
0707010000000F000081A400000000000000000000000167D72F5D0000086C000000000000000000000000000000000000001900000000ipp-usb-0.9.30/daemon.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
*
* Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
* See LICENSE for license terms and conditions
*
* Demonization
*/
package main
import (
"bytes"
"errors"
"fmt"
"io"
"os"
"strings"
"syscall"
"unicode"
)
// #include <unistd.h>
import "C"
// CloseStdInOutErr closes stdin/stdout/stderr handles
func CloseStdInOutErr() error {
nul, err := syscall.Open(os.DevNull, syscall.O_RDONLY, 0644)
if err != nil {
return fmt.Errorf("Open %q: %s", os.DevNull, err)
}
defer syscall.Close(nul)
// Note, syscall.Dup2 is not implemented on old Go
// versions for ARM64 Linux. So we use C.dup2 as a
// portable workaround
C.dup2(C.int(nul), 0)
C.dup2(C.int(nul), 1)
C.dup2(C.int(nul), 2)
return nil
}
// Daemon runs ipp-usb program in background
func Daemon() error {
// Obtain path to program's executable
exe, err := os.Executable()
if err != nil {
return err
}
// Create stdout/stderr pipes
rstdout, wstdout, err := os.Pipe()
if err != nil {
return fmt.Errorf("pipe(): %s", err)
}
rstderr, wstderr, err := os.Pipe()
if err != nil {
return fmt.Errorf("pipe(): %s", err)
}
devnull, err := os.Open(os.DevNull)
if err != nil {
return fmt.Errorf("Open %q: %s", os.DevNull, err)
}
// Initialize process attributes
attr := &os.ProcAttr{
Files: []*os.File{devnull, wstdout, wstderr},
Sys: &syscall.SysProcAttr{
Setsid: true,
},
}
// Initialize process arguments
args := []string{}
for _, arg := range os.Args {
if arg != "-bg" {
args = append(args, arg)
}
}
// Start new process
proc, err := os.StartProcess(exe, args, attr)
if err != nil {
return err
}
// Collect its initialization output
wstdout.Close()
wstderr.Close()
stdout := &bytes.Buffer{}
stderr := &bytes.Buffer{}
io.Copy(stdout, rstdout)
io.Copy(stderr, rstderr)
if stdout.Len() != 0 {
os.Stdout.Write(stdout.Bytes())
}
// Check for an error
if stderr.Len() > 0 {
s := strings.TrimFunc(stderr.String(), unicode.IsSpace)
proc.Kill() // Just in case
return errors.New(s)
}
proc.Release()
return nil
}
07070100000010000081A400000000000000000000000167D72F5D00001BDA000000000000000000000000000000000000001900000000ipp-usb-0.9.30/device.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
*
* Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
* See LICENSE for license terms and conditions
*
* Device object brings all parts together
*/
package main
import (
"context"
"fmt"
"net"
"net/http"
)
// Device object brings all parts together, namely:
// - HTTP proxy server
// - USB-backed http.Transport
// - DNS-SD advertiser
//
// There is one instance of Device object per USB device
type Device struct {
UsbAddr UsbAddr // Device's USB address
State *DevState // Persistent state
HTTPClient *http.Client // HTTP client for internal queries
HTTPProxy *HTTPProxy // HTTP proxy
UsbTransport *UsbTransport // Backing USB transport
DNSSdPublisher *DNSSdPublisher // DNS-SD publisher
Log *Logger // Device's logger
}
// NewDevice creates new Device object
func NewDevice(desc UsbDeviceDesc) (*Device, error) {
dev := &Device{
UsbAddr: desc.UsbAddr,
}
var err error
var info UsbDeviceInfo
var listener net.Listener
var ippinfo *IppPrinterInfo
var dnssdName string
var dnssdServices DNSSdServices
var log *LogMessage
var hwid string
var quirks Quirks
var httpstatus int
var canPrint bool
var canScan bool
// Create USB transport
dev.UsbTransport, err = NewUsbTransport(desc)
if err != nil {
goto ERROR
}
// Obtain quirks
quirks = dev.UsbTransport.Quirks()
// Obtain device's logger
dev.Log = dev.UsbTransport.Log()
// Obtain device info and derived information.
info = dev.UsbTransport.UsbDeviceInfo()
hwid = fmt.Sprintf("%4.4x&%4.4x", info.Vendor, info.Product)
canPrint = info.BasicCaps&UsbIppBasicCapsPrint != 0
canScan = info.BasicCaps&UsbIppBasicCapsScan != 0
// Load persistent state
dev.State = LoadDevState(info.Ident(), info.Comment())
// Create HTTP client for local queries
dev.HTTPClient = &http.Client{
Transport: dev.UsbTransport,
}
// Create net.Listener
listener, err = dev.State.HTTPListen()
if err != nil {
goto ERROR
}
// Configure transport for init
dev.UsbTransport.SetTimeout(quirks.GetInitTimeout())
// Create HTTP server
dev.HTTPProxy = NewHTTPProxy(dev.Log, listener, dev.UsbTransport)
// Obtain DNS-SD info for IPP
log = dev.Log.Begin()
defer log.Commit()
ippinfo, httpstatus, err = IppService(log, &dnssdServices,
dev.State.HTTPPort, info, dev.UsbTransport.Quirks(),
dev.HTTPClient)
if err != nil {
dev.Log.Error('!', "IPP: %s", err)
canRetry := httpstatus != 0 || ErrIsEOF(err)
if canRetry && canPrint && quirks.GetInitRetryPartial() {
dev.Log.Begin().
Info(' ', "Printer not ready (HTTP status %d)",
httpstatus).
Info(' ', "Retrying due to the %q quirk",
QuirkNmInitRetryPartial).
Commit()
err = ErrPartialInit
goto ERROR
}
}
log.Flush()
if dev.UsbTransport.TimeoutExpired() {
err = ErrInitTimedOut
goto ERROR
}
// Obtain DNS-SD name
if ippinfo != nil {
dnssdName = ippinfo.DNSSdName
} else {
dnssdName = info.DNSSdName()
}
// Update device state, if name changed
if dnssdName != dev.State.DNSSdName {
dev.State.DNSSdName = dnssdName
dev.State.DNSSdOverride = dnssdName
dev.State.Save()
}
// Obtain DNS-SD info for eSCL
httpstatus, err = EsclService(log, &dnssdServices, dev.State.HTTPPort, info,
ippinfo, dev.HTTPClient)
if err != nil {
dev.Log.Error('!', "ESCL: %s", err)
canRetry := httpstatus != 0 || ErrIsEOF(err)
if canRetry && canScan && quirks.GetInitRetryPartial() {
dev.Log.Begin().
Info(' ', "Scanner not ready (HTTP status %d)",
httpstatus).
Info(' ', "Retrying due to the %q quirk",
QuirkNmInitRetryPartial).
Commit()
err = ErrPartialInit
goto ERROR
}
}
log.Flush()
if dev.UsbTransport.TimeoutExpired() {
err = ErrInitTimedOut
goto ERROR
}
// Update IPP service advertising for scanner presence
if ippinfo != nil {
if ippSvc := &dnssdServices[ippinfo.IppSvcIndex]; err == nil {
ippSvc.Txt.Add("Scan", "T")
} else {
ippSvc.Txt.Add("Scan", "F")
}
}
// Skip the device, if it cannot do something useful
//
// Some devices (so far, only HP-rebranded Samsung devices
// known to have such a defect) offer 7/1/4 interfaces, but
// actually provide no functionality behind these interfaces
// and respond with `HTTP 404 Not found` to all the HTTP
// requests sent to USB
//
// ipp-usb ignores such devices to let a chance for
// legacy/proprietary drivers to work with them
if len(dnssdServices) == 0 {
err = ErrUnusable
goto ERROR
}
// Add common TXT records:
// - usb_SER=VCF9192281 ; Device USB serial number
// - usb_HWID=0482&069d ; Its vendor and device ID
for i := range dnssdServices {
svc := &dnssdServices[i]
svc.Txt.Add("usb_SER", info.SerialNumber)
svc.Txt.Add("usb_HWID", hwid)
}
// Advertise Web service. Assume it always exists
dnssdServices.Add(DNSSdSvcInfo{Type: "_http._tcp", Port: dev.State.HTTPPort})
// Advertise service with the following parameters:
// Instance: "BBPP", where BB and PP are bus and port numbers in hex
// Type: "_ipp-usb._tcp"
//
// The purpose of this advertising is to help legacy drivers to
// easily check for devices, handled by ipp-usb
//
// See the following for details:
// https://github.com/OpenPrinting/ipp-usb/issues/28
dnssdServices.Add(DNSSdSvcInfo{
Instance: fmt.Sprintf("%.2X%.2x", desc.Bus, info.PortNum),
Type: "_ipp-usb._tcp",
Port: dev.State.HTTPPort,
Loopback: true,
})
// Enable handling incoming requests
dev.UsbTransport.SetTimeout(0)
dev.HTTPProxy.Enable()
// Start DNS-SD publisher
for _, svc := range dnssdServices {
dev.Log.Debug('>', "%s: %s TXT record:", dnssdName, svc.Type)
for _, txt := range svc.Txt {
dev.Log.Debug(' ', " %s=%s", txt.Key, txt.Value)
}
}
if Conf.DNSSdEnable {
dev.DNSSdPublisher = NewDNSSdPublisher(dev.Log, dev.State,
dnssdServices)
err = dev.DNSSdPublisher.Publish()
if err != nil {
goto ERROR
}
}
return dev, nil
ERROR:
if dev.HTTPProxy != nil {
dev.HTTPProxy.Close()
}
if dev.UsbTransport != nil {
reset := true
switch err {
case ErrUnusable, ErrPartialInit:
reset = false
}
dev.UsbTransport.Close(reset)
}
if listener != nil {
listener.Close()
}
return nil, err
}
// Shutdown gracefully shuts down the device. If provided context
// expires before the shutdown is complete, Shutdown returns the
// context's error
func (dev *Device) Shutdown(ctx context.Context) error {
if dev.DNSSdPublisher != nil {
dev.DNSSdPublisher.Unpublish()
dev.DNSSdPublisher = nil
}
if dev.HTTPProxy != nil {
dev.HTTPProxy.Close()
dev.HTTPProxy = nil
}
if dev.UsbTransport != nil {
return dev.UsbTransport.Shutdown(ctx)
}
return nil
}
// Close the Device
func (dev *Device) Close() {
if dev.DNSSdPublisher != nil {
dev.DNSSdPublisher.Unpublish()
dev.DNSSdPublisher = nil
}
if dev.HTTPProxy != nil {
dev.HTTPProxy.Close()
dev.HTTPProxy = nil
}
if dev.UsbTransport != nil {
dev.UsbTransport.Close(false)
dev.UsbTransport = nil
}
}
07070100000011000081A400000000000000000000000167D72F5D00001741000000000000000000000000000000000000001B00000000ipp-usb-0.9.30/devstate.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
*
* Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
* See LICENSE for license terms and conditions
*
* Per-device persistent state
*/
package main
import (
"bytes"
"fmt"
"io"
"net"
"os"
"path/filepath"
"strconv"
)
// DevState manages a per-device persistent state (such as HTTP
// port allocation etc)
type DevState struct {
Ident string // Device identification
HTTPPort int // Allocated HTTP port
DNSSdName string // DNS-SD name, as reported by device
DNSSdOverride string // DNS-SD name after collision resolution
comment string // Comment in the state file
path string // Path to the disk file
}
// LoadDevState loads DevState from a disk file
//
// This function always succeeds, even in a case of file i/o errors.
// In a worst case we loose state persistence, not other functionality.
func LoadDevState(ident, comment string) *DevState {
state := &DevState{
Ident: ident,
comment: comment,
}
state.path = state.devStatePath()
// Read state file
ini, err := OpenIniFile(state.path)
if err == nil {
err = state.load(ini)
ini.Close()
}
if err != nil && err != io.EOF {
if !os.IsNotExist(err) {
Log.Error('!', "STATE LOAD: %s", state.error("%s", err))
}
}
return state
}
// LoadUsedPorts loads ports used by some of devices.
//
// The returned map contains one entry per used port. Value of this
// entry is a human-readable string, reasonable for logging
func LoadUsedPorts() (ports map[int]string) {
ports = make(map[int]string)
// Read the PathProgStateDev (normally "/var/ipp-usb/dev")
// directory.
var files []os.FileInfo
var err error
dir, err := os.Open(PathProgStateDev)
if err == nil {
files, err = dir.Readdir(0)
dir.Close()
}
if err != nil {
Log.Error('!', "Can't load existing ports allocation")
Log.Error('!', "%s", err)
return
}
if err != nil {
return
}
// Scan found files
for _, file := range files {
Log.Debug(' ', "== %s", file.Name())
if !file.Mode().IsRegular() {
continue
}
path := filepath.Join(PathProgStateDev, file.Name())
ini, err := OpenIniFile(path)
if err != nil {
Log.Error('!', "%s", err)
continue
}
state := &DevState{}
err = state.load(ini)
ini.Close()
if err != nil {
Log.Error('!', "%s", err)
continue
}
if state.HTTPPort != 0 {
ports[state.HTTPPort] = file.Name()
}
}
return
}
// load performs an actual work of loading the DevState file
func (state *DevState) load(ini *IniFile) error {
err := ini.Lock(FileLockWait)
if err == nil {
defer ini.Unlock()
}
for err == nil {
var rec *IniRecord
rec, err = ini.Next()
if err != nil {
break
}
switch rec.Section {
case "device":
switch rec.Key {
case "http-port":
err = state.loadTCPPort(&state.HTTPPort, rec)
case "dns-sd-name":
state.DNSSdName = rec.Value
case "dns-sd-override":
state.DNSSdOverride = rec.Value
}
}
}
if err == io.EOF {
err = nil
}
return err
}
// Load TCP port
func (state *DevState) loadTCPPort(out *int, rec *IniRecord) error {
port, err := strconv.Atoi(rec.Value)
if err != nil {
err = state.error("%s", err)
} else if port < 1 || port > 65535 {
err = state.error("%s: out of range", rec.Key)
}
if err != nil {
return err
}
*out = port
return nil
}
// Save updates DevState on disk
func (state *DevState) Save() {
os.MkdirAll(PathProgStateDev, 0755)
var buf bytes.Buffer
if state.comment != "" {
fmt.Fprintf(&buf, "; %s\n", state.comment)
}
fmt.Fprintf(&buf, "[device]\n")
fmt.Fprintf(&buf, "http-port = %d\n", state.HTTPPort)
fmt.Fprintf(&buf, "dns-sd-name = %q\n", state.DNSSdName)
fmt.Fprintf(&buf, "dns-sd-override = %q\n", state.DNSSdOverride)
err := state.save(buf.Bytes())
if err != nil {
err = state.error("%s", err)
Log.Error('!', "STATE SAVE: %s", err)
}
}
// save performs an actual work of saving state file
func (state *DevState) save(data []byte) error {
f, err := os.OpenFile(state.path,
os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
return err
}
err = FileLock(f, FileLockWait)
if err != nil {
f.Close()
return err
}
_, err = f.Write(data)
FileUnlock(f)
if err != nil {
f.Close()
return err
}
return f.Close()
}
// HTTPListen allocates HTTP port and updates persistent configuration
func (state *DevState) HTTPListen() (net.Listener, error) {
port := state.HTTPPort
// Check that preallocated port is within the configured range
if !(Conf.HTTPMinPort <= port && port <= Conf.HTTPMaxPort) {
port = 0
}
// Try to allocate port used before
if port != 0 {
listener, err := NewListener(port)
if err == nil {
return listener, nil
}
}
// Allocate a port. Don't reuse ports allocated by other
// devices.
ports := LoadUsedPorts()
for port = Conf.HTTPMinPort; port <= Conf.HTTPMaxPort; port++ {
used := ports[port]
if used != "" {
Log.Info(' ', "HTTP port %d used by %s", port, used)
continue
}
listener, err := NewListener(port)
if err == nil {
state.HTTPPort = port
state.Save()
return listener, nil
}
}
// No success so far. Repeat allocation attempt, ignoring
// existent allocations
for port = Conf.HTTPMinPort; port <= Conf.HTTPMaxPort; port++ {
listener, err := NewListener(port)
if err == nil {
state.HTTPPort = port
state.Save()
return listener, nil
}
}
// Give up and return an error
err := state.error("failed to allocate HTTP port", state.Ident)
Log.Error('!', "STATE PORT: %s", err)
return nil, err
}
// devStatePath returns a path to the DevState file
func (state *DevState) devStatePath() string {
return filepath.Join(PathProgStateDev, state.Ident+".state")
}
// error creates a state-related error
func (state *DevState) error(format string, args ...interface{}) error {
return fmt.Errorf(state.Ident+": "+format, args...)
}
07070100000012000081A400000000000000000000000167D72F5D00001E45000000000000000000000000000000000000001800000000ipp-usb-0.9.30/dnssd.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
*
* Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
* See LICENSE for license terms and conditions
*
* DNS-SD publisher: system-independent stuff
*/
package main
import (
"fmt"
"strings"
"sync"
"time"
)
// DNSSdTxtItem represents a single TXT record item
type DNSSdTxtItem struct {
Key, Value string // TXT entry: Key=Value
URL bool // It's an URL, hostname must be adjusted
}
// DNSSdTxtRecord represents a TXT record
type DNSSdTxtRecord []DNSSdTxtItem
// Add adds regular (non-URL) item to DNSSdTxtRecord
func (txt *DNSSdTxtRecord) Add(key, value string) {
*txt = append(*txt, DNSSdTxtItem{key, value, false})
}
// AddURL adds URL item to DNSSdTxtRecord
func (txt *DNSSdTxtRecord) AddURL(key, value string) {
*txt = append(*txt, DNSSdTxtItem{key, value, true})
}
// AddPDL adds PDL list (list of supported Page Description Languages, i.e.,
// document formats) to the DNSSdTxtRecord.
//
// Sometimes the PDL list that comes from device, is too large to fit
// TXT record (key=value pair must not exceed 255 bytes). At this case
// we take only as much as possible leading entries of the device-supplied
// list in hope that firmware is smart enough to place most common PDLs
// to the beginning of the list, while more exotic entries goes to the end
func (txt *DNSSdTxtRecord) AddPDL(key, value string) {
// How many space we have for value? Is it enough?
max := 255 - len(key) - 1
if max >= len(value) {
txt.Add(key, value)
return
}
// Safety check
if max <= 0 {
return
}
// Truncate the value to fit available space
value = value[:max+1]
i := strings.LastIndexByte(value, ',')
if i < 0 {
return
}
value = value[:i]
txt.Add(key, value)
}
// IfNotEmpty adds item to DNSSdTxtRecord if its value is not empty
//
// It returns true if item was actually added, false otherwise
func (txt *DNSSdTxtRecord) IfNotEmpty(key, value string) bool {
if value != "" {
txt.Add(key, value)
return true
}
return false
}
// URLIfNotEmpty works as IfNotEmpty, but for URLs
func (txt *DNSSdTxtRecord) URLIfNotEmpty(key, value string) bool {
if value != "" {
txt.AddURL(key, value)
return true
}
return false
}
// export DNSSdTxtRecord into Avahi format
func (txt DNSSdTxtRecord) export() [][]byte {
var exported [][]byte
// Note, for a some strange reason, Avahi published
// TXT record in reverse order, so compensate it here
for i := len(txt) - 1; i >= 0; i-- {
item := txt[i]
exported = append(exported, []byte(item.Key+"="+item.Value))
}
return exported
}
// DNSSdSvcInfo represents a DNS-SD service information
type DNSSdSvcInfo struct {
Instance string // If not "", override common instance name
Type string // Service type, i.e. "_ipp._tcp"
SubTypes []string // Service subtypes, if any
Port int // TCP port
Txt DNSSdTxtRecord // TXT record
Loopback bool // Advertise only on loopback interface
}
// DNSSdServices represents a collection of DNS-SD services
type DNSSdServices []DNSSdSvcInfo
// Add DNSSdSvcInfo to DNSSdServices
func (services *DNSSdServices) Add(srv DNSSdSvcInfo) {
*services = append(*services, srv)
}
// DNSSdPublisher represents a DNS-SD service publisher
// One publisher may publish multiple services unser the
// same Service Instance Name
type DNSSdPublisher struct {
Log *Logger // Device's logger
DevState *DevState // Device persistent state
Services DNSSdServices // Registered services
fin chan struct{} // Closed to terminate publisher goroutine
finDone sync.WaitGroup // To wait for goroutine termination
sysdep *dnssdSysdep // System-dependent stuff
}
// DNSSdStatus represents DNS-SD publisher status
type DNSSdStatus int
const (
// DNSSdNoStatus is used to indicate that status is
// not known (yet)
DNSSdNoStatus DNSSdStatus = iota
// DNSSdCollision indicates instance name collision
DNSSdCollision
// DNSSdFailure indicates publisher failure with any
// other reason that listed before
DNSSdFailure
// DNSSdSuccess indicates successful status
DNSSdSuccess
)
// String returns human-readable representation of DNSSdStatus
func (status DNSSdStatus) String() string {
switch status {
case DNSSdNoStatus:
return "DNSSdNoStatus"
case DNSSdCollision:
return "DNSSdCollision"
case DNSSdFailure:
return "DNSSdFailure"
case DNSSdSuccess:
return "DNSSdSuccess"
}
return fmt.Sprintf("Unknown DNSSdStatus %d", status)
}
// NewDNSSdPublisher creates new DNSSdPublisher
//
// Service instance name comes from the DevState, and if
// name changes as result of name collision resolution,
// DevState will be updated
func NewDNSSdPublisher(log *Logger,
devstate *DevState, services DNSSdServices) *DNSSdPublisher {
return &DNSSdPublisher{
Log: log,
DevState: devstate,
Services: services,
fin: make(chan struct{}),
}
}
// Publish all services
func (publisher *DNSSdPublisher) Publish() error {
instance := publisher.instance(0)
publisher.sysdep = newDnssdSysdep(publisher.Log, instance,
publisher.Services)
publisher.Log.Info('+', "DNS-SD: %s: publishing requested", instance)
publisher.finDone.Add(1)
go publisher.goroutine()
return nil
}
// Unpublish everything
func (publisher *DNSSdPublisher) Unpublish() {
close(publisher.fin)
publisher.finDone.Wait()
publisher.sysdep.Halt()
publisher.Log.Info('-', "DNS-SD: %s: removed", publisher.instance(0))
}
// Build service instance name with optional collision-resolution suffix
func (publisher *DNSSdPublisher) instance(suffix int) string {
name := publisher.DevState.DNSSdName
strSuffix := ""
switch {
// This happens when we try to resolve name conflict
case suffix != 0:
strSuffix = fmt.Sprintf(" (USB %d)", suffix)
// This happens when we've just initialized or reset DNSSdOverride,
// so append "(USB)" suffix
case publisher.DevState.DNSSdName == publisher.DevState.DNSSdOverride:
strSuffix = " (USB)"
// Otherwise, DNSSdOverride contains saved conflict-resolved device name
default:
name = publisher.DevState.DNSSdOverride
}
const MaxDNSSDName = 63
if len(name)+len(strSuffix) > MaxDNSSDName {
name = name[:MaxDNSSDName-len(strSuffix)]
}
return name + strSuffix
}
// Event handling goroutine
func (publisher *DNSSdPublisher) goroutine() {
// Catch panics to log
defer func() {
v := recover()
if v != nil {
Log.Panic(v)
}
}()
defer publisher.finDone.Done()
timer := time.NewTimer(time.Hour)
timer.Stop() // Not ticking now
defer timer.Stop() // And cleanup at return
var err error
var suffix int
instance := publisher.instance(0)
for {
fail := false
select {
case <-publisher.fin:
return
case status := <-publisher.sysdep.Chan():
switch status {
case DNSSdSuccess:
publisher.Log.Info(' ', "DNS-SD: %s: published", instance)
if instance != publisher.DevState.DNSSdOverride {
publisher.DevState.DNSSdOverride = instance
publisher.DevState.Save()
}
case DNSSdCollision:
publisher.Log.Error(' ', "DNS-SD: %s: name collision",
instance)
suffix++
fallthrough
case DNSSdFailure:
publisher.Log.Error(' ', "DNS-SD: %s: publishing failed",
instance)
fail = true
publisher.sysdep.Halt()
default:
publisher.Log.Error(' ', "DNS-SD: %s: unknown event %s",
instance, status)
}
case <-timer.C:
instance = publisher.instance(suffix)
publisher.sysdep = newDnssdSysdep(publisher.Log,
instance, publisher.Services)
if err != nil {
publisher.Log.Error('!', "DNS-SD: %s: %s", instance, err)
fail = true
}
}
if fail {
timer.Reset(DNSSdRetryInterval)
}
}
}
07070100000013000081A400000000000000000000000167D72F5D000026A3000000000000000000000000000000000000001E00000000ipp-usb-0.9.30/dnssd_avahi.go// +build linux freebsd
/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
*
* Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
* See LICENSE for license terms and conditions
*
* DNS-SD publisher: Avahi-based system-dependent part
*/
package main
// #cgo pkg-config: avahi-client
//
// #include <stdlib.h>
// #include <avahi-client/publish.h>
// #include <avahi-common/error.h>
// #include <avahi-common/thread-watch.h>
// #include <avahi-common/watch.h>
//
// void avahiClientCallback(AvahiClient*, AvahiClientState, void*);
// void avahiEntryGroupCallback(AvahiEntryGroup*, AvahiEntryGroupState, void*);
import "C"
import (
"bytes"
"errors"
"fmt"
"net/url"
"sync"
"unsafe"
)
var (
avahiInitLock sync.Mutex
avahiThreadedPoll *C.AvahiThreadedPoll
avahiClientMap = make(map[*C.AvahiClient]*dnssdSysdep)
avahiEgroupMap = make(map[*C.AvahiEntryGroup]*dnssdSysdep)
)
// dnssdSysdep represents a system-dependent DNS-SD advertiser
type dnssdSysdep struct {
log *Logger // Device's logger
instance string // Service Instance Name
fqdn string // Host's fully-qualified domain name
client *C.AvahiClient // Avahi client
egroup *C.AvahiEntryGroup // Avahi entry group
statusChan chan DNSSdStatus // Status notifications channel
}
// dnssdSysdepErr implements error interface on a top of
// Avahi error codes
type dnssdSysdepErr C.int
// Error returns error string for the dnssdSysdepErr
func (err dnssdSysdepErr) Error() string {
return "Avahi error: " + C.GoString(C.avahi_strerror(C.int(err)))
}
// newDnssdSysdep creates new dnssdSysdep instance
func newDnssdSysdep(log *Logger, instance string,
services DNSSdServices) *dnssdSysdep {
log.Debug(' ', "DNS-SD: %s: trying", instance)
var err error
var poll *C.AvahiPoll
var rc C.int
var proto, iface int
sysdep := &dnssdSysdep{
log: log,
instance: instance,
statusChan: make(chan DNSSdStatus, 10),
}
// Obtain index of loopback interface
loopback, err := Loopback()
if err != nil {
goto ERROR // Very unlikely to happen
}
// Obtain AvahiPoll
poll, err = avahiGetPoll()
if err != nil {
goto ERROR
}
// Synchronize with Avahi thread
avahiThreadLock()
defer avahiThreadUnlock()
// Create Avahi client
sysdep.client = C.avahi_client_new(
poll,
C.AVAHI_CLIENT_NO_FAIL,
C.AvahiClientCallback(C.avahiClientCallback),
nil,
&rc,
)
if sysdep.client == nil {
goto AVAHI_ERROR
}
avahiClientMap[sysdep.client] = sysdep
sysdep.fqdn = C.GoString(C.avahi_client_get_host_name_fqdn(sysdep.client))
sysdep.log.Debug(' ', "DNS-SD: FQDN: %q", sysdep.fqdn)
// Create entry group
sysdep.egroup = C.avahi_entry_group_new(
sysdep.client,
C.AvahiEntryGroupCallback(C.avahiEntryGroupCallback),
nil,
)
if sysdep.egroup == nil {
rc = C.avahi_client_errno(sysdep.client)
goto AVAHI_ERROR
}
avahiEgroupMap[sysdep.egroup] = sysdep
// Compute iface and proto, adjust fqdn
iface = C.AVAHI_IF_UNSPEC
if Conf.LoopbackOnly {
iface = loopback
old := sysdep.fqdn
sysdep.fqdn = "localhost"
sysdep.log.Debug(' ', "DNS-SD: FQDN: %q->%q", old, sysdep.fqdn)
}
proto = C.AVAHI_PROTO_UNSPEC
if !Conf.IPV6Enable {
proto = C.AVAHI_PROTO_INET
}
// Populate entry group
for _, svc := range services {
// Prepare TXT record
var cTxt *C.AvahiStringList
cTxt, err = sysdep.avahiTxtRecord(svc.Port, svc.Txt)
if err != nil {
goto ERROR
}
// Prepare C strings for service instance and type
cSvcType := C.CString(svc.Type)
var cInstance *C.char
if svc.Instance != "" {
cInstance = C.CString(svc.Instance)
} else {
cInstance = C.CString(instance)
}
// Handle loopback-only mode
ifaceInUse := iface
if svc.Loopback {
ifaceInUse = loopback
}
// Register service type
rc = C.avahi_entry_group_add_service_strlst(
sysdep.egroup,
C.AvahiIfIndex(ifaceInUse),
C.AvahiProtocol(proto),
0,
cInstance,
cSvcType,
nil, // Domain
nil, // Host
C.uint16_t(svc.Port),
cTxt,
)
// Register subtypes, if any
for _, subtype := range svc.SubTypes {
if rc != C.AVAHI_OK {
break
}
sysdep.log.Debug(' ', "DNS-SD: +subtype: %q", subtype)
cSubtype := C.CString(subtype)
rc = C.avahi_entry_group_add_service_subtype(
sysdep.egroup,
C.AvahiIfIndex(ifaceInUse),
C.AvahiProtocol(proto),
0,
cInstance,
cSvcType,
nil,
cSubtype,
)
C.free(unsafe.Pointer(cSubtype))
}
// Release C memory
C.free(unsafe.Pointer(cInstance))
C.free(unsafe.Pointer(cSvcType))
C.avahi_string_list_free(cTxt)
// Check for Avahi error
if rc != C.AVAHI_OK {
goto AVAHI_ERROR
}
}
// Commit changes
rc = C.avahi_entry_group_commit(sysdep.egroup)
if rc != C.AVAHI_OK {
goto AVAHI_ERROR
}
// Create and return dnssdSysdep
return sysdep
// Error: cleanup and exit
AVAHI_ERROR:
err = dnssdSysdepErr(rc)
ERROR:
// Raise an error event
sysdep.log.Error(' ', "DNS-SD: %s: %s", sysdep.instance, err)
sysdep.haltLocked()
if err == dnssdSysdepErr(C.AVAHI_ERR_COLLISION) {
sysdep.notify(DNSSdCollision)
} else {
sysdep.notify(DNSSdFailure)
}
return sysdep
}
// Halt dnssdSysdep
//
// It cancel all activity related to the dnssdSysdep instance,
// but sysdep.Chan() remains valid, though no notifications
// will be pushed there anymore
func (sysdep *dnssdSysdep) Halt() {
avahiThreadLock()
sysdep.haltLocked()
avahiThreadUnlock()
}
// Get status change notification channel
func (sysdep *dnssdSysdep) Chan() <-chan DNSSdStatus {
return sysdep.statusChan
}
// Halt dnssdSysdep -- internal version
//
// Must be called under avahiThreadLock
// Can be used with semi-constructed dnssdSysdep
func (sysdep *dnssdSysdep) haltLocked() {
// Free all Avahi stuff
if sysdep.egroup != nil {
C.avahi_entry_group_free(sysdep.egroup)
delete(avahiEgroupMap, sysdep.egroup)
sysdep.egroup = nil
}
if sysdep.client != nil {
C.avahi_client_free(sysdep.client)
delete(avahiClientMap, sysdep.client)
sysdep.client = nil
}
// Drain status channel
for len(sysdep.statusChan) > 0 {
<-sysdep.statusChan
}
}
// Push status change notification
func (sysdep *dnssdSysdep) notify(status DNSSdStatus) {
sysdep.statusChan <- status
}
// avahiTxtRecord converts DNSSdTxtRecord to AvahiStringList
func (sysdep *dnssdSysdep) avahiTxtRecord(port int, txt DNSSdTxtRecord) (
*C.AvahiStringList, error) {
var buf bytes.Buffer
var list, prev *C.AvahiStringList
for _, t := range txt {
buf.Reset()
buf.WriteString(t.Key)
buf.WriteByte('=')
if !t.URL || sysdep.fqdn == "" {
buf.WriteString(t.Value)
} else {
value := t.Value
if parsed, err := url.Parse(value); err == nil && parsed.IsAbs() {
parsed.Host = sysdep.fqdn
if port != 0 {
parsed.Host += fmt.Sprintf(":%d", port)
}
value = parsed.String()
}
buf.WriteString(value)
}
b := buf.Bytes()
prev, list = list, C.avahi_string_list_add_arbitrary(
list,
(*C.uint8_t)(unsafe.Pointer(&b[0])),
C.size_t(len(b)),
)
if list == nil {
C.avahi_string_list_free(prev)
return nil, ErrNoMemory
}
}
return C.avahi_string_list_reverse(list), nil
}
// avahiClientCallback called by Avahi client to notify us about
// client state change
//
//export avahiClientCallback
func avahiClientCallback(client *C.AvahiClient,
state C.AvahiClientState, _ unsafe.Pointer) {
sysdep := avahiClientMap[client]
if sysdep == nil {
return
}
status := DNSSdNoStatus
event := ""
switch state {
case C.AVAHI_CLIENT_S_REGISTERING:
event = "AVAHI_CLIENT_S_REGISTERING"
case C.AVAHI_CLIENT_S_RUNNING:
event = "AVAHI_CLIENT_S_RUNNING"
case C.AVAHI_CLIENT_S_COLLISION:
// This is host name collision. We can't recover
// it here, so lets consider it as DNSSdFailure
event = "AVAHI_CLIENT_S_COLLISION"
status = DNSSdFailure
case C.AVAHI_CLIENT_FAILURE:
event = "AVAHI_CLIENT_FAILURE"
status = DNSSdFailure
case C.AVAHI_CLIENT_CONNECTING:
event = "AVAHI_CLIENT_CONNECTING"
default:
event = fmt.Sprintf("Unknown event %d", state)
}
sysdep.log.Debug(' ', "DNS-SD: %s: %s", sysdep.instance, event)
if status != DNSSdNoStatus {
sysdep.notify(status)
}
}
// avahiEntryGroupCallback called by Avahi client to notify us about
// entry group state change
//
//export avahiEntryGroupCallback
func avahiEntryGroupCallback(egroup *C.AvahiEntryGroup,
state C.AvahiEntryGroupState, _ unsafe.Pointer) {
sysdep := avahiEgroupMap[egroup]
if sysdep == nil {
return
}
status := DNSSdNoStatus
event := ""
switch state {
case C.AVAHI_ENTRY_GROUP_UNCOMMITED:
event = "AVAHI_ENTRY_GROUP_UNCOMMITED"
case C.AVAHI_ENTRY_GROUP_REGISTERING:
event = "AVAHI_ENTRY_GROUP_REGISTERING"
case C.AVAHI_ENTRY_GROUP_ESTABLISHED:
event = "AVAHI_ENTRY_GROUP_ESTABLISHED"
status = DNSSdSuccess
case C.AVAHI_ENTRY_GROUP_COLLISION:
event = "AVAHI_ENTRY_GROUP_COLLISION"
status = DNSSdCollision
case C.AVAHI_ENTRY_GROUP_FAILURE:
event = "AVAHI_ENTRY_GROUP_FAILURE"
status = DNSSdFailure
}
sysdep.log.Debug(' ', "DNS-SD: %s: %s", sysdep.instance, event)
if status != DNSSdNoStatus {
sysdep.notify(status)
}
}
// avahiGetPoll returns pointer to AvahiPoll
// Avahi helper thread is created on demand
func avahiGetPoll() (*C.AvahiPoll, error) {
avahiInitLock.Lock()
defer avahiInitLock.Unlock()
if avahiThreadedPoll == nil {
avahiThreadedPoll = C.avahi_threaded_poll_new()
if avahiThreadedPoll == nil {
return nil, errors.New("initialization failed, not enough memory")
}
C.avahi_threaded_poll_start(avahiThreadedPoll)
}
return C.avahi_threaded_poll_get(avahiThreadedPoll), nil
}
// Lock Avahi thread
func avahiThreadLock() {
C.avahi_threaded_poll_lock(avahiThreadedPoll)
}
// Unlock Avahi thread
func avahiThreadUnlock() {
C.avahi_threaded_poll_unlock(avahiThreadedPoll)
}
07070100000014000081A400000000000000000000000167D72F5D00000418000000000000000000000000000000000000001600000000ipp-usb-0.9.30/err.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
*
* Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
* See LICENSE for license terms and conditions
*
* Common errors
*/
package main
import (
"errors"
"io"
"net/url"
)
// Error values for ipp-usb
var (
ErrLockIsBusy = errors.New("Lock is busy")
ErrNoMemory = errors.New("Not enough memory")
ErrShutdown = errors.New("Shutdown requested")
ErrBlackListed = errors.New("Device is blacklisted")
ErrInitTimedOut = errors.New("Device initialization timed out")
ErrUnusable = errors.New("Device doesn't implement print or scan service")
ErrNoIppUsb = errors.New("ipp-usb daemon not running")
ErrAccess = errors.New("Access denied")
ErrPartialInit = errors.New("Some parts of device not ready yet")
)
// ErrIsEOF tells if error is io.EOF, possibly wrapped by
// the Go HTTP client.
func ErrIsEOF(err error) bool {
if urlerr, ok := err.(*url.Error); ok {
return urlerr.Err == io.EOF
}
return err == io.EOF
}
07070100000015000081A400000000000000000000000167D72F5D00001B41000000000000000000000000000000000000001700000000ipp-usb-0.9.30/escl.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
*
* Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
* See LICENSE for license terms and conditions
*
* ESCL service registration
*/
package main
import (
"bytes"
"encoding/xml"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"sort"
"strings"
)
// EsclService queries eSCL ScannerCapabilities using provided
// http.Client and decodes received information into the form
// suitable for DNS-SD registration
//
// Discovered services will be added to the services collection
func EsclService(log *LogMessage, services *DNSSdServices,
port int, usbinfo UsbDeviceInfo, ippinfo *IppPrinterInfo,
c *http.Client) (httpstatus int, err error) {
uri := fmt.Sprintf("http://localhost:%d/eSCL/ScannerCapabilities", port)
decoder := newEsclCapsDecoder(ippinfo)
svc := DNSSdSvcInfo{
Type: "_uscan._tcp",
Port: port,
}
var xmlData []byte
var list []string
// Query ScannerCapabilities
resp, err := c.Get(uri)
if err != nil {
goto ERROR
}
if resp.StatusCode/100 != 2 {
resp.Body.Close()
httpstatus = resp.StatusCode
err = fmt.Errorf("HTTP status: %s", resp.Status)
goto ERROR
}
xmlData, err = ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
goto ERROR
}
log.Add(LogTraceESCL, '<', "ESCL Scanner Capabilities:")
log.LineWriter(LogTraceESCL, '<').WriteClose(xmlData)
log.Nl(LogTraceESCL)
log.Flush()
// Decode the XML
err = decoder.decode(bytes.NewBuffer(xmlData))
if err != nil {
goto ERROR
}
if decoder.uuid == "" {
decoder.uuid = usbinfo.UUID()
}
// If we have no data, assume eSCL response was invalud
// If we miss some essential data, assume eSCL response was invalid
switch {
case decoder.version == "":
err = errors.New("missed pwg:Version")
case len(decoder.cs) == 0:
err = errors.New("missed scan:ColorMode")
case len(decoder.pdl) == 0:
err = errors.New("missed pwg:DocumentFormat")
case !(decoder.platen || decoder.adf):
err = errors.New("missed pwg:DocumentFormat")
}
if err != nil {
goto ERROR
}
// Build eSCL DNSSdInfo
if decoder.duplex {
svc.Txt.Add("duplex", "T")
} else {
svc.Txt.Add("duplex", "F")
}
switch {
case decoder.platen && !decoder.adf:
svc.Txt.Add("is", "platen")
case !decoder.platen && decoder.adf:
svc.Txt.Add("is", "adf")
case decoder.platen && decoder.adf:
svc.Txt.Add("is", "platen,adf")
}
list = []string{}
for c := range decoder.cs {
list = append(list, c)
}
sort.Strings(list)
svc.Txt.IfNotEmpty("cs", strings.Join(list, ","))
svc.Txt.IfNotEmpty("UUID", decoder.uuid)
svc.Txt.URLIfNotEmpty("adminurl", decoder.adminurl)
svc.Txt.URLIfNotEmpty("representation", decoder.representation)
list = []string{}
for p := range decoder.pdl {
list = append(list, p)
}
sort.Strings(list)
svc.Txt.AddPDL("pdl", strings.Join(list, ","))
svc.Txt.Add("ty", usbinfo.ProductName)
svc.Txt.Add("rs", "eSCL")
svc.Txt.IfNotEmpty("vers", decoder.version)
svc.Txt.IfNotEmpty("txtvers", "1")
// Add to services
services.Add(svc)
return
// Handle a error
ERROR:
if !ErrIsEOF(err) {
err = fmt.Errorf("eSCL: %s", err)
}
return
}
// esclCapsDecoder represents eSCL ScannerCapabilities decoder
type esclCapsDecoder struct {
uuid string // Device UUID
adminurl string // Admin URL
representation string // Icon URL
version string // eSCL Version
platen, adf bool // Has platen/ADF
duplex bool // Has duplex
pdl, cs map[string]struct{} // Formats/colors
}
// newesclCapsDecoder creates new esclCapsDecoder
func newEsclCapsDecoder(ippinfo *IppPrinterInfo) *esclCapsDecoder {
decoder := &esclCapsDecoder{
pdl: make(map[string]struct{}),
cs: make(map[string]struct{}),
}
if ippinfo != nil {
decoder.uuid = ippinfo.UUID
decoder.adminurl = ippinfo.AdminURL
decoder.representation = ippinfo.IconURL
}
return decoder
}
// Decode scanner capabilities
func (decoder *esclCapsDecoder) decode(in io.Reader) error {
xmlDecoder := xml.NewDecoder(in)
var path bytes.Buffer
var lenStack []int
for {
token, err := xmlDecoder.RawToken()
if err != nil {
break
}
switch t := token.(type) {
case xml.StartElement:
lenStack = append(lenStack, path.Len())
path.WriteByte('/')
path.WriteString(t.Name.Space)
path.WriteByte(':')
path.WriteString(t.Name.Local)
decoder.element(path.String())
case xml.EndElement:
last := len(lenStack) - 1
path.Truncate(lenStack[last])
lenStack = lenStack[:last]
case xml.CharData:
data := bytes.TrimSpace(t)
if len(data) > 0 {
decoder.data(path.String(), string(data))
}
}
}
return nil
}
const (
// Relative to root
esclPlaten = "/scan:ScannerCapabilities/scan:Platen"
esclAdf = "/scan:ScannerCapabilities/scan:Adf"
esclPlatenInputCaps = esclPlaten + "/scan:PlatenInputCaps"
esclAdfSimplexCaps = esclAdf + "/scan:AdfSimplexInputCaps"
esclAdfDuplexCaps = esclAdf + "/scan:AdfDuplexInputCaps"
// Relative to esclPlatenInputCaps, esclAdfSimplexCaps or esclAdfDuplexCaps
esclSettingProfile = "/scan:SettingProfiles/scan:SettingProfile"
esclColorMode = esclSettingProfile + "/scan:ColorModes/scan:ColorMode"
esclDocumentFormat = esclSettingProfile + "/scan:DocumentFormats/pwg:DocumentFormat"
esclDocumentFormatExt = esclSettingProfile + "/scan:DocumentFormats/scan:DocumentFormatExt"
)
// handle beginning of XML element
func (decoder *esclCapsDecoder) element(path string) {
switch path {
case esclPlaten:
decoder.platen = true
case esclAdf:
decoder.adf = true
case esclAdfDuplexCaps:
decoder.duplex = true
}
}
// handle XML element data
func (decoder *esclCapsDecoder) data(path, data string) {
switch path {
case "/scan:ScannerCapabilities/scan:UUID":
uuid := UUIDNormalize(data)
if uuid != "" && decoder.uuid == "" {
decoder.uuid = data
}
case "/scan:ScannerCapabilities/scan:AdminURI":
decoder.adminurl = data
case "/scan:ScannerCapabilities/scan:IconURI":
decoder.representation = data
case "/scan:ScannerCapabilities/pwg:Version":
decoder.version = data
case esclPlatenInputCaps + esclColorMode,
esclAdfSimplexCaps + esclColorMode,
esclAdfDuplexCaps + esclColorMode:
data = strings.ToLower(data)
switch {
case strings.HasPrefix(data, "rgb"):
decoder.cs["color"] = struct{}{}
case strings.HasPrefix(data, "grayscale"):
decoder.cs["grayscale"] = struct{}{}
case strings.HasPrefix(data, "blackandwhite"):
decoder.cs["binary"] = struct{}{}
}
case esclPlatenInputCaps + esclDocumentFormat,
esclAdfSimplexCaps + esclDocumentFormat,
esclAdfDuplexCaps + esclDocumentFormat:
decoder.pdl[data] = struct{}{}
case esclPlatenInputCaps + esclDocumentFormatExt,
esclAdfSimplexCaps + esclDocumentFormatExt,
esclAdfDuplexCaps + esclDocumentFormatExt:
decoder.pdl[data] = struct{}{}
}
}
07070100000016000081A400000000000000000000000167D72F5D00000574000000000000000000000000000000000000001D00000000ipp-usb-0.9.30/flock_unix.go// +build darwin dragonfly freebsd linux nacl netbsd openbsd solaris
/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
*
* Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
* See LICENSE for license terms and conditions
*
* File locking -- UNIX version
*/
package main
/*
#include <errno.h>
#include <sys/file.h>
static inline int do_flock (int fd, int op) {
int rc = flock(fd, op);
if (rc < 0) {
rc = -errno;
}
return rc;
}
*/
import "C"
import (
"os"
"syscall"
)
// FileLockCmd represents set of possible values for the
// FileLock argument
type FileLockCmd C.int
const (
// FileLockWait command used to lock the file; wait if it is busy
FileLockWait = C.LOCK_EX
// FileLockNoWait command used to lock the file without wait.
// If file is busy it fails with ErrLockIsBusy error
FileLockNoWait = C.LOCK_EX | C.LOCK_NB
// FileLockUnlock command used to unlock the file
FileLockUnlock = C.LOCK_UN
)
// FileLock manages file lock
func FileLock(file *os.File, cmd FileLockCmd) error {
rc := C.do_flock(C.int(file.Fd()), C.int(cmd))
if rc == 0 {
return nil
}
var err error = syscall.Errno(-rc)
switch err {
case syscall.EACCES, syscall.EAGAIN:
err = ErrLockIsBusy
}
return err
}
// FileUnlock releases file lock
func FileUnlock(file *os.File) error {
return FileLock(file, FileLockUnlock)
}
07070100000017000081A400000000000000000000000167D72F5D0000060B000000000000000000000000000000000000001700000000ipp-usb-0.9.30/glob.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
*
* Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
* See LICENSE for license terms and conditions
*
* Glob-style pattern matching
*/
package main
// GlobMatch matches string against glob-style pattern.
// Pattern may contain wildcards and has a following syntax:
// * - matches any sequence of characters
// ? - matches exactly one character
// \ C - matches character C
// C - matches character C (C is not *, ? or \)
//
// It return a counter of matched non-wildcard characters, -1 if no match
func GlobMatch(str, pattern string) int {
return globMatchInternal(str, pattern, 0)
}
// globMatchInternal does the actual work of GlobMatch() function
func globMatchInternal(str, pattern string, count int) int {
for str != "" && pattern != "" {
p := pattern[0]
pattern = pattern[1:]
switch p {
case '*':
for pattern != "" && pattern[0] == '*' {
pattern = pattern[1:]
}
if pattern == "" {
return count
}
for i := 0; i < len(str); i++ {
c2 := globMatchInternal(str[i:], pattern, count)
if c2 >= 0 {
return c2
}
}
case '?':
str = str[1:]
case '\\':
if pattern == "" {
return -1
}
p, pattern = pattern[0], pattern[1:]
fallthrough
default:
if str[0] != p {
return -1
}
str = str[1:]
count++
}
}
for pattern != "" && pattern[0] == '*' {
pattern = pattern[1:]
}
if str == "" && pattern == "" {
return count
}
return -1
}
07070100000018000081A400000000000000000000000167D72F5D0000036B000000000000000000000000000000000000001C00000000ipp-usb-0.9.30/glob_test.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
*
* Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
* See LICENSE for license terms and conditions
*
* Tests for glob-style pattern matching
*/
package main
import (
"testing"
)
// Test GlobMatch
func TestGlobMatch(t *testing.T) {
testData := []struct {
model, pattern string
count int
}{
{"test", "test", 4},
{"test", "tes?", 3},
{"test", "te?t", 3},
{"test", "te??", 2},
{"test", "te??x", -1},
{"test", "te*", 2},
{"test", "te**", 2},
{"test", "*te**", 2},
{"", "*", 0},
{"test", "t\\est", 4},
{"t?st", "t\\?st", 4},
}
for _, data := range testData {
n := GlobMatch(data.model, data.pattern)
if n != data.count {
t.Errorf("matchModelName(%q,%q): expected %d got %d",
data.model, data.pattern, data.count, n)
}
}
}
07070100000019000081A400000000000000000000000167D72F5D0000005E000000000000000000000000000000000000001600000000ipp-usb-0.9.30/go.modmodule github.com/OpenPrinting/ipp-usb
go 1.11
require github.com/OpenPrinting/goipp v1.1.0
0707010000001A000081A400000000000000000000000167D72F5D000000B1000000000000000000000000000000000000001600000000ipp-usb-0.9.30/go.sumgithub.com/OpenPrinting/goipp v1.1.0 h1:AK19DwnuvCaqbF6ckT2ICe/Hc1o1sVSS+UzE59z4Dx0=
github.com/OpenPrinting/goipp v1.1.0/go.mod h1:ot2iw+QF7fVLaX+55JUNlF5YSDNiXVo2LRAv21iGcQI=
0707010000001B000081A400000000000000000000000167D72F5D000019F3000000000000000000000000000000000000001700000000ipp-usb-0.9.30/http.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
*
* Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
* See LICENSE for license terms and conditions
*
* HTTP proxy
*/
package main
import (
"context"
"errors"
"fmt"
"io"
"log"
"net"
"net/http"
"net/url"
"strings"
"sync/atomic"
)
var (
httpSessionID int32
)
// HTTPProxy represents HTTP protocol proxy backed by the
// specified http.RoundTripper. It implements http.Handler
// interface
type HTTPProxy struct {
log *Logger // Logger instance
server *http.Server // HTTP server
enable bool // Proxy can handle incoming requests
transport *UsbTransport // Transport for outgoing requests
closeWait chan struct{} // Closed at server close
}
// NewHTTPProxy creates new HTTP proxy
func NewHTTPProxy(logger *Logger,
listener net.Listener, transport *UsbTransport) *HTTPProxy {
proxy := &HTTPProxy{
log: logger,
transport: transport,
closeWait: make(chan struct{}),
}
proxy.server = &http.Server{
Handler: proxy,
ErrorLog: log.New(logger.LineWriter(LogError, '!'), "", 0),
}
go func() {
proxy.server.Serve(listener)
close(proxy.closeWait)
}()
return proxy
}
// Close the proxy
func (proxy *HTTPProxy) Close() {
proxy.server.Close()
<-proxy.closeWait
}
// Enable indicates that initialization is completed and
// incoming requests can be handled
func (proxy *HTTPProxy) Enable() {
proxy.enable = true
}
// Handle HTTP request
func (proxy *HTTPProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Catch panics to log
defer func() {
v := recover()
if v != nil {
Log.Panic(v)
}
}()
session := int(atomic.AddInt32(&httpSessionID, 1)-1) % 1000
// Perform sanity checking
if !proxy.enable {
proxy.httpError(session, w, r, http.StatusServiceUnavailable,
errors.New("ipp-usb is not ready for this device"))
return
}
if r.Method == "CONNECT" {
proxy.httpError(session, w, r, http.StatusMethodNotAllowed,
errors.New("CONNECT not allowed"))
return
}
if r.Header.Get("Upgrade") != "" {
proxy.httpError(session, w, r, http.StatusServiceUnavailable,
errors.New("Protocol upgrade is not implemented"))
return
}
if r.URL.IsAbs() {
proxy.httpError(session, w, r, http.StatusServiceUnavailable,
errors.New("Absolute URL not allowed"))
return
}
// Obtain request's client and server addresses
var clientAddr, serverAddr *net.TCPAddr
clientAddr, err := net.ResolveTCPAddr("tcp", r.RemoteAddr)
if err != nil {
proxy.httpError(session, w, r, http.StatusInternalServerError,
errors.New("Unable to get client address for request"))
return
}
if v := r.Context().Value(http.LocalAddrContextKey); v != nil {
if v != nil {
serverAddr, _ = v.(*net.TCPAddr)
}
}
if serverAddr == nil {
proxy.httpError(session, w, r, http.StatusInternalServerError,
errors.New("Unable to get server address for request"))
return
}
// Authenticate
if status, err := AuthHTTPRequest(proxy.log,
clientAddr, serverAddr, r); err != nil {
proxy.httpError(session, w, r, status, err)
return
}
// Adjust request headers
httpRemoveHopByHopHeaders(r.Header)
if r.Host == "" {
if serverAddr.IP.IsLoopback() {
r.Host = fmt.Sprintf("localhost:%d", serverAddr.Port)
} else {
r.Host = serverAddr.String()
}
}
r.URL.Scheme = "http"
r.URL.Host = r.Host
// If request is ordered to the loopback address, and r.Host is not
// "localhost" or "localhost:port", redirect request to the localhost
//
// Note, IPP over USB specification requires Host: to be always
// "localhost" or "localhost:port". Although most of the printers
// accept any syntactically correct Host: header, some of the OKI
// printers doesn't, and reject requests that violate this rule
//
// This redirection fixes compatibility with these printers for
// clients that follow redirects (i.e., web browser and sane-airscan;
// CUPS unfortunately doesn't follow redirects)
if serverAddr.IP.IsLoopback() &&
(r.Method == "GET" || r.Method == "HEAD") {
host := strings.ToLower(r.Host)
if host != "localhost" &&
!strings.HasPrefix(host, "localhost:") {
url := *r.URL
url.Host = fmt.Sprintf("localhost:%d", serverAddr.Port)
proxy.httpRedirect(session, w, r, http.StatusFound, &url)
return
}
}
// Send request and obtain response status and header
resp, err := proxy.transport.RoundTripWithSession(session, r)
if err != nil {
proxy.httpError(session, w, r, http.StatusServiceUnavailable, err)
return
}
httpRemoveHopByHopHeaders(resp.Header)
httpCopyHeaders(w.Header(), resp.Header)
w.WriteHeader(resp.StatusCode)
// Obtain response body, if any
_, err = io.Copy(w, resp.Body)
if err != nil {
proxy.log.HTTPError('!', session, "%s", err)
}
resp.Body.Close()
}
// Reject request with a error
func (proxy *HTTPProxy) httpError(session int, w http.ResponseWriter, r *http.Request,
status int, err error) {
proxy.log.Begin().
HTTPRqParams(LogDebug, '>', session, r).
HTTPRequest(LogTraceHTTP, '>', session, r).
Commit()
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
httpNoCache(w)
w.WriteHeader(status)
w.Write([]byte(err.Error()))
w.Write([]byte("\n"))
if err != context.Canceled {
proxy.log.HTTPError('!', session, "%s", err.Error())
} else {
proxy.log.HTTPDebug(' ', session, "request canceled by impatient client")
}
}
// Respond to request with the HTTP redirect
func (proxy *HTTPProxy) httpRedirect(session int, w http.ResponseWriter, r *http.Request,
status int, location *url.URL) {
proxy.log.Begin().
HTTPRqParams(LogDebug, '>', session, r).
HTTPRequest(LogTraceHTTP, '>', session, r).
Commit()
w.Header().Set("Location", location.String())
w.WriteHeader(status)
proxy.log.HTTPDebug(' ', session, "redirected to %s", location)
}
// Set response headers to disable cacheing
func httpNoCache(w http.ResponseWriter) {
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.Header().Set("Pragma", "no-cache")
w.Header().Set("Expires", "0")
}
// Remove HTTP hop-by-hop headers, RFC 7230, section 6.1
func httpRemoveHopByHopHeaders(hdr http.Header) {
if c := hdr.Get("Connection"); c != "" {
for _, f := range strings.Split(c, ",") {
if f = strings.TrimSpace(f); f != "" {
hdr.Del(f)
}
}
}
for _, c := range []string{"Connection", "Keep-Alive",
"Proxy-Authenticate", "Proxy-Connection",
"Proxy-Authorization", "Te", "Trailer", "Transfer-Encoding"} {
hdr.Del(c)
}
}
// Copy HTTP headers
func httpCopyHeaders(dst, src http.Header) {
for k, v := range src {
dst[k] = v
}
}
0707010000001C000081A400000000000000000000000167D72F5D000034DF000000000000000000000000000000000000001A00000000ipp-usb-0.9.30/inifile.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
*
* Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
* See LICENSE for license terms and conditions
*
* .INI file loader
*/
package main
import (
"bufio"
"bytes"
"fmt"
"math"
"os"
"strconv"
"strings"
"time"
)
// IniFile represents opened .INI file
type IniFile struct {
file *os.File // Underlying file
line int // Line in that file
reader *bufio.Reader // Reader on a top of file
buf bytes.Buffer // Temporary buffer to speed up things
rec IniRecord // Next record
withRecType bool // Return records of any type
}
// IniRecord represents a single .INI file record
type IniRecord struct {
Section string // Section name
Key, Value string // Key and value
File string // Origin file
Line int // Line in that file
Type IniRecordType // Record type
}
// IniRecordType represents IniRecord type
type IniRecordType int
// Record types:
//
// [section] <- IniRecordSection
// key - value <- IniRecordKeyVal
const (
IniRecordSection IniRecordType = iota
IniRecordKeyVal
)
// IniError represents an .INI file read error
type IniError struct {
File string // Origin file
Line int // Line in that file
Message string // Error message
}
// OpenIniFile opens the .INI file for reading
//
// If file is opened this way, (*IniFile) Next() returns
// records of IniRecordKeyVal type only
func OpenIniFile(path string) (ini *IniFile, err error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
ini = &IniFile{
file: f,
line: 1,
reader: bufio.NewReader(f),
rec: IniRecord{
File: path,
},
}
return ini, nil
}
// OpenIniFileWithRecType opens the .INI file for reading
//
// If file is opened this way, (*IniFile) Next() returns
// records of any type
func OpenIniFileWithRecType(path string) (ini *IniFile, err error) {
ini, err = OpenIniFile(path)
if ini != nil {
ini.withRecType = true
}
return
}
// Lock manages file lock on underlying disk file
func (ini *IniFile) Lock(cmd FileLockCmd) error {
return FileLock(ini.file, cmd)
}
// Unlock releases file lock
func (ini *IniFile) Unlock() error {
return FileUnlock(ini.file)
}
// Close the .INI file
func (ini *IniFile) Close() error {
return ini.file.Close()
}
// Next returns next IniRecord or an error
func (ini *IniFile) Next() (*IniRecord, error) {
for {
// Read until next non-space character, skipping all comments
c, err := ini.getcNonSpace()
for err == nil && ini.iscomment(c) {
ini.getcNl()
c, err = ini.getcNonSpace()
}
if err != nil {
return nil, err
}
// Parse next record
ini.rec.Line = ini.line
var token string
switch c {
case '[':
c, token, err = ini.token(']', false)
if err == nil && c == ']' {
ini.rec.Section = token
}
ini.getcNl()
ini.rec.Type = IniRecordSection
if ini.withRecType {
return &ini.rec, nil
}
case '=':
ini.getcNl()
return nil, ini.errorf("unexpected '=' character")
default:
ini.ungetc(c)
c, token, err = ini.token('=', false)
if err == nil && c == '=' {
ini.rec.Key = token
c, token, err = ini.token(-1, true)
if err == nil {
ini.rec.Value = token
ini.rec.Type = IniRecordKeyVal
return &ini.rec, nil
}
} else if err == nil {
return nil, ini.errorf("expected '=' character")
}
}
}
}
// Read next token
func (ini *IniFile) token(delimiter rune, linecont bool) (byte, string, error) {
var accumulator, count, trailingSpace int
var c byte
var err error
type prsState int
const (
prsSkipSpace prsState = iota
prsBody
prsString
prsStringBslash
prsStringHex
prsStringOctal
prsComment
)
// Parse the string
state := prsSkipSpace
ini.buf.Reset()
for {
c, err = ini.getc()
if err != nil || c == '\n' {
break
}
if (state == prsBody || state == prsSkipSpace) && rune(c) == delimiter {
break
}
switch state {
case prsSkipSpace:
if ini.isspace(c) {
break
}
state = prsBody
fallthrough
case prsBody:
if c == '"' {
state = prsString
} else if ini.iscomment(c) {
state = prsComment
} else if c == '\\' && linecont {
c2, _ := ini.getc()
if c2 == '\n' {
ini.buf.Truncate(ini.buf.Len() - trailingSpace)
trailingSpace = 0
state = prsSkipSpace
} else {
ini.ungetc(c2)
}
} else {
ini.buf.WriteByte(c)
}
if state == prsBody {
if ini.isspace(c) {
trailingSpace++
} else {
trailingSpace = 0
}
} else {
ini.buf.Truncate(ini.buf.Len() - trailingSpace)
trailingSpace = 0
}
case prsString:
if c == '\\' {
state = prsStringBslash
} else if c == '"' {
state = prsBody
} else {
ini.buf.WriteByte(c)
}
case prsStringBslash:
if c == 'x' || c == 'X' {
state = prsStringHex
accumulator, count = 0, 0
} else if ini.isoctal(c) {
state = prsStringOctal
accumulator = ini.hex2int(c)
count = 1
} else {
switch c {
case 'a':
c = '\a'
case 'b':
c = '\b'
case 'e':
c = '\x1b'
case 'f':
c = '\f'
case 'n':
c = '\n'
case 'r':
c = '\r'
case 't':
c = '\t'
case 'v':
c = '\v'
}
ini.buf.WriteByte(c)
state = prsString
}
case prsStringHex:
if ini.isxdigit(c) {
if count != 2 {
accumulator = accumulator*16 + ini.hex2int(c)
count++
}
} else {
state = prsString
ini.ungetc(c)
}
if state != prsStringHex {
ini.buf.WriteByte(c)
}
case prsStringOctal:
if ini.isoctal(c) {
accumulator = accumulator*8 + ini.hex2int(c)
count++
if count == 3 {
state = prsString
}
} else {
state = prsString
ini.ungetc(c)
}
if state != prsStringOctal {
ini.buf.WriteByte(c)
}
case prsComment:
// Nothing to do
}
}
// Remove trailing space, if any
ini.buf.Truncate(ini.buf.Len() - trailingSpace)
// Check for syntax error
if state != prsSkipSpace && state != prsBody && state != prsComment {
return 0, "", ini.errorf("unterminated string")
}
return c, ini.buf.String(), nil
}
// getc returns a next character from the input file
func (ini *IniFile) getc() (byte, error) {
c, err := ini.reader.ReadByte()
if c == '\n' {
ini.line++
}
return c, err
}
// getcNonSpace returns a next non-space character from the input file
func (ini *IniFile) getcNonSpace() (byte, error) {
for {
c, err := ini.getc()
if err != nil || !ini.isspace(c) {
return c, err
}
}
}
// getcNl returns a next newline character, or reads until EOF or error
func (ini *IniFile) getcNl() (byte, error) {
for {
c, err := ini.getc()
if err != nil || c == '\n' {
return c, err
}
}
}
// ungetc pushes a character back to the input stream
// only one character can be unread this way
func (ini *IniFile) ungetc(c byte) {
if c == '\n' {
ini.line--
}
ini.reader.UnreadByte()
}
// isspace returns true, if character is whitespace
func (ini *IniFile) isspace(c byte) bool {
switch c {
case ' ', '\t', '\n', '\r':
return true
}
return false
}
// iscomment returns true, if character is commentary
func (ini *IniFile) iscomment(c byte) bool {
return c == ';' || c == '#'
}
// isoctal returns true for octal digit
func (ini *IniFile) isoctal(c byte) bool {
return '0' <= c && c <= '7'
}
// isoctal returns true for hexadecimal digit
func (ini *IniFile) isxdigit(c byte) bool {
return ('0' <= c && c <= '7') ||
('a' <= c && c <= 'f') ||
('A' <= c && c <= 'F')
}
// hex2int return integer value of hexadecimal character
func (ini *IniFile) hex2int(c byte) int {
switch {
case '0' <= c && c <= '9':
return int(c - '0')
case 'a' <= c && c <= 'f':
return int(c-'a') + 10
case 'A' <= c && c <= 'F':
return int(c-'A') + 10
}
return 0
}
// errorf creates a new IniError
func (ini *IniFile) errorf(format string, args ...interface{}) *IniError {
return &IniError{
File: ini.rec.File,
Line: ini.rec.Line,
Message: fmt.Sprintf(format, args...),
}
}
// LoadIPPort loads IP port value
// The destination remains untouched in a case of an error
func (rec *IniRecord) LoadIPPort(out *int) error {
port, err := strconv.Atoi(rec.Value)
if err == nil && (port < 1 || port > 65535) {
err = rec.errBadValue("must be in range 1...65535")
}
if err != nil {
return err
}
*out = port
return nil
}
// LoadBool loads boolean value
// The destination remains untouched in a case of an error
func (rec *IniRecord) LoadBool(out *bool) error {
return rec.LoadNamedBool(out, "false", "true")
}
// LoadNamedBool loads boolean value
// Names for "true" and "false" values are specified explicitly
// The destination remains untouched in a case of an error
func (rec *IniRecord) LoadNamedBool(out *bool, vFalse, vTrue string) error {
switch rec.Value {
case vFalse:
*out = false
return nil
case vTrue:
*out = true
return nil
default:
return rec.errBadValue("must be %s or %s", vFalse, vTrue)
}
}
// LoadLogLevel loads LogLevel value
// The destination remains untouched in a case of an error
func (rec *IniRecord) LoadLogLevel(out *LogLevel) error {
var mask LogLevel
for _, s := range strings.Split(rec.Value, ",") {
s = strings.TrimSpace(s)
switch s {
case "":
case "error":
mask |= LogError
case "info":
mask |= LogInfo | LogError
case "debug":
mask |= LogDebug | LogInfo | LogError
case "trace-ipp":
mask |= LogTraceIPP | LogDebug | LogInfo | LogError
case "trace-escl":
mask |= LogTraceESCL | LogDebug | LogInfo | LogError
case "trace-http":
mask |= LogTraceHTTP | LogDebug | LogInfo | LogError
case "trace-usb":
mask |= LogTraceUSB | LogDebug | LogInfo | LogError
case "all", "trace-all":
mask |= LogAll & ^LogTraceUSB
default:
return rec.errBadValue("invalid log level %q", s)
}
}
*out = mask
return nil
}
// LoadDuration loads time.Duration value
// The destination remains untouched in a case of an error
func (rec *IniRecord) LoadDuration(out *time.Duration) error {
var ms uint
err := rec.LoadUint(&ms)
if err == nil {
*out = time.Millisecond * time.Duration(ms)
}
return err
}
// LoadSize loads size value (returned as int64)
// The syntax is following:
//
// 123 - size in bytes
// 123K - size in kilobytes, 1K == 1024
// 123M - size in megabytes, 1M == 1024K
//
// The destination remains untouched in a case of an error
func (rec *IniRecord) LoadSize(out *int64) error {
var units uint64 = 1
if l := len(rec.Value); l > 0 {
switch rec.Value[l-1] {
case 'k', 'K':
units = 1024
case 'm', 'M':
units = 1024 * 1024
}
if units != 1 {
rec.Value = rec.Value[:l-1]
}
}
sz, err := strconv.ParseUint(rec.Value, 10, 64)
if err != nil {
return rec.errBadValue("%q: invalid size", rec.Value)
}
if sz > uint64(math.MaxInt64/units) {
return rec.errBadValue("size too large")
}
*out = int64(sz * units)
return nil
}
// LoadUint loads unsigned integer value
// The destination remains untouched in a case of an error
func (rec *IniRecord) LoadUint(out *uint) error {
num, err := strconv.ParseUint(rec.Value, 10, 0)
if err != nil {
return rec.errBadValue("%q: invalid number", rec.Value)
}
*out = uint(num)
return nil
}
// LoadUintRange loads unsigned integer value within the range
// The destination remains untouched in a case of an error
func (rec *IniRecord) LoadUintRange(out *uint, min, max uint) error {
var val uint
err := rec.LoadUint(&val)
if err == nil && (val < min || val > max) {
err = rec.errBadValue("must be in range %d...%d", min, max)
}
if err != nil {
return err
}
*out = val
return nil
}
// LoadAuthUIDRules loads AuthUIDRule-s value and appends them
// to the destination
//
// The destination remains untouched in a case of an error
func (rec *IniRecord) LoadAuthUIDRules(out *[]*AuthUIDRule) error {
// Parse rec.Key -- it contains list of operations
allowed := AuthOpsNone
for _, s := range strings.Split(rec.Key, ",") {
s = strings.TrimSpace(s)
switch s {
case "all":
allowed |= AuthOpsAll
case "config":
allowed |= AuthOpsConfig
case "fax":
allowed |= AuthOpsFax
case "print":
allowed |= AuthOpsPrint
case "scan":
allowed |= AuthOpsScan
default:
return rec.errBadValue("invalid operation: %q", s)
}
}
// Parse rec.Value -- it contains list of users
rules := []*AuthUIDRule{}
users := make(map[string]struct{})
for _, s := range strings.Split(rec.Value, ",") {
s = strings.TrimSpace(s)
// Silently ignore empty users and groups
if s == "" || s == "@" {
continue
}
// Check for duplicates
if _, dup := users[s]; dup {
continue
}
users[s] = struct{}{}
// Skip rules that allows nothing
if allowed == AuthOpsNone {
continue
}
// Build rules, preserving the order (just in case for now)
rule := &AuthUIDRule{
Name: s,
Allowed: allowed,
}
rules = append(rules, rule)
}
// Save results
*out = append(*out, rules...)
return nil
}
// errBadValue creates a "bad value" error related to the INI record
func (rec *IniRecord) errBadValue(format string, args ...interface{}) error {
return &IniError{
File: rec.File,
Line: rec.Line,
Message: fmt.Sprintf(rec.Key+": "+format, args...),
}
}
// Error implements error interface for the IniError
func (err *IniError) Error() string {
return fmt.Sprintf("%s:%d: %s", err.File, err.Line, err.Message)
}
0707010000001D000081A400000000000000000000000167D72F5D0000068A000000000000000000000000000000000000001F00000000ipp-usb-0.9.30/inifile_test.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
*
* Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
* See LICENSE for license terms and conditions
*
* Tests for .INI reader
*/
package main
import (
"io"
"testing"
)
// Don't forget to update testData when ipp-ini.conf changes
var testData = []struct{ section, key, value string }{
{"network", "http-min-port", "60000"},
{"network", "http-max-port", "65535"},
{"network", "dns-sd", "enable"},
{"network", "interface", "loopback"},
{"network", "ipv6", "enable"},
{"logging", "device-log", "all"},
{"logging", "main-log", "debug"},
{"logging", "console-log", "debug"},
{"logging", "max-file-size", "256K"},
{"logging", "max-backup-files", "5"},
{"logging", "console-color", "enable"},
}
// Test .INI reader
func TestIniReader(t *testing.T) {
// Open ipp-usb.conf
ini, err := OpenIniFile("testdata/ipp-usb.conf")
if err != nil {
t.Fatalf("%s", err)
}
defer ini.Close()
// Read record by record
var rec *IniRecord
current := 0
for err == nil {
rec, err = ini.Next()
if err != nil {
break
}
if current >= len(testData) {
t.Errorf("unexpected record: [%s] %s = %s", rec.Section, rec.Key, rec.Value)
} else if rec.Section != testData[current].section ||
rec.Key != testData[current].key ||
rec.Value != testData[current].value {
t.Errorf("data mismatch:")
t.Errorf(" expected: [%s] %s = %s", testData[current].section, testData[current].key, testData[current].value)
t.Errorf(" present: [%s] %s = %s", rec.Section, rec.Key, rec.Value)
} else {
current++
}
}
if err != io.EOF {
t.Fatalf("%s", err)
}
}
0707010000001E000041ED00000000000000000000000267D72F5D00000000000000000000000000000000000000000000001E00000000ipp-usb-0.9.30/ipp-usb-quirks0707010000001F000081A400000000000000000000000167D72F5D0000013A000000000000000000000000000000000000002B00000000ipp-usb-0.9.30/ipp-usb-quirks/Brother.conf# ipp-usb quirks file -- quirks for Brother devices
# This label printer hangs when using more than one
# USB interface after a few minutes from startup
# until the USB device is closed and opened again
# (i.e. ipp-usb is restarted).
#
# So we use just 1 USB interface
[Brother QL-810W]
usb-max-interfaces = 1
07070100000020000081A400000000000000000000000167D72F5D0000012A000000000000000000000000000000000000002900000000ipp-usb-0.9.30/ipp-usb-quirks/Canon.conf# ipp-usb quirks file -- quirks for Canon devices
# This device responds to the Get-Printer-Attributes request with the
# server-error-internal-error status, but otherwise works correctly
#
# So we just ignore its returned IPP status as workaround
[Canon SELPHY CP1500]
ignore-ipp-status = true
07070100000021000081A400000000000000000000000167D72F5D000007D7000000000000000000000000000000000000002600000000ipp-usb-0.9.30/ipp-usb-quirks/HP.conf# ipp-usb quirks file -- quirks for HP devices
[HP LaserJet MFP M28-M31]
http-connection = keep-alive
[HP OfficeJet Pro 8730]
http-connection = close
# eSCL requests hangs on this device, if both USB interfaces are
# in use. Limiting number of interfaces to 1 makes scanning
# reliable in a cost of making scan cancellation impossible,
# as there is no second interface to send cancel request.
# (ADF scans still can be canceled between retrieval of
# subsequent pages).
[HP ScanJet Pro 4500 fn1]
usb-max-interfaces = 1
# HP Photosmart 6520 series doesn't implement true faxing,
# but instead implements internet-based eFax,
# which makes no sense when connected via USB
# so can be safely disabled for this kind of devices.
[HP Photosmart 6520 series]
disable-fax = true
# This device sometimes hangs when probing for fax support
# See long conversation here for details:
# https://github.com/OpenPrinting/ipp-usb/issues/48
[HP ENVY 5530 series]
disable-fax = true
# This device fails to initialize. This quirk helps.
#
# See le following link for details:
# https://github.com/OpenPrinting/ipp-usb/issues/75
[HP OfficeJet Pro 8710]
init-reset = soft
# These quirks fixes initialization of the HP HP Color LaserJet Flow X677
# and Hewlett-Packard HP Color LaserJet FlowMFP M578
#
# Probably, some other HP Enterprise series printers will need a similar
# set of quirks to work properly. However, it is hard to figure out the
# common match pattern to detect them all.
#
# See the following link for details:
# https://github.com/OpenPrinting/ipp-usb/issues/64
[*HP Color LaserJet Flow*]
init-timeout = 20s
init-retry-partial = true
zlp-recv-hack = true
# HP X677 device sometimes fails on printing large raster jobs.
# Looks like timing issue on firmware or hardware at the device
# side.
#
# See the following link for details:
# https://github.com/OpenPrinting/ipp-usb/issues/95
[*HP Color LaserJet Flow X677*]
usb-send-delay-threshold = 2048
usb-send-delay = 0.2ms
07070100000022000081A400000000000000000000000167D72F5D000002AE000000000000000000000000000000000000002A00000000ipp-usb-0.9.30/ipp-usb-quirks/Pantum.conf# ipp-usb quirks file -- quirks for Pantum devices
# Some Pantum devices (Pantum M7300FDW known to have this bug)
# encode IPP messages improperly.
#
# With this option, ipp-usb will recode IPP responses, so that
# CUPS will accept it.
#
# Note, it still doesn't solve compatibility issues, if device
# is connected over network, not over USB. Either CUPS patch is
# required or user needs to install Pantum proprietary driver
[Pantum*]
buggy-ipp-responses = sanitize
# This device pretends it has a fax, but actually fax unit is missed.
# Attempt to query it's printer-attributes sometimes times out, so
# it is better to disable it.
[Pantum BM5100ADN series]
disable-fax = true
07070100000023000081A400000000000000000000000167D72F5D00000069000000000000000000000000000000000000002500000000ipp-usb-0.9.30/ipp-usb-quirks/READMEThis directory contains a collection of quirks files for various
devices.
See `man ipp-usb` for details
07070100000024000081A400000000000000000000000167D72F5D00000114000000000000000000000000000000000000002D00000000ipp-usb-0.9.30/ipp-usb-quirks/blacklist.conf# ipp-usb quirks file -- blacklisted devices
# This device has IPP-over-USB interfaces, but responds HTTP 404 Not found
# status to all requests
[HP Inc. HP Laser MFP 135a]
blacklist = true
# And this device has the same problem
[HP Inc. HP Laser 107a]
blacklist = true
07070100000025000081A400000000000000000000000167D72F5D00000065000000000000000000000000000000000000002B00000000ipp-usb-0.9.30/ipp-usb-quirks/default.conf# ipp-usb quirks file -- defaults
[*]
# Drop Connection: header by default
http-connection = ""
07070100000026000081A400000000000000000000000167D72F5D00004B91000000000000000000000000000000000000001900000000ipp-usb-0.9.30/ipp-usb.8.\" generated with Ronn-NG/v0.10.1
.\" http://github.com/apjanke/ronn-ng/tree/0.10.1
.TH "IPP\-USB" "8" "March 2025" "" "ipp-usb.8"
.SH "NAME"
\fBipp\-usb\fR \- Daemon for IPP over USB printer support
.SH "DESCRIPTION"
\fBipp\-usb\fR daemon enables driver\-less printing and scanning on USB\-only AirPrint\-compatible printers and MFPs\.
.P
It works by connecting to the device by USB using IPP\-over\-USB protocol, and exposing the device to the network, including DNS\-SD (ZeroConf) advertising\.
.P
IPP printing, eSCL scanning and web console are fully supported\.
.SH "SYNOPSIS"
.SS "Usage:"
\fBipp\-usb mode [options]\fR
.SS "Modes are:"
.TP
\fBstandalone\fR
run forever, automatically discover IPP\-over\-USB devices and serve them all
.TP
\fBudev\fR
like standalone, but exit when last IPP\-over\-USB device is disconnected
.TP
\fBdebug\fR
logs duplicated on console, \-bg option is ignored
.TP
\fBcheck\fR
check configuration and exit\. It also prints a list of all connected devices
.TP
\fBstatus\fR
print status of the running \fBipp\-usb\fR daemon, including information of all connected devices
.SS "Options are"
.TP
\fB\-bg\fR
run in background (ignored in debug mode)
.SH "NETWORKING"
Essentially, \fBipp\-usb\fR makes printer or scanner accessible from the network, converting network\-side HTTP operations to the USB operations\.
.P
By default, \fBipp\-usb\fR exposes device only to the loopback interface, using the \fBlocalhost\fR address (both \fB127\.0\.0\.1\fR and \fB::1\fR, for IPv4 and IPv6, respectively)\. TCP ports are allocated automatically, and allocation is persisted in the association with the particular device, so the next time the device is plugged on, it will get the same port\. The default port range for TCP ports allocation is \fB60000\-65535\fR\.
.P
This default behavior can be changed, using configuration file\. See \fBCONFIGURATION\fR section below for details\.
.P
If you decide to publish your device to the real network, the following things should be taken into consideration:
.IP "1." 4
Your \fBprivate\fR device will become \fBpublic\fR and it will become accessible by other computers from the network
.IP "2." 4
Firewall rules needs to be updated appropriately\. The \fBipp\-usb\fR daemon will not do it automatically by itself
.IP "3." 4
IPP over USB specification explicitly require that the \fBHost\fR field in the HTTP request is set to \fBlocalhost\fR or \fBlocalhost:port\fR\. If device is accessed from the real network, \fBHost\fR header will reflect the real network address\. Most of devices allow it, but some are more restrictive and will not work in this configuration\.
.IP "" 0
.SH "DNS\-SD (AVAHI INTEGRATION)"
IPP over USB is intended to be used with the automatic device discovery, and for this purpose \fBipp\-usb\fR advertises all devices it handles, using DNS\-SD protocol\. On Linux, DNS\-SD is handled with a help of Avahi daemon\.
.P
DNS\-SD advertising can be disabled via configuration file\. Also, if Avahi is not installed or not running, \fBipp\-usb\fR will still work correctly, although DNS\-SD advertising will not work\.
.P
For every device the following services will be advertised:
.TS
allbox;
l l l.
Instance Type Subtypes
Device name _ipp\._tcp _universal\._sub\._ipp\._tcp
Device name _printer\._tcp \~
Device name _uscan\._tcp \~
Device name _http\._tcp \~
BBPP _ipp\-usb\._tcp \~
.TE
.P
Notes:
.IP "\(bu" 4
\fBDevice name\fR is the name under which device appears in the list of available devices, for example, in the printing dialog (it is DNS\-SD device name, in another words), and for most of devices will match the device's model name\. It is appended with the \fB" (USB)"\fR suffix, so if device is connected via network and via USB simultaneously, these two connections can be easily distinguished\. If there are two devices with the same name connected simultaneously, the suffix becomes \fB" (USB NNN)"\fR, with NNN number unique for each device, for disambiguation\. In another words, the single \fB"Kyocera ECOSYS M2040dn"\fR device will be listed as \fB"Kyocera ECOSYS M2040dn (USB)"\fR, and two such a devices will be listed as \fB"Kyocera ECOSYS M2040dn (USB 1)"\fR and \fB"Kyocera ECOSYS M2040dn (USB 2)"\fR
.IP "\(bu" 4
\fB_ipp\._tcp\fR and \fB_printer\._tcp\fR are only advertises for printer devices and MFPs
.IP "\(bu" 4
\fB_uscan\._tcp\fR is only advertised for scanner devices and MFPs
.IP "\(bu" 4
for the \fB_ipp\._tcp\fR service, the \fB_universal\._sub\._ipp\._tcp\fR subtype is also advertised for iOS compatibility
.IP "\(bu" 4
\fB_printer\._tcp\fR is advertised with TCP port set to 0\. Other services are advertised with the actual port number
.IP "\(bu" 4
\fB_http\._tcp\fR is device web\-console\. It is always advertises in assumption it is always exist
.IP "\(bu" 4
\fBBBPP\fR, used for the \fB_ipp\-usb\._tcp\fR service, is the USB bus (BB) and port (PP) numbers in hex\. The purpose of this advertising is to help CUPS and other possible "clients" to guess which devices are handled by the \fBipp\-usb\fR service, to avoid possible conflicts with the legacy USB drivers\.
.IP "" 0
.SH "Matching DNS\-SD announcements against local USB bus"
Client software may need to match local devices exposed by \fBipp\-usb\fR with those found on the local USB bus\. This can be useful, for example, to avoid offering USB devices in the print or scan dialog that cannot be used because they are already in use by \fBipp\-usb\fR\.
.P
As a rough guideline, clients might consider USB devices that offer interfaces with Class=7, SubClass=1, and Protocol=4 as belonging to \fBipp\-usb\fR\. However, this is not a precise method\. Some devices may not properly implement the IPP over USB protocol and could be blacklisted in the \fBipp\-usb\fR configuration for this reason\. Additionally, some devices may be automatically recognized by \fBipp\-usb\fR as incompatible and thus skipped\. It's also possible that \fBipp\-usb\fR is disabled on the system entirely\.
.P
A more reliable approach is to directly query the running \fBipp\-usb\fR instance to determine which devices it actually handles\.
.P
To facilitate the matching of devices that \fBipp\-usb\fR manages and announces via DNS\-SD against local devices found on the USB bus, \fBipp\-usb\fR adds two TXT records to each \fB_ipp\._tcp\fR, \fB_printer\._tcp\fR, and \fB_uscan\._tcp\fR services it announces:
.IP "\(bu" 4
usb_SER=VCF9192281 \- USB serial number for the device
.IP "\(bu" 4
usb_HWID=0482&069d \- USB vendor and hardware ID for the device in hex
.IP "" 0
.P
Please note that when matching devices discovered via DNS\-SD with USB devices, it is important to only consider DNS\-SD advertisements from IP addresses that are either loopback addresses (127\.0\.0\.1 or ::1) or belong to a local interface\.
.SH "CONFIGURATION"
\fBipp\-usb\fR searched for its configuration file in two places:
.IP "1." 4
\fB/etc/ipp\-usb/ipp\-usb\.conf\fR
.IP "2." 4
\fBipp\-usb\.conf\fR in the directory where executable file is located
.IP "" 0
.P
Configuration file syntax is very similar to \.INI files syntax\. It consist of named sections, and each section contains a set of named variables\. Comments are started from # or ; characters and continues until end of line:
.IP "" 4
.nf
# This is a comment
[section 1]
variable 1 = value 1 ; and another comment
variable 2 = value 2
.fi
.IP "" 0
.SS "Network parameters"
Network parameters are all in the \fB[network]\fR section:
.IP "" 4
.nf
[network]
# TCP ports for HTTP will be automatically allocated in the
# following range
http\-min\-port = 60000
http\-max\-port = 65535
# Enable or disable DNS\-SD advertisement
dns\-sd = enable # enable | disable
# Network interface to use\. Set to `all` if you want to expose you
# printer to the local network\. This way you can share your printer
# with other computers in the network, as well as with iOS and
# Android devices\.
interface = loopback # all | loopback
# Enable or disable IPv6
ipv6 = enable # enable | disable
.fi
.IP "" 0
.SS "Authentication"
By default, \fBipp\-usb\fR exposes locally connected USB printer to all users of the system\.
.P
Though this is reasonable behavior in most cases, when computer and printer are both in personal use, for bigger installation this approach can be too simple and primitive\.
.P
\fBipp\-usb\fR provides a mechanism, which allows to control local clients access based on UID the client program runs under\.
.P
Please note, this mechanism will not work for remote connections (disabled by default but supported)\. Authentication of remote users requires some different mechanism, which is under consideration but is not yet implemented\.
.P
Note also, this mechanism may or may not work in containerized installation (i\.e\., snap, flatpak and similar)\. The container namespace may be isolated from the system and/or user's namespaces, so even for local clients the UID as seen by the \fBipp\-usb\fR may be different from the system\-wide UID\.
.P
Authentication parameters are all in the [auth uid] section:
.IP "" 4
.nf
# Local user authentication by UID/GID
[auth uid]
# Syntax:
# operations = users
#
# Operations are comma\-separated list of following operations:
# all \- all operations
# config \- configuration web\-console
# fax \- faxing
# print \- printing
# scan \- scanning
#
# Users have the following suntax:
# user \- user name
# @group \- all users that belongs to the group
#
# Users and groups may be specified either by names or by
# numbers\. "*" means any
#
# Note, if user/group is not known in the context of request
# (for example, in the case of non\-local network connection),
# "*" used for matching, which will only match wildcard
# rules\.
#
# User/group names are resolved at the moment of request
# processing (and cached for a couple of seconds), so running
# daemon will see changes to the /etc/passwd and /etc/group
#
# Examples:
# fax, print = lp, @lp # Allow CUPS to do its work
# scan = * # Allow any user to scan
# config = @wheel # Only wheel group members can do that
all = *
.fi
.IP "" 0
.SS "Logging configuration"
Logging parameters are all in the \fB[logging]\fR section:
.IP "" 4
.nf
[logging]
# device\-log \- what logs are generated per device
# main\-log \- what common logs are generated
# console\-log \- what of generated logs goes to console
#
# parameter contains a comma\-separated list of
# the following keywords:
# error \- error messages
# info \- informative messages
# debug \- debug messages
# trace\-ipp, trace\-escl, trace\-http \- very detailed
# per\-protocol traces
# trace\-usb \- hex dump of all USB traffic
# all \- all logs
# trace\-all \- alias to all
#
# Note, trace\-* implies debug, debug implies info, info implies
# error
device\-log = all
main\-log = debug
console\-log = debug
# Log rotation parameters:
# log\-file\-size \- max log file before rotation\. Use suffix
# M for megabytes or K for kilobytes
# log\-backup\-files \- how many backup files to preserve during
# rotation
#
max\-file\-size = 256K
max\-backup\-files = 5
# Enable or disable ANSI colors on console
console\-color = enable # enable | disable
# ipp\-usb queries IPP printer attributes at the initialization time
# for its own purposes and writes received attributes to the log\.
# By default, only necessary attributes are requested from device\.
#
# If this parameter is set to true, all printer attributes will
# be requested\. Normally, it only affects the logging\. However,
# some enterprise\-level HP printers returns such huge amount of
# data and do it so slowly, so it can cause initialization timeout\.
# This is why this feature is not enabled by default
get\-all\-printer\-attrs = false # false | true
.fi
.IP "" 0
.SS "Quirks"
Some devices, due to their firmware bugs, require special handling, called device\-specific \fBquirks\fR\. \fBipp\-usb\fR loads quirks from the \fB/usr/share/ipp\-usb/quirks/*\.conf\fR files and from the \fB/etc/ipp\-usb/quirks/*\.conf\fR files\. The \fB/etc/ipp\-usb/quirks\fR directory is for system quirks overrides or admin changes\. These files have \.INI\-file syntax with the content that looks like this:
.IP "" 4
.nf
[HP LaserJet MFP M28\-M31]
http\-connection = keep\-alive
[HP OfficeJet Pro 8730]
http\-connection = close
[HP Inc\. HP Laser MFP 135a]
blacklist = true
# Default configuration
[*]
http\-connection = ""
.fi
.IP "" 0
.P
For each discovered device, its model name is matched against sections of the quirks files\. Section names may contain glob\-style wildcards: \fB*\fR that matches any sequence of characters and \fB?\fR , that matches any single character\. To match one of these characters (\fB*\fR and \fB?\fR) literally, use backslash as escape\.
.P
Note, the simplest way to guess the exact model name for the particular device is to use \fBipp\-usb check\fR command, which prints a list of all connected devices\.
.P
All matching sections from all quirks files are taken in consideration, and applied in priority order\. Priority is computed using the following algorithm:
.IP "\(bu" 4
When matching model name against section name, amount of non\-wildcard matched characters is counted, and the longer match wins
.IP "\(bu" 4
Otherwise, section loaded first wins\. Files are loaded in alphabetical order, sections read sequentially
.IP "" 0
.P
If some parameter exist in multiple sections, used its value from the most priority section
.P
The following parameters are defined:
.IP "\(bu" 4
\fBblacklist = true | false\fR
.br
If \fBtrue\fR, the matching device is ignored by the \fBipp\-usb\fR
.IP "\(bu" 4
\fBbuggy\-ipp\-responses = reject | allow | sanitize\fR
.br
Some devices send buggy (malformed) IPP responses that violate IPP specification\. \fBipp\-usb\fR may \fBreject\fR these responses (so \fBipp\-usb\fR initialization will fail), \fBallow\fR them (\fBipp\-usb\fR initialization will succeed, but CUPS needs to accept them as well) or \fBsanitize\fR them (fix IPP specs violations)\.
.IP "\(bu" 4
\fBdisable\-fax = true | false\fR
.br
If \fBtrue\fR, the matching device's fax capability is ignored\.
.IP "\(bu" 4
\fBhttp\-XXX = YYY\fR
.br
Set XXX header of the HTTP requests forwarded to device to YYY\. If YYY is empty string, XXX header is removed\.
.IP "\(bu" 4
\fBignore\-ipp\-status = true | false\fR
.br
If \fBtrue\fR, IPP status of IPP requests sent by the \fBipp\-usb\fR by itself will be ignored\. This quirk is useful, when device correctly handles IPP request but returned status is not reliable\. Affects only \fBipp\-usb\fR initialization\.
.IP "\(bu" 4
\fBinit\-delay = DELAY\fR
.br
Delay, between device is opened and, optionally, reset, and the first request is sent to device\.
.IP "\(bu" 4
\fBinit\-retry\-partial = true | false\fR
.br
Retry the initialization in case only part of the device's functions have been initialized, instead of continuing to operate with incomplete functionality\.
.IP
It can be useful if the device takes a long time to fully initialize\. During this period, some components may respond normally while others are still initializing\. For example, the device may quickly report its scanning capabilities shortly after startup, while its printing functionality may take several minutes to become operational\.
.IP
Some enterprise\-level HP printers are known to have this problem\.
.IP "\(bu" 4
\fBinit\-reset = none | soft | hard\fR
.br
How to reset device during initialization\. Default is \fBnone\fR
.IP "\(bu" 4
\fBinit\-timeout = DELAY\fR
.br
Timeout for HTTP requests send by the \fBipp\-usb\fR during initialization\.
.IP "\(bu" 4
\fBrequest\-delay = DELAY\fR
.br
Delay between subsequent HTTP requests, sent to device (this is not the same as \fBusb\-send\-delay\fR, which inserts delays between each subsequent USB send\-to\-device requests)\.
.IP "\(bu" 4
\fBusb\-max\-interfaces = N\fR
.br
Don't use more that N USB interfaces, even if more is available\.
.IP "\(bu" 4
\fBusb\-send\-delay = DELAY\fR
.br
Delay between low\-level USB send\-to\-device requests (this is not the same as \fBrequest\-delay\fR, which inserts delays between the whole HTTP\-level requests)\.
.IP "\(bu" 4
\fBusb\-send\-delay\-threshold = N\fR
.br
\fBusb\-send\-delay\fR only applied if USB send\-to\-device request size exceeds this threshold\.
.IP "\(bu" 4
\fBzlp\-recv\-hack = true | false\fR
.br
Some enterprise\-level HP devices, during the initialization phase (which can last several minutes), may respond with an HTTP 503 status or similar, which is expected\. However, the response body may be truncated (typically, the terminating '\en' is lost)\. In such cases, \fBipp\-usb\fR will wait indefinitely for a response to maintain synchronization with the device\.
.IP
At the same time, these devices send a zero\-length UDP packet at the end of the truncated output\. If the \fBzlp\-recv\-hack\fR quirk is enabled, when ipp\-usb receives a zero\-length packet from the USB followed by a receive timeout, it interprets this combination of events as a valid termination of the response body\. It works only at the initialization time and doesn't affect futher operations\.
.IP "\(bu" 4
\fBzlp\-send = true | false\fR
.br
Terminate outgoing transfers that a multiple of the endpoint's packet size win an extra zero length packet\.
.IP "" 0
.P
The DELAY parameter can be specified either as an unsigned integer (in milliseconds) or as a sequence of decimal numbers with an optional fraction and a unit suffix, such as "300ms," "0\.5s," or "2m30s\." Valid time units are "ns," "us" (or "µs"), "ms" "s" "m" and "h"
.P
If you found out about your device that it needs a quirk to work properly or it does not work with \fBipp\-usb\fR at all, although it provides IPP\-over\-USB interface, please report the issue at https://github\.com/OpenPrinting/ipp\-usb\. It will let us to update our collection of quirks, so helping other owners of such a device\.
.SH "FILES"
.IP "\(bu" 4
\fB/etc/ipp\-usb/ipp\-usb\.conf\fR: the daemon configuration file
.IP "\(bu" 4
\fB/var/log/ipp\-usb/main\.log\fR: the main log file
.IP "\(bu" 4
\fB/var/log/ipp\-usb/<DEVICE>\.log\fR: per\-device log files
.IP "\(bu" 4
\fB/var/ipp\-usb/dev/<DEVICE>\.state\fR: device state (HTTP port allocation, DNS\-SD name)
.IP "\(bu" 4
\fB/var/ipp\-usb/lock/ipp\-usb\.lock\fR: lock file, that helps to prevent multiple copies of daemon to run simultaneously
.IP "\(bu" 4
\fB/var/ipp\-usb/ctrl\fR: \fBipp\-usb\fR control socket\. Currently only used to obtain the per\-device status (printed by \fBipp\-usb status\fR), but its functionality may be extended in a future
.IP "\(bu" 4
\fB/usr/share/ipp\-usb/quirks/*\.conf\fR: device\-specific quirks (see above)
.IP "\(bu" 4
\fB/etc/ipp\-usb/quirks/*\.conf\fR: device\-specific quirks defined by sysadmin (see above)
.IP "" 0
.SH "COPYRIGHT"
Copyright (c) by Alexander Pevzner (pzz@apevzner\.com, pzz@pzz\.msk\.ru)
.br
All rights reserved\.
.P
This program is licensed under 2\-Clause BSD license\. See LICENSE file for details\.
.SH "SEE ALSO"
\fBcups(1)\fR
07070100000027000081A400000000000000000000000167D72F5D00004A8E000000000000000000000000000000000000001C00000000ipp-usb-0.9.30/ipp-usb.8.mdipp-usb(8) -- Daemon for IPP over USB printer support
=====================================================
## DESCRIPTION
`ipp-usb` daemon enables driver-less printing and scanning on
USB-only AirPrint-compatible printers and MFPs.
It works by connecting to the device by USB using IPP-over-USB
protocol, and exposing the device to the network, including
DNS-SD (ZeroConf) advertising.
IPP printing, eSCL scanning and web console are fully supported.
## SYNOPSIS
### Usage:
`ipp-usb mode [options]`
### Modes are:
* `standalone`:
run forever, automatically discover IPP-over-USB
devices and serve them all
* `udev`:
like standalone, but exit when last IPP-over-USB
device is disconnected
* `debug`:
logs duplicated on console, -bg option is ignored
* `check`:
check configuration and exit. It also prints a list
of all connected devices
* `status`:
print status of the running `ipp-usb` daemon, including information
of all connected devices
### Options are
* `-bg`:
run in background (ignored in debug mode)
## NETWORKING
Essentially, `ipp-usb` makes printer or scanner accessible from the
network, converting network-side HTTP operations to the USB operations.
By default, `ipp-usb` exposes device only to the loopback interface,
using the `localhost` address (both `127.0.0.1` and `::1`, for IPv4
and IPv6, respectively). TCP ports are allocated automatically, and
allocation is persisted in the association with the particular device,
so the next time the device is plugged on, it will get the same port.
The default port range for TCP ports allocation is `60000-65535`.
This default behavior can be changed, using configuration file. See
`CONFIGURATION` section below for details.
If you decide to publish your device to the real network, the following
things should be taken into consideration:
1. Your **private** device will become **public** and it will become
accessible by other computers from the network
2. Firewall rules needs to be updated appropriately. The `ipp-usb`
daemon will not do it automatically by itself
3. IPP over USB specification explicitly require that the
`Host` field in the HTTP request is set to `localhost`
or `localhost:port`. If device is accessed from the real
network, `Host` header will reflect the real network address.
Most of devices allow it, but some are more restrictive
and will not work in this configuration.
## DNS-SD (AVAHI INTEGRATION)
IPP over USB is intended to be used with the automatic device discovery,
and for this purpose `ipp-usb` advertises all devices it handles, using
DNS-SD protocol. On Linux, DNS-SD is handled with a help of Avahi daemon.
DNS-SD advertising can be disabled via configuration file. Also, if Avahi
is not installed or not running, `ipp-usb` will still work correctly,
although DNS-SD advertising will not work.
For every device the following services will be advertised:
| Instance | Type | Subtypes |
| ----------- | ------------- | ------------------------- |
| Device name | _ipp._tcp | _universal._sub._ipp._tcp |
| Device name | _printer._tcp | |
| Device name | _uscan._tcp | |
| Device name | _http._tcp | |
| BBPP | _ipp-usb._tcp | |
Notes:
* `Device name` is the name under which device appears in
the list of available devices, for example, in the printing
dialog (it is DNS-SD device name, in another words), and for
most of devices will match the device's model name. It
is appended with the `" (USB)"` suffix, so if device is
connected via network and via USB simultaneously, these
two connections can be easily distinguished. If there
are two devices with the same name connected simultaneously,
the suffix becomes `" (USB NNN)"`, with NNN number unique for
each device, for disambiguation. In another words, the single
`"Kyocera ECOSYS M2040dn"` device will be listed as
`"Kyocera ECOSYS M2040dn (USB)"`, and two such a devices will
be listed as `"Kyocera ECOSYS M2040dn (USB 1)"` and
`"Kyocera ECOSYS M2040dn (USB 2)"`
* `_ipp._tcp` and `_printer._tcp` are only advertises for
printer devices and MFPs
* `_uscan._tcp` is only advertised for scanner devices and MFPs
* for the `_ipp._tcp` service, the `_universal._sub._ipp._tcp`
subtype is also advertised for iOS compatibility
* `_printer._tcp` is advertised with TCP port set to 0. Other
services are advertised with the actual port number
* `_http._tcp` is device web-console. It is always advertises
in assumption it is always exist
* `BBPP`, used for the `_ipp-usb._tcp` service, is the
USB bus (BB) and port (PP) numbers in hex. The purpose
of this advertising is to help CUPS and other possible
"clients" to guess which devices are handled by the
`ipp-usb` service, to avoid possible conflicts with the
legacy USB drivers.
## Matching DNS-SD announcements against local USB bus
Client software may need to match local devices exposed by `ipp-usb` with
those found on the local USB bus. This can be useful, for example, to
avoid offering USB devices in the print or scan dialog that cannot be
used because they are already in use by `ipp-usb`.
As a rough guideline, clients might consider USB devices that offer
interfaces with Class=7, SubClass=1, and Protocol=4 as belonging to
`ipp-usb`. However, this is not a precise method. Some devices may not
properly implement the IPP over USB protocol and could be blacklisted in
the `ipp-usb` configuration for this reason. Additionally, some devices
may be automatically recognized by `ipp-usb` as incompatible and thus
skipped. It's also possible that `ipp-usb` is disabled on the system
entirely.
A more reliable approach is to directly query the running `ipp-usb`
instance to determine which devices it actually handles.
To facilitate the matching of devices that `ipp-usb` manages and
announces via DNS-SD against local devices found on the USB bus,
`ipp-usb` adds two TXT records to each `_ipp._tcp`, `_printer._tcp`,
and `_uscan._tcp` services it announces:
* usb_SER=VCF9192281 - USB serial number for the device
* usb_HWID=0482&069d - USB vendor and hardware ID for the device in hex
Please note that when matching devices discovered via DNS-SD with USB
devices, it is important to only consider DNS-SD advertisements from IP
addresses that are either loopback addresses (127.0.0.1 or ::1) or
belong to a local interface.
## CONFIGURATION
`ipp-usb` searched for its configuration file in two places:
1. `/etc/ipp-usb/ipp-usb.conf`
2. `ipp-usb.conf` in the directory where executable file is located
Configuration file syntax is very similar to .INI files syntax.
It consist of named sections, and each section contains a set of
named variables. Comments are started from # or ; characters and
continues until end of line:
# This is a comment
[section 1]
variable 1 = value 1 ; and another comment
variable 2 = value 2
### Network parameters
Network parameters are all in the `[network]` section:
[network]
# TCP ports for HTTP will be automatically allocated in the
# following range
http-min-port = 60000
http-max-port = 65535
# Enable or disable DNS-SD advertisement
dns-sd = enable # enable | disable
# Network interface to use. Set to `all` if you want to expose you
# printer to the local network. This way you can share your printer
# with other computers in the network, as well as with iOS and
# Android devices.
interface = loopback # all | loopback
# Enable or disable IPv6
ipv6 = enable # enable | disable
### Authentication
By default, `ipp-usb` exposes locally connected USB printer to all users
of the system.
Though this is reasonable behavior in most cases, when computer and printer
are both in personal use, for bigger installation this approach can be too
simple and primitive.
`ipp-usb` provides a mechanism, which allows to control local clients
access based on UID the client program runs under.
Please note, this mechanism will not work for remote connections (disabled
by default but supported). Authentication of remote users requires some
different mechanism, which is under consideration but is not yet implemented.
Note also, this mechanism may or may not work in containerized installation
(i.e., snap, flatpak and similar). The container namespace may be isolated
from the system and/or user's namespaces, so even for local clients the UID
as seen by the `ipp-usb` may be different from the system-wide UID.
Authentication parameters are all in the [auth uid] section:
# Local user authentication by UID/GID
[auth uid]
# Syntax:
# operations = users
#
# Operations are comma-separated list of following operations:
# all - all operations
# config - configuration web-console
# fax - faxing
# print - printing
# scan - scanning
#
# Users have the following suntax:
# user - user name
# @group - all users that belongs to the group
#
# Users and groups may be specified either by names or by
# numbers. "*" means any
#
# Note, if user/group is not known in the context of request
# (for example, in the case of non-local network connection),
# "*" used for matching, which will only match wildcard
# rules.
#
# User/group names are resolved at the moment of request
# processing (and cached for a couple of seconds), so running
# daemon will see changes to the /etc/passwd and /etc/group
#
# Examples:
# fax, print = lp, @lp # Allow CUPS to do its work
# scan = * # Allow any user to scan
# config = @wheel # Only wheel group members can do that
all = *
### Logging configuration
Logging parameters are all in the `[logging]` section:
[logging]
# device-log - what logs are generated per device
# main-log - what common logs are generated
# console-log - what of generated logs goes to console
#
# parameter contains a comma-separated list of
# the following keywords:
# error - error messages
# info - informative messages
# debug - debug messages
# trace-ipp, trace-escl, trace-http - very detailed
# per-protocol traces
# trace-usb - hex dump of all USB traffic
# all - all logs
# trace-all - alias to all
#
# Note, trace-* implies debug, debug implies info, info implies
# error
device-log = all
main-log = debug
console-log = debug
# Log rotation parameters:
# log-file-size - max log file before rotation. Use suffix
# M for megabytes or K for kilobytes
# log-backup-files - how many backup files to preserve during
# rotation
#
max-file-size = 256K
max-backup-files = 5
# Enable or disable ANSI colors on console
console-color = enable # enable | disable
# ipp-usb queries IPP printer attributes at the initialization time
# for its own purposes and writes received attributes to the log.
# By default, only necessary attributes are requested from device.
#
# If this parameter is set to true, all printer attributes will
# be requested. Normally, it only affects the logging. However,
# some enterprise-level HP printers returns such huge amount of
# data and do it so slowly, so it can cause initialization timeout.
# This is why this feature is not enabled by default
get-all-printer-attrs = false # false | true
### Quirks
Some devices, due to their firmware bugs, require special handling,
called device-specific **quirks**. `ipp-usb` loads quirks from the
`/usr/share/ipp-usb/quirks/*.conf` files and from the `/etc/ipp-usb/quirks/*.conf`
files. The `/etc/ipp-usb/quirks` directory is for system quirks overrides or
admin changes. These files have .INI-file syntax with the content that looks like this:
[HP LaserJet MFP M28-M31]
http-connection = keep-alive
[HP OfficeJet Pro 8730]
http-connection = close
[HP Inc. HP Laser MFP 135a]
blacklist = true
# Default configuration
[*]
http-connection = ""
For each discovered device, its model name is matched against sections of the
quirks files. Section names may contain glob-style wildcards: `*` that matches
any sequence of characters and `?` , that matches any single character. To
match one of these characters (`*` and `?`) literally, use backslash as escape.
Note, the simplest way to guess the exact model name for the particular
device is to use `ipp-usb check` command, which prints a list of all
connected devices.
All matching sections from all quirks files are taken in consideration,
and applied in priority order. Priority is computed using the following
algorithm:
* When matching model name against section name, amount of non-wildcard
matched characters is counted, and the longer match wins
* Otherwise, section loaded first wins. Files are loaded in alphabetical
order, sections read sequentially
If some parameter exist in multiple sections, used its value from the
most priority section
The following parameters are defined:
* `blacklist = true | false`<br>
If `true`, the matching device is ignored by the `ipp-usb`
* `buggy-ipp-responses = reject | allow | sanitize`<br>
Some devices send buggy (malformed) IPP responses that violate
IPP specification. `ipp-usb` may `reject` these responses
(so `ipp-usb` initialization will fail), `allow` them (`ipp-usb`
initialization will succeed, but CUPS needs to accept them
as well) or `sanitize` them (fix IPP specs violations).
* `disable-fax = true | false`<br>
If `true`, the matching device's fax capability is ignored.
* `http-XXX = YYY`<br>
Set XXX header of the HTTP requests forwarded to device to YYY.
If YYY is empty string, XXX header is removed.
* `ignore-ipp-status = true | false`<br>
If `true`, IPP status of IPP requests sent by the `ipp-usb` by
itself will be ignored. This quirk is useful, when device correctly
handles IPP request but returned status is not reliable. Affects
only `ipp-usb` initialization.
* `init-delay = DELAY`<br>
Delay, between device is opened and, optionally, reset, and the
first request is sent to device.
* `init-retry-partial = true | false`<br>
Retry the initialization in case only part of the device's functions
have been initialized, instead of continuing to operate with incomplete
functionality.
It can be useful if the device takes a long time to fully initialize.
During this period, some components may respond normally while others
are still initializing. For example, the device may quickly report its
scanning capabilities shortly after startup, while its printing
functionality may take several minutes to become operational.
Some enterprise-level HP printers are known to have this problem.
* `init-reset = none | soft | hard`<br>
How to reset device during initialization. Default is `none`
* `init-timeout = DELAY`<br>
Timeout for HTTP requests send by the `ipp-usb` during initialization.
* `request-delay = DELAY`<br>
Delay between subsequent HTTP requests, sent to device (this is not
the same as `usb-send-delay`, which inserts delays between each
subsequent USB send-to-device requests).
* `usb-max-interfaces = N`<br>
Don't use more that N USB interfaces, even if more is available.
* `usb-send-delay = DELAY`<br>
Delay between low-level USB send-to-device requests (this is not
the same as `request-delay`, which inserts delays between the
whole HTTP-level requests).
* `usb-send-delay-threshold = N`<br>
`usb-send-delay` only applied if USB send-to-device request size
exceeds this threshold.
* `zlp-recv-hack = true | false`<br>
Some enterprise-level HP devices, during the initialization phase
(which can last several minutes), may respond with an HTTP 503
status or similar, which is expected. However, the response body may
be truncated (typically, the terminating '\n' is lost). In such
cases, `ipp-usb` will wait indefinitely for a response to maintain
synchronization with the device.
At the same time, these devices send a zero-length UDP packet at the
end of the truncated output. If the `zlp-recv-hack` quirk is enabled,
when ipp-usb receives a zero-length packet from the USB followed by
a receive timeout, it interprets this combination of events as a
valid termination of the response body. It works only at the
initialization time and doesn't affect futher operations.
* `zlp-send = true | false`<br>
Terminate outgoing transfers that a multiple of the endpoint's
packet size win an extra zero length packet.
The DELAY parameter can be specified either as an unsigned integer (in
milliseconds) or as a sequence of decimal numbers with an optional
fraction and a unit suffix, such as "300ms," "0.5s," or "2m30s." Valid
time units are "ns," "us" (or "µs"), "ms" "s" "m" and "h"
If you found out about your device that it needs a quirk to work properly or it
does not work with `ipp-usb` at all, although it provides IPP-over-USB
interface, please report the issue at https://github.com/OpenPrinting/ipp-usb.
It will let us to update our collection of quirks, so helping other owners
of such a device.
## FILES
* `/etc/ipp-usb/ipp-usb.conf`:
the daemon configuration file
* `/var/log/ipp-usb/main.log`:
the main log file
* `/var/log/ipp-usb/<DEVICE>.log`:
per-device log files
* `/var/ipp-usb/dev/<DEVICE>.state`:
device state (HTTP port allocation, DNS-SD name)
* `/var/ipp-usb/lock/ipp-usb.lock`:
lock file, that helps to prevent multiple copies of daemon to run simultaneously
* `/var/ipp-usb/ctrl`:
`ipp-usb` control socket. Currently only used to obtain the
per-device status (printed by `ipp-usb status`), but its
functionality may be extended in a future
* `/usr/share/ipp-usb/quirks/*.conf`: device-specific quirks (see above)
* `/etc/ipp-usb/quirks/*.conf`: device-specific quirks defined by sysadmin (see above)
## COPYRIGHT
Copyright (c) by Alexander Pevzner (pzz@apevzner.com, pzz@pzz.msk.ru)<br/>
All rights reserved.
This program is licensed under 2-Clause BSD license. See LICENSE file for details.
## SEE ALSO
**cups(1)**
# vim:ts=8:sw=4:et
07070100000028000081A400000000000000000000000167D72F5D00000D3B000000000000000000000000000000000000001C00000000ipp-usb-0.9.30/ipp-usb.conf# ipp-usb.conf: example configuration file
# Networking parameters
[network]
# TCP ports for HTTP will be automatically allocated in the following range
http-min-port = 60000
http-max-port = 65535
# Enable or disable DNS-SD advertisement
dns-sd = enable # enable | disable
# Network interface to use. Set to `all` if you want to expose you
# printer to the local network. This way you can share your printer
# with other computers in the network, as well as with iOS and Android
# devices.
interface = loopback # all | loopback
# Enable or disable IPv6
ipv6 = enable # enable | disable
# Local user authentication by UID/GID
[auth uid]
# Syntax:
# operations = users
#
# Operations are comma-separated list of following operations:
# all - all operations
# config - configuration web-console
# fax - faxing
# print - printing
# scan - scanning
#
# Users have the following suntax:
# user - user name
# @group - all users that belongs to the group
#
# Users and groups may be specified either by names or by
# numbers. "*" means any
#
# Note, if user/group is not known in the context of request
# (for example, in the case of non-local network connection),
# "*" is used for matching, which will only match wildcard
# rules.
#
# User/group names are resolved at the moment of request
# processing (and cached for a couple of seconds), so running
# daemon will see changes to the /etc/passwd and /etc/group
#
# Examples:
# fax, print = lp, @lp # Allow CUPS to do its work
# scan = * # Allow any user to scan
# config = @wheel # Only wheel group members can do that
all = *
# Logging configuration
[logging]
# device-log - per-device log levels
# main-log - main log levels
# console-log - console log levels
#
# parameter contains a comma-separated list of
# the following keywords:
# error - error messages
# info - informative messages
# debug - debug messages
# trace-ipp, trace-escl, trace-http - very detailed per-protocol traces
# trace-usb - hex dump of all USB traffic
# all - all logs
# trace-all - alias to all
#
# Note, trace-* implies debug, debug implies info, info implies error
device-log = all
main-log = debug
console-log = debug
# Log rotation parameters:
# max-file-size - max log file before rotation. Use suffix M
# for megabytes or K for kilobytes
# max-backup-files - how many backup files to preserve during rotation
#
max-file-size = 256K
max-backup-files = 5
# Enable or disable ANSI colors on console
console-color = enable # enable | disable
# ipp-usb queries IPP printer attributes at the initialization time
# for its own purposes and writes received attributes to the log.
# By default, only necessary attributes are requested from device.
#
# If this parameter is set to true, all printer attributes will
# be requested. Normally, it only affects the logging. However,
# some enterprise-level HP printers returns such huge amount of
# data and do it so slowly, so it can cause initialization timeout.
# This is why this feature is not enabled by default
get-all-printer-attrs = false # false | true
# vim:ts=8:sw=2:et
07070100000029000081A400000000000000000000000167D72F5D000035B1000000000000000000000000000000000000001600000000ipp-usb-0.9.30/ipp.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
*
* Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
* See LICENSE for license terms and conditions
*
* IPP service registration
*/
package main
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"strings"
"github.com/OpenPrinting/goipp"
)
// IppPrinterInfo represents additional printer information, which
// is not included into DNS-SD TXT record, but still needed for
// other purposes
type IppPrinterInfo struct {
DNSSdName string // DNS-SD device name
UUID string // Device UUID
AdminURL string // Admin URL
IconURL string // Device icon URL
IppSvcIndex int // IPP DNSSdSvcInfo index within array of services
}
// IppService performs IPP Get-Printer-Attributes query using provided
// http.Client and decodes received information into the form suitable
// for DNS-SD registration
//
// Discovered services will be added to the services collection
func IppService(log *LogMessage, services *DNSSdServices,
port int, usbinfo UsbDeviceInfo, quirks Quirks,
c *http.Client) (ippinfo *IppPrinterInfo, httpstatus int, err error) {
// Query printer attributes
uri := fmt.Sprintf("ipp://localhost:%d/ipp/print", port)
msg, httpstatus, err := ippGetPrinterAttributes(log, c, quirks, uri)
if err != nil {
return
}
// Decode IPP service info
attrs := newIppDecoder(msg)
ippinfo, ippSvc := attrs.decode(usbinfo)
// Check for fax support
canFax := false
if usbinfo.BasicCaps&UsbIppBasicCapsFax != 0 &&
!quirks.GetDisableFax() {
// Note, as device lists Fax on its basic capabilities,
// this probe most likely is not needed, but as the
// ipp-usb version 0.9.19 and earlier used to guess
// for fax support based on the /ipp/faxout probe,
// not on device capabilities, lets leave it here
// for now, just in case. Firmwares in general are
// too buggy, I can't trust them :-(
uri = fmt.Sprintf("ipp://localhost:%d/ipp/faxout", port)
_, _, err2 := ippGetPrinterAttributes(log, c, quirks, uri)
if err2 == nil {
canFax = true
log.Debug(' ', "IPP FaxOut service detected")
} else {
log.Error('!', "IPP FaxOut probe failed: %s", err2)
}
} else {
log.Debug(' ', "IPP FaxOut service not in capabilities")
}
if canFax {
ippSvc.Txt.Add("Fax", "T")
ippSvc.Txt.Add("rfo", "ipp/faxout")
} else {
ippSvc.Txt.Add("Fax", "F")
}
// Construct LPD info. Per Apple spec, we MUST advertise
// LPD with zero port, even if we don't support it
lpdSvc := DNSSdSvcInfo{
Type: "_printer._tcp",
Port: 0,
Txt: nil,
}
// Pack it all together
ippSvc.Port = port
services.Add(lpdSvc)
ippinfo.IppSvcIndex = len(*services)
services.Add(ippSvc)
return
}
// ippGetPrinterAttributes performs GetPrinterAttributes query,
// using the specified http.Client and uri
//
// If this function returns nil error, it means that:
// 1. HTTP transaction performed successfully
// 2. Received reply successfully decoded
// 3. It is not an IPP error response
//
// Otherwise, the appropriate error is generated and returned
func ippGetPrinterAttributes(log *LogMessage, c *http.Client, quirks Quirks,
uri string) (msg *goipp.Message, httpstatus int, err error) {
// Query printer attributes
msg = goipp.NewRequest(goipp.DefaultVersion, goipp.OpGetPrinterAttributes, 1)
msg.Operation.Add(goipp.MakeAttribute("attributes-charset",
goipp.TagCharset, goipp.String("utf-8")))
msg.Operation.Add(goipp.MakeAttribute("attributes-natural-language",
goipp.TagLanguage, goipp.String("en-US")))
msg.Operation.Add(goipp.MakeAttribute("printer-uri",
goipp.TagURI, goipp.String(uri)))
rq := goipp.Attribute{Name: "requested-attributes"}
if Conf.LogAllPrinterAttrs {
rq.Values.Add(goipp.TagKeyword, goipp.String("all"))
} else {
rq.Values.Add(goipp.TagKeyword, goipp.String("color-supported"))
rq.Values.Add(goipp.TagKeyword, goipp.String("document-format-supported"))
rq.Values.Add(goipp.TagKeyword, goipp.String("media-size-supported"))
rq.Values.Add(goipp.TagKeyword, goipp.String("mopria-certified"))
rq.Values.Add(goipp.TagKeyword, goipp.String("printer-device-id"))
rq.Values.Add(goipp.TagKeyword, goipp.String("printer-dns-sd-name"))
rq.Values.Add(goipp.TagKeyword, goipp.String("printer-icons"))
rq.Values.Add(goipp.TagKeyword, goipp.String("printer-info"))
rq.Values.Add(goipp.TagKeyword, goipp.String("printer-kind"))
rq.Values.Add(goipp.TagKeyword, goipp.String("printer-location"))
rq.Values.Add(goipp.TagKeyword, goipp.String("printer-make-and-model"))
rq.Values.Add(goipp.TagKeyword, goipp.String("printer-more-info"))
rq.Values.Add(goipp.TagKeyword, goipp.String("printer-uuid"))
rq.Values.Add(goipp.TagKeyword, goipp.String("sides-supported"))
rq.Values.Add(goipp.TagKeyword, goipp.String("urf-supported"))
}
msg.Operation.Add(rq)
log.Add(LogTraceIPP, '>', "IPP request:").
IppRequest(LogTraceIPP, '>', msg).
Nl(LogTraceIPP).
Flush()
req, _ := msg.EncodeBytes()
resp, err := c.Post(uri, goipp.ContentType, bytes.NewBuffer(req))
if err != nil {
if !ErrIsEOF(err) {
err = fmt.Errorf("HTTP: %s", err)
}
return
}
defer resp.Body.Close()
// Check HTTP status
if resp.StatusCode/100 != 2 {
httpstatus = resp.StatusCode
err = fmt.Errorf("HTTP: %s", resp.Status)
return
}
// Decode IPP response message
respData, err := ioutil.ReadAll(resp.Body)
if err != nil {
err = fmt.Errorf("HTTP: %s", err)
return
}
opts := goipp.DecoderOptions{}
if quirks.GetBuggyIppRsp() == QuirkBuggyIppRspAllow {
opts.EnableWorkarounds = true
}
err = msg.DecodeBytesEx(respData, opts)
if err != nil {
log.Debug(' ', "Failed to decode IPP message: %s", err)
log.HexDump(LogTraceIPP, ' ', respData)
err = fmt.Errorf("IPP decode: %s", err)
return
}
log.Add(LogTraceIPP, '<', "IPP response:").
IppResponse(LogTraceIPP, '<', msg).
Nl(LogTraceIPP).
Flush()
// Check response status
if msg.Code >= 0x100 && !quirks.GetIgnoreIppStatus() {
err = fmt.Errorf("IPP: %s", goipp.Status(msg.Code))
return
}
return
}
// ippAttrs represents a collection of IPP printer attributes,
// enrolled into a map for convenient access
type ippAttrs map[string]goipp.Values
// Create new ippAttrs
func newIppDecoder(msg *goipp.Message) ippAttrs {
attrs := make(ippAttrs)
// Note, we move from the end of list to the beginning, so
// in a case of duplicated attributes, first occurrence wins
for i := len(msg.Printer) - 1; i >= 0; i-- {
attr := msg.Printer[i]
attrs[attr.Name] = attr.Values
}
return attrs
}
// Decode printer attributes and build TXT record for IPP service
//
// This is where information comes from:
//
// DNS-SD name: "printer-dns-sd-name" with fallback to "printer-info",
// "printer-make-and-model" and finally to MfgAndProduct
// from the UsbDeviceInfo
//
// TXT fields:
// air: hardcoded as "none"
// mopria-certified: "mopria-certified"
// rp: hardcoded as "ipp/print"
// kind: "printer-kind"
// PaperMax: based on decoding "media-size-supported"
// URF: "urf-supported" with fallback to
// URF extracted from "printer-device-id"
// UUID: "printer-uuid", without "urn:uuid:" prefix
// Color: "color-supported"
// Duplex: search "sides-supported" for strings with
// prefix "one" or "two"
// note: "printer-location"
// qtotal: hardcoded as "1"
// usb_MDL: MDL, extracted from "printer-device-id"
// usb_MFG: MFG, extracted from "printer-device-id"
// usb_CMD: CMD, extracted from "printer-device-id"
// ty: "printer-make-and-model"
// priority: hardcoded as "50"
// product: "printer-make-and-model", in round brackets
// pdl: "document-format-supported"
// txtvers: hardcoded as "1"
// adminurl: "printer-more-info"
func (attrs ippAttrs) decode(usbinfo UsbDeviceInfo) (
ippinfo *IppPrinterInfo, svc DNSSdSvcInfo) {
svc = DNSSdSvcInfo{
Type: "_ipp._tcp",
SubTypes: []string{"_universal._sub._ipp._tcp"},
}
// Obtain IppPrinterInfo
ippinfo = &IppPrinterInfo{
AdminURL: attrs.strSingle("printer-more-info"),
IconURL: attrs.strSingle("printer-icons"),
}
// Obtain DNSSdName
ippinfo.DNSSdName = attrs.strSingle("printer-dns-sd-name")
if ippinfo.DNSSdName == "" {
ippinfo.DNSSdName = attrs.strSingle("printer-info")
}
if ippinfo.DNSSdName == "" {
ippinfo.DNSSdName = attrs.strSingle("printer-make-and-model")
}
if ippinfo.DNSSdName == "" {
ippinfo.DNSSdName = usbinfo.MfgAndProduct
}
// Obtain UUID
ippinfo.UUID = attrs.getUUID()
if ippinfo.UUID == "" {
ippinfo.UUID = usbinfo.UUID()
}
// Obtain and parse IEEE 1284 device ID
devid := make(map[string]string)
for _, id := range strings.Split(attrs.strSingle("printer-device-id"), ";") {
keyval := strings.SplitN(id, ":", 2)
if len(keyval) == 2 {
devid[keyval[0]] = keyval[1]
}
}
svc.Txt.Add("air", "none")
svc.Txt.IfNotEmpty("mopria-certified", attrs.strSingle("mopria-certified"))
svc.Txt.Add("rp", "ipp/print")
svc.Txt.Add("priority", "50")
svc.Txt.IfNotEmpty("kind", attrs.strJoined("printer-kind"))
svc.Txt.IfNotEmpty("PaperMax", attrs.getPaperMax())
if !svc.Txt.IfNotEmpty("URF", attrs.strJoined("urf-supported")) {
svc.Txt.IfNotEmpty("URF", devid["URF"])
}
svc.Txt.IfNotEmpty("UUID", ippinfo.UUID)
svc.Txt.IfNotEmpty("Color", attrs.getBool("color-supported"))
svc.Txt.IfNotEmpty("Duplex", attrs.getDuplex())
svc.Txt.Add("note", attrs.strSingle("printer-location"))
svc.Txt.Add("qtotal", "1")
svc.Txt.IfNotEmpty("usb_MDL", devid["MDL"])
svc.Txt.IfNotEmpty("usb_MFG", devid["MFG"])
svc.Txt.IfNotEmpty("usb_CMD", devid["CMD"])
svc.Txt.IfNotEmpty("ty", attrs.strSingle("printer-make-and-model"))
svc.Txt.IfNotEmpty("product", attrs.strBrackets("printer-make-and-model"))
svc.Txt.AddPDL("pdl", attrs.strJoined("document-format-supported"))
svc.Txt.Add("txtvers", "1")
svc.Txt.URLIfNotEmpty("adminurl", ippinfo.AdminURL)
return
}
// getUUID returns printer UUID, or "", if UUID not available
func (attrs ippAttrs) getUUID() string {
uuid := attrs.strSingle("printer-uuid")
return UUIDNormalize(uuid)
}
// getDuplex returns "T" if printer supports two-sided
// printing, "F" if not and "" if it cant' tell
func (attrs ippAttrs) getDuplex() string {
vals := attrs.getAttr(goipp.TypeString, "sides-supported")
one, two := false, false
for _, v := range vals {
s := string(v.(goipp.String))
switch {
case strings.HasPrefix(s, "one"):
one = true
case strings.HasPrefix(s, "two"):
two = true
}
}
if two {
return "T"
}
if one {
return "F"
}
return ""
}
// getPaperMax returns max paper size, supported by printer
//
// According to Bonjour Printing Specification, Version 1.2.1,
// it can take one of following values:
//
// "<legal-A4"
// "legal-A4"
// "tabloid-A3"
// "isoC-A2"
// ">isoC-A2"
//
// If PaperMax cannot be guessed, it returns empty string
func (attrs ippAttrs) getPaperMax() string {
// Roll over "media-size-supported", extract
// max x-dimension and max y-dimension
vals := attrs.getAttr(goipp.TypeCollection, "media-size-supported")
if vals == nil {
return ""
}
var xDimMax, yDimMax int
for _, collection := range vals {
var xDimAttr, yDimAttr goipp.Attribute
attrs := collection.(goipp.Collection)
for i := len(attrs) - 1; i >= 0; i-- {
switch attrs[i].Name {
case "x-dimension":
xDimAttr = attrs[i]
case "y-dimension":
yDimAttr = attrs[i]
}
}
if len(xDimAttr.Values) > 0 {
switch dim := xDimAttr.Values[0].V.(type) {
case goipp.Integer:
if int(dim) > xDimMax {
xDimMax = int(dim)
}
case goipp.Range:
if int(dim.Upper) > xDimMax {
xDimMax = int(dim.Upper)
}
}
}
if len(yDimAttr.Values) > 0 {
switch dim := yDimAttr.Values[0].V.(type) {
case goipp.Integer:
if int(dim) > yDimMax {
yDimMax = int(dim)
}
case goipp.Range:
if int(dim.Upper) > yDimMax {
yDimMax = int(dim.Upper)
}
}
}
}
if xDimMax == 0 || yDimMax == 0 {
return ""
}
// Now classify by paper size
return PaperSize{xDimMax, yDimMax}.Classify()
}
// Get a single-string attribute.
func (attrs ippAttrs) strSingle(name string) string {
strs := attrs.getStrings(name)
if len(strs) == 0 {
return ""
}
return strs[0]
}
// Get a multi-string attribute, represented as a comma-separated list
func (attrs ippAttrs) strJoined(name string) string {
strs := attrs.getStrings(name)
return strings.Join(strs, ",")
}
// Get a single string, and put it into brackets
func (attrs ippAttrs) strBrackets(name string) string {
s := attrs.strSingle(name)
if s != "" {
s = "(" + s + ")"
}
return s
}
// Get attribute's []string value by attribute name
func (attrs ippAttrs) getStrings(name string) []string {
vals := attrs.getAttr(goipp.TypeString, name)
strs := make([]string, len(vals))
for i := range vals {
strs[i] = string(vals[i].(goipp.String))
}
return strs
}
// Get boolean attribute. Returns "F" or "T" if attribute is found,
// empty string otherwise.
func (attrs ippAttrs) getBool(name string) string {
vals := attrs.getAttr(goipp.TypeBoolean, name)
if vals == nil {
return ""
}
if vals[0].(goipp.Boolean) {
return "T"
}
return "F"
}
// Get attribute's value by attribute name
// Value type is checked and enforced
func (attrs ippAttrs) getAttr(t goipp.Type, name string) []goipp.Value {
v, ok := attrs[name]
if ok && v[0].V.Type() == t {
var vals []goipp.Value
for i := range v {
vals = append(vals, v[i].V)
}
return vals
}
return nil
}
0707010000002A000081A400000000000000000000000167D72F5D0000074D000000000000000000000000000000000000001D00000000ipp-usb-0.9.30/linewriter.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
*
* Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
* See LICENSE for license terms and conditions
*
* LineWriter is a helper object, implementing io.Writer interface
* on a top of write-line callback. It is used by logger.
*/
package main
import (
"bytes"
)
// LineWriter implements io.Write and io.Close interfaces
// It splits stream into text lines and calls a provided
// callback for each complete line.
//
// Line passed to callback is not terminated by '\n'
// character. Close flushes last incomplete line, if any
type LineWriter struct {
Func func([]byte) // write-line callback
Prefix string // Prefix prepended to each line
buf bytes.Buffer // buffer for incomplete lines
}
// Write implements io.Writer interface
func (lw *LineWriter) Write(text []byte) (n int, err error) {
n = len(text)
for len(text) > 0 {
// Fetch next line
var line []byte
var unfinished bool
if l := bytes.IndexByte(text, '\n'); l >= 0 {
l++
line = text[:l-1]
text = text[l:]
} else {
line = text
text = nil
unfinished = true
}
// Dispatch next line
if lw.buf.Len() == 0 {
lw.buf.Write([]byte(lw.Prefix))
}
lw.buf.Write(line)
if !unfinished {
lw.Func(lw.buf.Bytes())
lw.buf.Reset()
}
}
return
}
// Close implements io.Closer interface
//
// Close flushes the last incomplete line from the
// internal buffer. Close is not needed, if it is
// known that there is no such a line, or if its
// presence doesn't matter (without Close its content
// will be lost)
func (lw *LineWriter) Close() error {
if lw.buf.Len() > 0 {
lw.Func(lw.buf.Bytes())
}
return nil
}
// WriteClose writes text to LineWriter and then closes it
func (lw *LineWriter) WriteClose(text []byte) {
lw.Write(text)
lw.Close()
}
0707010000002B000081A400000000000000000000000167D72F5D000006FC000000000000000000000000000000000000001B00000000ipp-usb-0.9.30/listener.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
*
* Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
* See LICENSE for license terms and conditions
*
* HTTP listener
*/
package main
import (
"net"
"strconv"
"time"
)
// Listener wraps net.Listener
//
// Note, if IP address is not specified, go stdlib
// creates a beautiful listener, able to listen to
// IPv4 and IPv6 simultaneously. But it cannot do it,
// if IP address is given
//
// So it is much simpler to always create a broadcast listener
// and to filter incoming connection in Accept() wrapper rather
// that create separate IPv4 and IPv6 listeners and dial with
// them both
type Listener struct {
net.Listener // Underlying net.Listener
}
// NewListener creates new listener
func NewListener(port int) (net.Listener, error) {
// Setup network and address
network := "tcp4"
if Conf.IPV6Enable {
network = "tcp"
}
addr := ":" + strconv.Itoa(port)
// Create net.Listener
nl, err := net.Listen(network, addr)
if err != nil {
return nil, err
}
// Wrap into Listener
return Listener{nl}, nil
}
// Accept new connection
func (l Listener) Accept() (net.Conn, error) {
for {
// Accept new connection
conn, err := l.Listener.Accept()
if err != nil {
return nil, err
}
// Obtain underlying net.TCPConn
tcpconn, ok := conn.(*net.TCPConn)
if !ok {
// Should never happen, actually
conn.Close()
continue
}
// Reject non-loopback connections, if required
if Conf.LoopbackOnly &&
!tcpconn.LocalAddr().(*net.TCPAddr).IP.IsLoopback() {
tcpconn.SetLinger(0)
tcpconn.Close()
continue
}
// Setup TCP parameters
tcpconn.SetKeepAlive(true)
tcpconn.SetKeepAlivePeriod(20 * time.Second)
return tcpconn, nil
}
}
0707010000002C000081A400000000000000000000000167D72F5D000046F0000000000000000000000000000000000000001900000000ipp-usb-0.9.30/logger.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
*
* Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
* See LICENSE for license terms and conditions
*
* Logging
*/
package main
import (
"bytes"
"compress/gzip"
"context"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"runtime/debug"
"sort"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/OpenPrinting/goipp"
)
const (
// LogMinFileSize specifies a minimum value for the
// max-file-size parameter
LogMinFileSize = 16 * 1024
)
// Standard loggers
var (
// This is the default logger
Log = NewLogger().ToMainFile()
// Console logger always writes to console
Console = NewLogger().ToConsole()
// Initlog used only on initialization time
// It writes to Stdout or Stderr, depending
// on log level
InitLog = NewLogger().ToStdOutErr()
)
// LogLevel enumerates possible log levels
type LogLevel int
// LogLevel constants
const (
LogError LogLevel = 1 << iota
LogInfo
LogDebug
LogTraceIPP
LogTraceESCL
LogTraceHTTP
LogTraceUSB
LogAll = LogError | LogInfo | LogDebug | LogTraceAll
LogTraceAll = LogTraceIPP | LogTraceESCL | LogTraceHTTP | LogTraceUSB
)
// Adjust LogLevel mask, so more detailed log levels
// imply less detailed
func (levels *LogLevel) Adjust() {
switch {
case *levels&LogTraceAll != 0:
*levels |= LogDebug | LogInfo | LogError
case *levels&LogDebug != 0:
*levels |= LogInfo | LogError
case *levels&LogInfo != 0:
*levels |= LogError
}
}
// loggerMode enumerates possible Logger modes
type loggerMode int
const (
loggerNoMode loggerMode = iota // Mode not yet set; log is buffered
loggerDiscard // Log goes to nowhere
loggerConsole // Log goes to console
loggerColorConsole // Log goes to console and uses ANSI colors
loggerFile // Log goes to disk file
)
// Logger implements logging facilities
type Logger struct {
LogMessage // "Root" log message
levels LogLevel // Levels generated by this logger
ccLevels LogLevel // Sum of Cc's levels
paused int32 // Logger paused, if counter > 0
mode loggerMode // Logger mode
lock sync.Mutex // Write lock
path string // Path to log file
cc []*Logger // Loggers to send carbon copy to
out io.Writer // Output stream, may be *os.File
outhook func(io.Writer, // Output hook
LogLevel, []byte)
// Don't reexport these methods from the root message
Commit, Flush, Reject struct{}
}
// NewLogger creates new logger. Logger mode is not set,
// so logs written to this logger a buffered until mode
// (and direction) is set
func NewLogger() *Logger {
l := &Logger{
mode: loggerNoMode,
levels: LogAll,
ccLevels: 0,
outhook: func(w io.Writer, _ LogLevel, line []byte) {
w.Write(line)
},
}
l.LogMessage.logger = l
return l
}
// ToNowhere redirects log to nowhere
func (l *Logger) ToNowhere() *Logger {
l.mode = loggerDiscard
l.out = ioutil.Discard
return l
}
// ToConsole redirects log to console
func (l *Logger) ToConsole() *Logger {
l.mode = loggerConsole
l.out = os.Stdout
return l
}
// ToColorConsole redirects log to console with ANSI colors
func (l *Logger) ToColorConsole() *Logger {
if logIsAtty(os.Stdout) {
l.outhook = logColorConsoleWrite
}
return l.ToConsole()
}
// ToStdOutErr redirects log to Stdout or Stderr, depending
// on LogLevel
func (l *Logger) ToStdOutErr() *Logger {
l.outhook = func(out io.Writer, level LogLevel, line []byte) {
if level == LogError {
out = os.Stderr
}
out.Write(line)
}
return l.ToConsole()
}
// ToFile redirects log to arbitrary log file
func (l *Logger) ToFile(path string) *Logger {
l.path = path
l.mode = loggerFile
l.out = nil // Will be opened on demand
return l
}
// ToMainFile redirects log to the main log file
func (l *Logger) ToMainFile() *Logger {
return l.ToFile(PathLogFile)
}
// ToDevFile redirects log to per-device log file
func (l *Logger) ToDevFile(info UsbDeviceInfo) *Logger {
return l.ToFile(filepath.Join(PathLogDir, info.Ident()+".log"))
}
// Cc adds Logger to send "carbon copy" to.
func (l *Logger) Cc(to *Logger) *Logger {
l.cc = append(l.cc, to)
l.ccLevels |= to.levels
return l
}
// Close the logger
func (l *Logger) Close() {
if l.mode == loggerFile && l.out != nil {
if file, ok := l.out.(*os.File); ok {
file.Close()
}
}
}
// SetLevels set logger's log levels
func (l *Logger) SetLevels(levels LogLevel) *Logger {
levels.Adjust()
l.levels = levels
return l
}
// Pause the logger. All output will be buffered,
// and flushed to destination when logger is resumed
func (l *Logger) Pause() *Logger {
atomic.AddInt32(&l.paused, 1)
return l
}
// Resume the logger. All buffered output will be
// flushed
func (l *Logger) Resume() *Logger {
if atomic.AddInt32(&l.paused, -1) == 0 {
l.LogMessage.Flush()
}
return l
}
// Panic writes to log a panic message, including
// call stack, and terminates a program
func (l *Logger) Panic(v interface{}) {
l.Error('!', "panic: %v", v)
l.Error('!', "")
w := l.LineWriter(LogError, '!')
w.Write(debug.Stack())
w.Close()
os.Exit(1)
}
// Format a time prefix
func (l *Logger) fmtTime() *logLineBuf {
buf := logLineBufAlloc(0, 0)
if l.mode == loggerFile {
now := time.Now()
year, month, day := now.Date()
hour, min, sec := now.Clock()
fmt.Fprintf(buf, "%2.2d-%2.2d-%4.4d %2.2d:%2.2d:%2.2d:",
day, month, year,
hour, min, sec)
}
return buf
}
// Handle log rotation
func (l *Logger) rotate() {
// Do we need to rotate?
file, ok := l.out.(*os.File)
if !ok {
return
}
stat, err := file.Stat()
if err != nil || stat.Size() <= Conf.LogMaxFileSize {
return
}
// Perform rotation
if Conf.LogMaxBackupFiles > 0 {
prevpath := ""
for i := Conf.LogMaxBackupFiles; i > 0; i-- {
nextpath := fmt.Sprintf("%s.%d.gz", l.path, i-1)
if i == Conf.LogMaxBackupFiles {
os.Remove(nextpath)
} else {
os.Rename(nextpath, prevpath)
}
prevpath = nextpath
}
err := l.gzip(l.path, prevpath)
if err != nil {
return
}
}
file.Truncate(0)
}
// gzip the log file
func (l *Logger) gzip(ipath, opath string) error {
// Open input file
ifile, err := os.Open(ipath)
if err != nil {
return err
}
defer ifile.Close()
// Open output file
ofile, err := os.OpenFile(opath, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0644)
if err != nil {
return err
}
// gzip ifile->ofile
w := gzip.NewWriter(ofile)
_, err = io.Copy(w, ifile)
err2 := w.Close()
err3 := ofile.Close()
switch {
case err == nil && err2 != nil:
err = err2
case err == nil && err3 != nil:
err = err3
}
// Cleanup and exit
if err != nil {
os.Remove(opath)
}
return err
}
// LogMessage represents a single (possible multi line) log
// message, which will appear in the output log atomically,
// and will be not interrupted in the middle by other log activity
type LogMessage struct {
logger *Logger // Underlying logger
parent *LogMessage // Parent message
lines []*logLineBuf // One buffer per line
}
// logMessagePool manages a pool of reusable LogMessages
var logMessagePool = sync.Pool{New: func() interface{} { return &LogMessage{} }}
// Begin returns a child (nested) LogMessage. Writes to this
// child message appended to the parent message
func (msg *LogMessage) Begin() *LogMessage {
msg2 := logMessagePool.Get().(*LogMessage)
msg2.logger = msg.logger
msg2.parent = msg
return msg2
}
// Add formats a next line of log message, with level and prefix char
func (msg *LogMessage) Add(level LogLevel, prefix byte,
format string, args ...interface{}) *LogMessage {
if (msg.logger.levels|msg.logger.ccLevels)&level != 0 {
buf := logLineBufAlloc(level, prefix)
fmt.Fprintf(buf, format, args...)
msg.appendLineBuf(buf)
}
return msg
}
// Nl adds empty line to the log message
func (msg *LogMessage) Nl(level LogLevel) *LogMessage {
return msg.Add(level, ' ', "")
}
// addBytes adds a next line of log message, taking slice of bytes as input
func (msg *LogMessage) addBytes(level LogLevel, prefix byte, line []byte) *LogMessage {
if (msg.logger.levels|msg.logger.ccLevels)&level != 0 {
buf := logLineBufAlloc(level, prefix)
buf.Write(line)
msg.appendLineBuf(buf)
}
return msg
}
// appendLineBuf appends line buffer to msg.lines
func (msg *LogMessage) appendLineBuf(buf *logLineBuf) {
if msg.parent == nil {
// Note, many threads may write to the root
// message simultaneously
msg.logger.lock.Lock()
msg.lines = append(msg.lines, buf)
msg.logger.lock.Unlock()
msg.Flush()
} else {
msg.lines = append(msg.lines, buf)
}
}
// Debug appends a LogDebug line to the message
func (msg *LogMessage) Debug(prefix byte, format string, args ...interface{}) *LogMessage {
return msg.Add(LogDebug, prefix, format, args...)
}
// Info appends a LogInfo line to the message
func (msg *LogMessage) Info(prefix byte, format string, args ...interface{}) *LogMessage {
return msg.Add(LogInfo, prefix, format, args...)
}
// Error appends a LogError line to the message
func (msg *LogMessage) Error(prefix byte, format string, args ...interface{}) *LogMessage {
return msg.Add(LogError, prefix, format, args...)
}
// Exit appends a LogError line to the message, flushes the message and
// all its parents and terminates a program by calling os.Exit(1)
func (msg *LogMessage) Exit(prefix byte, format string, args ...interface{}) {
if msg.logger.mode == loggerNoMode {
msg.logger.ToConsole()
}
msg.Error(prefix, format, args...)
for msg.parent != nil {
msg.Flush()
msg = msg.parent
}
os.Exit(1)
}
// Check calls msg.Exit(), if err is not nil
func (msg *LogMessage) Check(err error) {
if err != nil {
msg.Exit(0, "%s", err)
}
}
// HexDump appends a HEX dump to the log message
func (msg *LogMessage) HexDump(level LogLevel, prefix byte,
data []byte) *LogMessage {
if (msg.logger.levels|msg.logger.ccLevels)&level == 0 {
return msg
}
hex := logLineBufAlloc(0, 0)
chr := logLineBufAlloc(0, 0)
defer hex.free()
defer chr.free()
off := 0
for len(data) > 0 {
hex.Reset()
chr.Reset()
sz := len(data)
if sz > 16 {
sz = 16
}
i := 0
for ; i < sz; i++ {
c := data[i]
fmt.Fprintf(hex, "%2.2x", data[i])
if i%4 == 3 {
hex.Write([]byte(":"))
} else {
hex.Write([]byte(" "))
}
if 0x20 <= c && c < 0x80 {
chr.WriteByte(c)
} else {
chr.WriteByte('.')
}
}
for ; i < 16; i++ {
hex.WriteString(" ")
}
msg.Add(level, prefix, "%4.4x: %s %s", off, hex, chr)
off += sz
data = data[sz:]
}
return msg
}
// HTTPRequest dumps HTTP request (except body) to the log message
func (msg *LogMessage) HTTPRequest(level LogLevel, prefix byte,
session int, rq *http.Request) *LogMessage {
if (msg.logger.levels|msg.logger.ccLevels)&level == 0 {
return msg
}
// Clone request, drop body
rq = rq.WithContext(context.Background())
rq.Body = struct{ io.ReadCloser }{http.NoBody}
// Write it to the log
msg.Add(level, prefix, "HTTP[%3.3d]: HTTP request header:", session)
buf := &bytes.Buffer{}
rq.Write(buf)
for _, l := range bytes.Split(buf.Bytes(), []byte("\n")) {
if sz := len(l); sz > 0 && l[sz-1] == '\r' {
l = l[:sz-1]
}
msg.Add(level, prefix, " %s", l)
if len(l) == 0 {
break
}
}
return msg
}
// HTTPResponse dumps HTTP response (expect body) to the log message
func (msg *LogMessage) HTTPResponse(level LogLevel, prefix byte,
session int, rsp *http.Response) *LogMessage {
if (msg.logger.levels|msg.logger.ccLevels)&level == 0 {
return msg
}
// Clone response header. Avoid rsp.Header.Clone(),
// because Go 11 doesn't support it yet
hdr := make(http.Header, len(rsp.Header))
for k, v := range rsp.Header {
hdr[k] = v
}
// Go stdlib strips Transfer-Encoding header, so reconstruct it
if rsp.TransferEncoding != nil {
hdr.Add("Transfer-Encoding",
strings.Join(rsp.TransferEncoding, ", "))
}
// Write it to the log
msg.Add(level, prefix, "HTTP[%3.3d]: HTTP response header:", session)
msg.Add(level, prefix, " %s %s", rsp.Proto, rsp.Status)
keys := make([]string, 0, len(hdr))
for k := range hdr {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
msg.Add(level, prefix, " %s: %s", k, hdr.Get(k))
}
msg.Add(level, prefix, " ")
return msg
}
// HTTPRqParams dumps HTTP request parameters into the log message
func (msg *LogMessage) HTTPRqParams(level LogLevel, prefix byte,
session int, rq *http.Request) *LogMessage {
msg.Add(level, prefix, "HTTP[%3.3d]: %s %s", session, rq.Method, rq.URL)
return msg
}
// HTTPRspStatus dumps HTTP response status into the log message
func (msg *LogMessage) HTTPRspStatus(level LogLevel, prefix byte,
session int, rq *http.Request, rsp *http.Response) *LogMessage {
msg.Add(level, prefix, "HTTP[%3.3d]: %s %s - %s",
session, rq.Method, rq.URL, rsp.Status)
return msg
}
// HTTPError writes HTTP error into the log message
func (msg *LogMessage) HTTPError(prefix byte,
session int, format string, args ...interface{}) *LogMessage {
msg.Error(prefix, "HTTP[%3.3d]: %s", session, fmt.Sprintf(format, args...))
return msg
}
// HTTPDebug writes HTTP debug line into the log message
func (msg *LogMessage) HTTPDebug(prefix byte,
session int, format string, args ...interface{}) *LogMessage {
msg.Debug(prefix, "HTTP[%3.3d]: %s", session, fmt.Sprintf(format, args...))
return msg
}
// IppRequest dumps IPP request into the log message
func (msg *LogMessage) IppRequest(level LogLevel, prefix byte,
m *goipp.Message) *LogMessage {
if (msg.logger.levels|msg.logger.ccLevels)&level != 0 {
m.Print(msg.LineWriter(level, prefix), true)
}
return msg
}
// IppResponse dumps IPP response into the log message
func (msg *LogMessage) IppResponse(level LogLevel, prefix byte,
m *goipp.Message) *LogMessage {
if (msg.logger.levels|msg.logger.ccLevels)&level != 0 {
m.Print(msg.LineWriter(level, prefix), false)
}
return msg
}
// LineWriter creates a LineWriter that writes to the LogMessage,
// using specified LogLevel and prefix
func (msg *LogMessage) LineWriter(level LogLevel, prefix byte) *LineWriter {
return &LineWriter{
Func: func(line []byte) { msg.addBytes(level, prefix, line) },
}
}
// Commit message to the log
func (msg *LogMessage) Commit() {
msg.Flush()
msg.free()
}
// Flush message content to the log
//
// This is equal to committing the message and starting
// the new message, with the exception that old message
// pointer remains valid. Message logical atomicity is not
// preserved between flushes
func (msg *LogMessage) Flush() {
// Lock the logger
msg.logger.lock.Lock()
defer msg.logger.lock.Unlock()
// Ignore empty messages
if len(msg.lines) == 0 {
return
}
// If message has a parent, simply flush our content there
if msg.parent != nil {
msg.parent.lines = append(msg.parent.lines, msg.lines...)
msg.lines = msg.lines[:0]
// If our parent is root, we need to flush root as well
if msg.parent.parent == nil {
msg = msg.parent
} else {
return
}
}
// Do nothing, if logger is paused
if atomic.LoadInt32(&msg.logger.paused) != 0 {
return
}
// Open log file on demand
if msg.logger.out == nil && msg.logger.mode == loggerFile {
os.MkdirAll(PathLogDir, 0755)
msg.logger.out, _ = os.OpenFile(msg.logger.path,
os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644)
}
if msg.logger.out == nil {
return
}
// Rotate now
if msg.logger.mode == loggerFile {
msg.logger.rotate()
}
// Prepare to carbon-copy
var cclist []struct {
levels LogLevel
msg *LogMessage
}
for _, cc := range msg.logger.cc {
cclist = append(cclist, struct {
levels LogLevel
msg *LogMessage
}{cc.levels, cc.Begin()})
}
// Send message content to the logger
buf := msg.logger.fmtTime()
defer buf.free()
timeLen := buf.Len()
for _, l := range msg.lines {
l.trim()
// Generate own output
buf.Truncate(timeLen)
if l.level&msg.logger.levels != 0 {
if !l.empty() {
if timeLen != 0 {
buf.WriteByte(' ')
}
buf.Write(l.Bytes())
}
buf.WriteByte('\n')
msg.logger.outhook(msg.logger.out, l.level, buf.Bytes())
}
// Send carbon copies
for _, cc := range cclist {
if (cc.levels & l.level) != 0 {
cc.msg.addBytes(l.level, 0, l.Bytes())
}
}
l.free()
}
// Commit carbon copies
for _, cc := range cclist {
cc.msg.Commit()
}
// Reset the message
msg.lines = msg.lines[:0]
}
// Reject the message
func (msg *LogMessage) Reject() {
msg.free()
}
// Return message to the logMessagePool
func (msg *LogMessage) free() {
// Free all lines
for _, l := range msg.lines {
l.free()
}
// Reset the message and put it to the pool
if len(msg.lines) < 16 {
msg.lines = msg.lines[:0] // Keep memory, reset content
} else {
msg.lines = nil // Drop this large buffer
}
msg.logger = nil
logMessagePool.Put(msg)
}
// logLineBuf represents a single log line buffer
type logLineBuf struct {
bytes.Buffer // Underlying buffer
level LogLevel // Log level the line was written on
}
// logLinePool manages a pool of reusable logLines
var logLineBufPool = sync.Pool{New: func() interface{} {
return &logLineBuf{
Buffer: bytes.Buffer{},
}
}}
// logLineAlloc() allocates a logLineBuf
func logLineBufAlloc(level LogLevel, prefix byte) *logLineBuf {
buf := logLineBufPool.Get().(*logLineBuf)
buf.level = level
if prefix != 0 {
buf.Write([]byte{prefix, ' '})
}
return buf
}
// free returns the logLineBuf to the pool
func (buf *logLineBuf) free() {
if buf.Cap() <= 256 {
buf.Reset()
logLineBufPool.Put(buf)
}
}
// trim removes trailing spaces
func (buf *logLineBuf) trim() {
bytes := buf.Bytes()
var i int
loop:
for i = len(bytes); i > 0; i-- {
c := bytes[i-1]
switch c {
case '\t', '\n', '\v', '\f', '\r', ' ', 0x85, 0xA0:
default:
break loop
}
}
buf.Truncate(i)
}
// empty returns true if logLineBuf is empty (no text, no prefix)
func (buf *logLineBuf) empty() bool {
return buf.Len() == 0
}
0707010000002D000081A400000000000000000000000167D72F5D00000437000000000000000000000000000000000000001E00000000ipp-usb-0.9.30/logger_unix.go// +build darwin dragonfly freebsd linux nacl netbsd openbsd solaris
/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
*
* Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
* See LICENSE for license terms and conditions
*
* Logging, system-dependent part for UNIX
*/
package main
import (
"io"
"os"
)
// #include <unistd.h>
import "C"
// logIsAtty returns true, if os.File refers to a terminal
func logIsAtty(file *os.File) bool {
fd := file.Fd()
return C.isatty(C.int(fd)) == 1
}
// logColorConsoleWrite writes a colorized line to console
func logColorConsoleWrite(out io.Writer, level LogLevel, line []byte) {
var beg, end string
switch {
case (level & LogError) != 0:
beg, end = "\033[31;1m", "\033[0m" // Red
case (level & LogInfo) != 0:
beg, end = "\033[32;1m", "\033[0m" // Green
case (level & LogDebug) != 0:
beg, end = "\033[37;1m", "\033[0m" // White
case (level & LogTraceAll) != 0:
beg, end = "\033[37m", "\033[0m" // Gray
}
out.Write([]byte(beg))
out.Write(line)
out.Write([]byte(end))
}
0707010000002E000081A400000000000000000000000167D72F5D00000292000000000000000000000000000000000000001B00000000ipp-usb-0.9.30/loopback.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
*
* Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
* See LICENSE for license terms and conditions
*
* Loopback interface index discovery
*/
package main
import (
"errors"
"fmt"
"net"
)
// Loopback returns index of loopback interface
func Loopback() (int, error) {
interfaces, err := net.Interfaces()
if err == nil {
for _, iface := range interfaces {
if (iface.Flags & net.FlagLoopback) != 0 {
return iface.Index, nil
}
}
}
if err == nil {
err = errors.New("not found")
}
return 0, fmt.Errorf("Loopback discovery: %s", err)
}
0707010000002F000081A400000000000000000000000167D72F5D00001D54000000000000000000000000000000000000001700000000ipp-usb-0.9.30/main.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
*
* Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
* See LICENSE for license terms and conditions
*
* The main function
*/
package main
import (
"bytes"
"fmt"
"os"
"sort"
)
const usageText = `Usage:
%s mode [options]
Modes are:
standalone - run forever, automatically discover IPP-over-USB
devices and serve them all
udev - like standalone, but exit when last IPP-over-USB
device is disconnected
debug - logs duplicated on console, -bg option is
ignored
check - check configuration and exit
status - print ipp-usb status and exit
Options are
-bg - run in background (ignored in debug mode)
`
// RunMode represents the program run mode
type RunMode int
// Run modes:
// RunStandalone - run forever, automatically discover IPP-over-USB
// devices and serve them all
// RunUdev - like RunStandalone, but exit when last IPP-over-USB
// device is disconnected
// RunDebug - logs duplicated on console, -bg option is ignored
// RunCheck - check configuration and exit
// RunStatus - print ipp-usb status and exit
const (
RunDefault RunMode = iota
RunStandalone
RunUdev
RunDebug
RunCheck
RunStatus
)
// String returns RunMode name
func (m RunMode) String() string {
switch m {
case RunDefault:
return "default"
case RunStandalone:
return "standalone"
case RunUdev:
return "udev"
case RunDebug:
return "debug"
case RunCheck:
return "check"
case RunStatus:
return "status"
}
return fmt.Sprintf("unknown (%d)", int(m))
}
// RunParameters represents the program run parameters
type RunParameters struct {
Mode RunMode // Run mode
Background bool // Run in background
}
// usage prints detailed usage and exits
func usage() {
fmt.Printf(usageText, os.Args[0])
os.Exit(0)
}
// usage_error prints usage error and exits
func usageError(format string, args ...interface{}) {
if format != "" {
fmt.Printf(format+"\n", args...)
}
fmt.Printf("Try %s -h for more information\n", os.Args[0])
os.Exit(1)
}
// parseArgv parses program parameters. In a case of usage error,
// it prints a error message and exits
func parseArgv() (params RunParameters) {
// Catch panics to log
defer func() {
v := recover()
if v != nil {
Log.Panic(v)
}
}()
// For now, default mode is debug mode. It may change in a future
params.Mode = RunDebug
modes := 0
for _, arg := range os.Args[1:] {
switch arg {
case "-h", "-help", "--help":
usage()
case "standalone":
params.Mode = RunStandalone
modes++
case "udev":
params.Mode = RunUdev
modes++
case "debug":
params.Mode = RunDebug
modes++
case "check":
params.Mode = RunCheck
modes++
case "status":
params.Mode = RunStatus
modes++
case "-bg":
params.Background = true
default:
usageError("Invalid argument %s", arg)
}
}
if modes > 1 {
usageError("Conflicting run modes")
}
if params.Mode == RunDebug {
params.Background = false
}
return
}
// printStatus prints status of running ipp-usb daemon, if any
func printStatus() {
// Fetch status
text, err := StatusRetrieve()
if err != nil {
InitLog.Info(0, "%s", err)
return
}
// Split into lines
text = bytes.Trim(text, "\n")
lines := bytes.Split(text, []byte("\n"))
// Strip empty lines at the end
for len(lines) > 0 && len(lines[len(lines)-1]) == 0 {
lines = lines[0 : len(lines)-1]
}
// Write to log, line by line
for _, line := range lines {
InitLog.Info(0, "%s", line)
}
}
// The main function
func main() {
var err error
// Parse arguments
params := parseArgv()
// Load configuration file
err = ConfLoad()
InitLog.Check(err)
// Setup logging
if params.Mode != RunDebug &&
params.Mode != RunCheck &&
params.Mode != RunStatus {
Console.ToNowhere()
} else if Conf.ColorConsole {
Console.ToColorConsole()
}
Log.SetLevels(Conf.LogMain)
Console.SetLevels(Conf.LogConsole)
Log.Cc(Console)
// In RunCheck mode, list IPP-over-USB devices
if params.Mode == RunCheck {
// If we are here, configuration is OK
InitLog.Info(0, "Configuration files: OK")
var descs map[UsbAddr]UsbDeviceDesc
err = UsbInit(true)
if err == nil {
descs, err = UsbGetIppOverUsbDeviceDescs()
}
if err != nil {
InitLog.Info(0, "Can't read list of USB devices: %s", err)
} else if descs == nil || len(descs) == 0 {
InitLog.Info(0, "No IPP over USB devices found")
} else {
// Repack into the sorted list
var list []UsbDeviceDesc
var buf bytes.Buffer
for _, desc := range descs {
list = append(list, desc)
}
sort.Slice(list, func(i, j int) bool {
return list[i].UsbAddr.Less(list[j].UsbAddr)
})
InitLog.Info(0, "IPP over USB devices:")
InitLog.Info(0, " Num Device Vndr:Prod Model")
for i, dev := range list {
buf.Reset()
fmt.Fprintf(&buf, "%3d. %s", i+1, dev.UsbAddr)
if info, err := dev.GetUsbDeviceInfo(); err == nil {
fmt.Fprintf(&buf, " %4.4x:%.4x %q",
info.Vendor, info.Product, info.MfgAndProduct)
}
InitLog.Info(0, " %s", buf.String())
}
}
}
// In RunStatus mode, print ipp-usb status, and we are done
if params.Mode == RunStatus {
printStatus()
os.Exit(0)
}
// Check user privileges
if os.Geteuid() != 0 {
InitLog.Exit(0, "This program requires root privileges")
}
// If mode is "check", we are done
if params.Mode == RunCheck {
os.Exit(0)
}
// If background run is requested, it's time to fork
if params.Background {
err = Daemon()
InitLog.Check(err)
os.Exit(0)
}
// Prevent multiple copies of ipp-usb from being running
// in a same time
os.MkdirAll(PathLockDir, 0755)
lock, err := os.OpenFile(PathLockFile,
os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
InitLog.Check(err)
defer lock.Close()
err = FileLock(lock, FileLockNoWait)
if err == ErrLockIsBusy {
if params.Mode == RunUdev {
// It's not an error in udev mode
os.Exit(0)
} else {
InitLog.Exit(0, "ipp-usb already running")
}
}
InitLog.Check(err)
// Write to log that we are here
if params.Mode != RunCheck && params.Mode != RunStatus {
Log.Info(' ', "===============================")
Log.Info(' ', "ipp-usb started in %q mode, pid=%d",
params.Mode, os.Getpid())
defer Log.Info(' ', "ipp-usb finished")
}
// Initialize USB
err = UsbInit(false)
InitLog.Check(err)
// Close stdin/stdout/stderr, unless running in debug mode
if params.Mode != RunDebug {
err = CloseStdInOutErr()
InitLog.Check(err)
}
// Run PnP manager
for {
exitReason := PnPStart(params.Mode == RunUdev)
// The following race is possible here:
// 1) last device disappears, ipp-usb is about to exit
// 2) new device connected, new ipp-usb started
// 3) new ipp-usp exits, because lock is still held
// by the old ipp-usb
// 4) old ipp-usb finally exits
//
// So after releasing a lock, we rescan for IPP-over-USB
// devices, and if something was found, we try to reacquire
// the lock, and if it succeeds, we continue to serve
// these devices instead of exiting
if exitReason == PnPIdle && params.Mode == RunUdev {
err = FileUnlock(lock)
Log.Check(err)
if UsbCheckIppOverUsbDevices() &&
FileLock(lock, FileLockNoWait) == nil {
Log.Info(' ', "New IPP-over-USB device found")
continue
}
}
break
}
}
07070100000030000081A400000000000000000000000167D72F5D0000080E000000000000000000000000000000000000001800000000ipp-usb-0.9.30/paper.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
*
* Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
* See LICENSE for license terms and conditions
*
* Paper Size Classifier
*/
package main
// PaperSize represents paper size, in IPP units (1/100 mm)
type PaperSize struct {
Width, Height int // Paper width and height
}
// Standard paper sizes
// US name US inches US mm ISO mm
// "legal-A4" A, Legal 8.5 x 14 215.9 x 355.6 A4: 210 x 297
// "tabloid-A3" B, Tabloid 11 x 17 279.4 x 431.8 A3: 297 x 420
// "isoC-A2" C 17 × 22 431.8 × 558.8 A2: 420 x 594
//
// Please note, Apple in the "Bonjour Printing Specification"
// incorrectly states paper sizes as 9x14, 13x19 and 18x24 inches
var (
PaperLegal = PaperSize{21590, 35560}
PaperA4 = PaperSize{21000, 29700}
PaperTabloid = PaperSize{27940, 43180}
PaperA3 = PaperSize{29700, 42000}
PaperC = PaperSize{43180, 55880}
PaperA2 = PaperSize{42000, 59400}
)
// Less checks that p is less that p2, which means:
// * Either p.Width or p.Height is less that p2.Width or p2.Heigh
// * Neither of p.Width or p.Height is greater that p2.Width or p2.Heigh
func (p PaperSize) Less(p2 PaperSize) bool {
return (p.Width < p2.Width && p.Height <= p2.Height) ||
(p.Height < p2.Height && p.Width <= p2.Width)
}
// Classify paper size according to Apple Bonjour rules
// Returns:
// ">isoC-A2" for paper larger that C or A2
// "isoC-A2" for C or A2 paper
// "tabloid-A3" for Tabloid or A3 paper
// "legal-A4" for Legal or A4 paper
// "<legal-A4" for paper smaller that Legal or A4
func (p PaperSize) Classify() string {
switch {
case PaperC.Less(p) || PaperA2.Less(p):
return ">isoC-A2"
case !p.Less(PaperC) || !p.Less(PaperA2):
return "isoC-A2"
case !p.Less(PaperTabloid) || !p.Less(PaperA3):
return "tabloid-A3"
case !p.Less(PaperLegal) || !p.Less(PaperA4):
return "legal-A4"
default:
return "<legal-A4"
}
}
07070100000031000081A400000000000000000000000167D72F5D000009AC000000000000000000000000000000000000001D00000000ipp-usb-0.9.30/paper_test.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
*
* Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
* See LICENSE for license terms and conditions
*
* Tests for paper.go
*/
package main
import (
"testing"
)
var allSizes = []PaperSize{
PaperLegal,
PaperA4,
PaperTabloid,
PaperA3,
PaperC,
PaperA2,
}
// Compute p.Less(p2) and check answer
func testPaperSizeLess(t *testing.T, p, p2 PaperSize, answer bool) {
rsp := p.Less(p2)
if rsp != answer {
t.Errorf("PaperSize{%d,%d}.Less(PaperSize{%d,%d}): %v, must be %v",
p.Width, p.Height,
p2.Width, p2.Height,
rsp, answer,
)
}
}
// Compute p.Classify() and check answer
func testPaperSizeClassify(t *testing.T, p PaperSize, answer string) {
rsp := p.Classify()
if rsp != answer {
t.Errorf("PaperSize{%d,%d}.Classify(): %v, must be %v",
p.Width, p.Height,
rsp, answer,
)
}
}
// Test (PaperSize) Less()
func TestPaperSizeLess(t *testing.T) {
var p2 PaperSize
for _, p := range allSizes {
testPaperSizeLess(t, p, p, false)
if p.Less(p) {
t.Fail()
}
p2 = PaperSize{p.Width - 1, p.Height}
testPaperSizeLess(t, p, p2, false)
testPaperSizeLess(t, p2, p, true)
p2 = PaperSize{p.Width, p.Height - 1}
testPaperSizeLess(t, p, p2, false)
testPaperSizeLess(t, p2, p, true)
p2 = PaperSize{p.Width - 1, p.Height + 1}
testPaperSizeLess(t, p, p2, false)
testPaperSizeLess(t, p2, p, false)
p2 = PaperSize{p.Width + 1, p.Height - 1}
testPaperSizeLess(t, p, p2, false)
testPaperSizeLess(t, p2, p, false)
}
}
// Test (PaperSize) Classify()
func TestPaperSizeClassify(t *testing.T) {
testPaperSizeClassify(t, PaperLegal, "legal-A4")
testPaperSizeClassify(t, PaperA4, "legal-A4")
testPaperSizeClassify(t, PaperTabloid, "tabloid-A3")
testPaperSizeClassify(t, PaperA3, "tabloid-A3")
testPaperSizeClassify(t, PaperC, "isoC-A2")
testPaperSizeClassify(t, PaperA2, "isoC-A2")
var sizes []PaperSize
sizes = []PaperSize{
{PaperA4.Width - 1, PaperA4.Height},
{PaperA4.Width, PaperA4.Height - 1},
}
for _, p := range sizes {
testPaperSizeClassify(t, p, "<legal-A4")
}
sizes = []PaperSize{
{PaperC.Width + 1, PaperC.Height},
{PaperC.Width, PaperC.Height + 1},
{PaperA2.Width + 1, PaperA2.Height},
{PaperA2.Width, PaperA2.Height + 1},
}
for _, p := range sizes {
testPaperSizeClassify(t, p, ">isoC-A2")
}
// HP LaserJet MFP M28
testPaperSizeClassify(t, PaperSize{21590, 29692}, "legal-A4")
}
07070100000032000081A400000000000000000000000167D72F5D000004DC000000000000000000000000000000000000001800000000ipp-usb-0.9.30/paths.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
*
* Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
* See LICENSE for license terms and conditions
*
* Common paths
*/
package main
const (
// PathConfDir defines path to configuration directory
PathConfDir = "/etc/ipp-usb"
// PathConfQuirksDir defines path to quirks files in configuration directory
PathConfQuirksDir = "/etc/ipp-usb/quirks"
// PathQuirksDir defines path to quirks files
PathQuirksDir = "/usr/share/ipp-usb/quirks"
// PathProgState defines path to program state directory
PathProgState = "/var/ipp-usb"
// PathLockDir defines path to directory that contains lock files
PathLockDir = PathProgState + "/lock"
// PathLockFile defines path to lock file
PathLockFile = PathLockDir + "/ipp-usb.lock"
// PathControlSocket defines path to the control socket
PathControlSocket = PathProgState + "/ctrl"
// PathProgStateDev defines path to directory where per-device state
// files are saved to
PathProgStateDev = PathProgState + "/dev"
// PathLogDir defines path to log directory
PathLogDir = "/var/log/ipp-usb"
// PathLogFile defines path to the main log file
PathLogFile = PathLogDir + "/main.log"
)
07070100000033000081A400000000000000000000000167D72F5D00000FD5000000000000000000000000000000000000001600000000ipp-usb-0.9.30/pnp.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
*
* Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
* See LICENSE for license terms and conditions
*
* PnP manager
*/
package main
import (
"context"
"os"
"os/signal"
"sync"
"syscall"
"time"
)
// PnPExitReason explains why PnP manager has exited
type PnPExitReason int
// PnPExitReason constants
const (
PnPIdle PnPExitReason = iota // No more connected devices
PnPTerm // Terminating signal received
)
// pnpRetryTime returns time of next retry of failed device initialization
func pnpRetryTime(err error) time.Time {
if err == ErrBlackListed || err == ErrUnusable {
// These errors are unrecoverable.
// Forget about device for the next million hours :-)
return time.Now().Add(time.Hour * 1e6)
}
return time.Now().Add(DevInitRetryInterval)
}
// pnpRetryExpired checks if device initialization retry time expired
func pnpRetryExpired(tm time.Time) bool {
return !time.Now().Before(tm)
}
// PnPStart start PnP manager
//
// If exitWhenIdle is true, PnP manager will exit, when there is no more
// devices to serve
func PnPStart(exitWhenIdle bool) PnPExitReason {
devices := UsbAddrList{}
devByAddr := make(map[UsbAddr]*Device)
retryByAddr := make(map[UsbAddr]time.Time)
sigChan := make(chan os.Signal, 1)
ticker := time.NewTicker(DevInitRetryInterval / 4)
tickerRunning := true
signal.Notify(sigChan,
os.Signal(syscall.SIGINT),
os.Signal(syscall.SIGTERM),
os.Signal(syscall.SIGHUP))
// Start control socket server
err := CtrlsockStart()
if err == nil {
defer CtrlsockStop()
}
// Serve PnP events until terminated
loop:
for {
devDescs, err := UsbGetIppOverUsbDeviceDescs()
if err == nil {
newdevices := UsbAddrList{}
for _, desc := range devDescs {
newdevices.Add(desc.UsbAddr)
}
added, removed := devices.Diff(newdevices)
devices = newdevices
// Handle added devices
for _, addr := range added {
Log.Debug('+', "PNP %s: added", addr)
dev, err := NewDevice(devDescs[addr])
port := 0
if dev != nil {
port = dev.State.HTTPPort
}
StatusSet(addr, devDescs[addr], port, err)
if err == nil {
devByAddr[addr] = dev
} else {
Log.Error('!', "PNP %s: %s", addr, err)
retryByAddr[addr] = pnpRetryTime(err)
}
}
// Handle removed devices
for _, addr := range removed {
Log.Debug('-', "PNP %s: removed", addr)
delete(retryByAddr, addr)
StatusDel(addr)
dev, ok := devByAddr[addr]
if ok {
dev.Close()
delete(devByAddr, addr)
}
}
// Handle devices, waiting for retry
for addr, tm := range retryByAddr {
if !pnpRetryExpired(tm) {
continue
}
Log.Debug('+', "PNP %s: retry", addr)
dev, err := NewDevice(devDescs[addr])
port := 0
if dev != nil {
port = dev.State.HTTPPort
}
StatusSet(addr, devDescs[addr], port, err)
if err == nil {
devByAddr[addr] = dev
delete(retryByAddr, addr)
} else {
Log.Error('!', "PNP %s: %s", addr, err)
retryByAddr[addr] = pnpRetryTime(err)
}
}
}
// Handle exit when idle
if exitWhenIdle && len(devices) == 0 {
Log.Info(' ', "No IPP-over-USB devices present, exiting")
return PnPIdle
}
// Update ticker
switch {
case tickerRunning && len(retryByAddr) == 0:
ticker.Stop()
tickerRunning = false
case !tickerRunning && len(retryByAddr) != 0:
ticker = time.NewTicker(DevInitRetryInterval / 4)
tickerRunning = true
}
// Wait for the next event
select {
case <-UsbHotPlugChan:
case <-ticker.C:
case sig := <-sigChan:
Log.Info(' ', "%s signal received, exiting", sig)
break loop
}
}
// Close remaining devices
ctx, cancel := context.WithTimeout(context.Background(),
DevShutdownTimeout)
defer cancel()
var done sync.WaitGroup
for _, dev := range devByAddr {
done.Add(1)
go func(dev *Device) {
dev.Shutdown(ctx)
dev.Close()
done.Done()
}(dev)
}
done.Wait()
return PnPTerm
}
07070100000034000081A400000000000000000000000167D72F5D0000379F000000000000000000000000000000000000001900000000ipp-usb-0.9.30/quirks.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
*
* Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
* See LICENSE for license terms and conditions
*
* Device-specific quirks
*/
package main
import (
"fmt"
"io"
"io/ioutil"
"math"
"net/http"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"time"
)
// Quirk represents a single quirk
type Quirk struct {
Origin string // file:line of definition
Match string // Match pattern
Name string // Quirk name
RawValue string // Quirk raw (not parsed) value
Parsed interface{} // Parsed Value
LoadOrder int // Incremented in order of loading
}
// Quirk names. Use these constants instead of literal strings,
// so compiler will catch a mistake:
const (
QuirkNmBlacklist = "blacklist"
QuirkNmBuggyIppResponses = "buggy-ipp-responses"
QuirkNmDisableFax = "disable-fax"
QuirkNmIgnoreIppStatus = "ignore-ipp-status"
QuirkNmInitDelay = "init-delay"
QuirkNmInitReset = "init-reset"
QuirkNmInitRetryPartial = "init-retry-partial"
QuirkNmInitTimeout = "init-timeout"
QuirkNmRequestDelay = "request-delay"
QuirkNmUsbMaxInterfaces = "usb-max-interfaces"
QuirkNmUsbSendDelayThreshold = "usb-send-delay-threshold"
QuirkNmUsbSendDelay = "usb-send-delay"
QuirkNmZlpRecvHack = "zlp-recv-hack"
QuirkNmZlpSend = "zlp-send"
)
// quirkParse maps quirk names into appropriate parsing methods,
// which defines value syntax and resulting type.
var quirkParse = map[string]func(*Quirk) error{
QuirkNmBlacklist: (*Quirk).parseBool,
QuirkNmBuggyIppResponses: (*Quirk).parseQuirkBuggyIppRsp,
QuirkNmDisableFax: (*Quirk).parseBool,
QuirkNmIgnoreIppStatus: (*Quirk).parseBool,
QuirkNmInitDelay: (*Quirk).parseDuration,
QuirkNmInitReset: (*Quirk).parseQuirkResetMethod,
QuirkNmInitRetryPartial: (*Quirk).parseBool,
QuirkNmInitTimeout: (*Quirk).parseDuration,
QuirkNmRequestDelay: (*Quirk).parseDuration,
QuirkNmUsbMaxInterfaces: (*Quirk).parseUint,
QuirkNmUsbSendDelayThreshold: (*Quirk).parseUint,
QuirkNmUsbSendDelay: (*Quirk).parseDuration,
QuirkNmZlpRecvHack: (*Quirk).parseBool,
QuirkNmZlpSend: (*Quirk).parseBool,
}
// quirkDefaultStrings contains default values for quirks, in
// a string form.
var quirkDefaultStrings = map[string]string{
QuirkNmBlacklist: "false",
QuirkNmBuggyIppResponses: "reject",
QuirkNmDisableFax: "false",
QuirkNmIgnoreIppStatus: "false",
QuirkNmInitDelay: "0",
QuirkNmInitRetryPartial: "false",
QuirkNmInitReset: "none",
QuirkNmInitTimeout: DevInitTimeout.String(),
QuirkNmRequestDelay: "0",
QuirkNmUsbMaxInterfaces: "0",
QuirkNmUsbSendDelayThreshold: "0",
QuirkNmUsbSendDelay: "0",
QuirkNmZlpRecvHack: "false",
QuirkNmZlpSend: "false",
}
// quirkDefault contains default values for quirks, precompiled.
var quirkDefault = make(map[string]*Quirk)
// init populates quirkDefault using quirk values from quirkDefaultStrings.
func init() {
for name, value := range quirkDefaultStrings {
q := &Quirk{
Origin: "default",
Match: "*",
Name: name,
RawValue: value,
LoadOrder: math.MaxInt32,
}
parse := quirkParse[name]
err := parse(q)
if err != nil {
panic(err)
}
quirkDefault[name] = q
}
}
// parseBool parses and saves [Quirk.RawValue] as bool.
func (q *Quirk) parseBool() error {
switch q.RawValue {
case "true":
q.Parsed = true
case "false":
q.Parsed = false
default:
return fmt.Errorf("%q: must be true or false", q.RawValue)
}
return nil
}
// parseUind parses [Quirk.RawValue] as bool.
func (q *Quirk) parseUint() error {
v, err := strconv.ParseUint(q.RawValue, 10, 32)
if err != nil {
return fmt.Errorf("%q: invalid unsigned integer", q.RawValue)
}
q.Parsed = uint(v)
return nil
}
// parseDuration parses [Quirk.RawValue] as time.Duration.
func (q *Quirk) parseDuration() error {
// Try to parse as uint. If OK, interpret it
// as a millisecond time.
ms, err := strconv.ParseUint(q.RawValue, 10, 32)
if err == nil {
q.Parsed = time.Millisecond * time.Duration(ms)
return nil
}
// Try to use time.ParseDuration.
//
if strings.HasPrefix(q.RawValue, "+") ||
strings.HasPrefix(q.RawValue, "-") {
// Note, time.ParseDuration allows signed duration,
// but we don't.
return fmt.Errorf("%q: invalid duration", q.RawValue)
}
v, err := time.ParseDuration(q.RawValue)
if err == nil && v >= 0 {
q.Parsed = v
return nil
}
return fmt.Errorf("%q: invalid duration", q.RawValue)
}
// parseQuirkBuggyIppRsp parses [Quirk.RawValue] as QuirkBuggyIppRsp.
func (q *Quirk) parseQuirkBuggyIppRsp() error {
switch q.RawValue {
case "allow":
q.Parsed = QuirkBuggyIppRspAllow
case "reject":
q.Parsed = QuirkBuggyIppRspReject
case "sanitize":
q.Parsed = QuirkBuggyIppRspSanitize
default:
s := q.RawValue
return fmt.Errorf("%q: must be allow, reject or sanitize", s)
}
return nil
}
// parseQuirkResetMethod parses [Quirk.RawValue] as QuirkResetMethod.
func (q *Quirk) parseQuirkResetMethod() error {
switch q.RawValue {
case "none":
q.Parsed = QuirkResetNone
case "soft":
q.Parsed = QuirkResetSoft
case "hard":
q.Parsed = QuirkResetHard
default:
return fmt.Errorf("%q: must be none, soft or hard", q.RawValue)
}
return nil
}
// prioritize returns more prioritized Quirk, choosing between q and q2.
func (q *Quirk) prioritize(q2 *Quirk, model string) *Quirk {
matchlen := GlobMatch(model, q.Match)
matchlen2 := GlobMatch(model, q2.Match)
switch {
// Choose by match length (more specific match wins)
case matchlen > matchlen2:
return q
case matchlen < matchlen2:
return q2
// Choose by load order (first loaded wins)
case q.LoadOrder > q2.LoadOrder:
return q
}
return q
}
// QuirkResetMethod represents how to reset a device
// during initialization
type QuirkResetMethod int
// QuirkResetUnset - reset method not specified
// QuirkResetNone - don't reset device at all
// QuirkResetSoft - use class-specific soft reset
// QuirkResetHard - use USB hard reset
const (
QuirkResetNone QuirkResetMethod = iota
QuirkResetSoft
QuirkResetHard
)
// String returns textual representation of QuirkResetMethod
func (m QuirkResetMethod) String() string {
switch m {
case QuirkResetNone:
return "none"
case QuirkResetSoft:
return "soft"
case QuirkResetHard:
return "hard"
}
return fmt.Sprintf("unknown (%d)", int(m))
}
// QuirkBuggyIppRsp defines, how to handle buggy IPP responses
type QuirkBuggyIppRsp int
// QuirkBuggyIppRspReject - ipp-usb will reject bad IPP responses
// QuirkBuggyIppRspAllow - ipp-usb will allow bad IPP responses
// QuirkBuggyIppRspSanitize - bad ipp responses will be sanitized (fixed)
const (
QuirkBuggyIppRspReject QuirkBuggyIppRsp = iota
QuirkBuggyIppRspAllow
QuirkBuggyIppRspSanitize
)
// String returns textual representation of QuirkBuggyIppRsp
func (m QuirkBuggyIppRsp) String() string {
switch m {
case QuirkBuggyIppRspReject:
return "reject"
case QuirkBuggyIppRspAllow:
return "allow"
case QuirkBuggyIppRspSanitize:
return "sanitize"
}
return fmt.Sprintf("unknown (%d)", int(m))
}
// Quirks is the collection of Quirk-s.
type Quirks struct {
byName map[string]*Quirk // Quirks by name
HTTPHeaders map[string]string // HTTP header override
}
// Get returns quirk by name.
func (quirks Quirks) Get(name string) *Quirk {
q := quirks.byName[name]
if q == nil {
q = quirkDefault[name]
}
return q
}
// All returns all quirks in the collection. This method is
// intended mostly for diagnostic purposes (logging, dumping,
// testing and so on).
func (quirks Quirks) All() []*Quirk {
qq := make([]*Quirk, 0, len(quirks.byName))
for _, q := range quirks.byName {
qq = append(qq, q)
}
sort.Slice(qq, func(i, j int) bool {
return qq[i].Name < qq[j].Name
})
return qq
}
// GetBlacklist returns effective "blacklist" parameter,
// taking the whole set into consideration.
func (quirks Quirks) GetBlacklist() bool {
return quirks.Get(QuirkNmBlacklist).Parsed.(bool)
}
// GetBuggyIppRsp returns effective "buggy-ipp-responses" parameter
// taking the whole set into consideration.
func (quirks Quirks) GetBuggyIppRsp() QuirkBuggyIppRsp {
return quirks.Get(QuirkNmBuggyIppResponses).Parsed.(QuirkBuggyIppRsp)
}
// GetDisableFax returns effective "disable-fax" parameter,
// taking the whole set into consideration.
func (quirks Quirks) GetDisableFax() bool {
return quirks.Get(QuirkNmDisableFax).Parsed.(bool)
}
// GetIgnoreIppStatus returns effective "ignore-ipp-status" parameter,
// taking the whole set into consideration.
func (quirks Quirks) GetIgnoreIppStatus() bool {
return quirks.Get(QuirkNmIgnoreIppStatus).Parsed.(bool)
}
// GetInitDelay returns effective "init-delay" parameter
// taking the whole set into consideration.
func (quirks Quirks) GetInitDelay() time.Duration {
return quirks.Get(QuirkNmInitDelay).Parsed.(time.Duration)
}
// GetInitRetryPartial returns effective "init-retry-partial" parameter
// taking the whole set into consideration.
func (quirks Quirks) GetInitRetryPartial() bool {
return quirks.Get(QuirkNmInitRetryPartial).Parsed.(bool)
}
// GetInitReset returns effective "init-reset" parameter
// taking the whole set into consideration.
func (quirks Quirks) GetInitReset() QuirkResetMethod {
return quirks.Get(QuirkNmInitReset).Parsed.(QuirkResetMethod)
}
// GetInitTimeout returns effective "init-timeout" parameter
// taking the whole set into consideration.
func (quirks Quirks) GetInitTimeout() time.Duration {
return quirks.Get(QuirkNmInitTimeout).Parsed.(time.Duration)
}
// GetRequestDelay returns effective "request-delay" parameter
// taking the whole set into consideration.
func (quirks Quirks) GetRequestDelay() time.Duration {
return quirks.Get(QuirkNmRequestDelay).Parsed.(time.Duration)
}
// GetUsbMaxInterfaces returns effective "usb-max-interfaces" parameter,
// taking the whole set into consideration.
func (quirks Quirks) GetUsbMaxInterfaces() uint {
return quirks.Get(QuirkNmUsbMaxInterfaces).Parsed.(uint)
}
// GetUsbSendDelayThreshold returns effective "usb-send-delay-threshold"
// parameter taking the whole set into consideration.
func (quirks Quirks) GetUsbSendDelayThreshold() uint {
return quirks.Get(QuirkNmUsbSendDelay).Parsed.(uint)
}
// GetUsbSendDelay returns effective "usb-send-delay" parameter
// taking the whole set into consideration.
func (quirks Quirks) GetUsbSendDelay() time.Duration {
return quirks.Get(QuirkNmUsbSendDelay).Parsed.(time.Duration)
}
// GetZlpRecvHack returns effective "zlp-send" parameter,
// taking the whole set into consideration.
func (quirks Quirks) GetZlpRecvHack() bool {
return quirks.Get(QuirkNmZlpRecvHack).Parsed.(bool)
}
// GetZlpSend returns effective "zlp-send" parameter,
// taking the whole set into consideration.
func (quirks Quirks) GetZlpSend() bool {
return quirks.Get(QuirkNmZlpSend).Parsed.(bool)
}
// QuirksSet represents collection of quirks
type QuirksSet []*Quirks
// LoadQuirksSet creates new QuirksSet and loads its content from a directory
func LoadQuirksSet(paths ...string) (QuirksSet, error) {
qset := QuirksSet{}
for _, path := range paths {
err := qset.readDir(path)
if err != nil {
return nil, err
}
}
return qset, nil
}
// readDir loads all Quirks from a directory
func (qset *QuirksSet) readDir(path string) error {
files, err := ioutil.ReadDir(path)
if err != nil {
if os.IsNotExist(err) {
err = nil
}
return err
}
for _, file := range files {
if file.Mode().IsRegular() &&
strings.HasSuffix(file.Name(), ".conf") {
err = qset.readFile(filepath.Join(path, file.Name()))
if err != nil {
return err
}
}
}
return nil
}
// readFile reads all Quirks from a file
func (qset *QuirksSet) readFile(file string) error {
// Open quirks file
ini, err := OpenIniFileWithRecType(file)
if err != nil {
return err
}
defer ini.Close()
// Load all quirks
var quirks *Quirks
var loadOrder int
for err == nil {
var rec *IniRecord
rec, err = ini.Next()
if err != nil {
break
}
origin := fmt.Sprintf("%s:%d", rec.File, rec.Line)
// Get Quirks structure
if rec.Type == IniRecordSection {
quirks = &Quirks{
byName: make(map[string]*Quirk),
HTTPHeaders: make(map[string]string),
}
qset.Add(quirks)
continue
} else if quirks == nil {
err = fmt.Errorf("%s: %q = %q out of any section",
origin, rec.Key, rec.Value)
break
}
if found := quirks.byName[rec.Key]; found != nil {
err = fmt.Errorf("%s: %q already defined at %s",
origin, rec.Key, found.Origin)
return err
}
q := &Quirk{
Origin: origin,
Match: rec.Section,
Name: rec.Key,
RawValue: rec.Value,
LoadOrder: loadOrder,
}
loadOrder++
if strings.HasPrefix(rec.Key, "http-") {
// Canonicalize HTTP header name
q.Name = strings.ToLower(q.Name)
q.Parsed = q.RawValue
hdr := http.CanonicalHeaderKey(rec.Key[5:])
quirks.HTTPHeaders[hdr] = q.RawValue
} else {
parse := quirkParse[rec.Key]
if parse == nil {
// Ignore unknown keys, it may be due to
// downgrade of the ipp-usb
continue
}
err := parse(q)
if err != nil {
err = fmt.Errorf("%s: %s", origin, err)
return err
}
}
quirks.byName[rec.Key] = q
}
if err == io.EOF {
err = nil
}
return err
}
// Add appends Quirks to QuirksSet
func (qset *QuirksSet) Add(q *Quirks) {
*qset = append(*qset, q)
}
// MatchByModelName returns collection of quirks, applicable for
// specific device, matched by model name.
func (qset QuirksSet) MatchByModelName(model string) Quirks {
ret := Quirks{
byName: make(map[string]*Quirk),
}
for _, quirks := range qset {
for name, q := range quirks.byName {
if GlobMatch(model, q.Match) >= 0 {
q2 := ret.byName[name]
if q2 != nil {
q = q.prioritize(q2, model)
}
ret.byName[name] = q
}
}
}
return ret
}
07070100000035000081A400000000000000000000000167D72F5D00002286000000000000000000000000000000000000001E00000000ipp-usb-0.9.30/quirks_test.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
*
* Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
* See LICENSE for license terms and conditions
*
* Tests for device-specific quirks
*/
package main
import (
"reflect"
"testing"
"time"
)
// TestQuirksLookup tests lookup of various parameters
func TestQuirksLookup(t *testing.T) {
const path = "testdata/quirks"
// Load quirks
qset, err := LoadQuirksSet(path)
if err != nil {
t.Fatalf("LoadQuirksSet(%q): %s", path, err)
}
// Test loaded values against expected
type testData struct {
model string // Model name
param string // Parameter (quirk) name
get func(Quirks) interface{} // Lookup function
match string // Expected match
value interface{} // Expected value
origin string // Expected origin
}
tests := []testData{
// Default values for unknown device
{
model: "Unknown Device",
param: QuirkNmBlacklist,
get: func(quirks Quirks) interface{} {
return quirks.GetBlacklist()
},
match: "*",
value: false,
origin: "testdata/quirks/default.conf:4",
},
{
model: "Unknown Device",
param: QuirkNmBuggyIppResponses,
get: func(quirks Quirks) interface{} {
return quirks.GetBuggyIppRsp()
},
match: "*",
value: QuirkBuggyIppRspReject,
origin: "default",
},
{
model: "Unknown Device",
param: QuirkNmDisableFax,
get: func(quirks Quirks) interface{} {
return quirks.GetDisableFax()
},
match: "*",
value: false,
origin: "default",
},
{
model: "Unknown Device",
param: QuirkNmIgnoreIppStatus,
get: func(quirks Quirks) interface{} {
return quirks.GetIgnoreIppStatus()
},
match: "*",
value: false,
origin: "default",
},
{
model: "Unknown Device",
param: QuirkNmInitDelay,
get: func(quirks Quirks) interface{} {
return quirks.GetInitDelay()
},
match: "*",
value: time.Duration(0),
origin: "default",
},
{
model: "Unknown Device",
param: QuirkNmInitRetryPartial,
get: func(quirks Quirks) interface{} {
return quirks.GetInitRetryPartial()
},
match: "*",
value: false,
origin: "default",
},
{
model: "Unknown Device",
param: QuirkNmInitReset,
get: func(quirks Quirks) interface{} {
return quirks.GetInitReset()
},
match: "*",
value: QuirkResetNone,
origin: "default",
},
{
model: "Unknown Device",
param: QuirkNmInitTimeout,
get: func(quirks Quirks) interface{} {
return quirks.GetInitTimeout()
},
match: "*",
value: DevInitTimeout,
origin: "default",
},
{
model: "Unknown Device",
param: QuirkNmRequestDelay,
get: func(quirks Quirks) interface{} {
return quirks.GetRequestDelay()
},
match: "*",
value: time.Duration(0),
origin: "default",
},
{
model: "Unknown Device",
param: QuirkNmUsbMaxInterfaces,
get: func(quirks Quirks) interface{} {
return quirks.GetUsbMaxInterfaces()
},
match: "*",
value: uint(0),
origin: "default",
},
{
model: "Unknown Device",
param: QuirkNmZlpRecvHack,
get: func(quirks Quirks) interface{} {
return quirks.GetZlpRecvHack()
},
match: "*",
value: false,
origin: "default",
},
{
model: "Unknown Device",
param: QuirkNmZlpSend,
get: func(quirks Quirks) interface{} {
return quirks.GetZlpSend()
},
match: "*",
value: false,
origin: "default",
},
// Quirks for some known devices
{
model: "HP ScanJet Pro 4500 fn1",
param: QuirkNmUsbMaxInterfaces,
get: func(quirks Quirks) interface{} {
return quirks.GetUsbMaxInterfaces()
},
match: "HP ScanJet Pro 4500 fn1",
value: uint(1),
origin: "testdata/quirks/HP.conf:16",
},
{
model: "HP ScanJet Pro 4500 fn1",
param: QuirkNmRequestDelay,
get: func(quirks Quirks) interface{} {
return quirks.GetRequestDelay()
},
match: "*",
value: time.Duration(0),
origin: "default",
},
{
// Here we test that more specific 'http-connection'
// for the particular model overrides less specific
// default value.
model: "HP OfficeJet Pro 8730",
param: "http-connection",
get: func(quirks Quirks) interface{} {
q := quirks.Get("http-connection")
return q.Parsed
},
match: "HP OfficeJet Pro 8730",
value: "close",
origin: "testdata/quirks/HP.conf:7",
},
}
for _, test := range tests {
quirks := qset.MatchByModelName(test.model)
q := quirks.Get(test.param)
v := test.get(quirks)
if !reflect.DeepEqual(v, test.value) {
t.Errorf("model: %q, param: %q: value mismatch\n"+
"expected: %s(%v)\n"+
"present: %s(%v)",
test.model, test.param,
reflect.TypeOf(test.value), test.value,
reflect.TypeOf(v), v)
}
if q.Match != test.match {
t.Errorf("model: %q, param: %q: match mismatch\n"+
"expected: %q\n"+
"present: %q",
test.model, test.param, test.match, q.Match)
}
if q.Origin != test.origin {
t.Errorf("model: %q, param: %q: origin mismatch\n"+
"expected: %q\n"+
"present: %q",
test.model, test.param, test.origin, q.Origin)
}
}
}
// TestQuirksParsers tests parsers for quirks
func TestQuirksParsers(t *testing.T) {
type testData struct {
parser func(*Quirk) error // Parser to test
input string // Input string
value interface{} // Expected output value
err string // Or expected error
}
tests := []testData{
// parseBool
{
parser: (*Quirk).parseBool,
input: "true",
value: true,
},
{
parser: (*Quirk).parseBool,
input: "false",
value: false,
},
{
parser: (*Quirk).parseBool,
input: "invalid",
err: `"invalid": must be true or false`,
},
// parseQuirkBuggyIppRsp
{
parser: (*Quirk).parseQuirkBuggyIppRsp,
input: "allow",
value: QuirkBuggyIppRspAllow,
},
{
parser: (*Quirk).parseQuirkBuggyIppRsp,
input: "reject",
value: QuirkBuggyIppRspReject,
},
{
parser: (*Quirk).parseQuirkBuggyIppRsp,
input: "sanitize",
value: QuirkBuggyIppRspSanitize,
},
{
parser: (*Quirk).parseQuirkBuggyIppRsp,
input: "invalid",
err: `"invalid": must be allow, reject or sanitize`,
},
// parseDuration
{
parser: (*Quirk).parseDuration,
input: "0",
value: time.Duration(0),
},
{
parser: (*Quirk).parseDuration,
input: "0s",
value: time.Duration(0),
},
{
parser: (*Quirk).parseDuration,
input: "12345",
value: 12345 * time.Millisecond,
},
{
parser: (*Quirk).parseDuration,
input: "1h2m3s",
value: time.Hour +
2*time.Minute +
3*time.Second,
},
{
parser: (*Quirk).parseDuration,
input: "0.5s",
value: time.Second / 2,
},
{
parser: (*Quirk).parseDuration,
input: "+0s",
err: `"+0s": invalid duration`,
},
{
parser: (*Quirk).parseDuration,
input: "-0s",
err: `"-0s": invalid duration`,
},
{
parser: (*Quirk).parseDuration,
input: "hello",
err: `"hello": invalid duration`,
},
// parseQuirkResetMethod
{
parser: (*Quirk).parseQuirkResetMethod,
input: "none",
value: QuirkResetNone,
},
{
parser: (*Quirk).parseQuirkResetMethod,
input: "soft",
value: QuirkResetSoft,
},
{
parser: (*Quirk).parseQuirkResetMethod,
input: "hard",
value: QuirkResetHard,
},
{
parser: (*Quirk).parseQuirkResetMethod,
input: "invalid",
err: `"invalid": must be none, soft or hard`,
},
// parseUint
{
parser: (*Quirk).parseUint,
input: "0",
value: uint(0),
},
{
parser: (*Quirk).parseUint,
input: "12345",
value: uint(12345),
},
{
parser: (*Quirk).parseUint,
input: "hello",
err: `"hello": invalid unsigned integer`,
},
}
for _, test := range tests {
q := Quirk{
RawValue: test.input,
}
err := test.parser(&q)
errstr := ""
if err != nil {
errstr = err.Error()
}
if errstr != test.err {
t.Errorf("error mismatch:\n"+
"expected: %s\n"+
"present: %s",
test.err, errstr)
continue
}
if q.Parsed != test.value {
t.Errorf("value mismatch:\n"+
"expected: %s(%v)\n"+
"present: %s(%v)",
reflect.TypeOf(test.value), test.value,
reflect.TypeOf(q.Parsed), q.Parsed)
}
}
}
// TestQuirksSetLoad tests LoadQuirksSet
func TestQuirksSetLoad(t *testing.T) {
const path = "testdata/quirks"
const badPath = path + "-not-exist"
// Try non-existent directory
_, err := LoadQuirksSet(badPath)
if err != nil {
t.Fatalf("LoadQuirksSet(%q): %s", badPath, err)
}
// Try test data
_, err = LoadQuirksSet(path)
if err != nil {
t.Fatalf("LoadQuirksSet(%q): %s", path, err)
}
}
07070100000036000041ED00000000000000000000000267D72F5D00000000000000000000000000000000000000000000001400000000ipp-usb-0.9.30/rock07070100000037000081A400000000000000000000000167D72F5D00000E1E000000000000000000000000000000000000002300000000ipp-usb-0.9.30/rock/rockcraft.yamlname: ipp-usb
base: ubuntu@24.04
version: "latest"
summary: IPP-over-USB - Driverless IPP printing on USB-connected printers
description: |
ipp-usb is a daemon that enables driverless IPP printing on USB-connected
printers. It emulates an IPP network printer, providing full access to the
physical printer: Printing, scanning, fax out, and the admin web interface.
license: Apache-2.0
adopt-info: ipp-usb
platforms:
amd64:
arm64:
armhf:
services:
dbus:
command: /scripts/run-dbus.sh
override: replace
on-failure: restart
startup: enabled
ipp-usb:
command: /scripts/run-ipp-usb.sh
startup: enabled
override: replace
on-failure: restart
after: [dbus]
parts:
goipp:
plugin: go
source: https://github.com/OpenPrinting/goipp.git
source-type: git
source-tag: v1.1.0
source-depth: 1
# ext:updatesnap
# version-format:
# lower-than: '2'
# no-9x-revisions: true
build-packages:
- golang-go
override-prime: ""
ipp-usb:
plugin: go
source: https://github.com/OpenPrinting/ipp-usb.git
source-type: git
source-tag: 0.9.29
source-depth: 1
# ext:updatesnap
# version-format:
# lower-than: '1'
# no-9x-revisions: true
override-build: |
set -eux
craftctl default
mkdir -p ${CRAFT_PART_INSTALL}/usr/sbin
mv ${CRAFT_PART_INSTALL}/bin/ipp-usb ${CRAFT_PART_INSTALL}/usr/sbin/
mkdir -p ${CRAFT_PART_INSTALL}/etc
cp ipp-usb.conf ${CRAFT_PART_INSTALL}/etc
mkdir -p ${CRAFT_PART_INSTALL}/usr/share/ipp-usb/quirks
cp ipp-usb-quirks/* ${CRAFT_PART_INSTALL}/usr/share/ipp-usb/quirks/
build-packages:
- golang-go
- libavahi-client-dev
- libavahi-common-dev
- libavahi-compat-libdnssd-dev
- libdbus-1-dev
- ronn
stage-packages:
- libavahi-client3
- libavahi-common3
prime:
- etc
- -etc/init.d
- usr/sbin
- -usr/sbin/systemd-hwdb
- usr/lib
- usr/share/ipp-usb
after: [goipp, libusb]
libusb:
plugin: autotools
source: https://github.com/libusb/libusb.git
source-type: git
source-tag: 'v1.0.27'
source-depth: 1
# ext:updatesnap
# version-format:
# lower-than: '2'
# no-9x-revisions: true
autotools-configure-parameters:
- --disable-udev
- --prefix=/usr
build-packages:
- build-essential
- autoconf
- automake
- libtool
- pkg-config
- git
stage:
- usr/lib/*
- usr/include/*
prime:
- usr/lib/*
- usr/include/*
override-build: |
set -e
craftctl default
make -j$CRAFT_PARALLEL_BUILD_COUNT
make install DESTDIR=$CRAFT_PART_INSTALL
avahi-daemon:
plugin: nil
build-packages:
- avahi-daemon
overlay-packages:
- avahi-utils
- libnss-mdns
- mdns-scan
- dbus
- libavahi-client3
- libavahi-common3
override-build: |
craftctl default
mkdir -p "${CRAFT_PART_INSTALL}/usr/share/dbus-1/"
cp -r /usr/share/dbus-1/* "${CRAFT_PART_INSTALL}/usr/share/dbus-1/"
scripts:
plugin: dump
source: scripts/
organize:
run-ipp-usb.sh: scripts/run-ipp-usb.sh
run-dbus.sh: scripts/run-dbus.sh
override-prime: |
set -eux
craftctl default
if [ -f "$CRAFT_PRIME/scripts/run-ipp-usb.sh" ]; then
chmod +x "$CRAFT_PRIME/scripts/run-ipp-usb.sh"
fi
if [ -f "$CRAFT_PRIME/scripts/run-dbus.sh" ]; then
chmod +x "$CRAFT_PRIME/scripts/run-dbus.sh"
fi
after: [ipp-usb, avahi-daemon]
07070100000038000041ED00000000000000000000000267D72F5D00000000000000000000000000000000000000000000001C00000000ipp-usb-0.9.30/rock/scripts07070100000039000081A400000000000000000000000167D72F5D00000459000000000000000000000000000000000000002800000000ipp-usb-0.9.30/rock/scripts/run-dbus.sh#!/bin/sh
set -eux
echo "Creating system users..."
# Check if users exist before attempting to create them
if ! id -u systemd-resolve >/dev/null 2>&1; then
useradd --system --no-create-home --shell /usr/sbin/nologin systemd-resolve
fi
if ! id -u systemd-network >/dev/null 2>&1; then
useradd --system --no-create-home --shell /usr/sbin/nologin systemd-network
fi
echo "Ensuring necessary directories exist..."
# Ensure /run/dbus exists with correct permissions
if [ ! -d /run/dbus ]; then
mkdir -p /run/dbus
chmod 755 /run/dbus
chown root:root /run/dbus
fi
echo "Starting dbus service..."
# Start dbus and verify it's running
service dbus start
if ! pgrep -x "dbus-daemon" >/dev/null; then
echo "Failed to start dbus-daemon!" >&2
exit 1
fi
echo "Starting avahi-daemon..."
# Start avahi-daemon and ensure it's running
avahi-daemon --daemonize --no-drop-root
if ! pgrep -x "avahi-daemon" >/dev/null; then
echo "Failed to start avahi-daemon!" >&2
exit 1
fi
echo "Services started successfully."
# Keep the container alive using a foreground process
exec sleep infinity
0707010000003A000081A400000000000000000000000167D72F5D0000023A000000000000000000000000000000000000002B00000000ipp-usb-0.9.30/rock/scripts/run-ipp-usb.sh#!/bin/sh
#set -e -x
# Create needed directories (ignore errors)
mkdir -p /etc/ipp-usb || :
mkdir -p /var/log/ipp-usb || :
mkdir -p /var/lock || :
mkdir -p /var/dev || :
mkdir -p /usr/share/ipp-usb/quirks || :
# Put config files in place (do not overwrite existing user config)
yes no | cp -i /usr/share/ipp-usb/quirks/* /etc/ipp-usb/quirks >/dev/null 2>&1 || :
if [ ! -f /etc/ipp-usb/ipp-usb.conf ]; then
cp /usr/share/ipp-usb/ipp-usb.conf /etc/ipp-usb/ >/dev/null 2>&1 || :
fi
# Run ipp-usb with the provided command-line arguments
exec /usr/sbin/ipp-usb "$@"
0707010000003B000041ED00000000000000000000000267D72F5D00000000000000000000000000000000000000000000001400000000ipp-usb-0.9.30/snap0707010000003C000041ED00000000000000000000000267D72F5D00000000000000000000000000000000000000000000001A00000000ipp-usb-0.9.30/snap/local0707010000003D000081ED00000000000000000000000167D72F5D000002E7000000000000000000000000000000000000002600000000ipp-usb-0.9.30/snap/local/run-ipp-usb#!/bin/sh
#set -e -x
# Create needed directories
# Ignore errors
mkdir -p $SNAP_COMMON/etc || :
mkdir -p $SNAP_COMMON/var/log || :
mkdir -p $SNAP_COMMON/var/lock || :
mkdir -p $SNAP_COMMON/var/dev || :
mkdir -p $SNAP_COMMON/quirks || :
# Put config files in place
#
# Do not overwrite files and ignore errors, to not reset user configuration
# when running as root and to not have ugly error messages when running as
# non-root.
yes no | cp -i $SNAP/usr/share/ipp-usb/quirks/* $SNAP_COMMON/quirks >/dev/null 2>&1 || :
if [ ! -f $SNAP_COMMON/etc/ipp-usb.conf ]; then
cp $SNAP/etc/ipp-usb.conf $SNAP_COMMON/etc/ >/dev/null 2>&1 || :
fi
# Run ipp-usb with the command line arguments with which we were called
exec $SNAP/sbin/ipp-usb "$@"
0707010000003E000081ED00000000000000000000000167D72F5D000006AD000000000000000000000000000000000000002D00000000ipp-usb-0.9.30/snap/local/run-ipp-usb-server#!/bin/sh
#set -e -x
# Create needed directories
mkdir -p $SNAP_COMMON/etc
mkdir -p $SNAP_COMMON/var/log
mkdir -p $SNAP_COMMON/var/lock
mkdir -p $SNAP_COMMON/var/dev
mkdir -p $SNAP_COMMON/quirks
# Put config files in place
cp $SNAP/usr/share/ipp-usb/quirks/* $SNAP_COMMON/quirks
if [ ! -f $SNAP_COMMON/etc/ipp-usb.conf ]; then
cp $SNAP/etc/ipp-usb.conf $SNAP_COMMON/etc/
fi
# Monitor appearing/disappearing of USB devices
udevadm monitor -k -s usb | while read START OP DEV REST; do
START_IPP_USB=0
if test "$START" = "KERNEL"; then
# First lines of "udevadm monitor" output, check for already plugged
# devices. Consider only IPP-over-USB devices (interface 7/1/4)
if [ `udevadm trigger -v -n --subsystem-match=usb --property-match=ID_USB_INTERFACES='*:070104:*' | wc -l` -gt 0 ]; then
# IPP-over-USB device already connected
START_IPP_USB=1
fi
elif test "$OP" = "add"; then
# New device got added
if [ -z $DEV ]; then
# Missing device path
continue
else
# Does the device support IPP-over-USB (interface 7/1/4)?
# Retry 5 times as sometimes the ID_USB_INTERFACES property is not
# immediately set
for i in 1 2 3 4 5; do
# Give some time for ID_USB_INTERFACES property to appear
sleep 0.02
# Check ID_USB_INTERFACE for 7/1/4 interface
if udevadm info -q property -p $DEV | grep -q ID_USB_INTERFACES=.*:070104:.*; then
# IPP-over-USB device got connected now
START_IPP_USB=1
break
fi
done
fi
fi
if [ $START_IPP_USB = 1 ]; then
# Start ipp-usb
$SNAP/sbin/ipp-usb udev
fi
done
0707010000003F000081A400000000000000000000000167D72F5D00000B6C000000000000000000000000000000000000002300000000ipp-usb-0.9.30/snap/snapcraft.yamlname: ipp-usb
base: core22
version: git
summary: IPP-over-USB - Driverless IPP printing on USB-connected printers
description: |
ipp-usb is a daemon to allow driverless IPP printing on USB-connected
printers.It emulates an IPP network printer on the local machine, giving
full access to the physical printer: Printing, scanning, fax out, and
the admin web interface.
grade: stable
confinement: strict
adopt-info: ipp-usb
# Only build on the architectures supported
architectures:
- build-on: amd64
- build-on: arm64
- build-on: armhf
apps:
ipp-usb-server:
command: scripts/run-ipp-usb-server
daemon: simple
plugs: [avahi-control, network, network-bind, raw-usb, hardware-observe]
ipp-usb:
command: scripts/run-ipp-usb
plugs: [raw-usb, hardware-observe]
parts:
goipp:
plugin: go
source: https://github.com/OpenPrinting/goipp.git
source-type: git
source-tag: v1.1.0
source-depth: 1
# ext:updatesnap
# version-format:
# lower-than: '2'
# no-9x-revisions: true
build-packages:
- golang-go
override-prime: ""
ipp-usb:
plugin: go
source: .
source-type: git
source-tag: 0.9.29
source-depth: 1
# ext:updatesnap
# version-format:
# lower-than: '1'
# no-9x-revisions: true
override-build: |
set -eux
# Correct hard-coded paths in paths.go
# Not only the config file ipp-usb.conf will be put into a user-editable
# space but also the quirks file, so that the user can add and debug
# quirks
perl -p -i -e 's:/etc/:/var/snap/ipp-usb/common/etc/:' paths.go
perl -p -i -e 's:/var/ipp-usb:/var/snap/ipp-usb/common/var:' paths.go
perl -p -i -e 's:/usr/share/ipp-usb/quirks:/var/snap/hplip-printer-app/common/quirks:' paths.go
perl -p -i -e 's:/var/log/ipp-usb:/var/snap/ipp-usb/common/var/log:' paths.go
# Build the executable
craftctl default
# Place the executable in /sbin, it's a system daemon
mv ../install/bin ../install/sbin
# Install the config file and the quirk files
mkdir -p ../install/etc
cp ipp-usb.conf ../install/etc
mkdir -p ../install/usr/share/ipp-usb/quirks
cp ipp-usb-quirks/* ../install/usr/share/ipp-usb/quirks
build-packages:
- golang-go
- libavahi-client-dev
- libavahi-common-dev
- libusb-1.0-0-dev
- ronn
- perl-base
stage-packages:
- libavahi-client3
- libavahi-common3
- libusb-1.0-0
- udev
prime:
- etc
- -etc/init.d
- -etc/udev
- sbin
- -sbin/systemd-hwdb
- lib
- -lib/modprobe.d
- -lib/systemd
- -lib/udev
- usr/lib
- -usr/lib/tmpfiles.d
- usr/share/ipp-usb
after: [goipp]
scripts:
plugin: dump
source: snap/local/
organize:
run-ipp-usb*: scripts/
prime:
- scripts/
after: [ipp-usb]
07070100000040000081A400000000000000000000000167D72F5D00000BAD000000000000000000000000000000000000001900000000ipp-usb-0.9.30/status.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
*
* Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
* See LICENSE for license terms and conditions
*
* ipp-usb status support
*/
package main
import (
"bytes"
"fmt"
"io/ioutil"
"net"
"net/http"
"sort"
"strconv"
"sync"
)
// statusOfDevice represents a status of the particular device
type statusOfDevice struct {
desc UsbDeviceDesc // Device descriptor
init error // Initialization error, nil if none
HTTPPort int // Assigned http port for the device
}
var (
// statusTable maintains a per-device status,
// indexed by the UsbAddr
statusTable = make(map[UsbAddr]*statusOfDevice)
// statusLock protects access to the statusTable
statusLock sync.RWMutex
)
// StatusRetrieve connects to the running ipp-usb daemon, retrieves
// its status and returns retrieved status as a printable text
func StatusRetrieve() ([]byte, error) {
t := &http.Transport{
Dial: func(network, addr string) (net.Conn, error) {
return CtrlsockDial()
},
}
c := &http.Client{
Transport: t,
}
rsp, err := c.Get("http://localhost/status")
if err != nil {
return nil, err
}
defer rsp.Body.Close()
return ioutil.ReadAll(rsp.Body)
}
// StatusFormat formats ipp-usb status as a text
func StatusFormat() []byte {
buf := &bytes.Buffer{}
// Lock the statusTable
statusLock.RLock()
defer statusLock.RUnlock()
// Dump ipp-usb daemon status. If we are here, we are
// definitely running :-)
buf.WriteString("ipp-usb daemon: running\n")
// Sort devices by address
devs := make([]*statusOfDevice, len(statusTable))
i := 0
for _, status := range statusTable {
devs[i] = status
i++
}
sort.Slice(devs, func(i, j int) bool {
return devs[i].desc.UsbAddr.Less(devs[j].desc.UsbAddr)
})
// Format per-device status
buf.WriteString("ipp-usb devices:")
if len(statusTable) == 0 {
buf.WriteString(" not found\n")
} else {
buf.WriteString("\n")
fmt.Fprintf(buf, " Num Device Vndr:Prod Port Model\n")
for i, status := range devs {
info, _ := status.desc.GetUsbDeviceInfo()
s := "-"
if status.HTTPPort != 0 {
s = strconv.Itoa(status.HTTPPort)
}
fmt.Fprintf(buf, " %3d. %s %4.4x:%.4x %-5s %q\n",
i+1, status.desc.UsbAddr,
info.Vendor, info.Product, s,
info.MfgAndProduct)
s = "OK"
if status.init != nil {
s = devs[i].init.Error()
}
fmt.Fprintf(buf, " status: %s\n", s)
}
}
return buf.Bytes()
}
// StatusSet adds device to the status table or updates status
// of the already known device
func StatusSet(addr UsbAddr, desc UsbDeviceDesc, HTTPPort int, init error) {
statusLock.Lock()
statusTable[addr] = &statusOfDevice{
desc: desc,
init: init,
HTTPPort: HTTPPort,
}
statusLock.Unlock()
}
// StatusDel deletes device from the status table
func StatusDel(addr UsbAddr) {
statusLock.Lock()
delete(statusTable, addr)
statusLock.Unlock()
}
07070100000041000041ED00000000000000000000000267D72F5D00000000000000000000000000000000000000000000001C00000000ipp-usb-0.9.30/systemd-udev07070100000042000081A400000000000000000000000167D72F5D00000275000000000000000000000000000000000000002D00000000ipp-usb-0.9.30/systemd-udev/71-ipp-usb.rules# Standard IPP over USB devices, with Class/SubClass/Protocol = 7/1/4
ACTION=="add", SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", ENV{ID_USB_INTERFACES}=="*:070104:*", OWNER="root", GROUP="lp", MODE="0664", TAG+="systemd", ENV{SYSTEMD_WANTS}+="ipp-usb.service"
# Non-standard HP devices with 255/9/1 combination
# Tested with following devices:
# HP LaserJet MFP M426fdn
# HP ColorLaserJet MFP M278-M281
ACTION=="add", SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", ENV{ID_VENDOR_ID}=="03f0", ENV{ID_USB_INTERFACES}=="*:ff0901:*", OWNER="root", GROUP="lp", MODE="0664", TAG+="systemd", ENV{SYSTEMD_WANTS}+="ipp-usb.service"
07070100000043000081A400000000000000000000000167D72F5D000000CF000000000000000000000000000000000000002C00000000ipp-usb-0.9.30/systemd-udev/ipp-usb.service[Unit]
Description=Daemon for IPP over USB printer support
Documentation=man:ipp-usb(8)
After=cups.service avahi-daemon.service
Wants=avahi-daemon.service
[Service]
Type=simple
ExecStart=/sbin/ipp-usb udev
07070100000044000081A400000000000000000000000167D72F5D00001083000000000000000000000000000000000000001F00000000ipp-usb-0.9.30/tcpuid_linux.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
*
* Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
* See LICENSE for license terms and conditions
*
* UID discovery for TCP connection over loopback -- Linux version
*/
package main
import (
"encoding/binary"
"fmt"
"net"
"syscall"
"unsafe"
)
// #include <linux/inet_diag.h>
// #include <linux/in.h>
// #include <linux/netlink.h>
// #include <linux/sock_diag.h>
// #include <netinet/tcp.h>
// #include <stdint.h>
// #include <sys/socket.h>
//
// typedef struct inet_diag_req_v2 inet_diag_req_v2_struct;
// typedef struct inet_diag_sockid inet_diag_sockid_struct;
// typedef struct nlmsgerr nlmsgerr_struct;
// typedef struct inet_diag_msg inet_diag_msg_struct;
//
// typedef struct {
// struct nlmsghdr hdr;
// struct inet_diag_req_v2 data;
// } sock_diag_request;
//
import "C"
// TCPClientUIDSupported tells if TCPClientUID supported on this platform
func TCPClientUIDSupported() bool {
return true
}
// TCPClientUID obtains UID of client process that created
// TCP connection over the loopback interface
func TCPClientUID(client, server *net.TCPAddr) (int, error) {
// Obtain protocol family. Check for mismatch.
clientIs4 := client.IP.To4() != nil
serverIs4 := server.IP.To4() != nil
if clientIs4 != serverIs4 {
return -1, fmt.Errorf("TCPClientUID: IP4/IP6 mismatchh")
}
// Open NETLINK_SOCK_DIAG socket
sock, err := sockDiagOpen()
if err != nil {
return -1, err
}
defer syscall.Close(sock)
// Prepare request
rq := C.sock_diag_request{}
rq.hdr.nlmsg_len = C.uint32_t(unsafe.Sizeof(rq))
rq.hdr.nlmsg_type = C.uint16_t(C.SOCK_DIAG_BY_FAMILY)
rq.hdr.nlmsg_flags = C.uint16_t(C.NLM_F_REQUEST)
if clientIs4 {
rq.data.sdiag_family = C.AF_INET
copy((*[16]byte)(unsafe.Pointer(&rq.data.id.idiag_src))[:],
client.IP.To4())
copy((*[16]byte)(unsafe.Pointer(&rq.data.id.idiag_dst))[:],
server.IP.To4())
} else {
rq.data.sdiag_family = C.AF_INET6
copy((*[16]byte)(unsafe.Pointer(&rq.data.id.idiag_src))[:],
client.IP.To16())
copy((*[16]byte)(unsafe.Pointer(&rq.data.id.idiag_dst))[:],
server.IP.To16())
}
rq.data.sdiag_protocol = C.IPPROTO_TCP
rq.data.idiag_states = 1 << C.TCP_ESTABLISHED
rq.data.id.idiag_sport = C.uint16_t(toBE16((uint16(client.Port))))
rq.data.id.idiag_dport = C.uint16_t(toBE16((uint16(server.Port))))
rq.data.id.idiag_cookie[0] = C.INET_DIAG_NOCOOKIE
rq.data.id.idiag_cookie[1] = C.INET_DIAG_NOCOOKIE
// Send request
rqData := (*[unsafe.Sizeof(rq)]byte)(unsafe.Pointer(&rq))
rqAddr := &syscall.SockaddrNetlink{Family: syscall.AF_NETLINK}
err = syscall.Sendto(sock, rqData[:], 0, rqAddr)
if err != nil {
return -1, fmt.Errorf("sock_diag: sendto(): %s", err)
}
// Receive responses
buf := make([]byte, syscall.Getpagesize())
for {
num, _, err := syscall.Recvfrom(sock, buf, 0)
if err != nil {
return -1, fmt.Errorf("sock_diag: recvfrom(): %s", err)
}
msgs, err := syscall.ParseNetlinkMessage(buf[:num])
if err != nil {
return -1, fmt.Errorf("sock_diag: can't parse response")
}
for _, msg := range msgs {
data := unsafe.Pointer(&msg.Data[0])
switch msg.Header.Type {
case syscall.NLMSG_ERROR:
rsp := (*C.nlmsgerr_struct)(data)
err = syscall.Errno(-rsp.error)
err = fmt.Errorf("NLMSG_ERROR: %s", err)
return -1, err
case uint16(C.SOCK_DIAG_BY_FAMILY):
rsp := (*C.inet_diag_msg_struct)(data)
return int(rsp.idiag_uid), nil
}
}
}
}
// sockDiagOpen opens NETLINK_SOCK_DIAG socket
func sockDiagOpen() (int, error) {
const stype = syscall.SOCK_DGRAM | syscall.SOCK_CLOEXEC
const proto = int(C.NETLINK_SOCK_DIAG)
sock, err := syscall.Socket(syscall.AF_NETLINK, stype, proto)
if err != nil {
return -1, fmt.Errorf("sock_diag: socket(): %s", err)
}
sa := &syscall.SockaddrNetlink{Family: syscall.AF_NETLINK}
err = syscall.Bind(sock, sa)
if err != nil {
syscall.Close(sock)
return -1, fmt.Errorf("sock_diag: bind(): %s", err)
}
return sock, nil
}
// toBE16 converts uint16 to big endian
func toBE16(in uint16) uint16 {
var out uint16
p := (*[2]byte)(unsafe.Pointer(&out))
binary.BigEndian.PutUint16(p[:], in)
return out
}
07070100000045000081A400000000000000000000000167D72F5D000003D2000000000000000000000000000000000000001F00000000ipp-usb-0.9.30/tcpuid_other.go// +build !linux
/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
*
* Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
* See LICENSE for license terms and conditions
*
* UID discovery for TCP connection over loopback -- default version
*
* If you've have added support for yet another platform, please don't
* forget to update build tag at the top of this file to exclude your
* platform
*/
package main
import (
"net"
)
// TCPClientUIDSupported tells if TCPClientUID supported on this platform
//
// If this function returns false, TCPClientUID should never be called
func TCPClientUIDSupported() bool {
return false
}
// TCPClientUID obtains UID of client process that created
// TCP connection over the loopback interface
func TCPClientUID(client, server *net.TCPAddr) (int, error) {
// Note, TCPClientUID should never be called, if
// TCPClientUIDSupported returns false
panic("TCPClientUID not supported")
}
07070100000046000081A400000000000000000000000167D72F5D0000098D000000000000000000000000000000000000001E00000000ipp-usb-0.9.30/tcpuid_test.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
*
* Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
* See LICENSE for license terms and conditions
*
* Tests for TCPClientUID
*/
package main
import (
"net"
"os"
"testing"
)
// doTestTCPClientUID performs TCPClientUID for the specified
// network and loopback address
func doTestTCPClientUID(t *testing.T, ip4 bool) {
// Do nothing if TCPClientUID is not supported by the platform
if !TCPClientUIDSupported() {
return
}
// Log local addresses. Check that we have appropriate
// address family support, configured in the system.
var haveIP4, haveIP6 bool
if ift, err := net.Interfaces(); err == nil {
for _, ifi := range ift {
if addrs, err := ifi.Addrs(); err == nil {
t.Logf("%s:", ifi.Name)
for _, addr := range addrs {
t.Logf(" %s", addr)
if ipnet, ok := addr.(*net.IPNet); ok {
if ipnet.IP.To4() != nil {
haveIP4 = true
} else {
haveIP6 = true
}
}
}
}
}
}
// Skip incompatible address families
if ip4 && !haveIP4 {
return
}
if !ip4 && !haveIP6 {
return
}
// Create loopback listener -- it gives us a port
network := "tcp4"
loopback := "127.0.0.1"
if !ip4 {
loopback = "[::1]"
network = "tcp6"
}
l, err := net.Listen(network, loopback+":")
if err != nil {
t.Fatalf("net.Listen(%q,%q): %s", network, loopback+":", err)
}
defer l.Close()
// Dial client connection
addr := l.Addr()
clnt, err := net.Dial("tcp", addr.String())
if err != nil {
t.Fatalf("net.Dial(%q,%q): %s", network, addr, err)
}
defer clnt.Close()
// Accept server connection
srv, err := l.Accept()
if err != nil {
t.Fatalf("net.Accept(%q,%q): %s", network, addr, err)
}
defer srv.Close()
// Get and check Client UID
uid, err := TCPClientUID(clnt.LocalAddr().(*net.TCPAddr),
srv.LocalAddr().(*net.TCPAddr))
if err != nil {
t.Fatalf("TCPClientUID(%q,%q): %s",
clnt.LocalAddr(), srv.LocalAddr(), err)
}
if uid != os.Getuid() {
t.Fatalf("TCPClientUID(%q,%q): uid mismatch (expected %d, present %d)",
clnt.LocalAddr(), srv.LocalAddr(), os.Getuid(), uid)
}
}
// TestTCPClientUIDIp4 performs TCPClientUID test for IPv4
func TestTCPClientUIDIp4(t *testing.T) {
doTestTCPClientUID(t, true)
}
// TestTCPClientUIDIp6 performs TCPClientUID test for IPv6
func TestTCPClientUIDIp6(t *testing.T) {
doTestTCPClientUID(t, false)
}
07070100000047000041ED00000000000000000000000267D72F5D00000000000000000000000000000000000000000000001800000000ipp-usb-0.9.30/testdata07070100000048000081A400000000000000000000000167D72F5D00000660000000000000000000000000000000000000002500000000ipp-usb-0.9.30/testdata/ipp-usb.conf# ipp-usb.conf: example configuration file
# Networking parameters
[network]
# TCP ports for HTTP will be automatically allocated in the following range
http-min-port = 60000
http-max-port = 65535
# Enable or disable DNS-SD advertisement
dns-sd = enable # enable | disable
# Network interface to use. Set to `all` if you want to expose you
# printer to the local network. This way you can share your printer
# with other computers in the network, as well as with iOS and Android
# devices.
interface = loopback # all | loopback
# Enable or disable IPv6
ipv6 = enable # enable | disable
# Logging configuration
[logging]
# device-log - per-device log levels
# main-log - main log levels
# console-log - console log levels
#
# parameter contains a comma-separated list of
# the following keywords:
# error - error messages
# info - informative messages
# debug - debug messages
# trace-ipp, trace-escl, trace-http - very detailed per-protocol traces
# all - all logs
# trace-all - alias to all
#
# Note, trace-* implies debug, debug implies info, info implies error
device-log = all
main-log = debug
console-log = debug
# Log rotation parameters:
# max-file-size - max log file before rotation. Use suffix M
# for megabytes or K for kilobytes
# max-backup-files - how many backup files to preserve during rotation
#
max-file-size = 256K
max-backup-files = 5
# Enable or disable ANSI colors on console
console-color = enable # enable | disable
# vim:ts=8:sw=2:et
07070100000049000041ED00000000000000000000000267D72F5D00000000000000000000000000000000000000000000001F00000000ipp-usb-0.9.30/testdata/quirks0707010000004A000081A400000000000000000000000167D72F5D0000012A000000000000000000000000000000000000002A00000000ipp-usb-0.9.30/testdata/quirks/Canon.conf# ipp-usb quirks file -- quirks for Canon devices
# This device responds to the Get-Printer-Attributes request with the
# server-error-internal-error status, but otherwise works correctly
#
# So we just ignore its returned IPP status as workaround
[Canon SELPHY CP1500]
ignore-ipp-status = true
0707010000004B000081A400000000000000000000000167D72F5D00000496000000000000000000000000000000000000002700000000ipp-usb-0.9.30/testdata/quirks/HP.conf# ipp-usb quirks file -- quirks for HP devices
[HP LaserJet MFP M28-M31]
http-connection = keep-alive
[HP OfficeJet Pro 8730]
http-connection = close
# eSCL requests hangs on this device, if both USB interfaces are
# in use. Limiting number of interfaces to 1 makes scanning
# reliable in a cost of making scan cancellation impossible,
# as there is no second interface to send cancel request.
# (ADF scans still can be canceled between retrieval of
# subsequent pages).
[HP ScanJet Pro 4500 fn1]
usb-max-interfaces = 1
# HP Photosmart 6520 series doesn't implement true faxing,
# but instead implements internet-based eFax,
# which makes no sense when connected via USB
# so can be safely disabled for this kind of devices.
[HP Photosmart 6520 series]
disable-fax = true
# This device sometimes hangs when probing for fax support
# See long conversation here for details:
# https://github.com/OpenPrinting/ipp-usb/issues/48
[HP ENVY 5530 series]
disable-fax = true
# This device fails to initialize. This quirk helps.
#
# See le following link for details:
# https://github.com/OpenPrinting/ipp-usb/issues/75
[HP OfficeJet Pro 8710]
init-reset = soft
0707010000004C000081A400000000000000000000000167D72F5D000002AE000000000000000000000000000000000000002B00000000ipp-usb-0.9.30/testdata/quirks/Pantum.conf# ipp-usb quirks file -- quirks for Pantum devices
# Some Pantum devices (Pantum M7300FDW known to have this bug)
# encode IPP messages improperly.
#
# With this option, ipp-usb will recode IPP responses, so that
# CUPS will accept it.
#
# Note, it still doesn't solve compatibility issues, if device
# is connected over network, not over USB. Either CUPS patch is
# required or user needs to install Pantum proprietary driver
[Pantum*]
buggy-ipp-responses = sanitize
# This device pretends it has a fax, but actually fax unit is missed.
# Attempt to query it's printer-attributes sometimes times out, so
# it is better to disable it.
[Pantum BM5100ADN series]
disable-fax = true
0707010000004D000081A400000000000000000000000167D72F5D00000114000000000000000000000000000000000000002E00000000ipp-usb-0.9.30/testdata/quirks/blacklist.conf# ipp-usb quirks file -- blacklisted devices
# This device has IPP-over-USB interfaces, but responds HTTP 404 Not found
# status to all requests
[HP Inc. HP Laser MFP 135a]
blacklist = true
# And this device has the same problem
[HP Inc. HP Laser 107a]
blacklist = true
0707010000004E000081A400000000000000000000000167D72F5D0000007A000000000000000000000000000000000000002C00000000ipp-usb-0.9.30/testdata/quirks/default.conf# ipp-usb quirks file -- defaults
[*]
blacklist = false
# Drop Connection: header by default
http-connection = ""
0707010000004F000081A400000000000000000000000167D72F5D0000011F000000000000000000000000000000000000002800000000ipp-usb-0.9.30/testdata/quirks/pzz.conf# ipp-usb quirks file -- quirks for Canon devices
# This device responds to the Get-Printer-Attributes request with the
# server-error-internal-error status, but otherwise works correctly
#
# So we just ignore its returned IPP status as workaround
[Kyocera*]
ignore-ipp-status = true
07070100000050000081A400000000000000000000000167D72F5D00001F53000000000000000000000000000000000000001C00000000ipp-usb-0.9.30/usbcommon.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
*
* Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
* See LICENSE for license terms and conditions
*
* Common types for USB
*/
package main
import (
"crypto/sha1"
"fmt"
"sort"
"strings"
)
// UsbAddr represents an USB device address
type UsbAddr struct {
Bus int // The bus on which the device was detected
Address int // The address of the device on the bus
}
// String returns a human-readable representation of UsbAddr
func (addr UsbAddr) String() string {
return fmt.Sprintf("Bus %.3d Device %.3d", addr.Bus, addr.Address)
}
// Less returns true, if addr is "less" that addr2, for sorting
func (addr UsbAddr) Less(addr2 UsbAddr) bool {
return addr.Bus < addr2.Bus ||
(addr.Bus == addr2.Bus && addr.Address < addr2.Address)
}
// UsbAddrList represents a list of USB addresses
//
// For faster lookup and comparable logging, address list
// is always sorted in acceding order. To maintain this
// invariant, never modify list directly, and use the provided
// (*UsbAddrList) Add() function
type UsbAddrList []UsbAddr
// Add UsbAddr to UsbAddrList
func (list *UsbAddrList) Add(addr UsbAddr) {
// Find the smallest index of address list
// item which is greater or equal to the
// newly inserted address
//
// Note, of "not found" case sort.Search()
// returns len(*list)
i := sort.Search(len(*list), func(n int) bool {
return !(*list)[n].Less(addr)
})
// Check for duplicate
if i < len(*list) && (*list)[i] == addr {
return
}
// The simple case: all items are less
// that newly added, so just append new
// address to the end
if i == len(*list) {
*list = append(*list, addr)
return
}
// Insert item in the middle
*list = append(*list, (*list)[i])
(*list)[i] = addr
}
// Find address in a list. Returns address index,
// if address is found, -1 otherwise
func (list UsbAddrList) Find(addr UsbAddr) int {
i := sort.Search(len(list), func(n int) bool {
return !list[n].Less(addr)
})
if i < len(list) && list[i] == addr {
return i
}
return -1
}
// Diff computes a difference between two address lists,
// returning lists of elements to be added and to be removed
// to/from the list to convert it to the list2
func (list UsbAddrList) Diff(list2 UsbAddrList) (added, removed UsbAddrList) {
// Note, there is no needs to sort added and removed
// lists, they are already created sorted
for _, a := range list2 {
if list.Find(a) < 0 {
added.Add(a)
}
}
for _, a := range list {
if list2.Find(a) < 0 {
removed.Add(a)
}
}
return
}
// UsbIfAddr represents a full "address" of the USB interface
type UsbIfAddr struct {
UsbAddr // Device address
Num int // Interface number within Config
Alt int // Number of alternate setting
In, Out int // Input/output endpoint numbers
}
// String returns a human readable short representation of UsbIfAddr
func (ifaddr UsbIfAddr) String() string {
return fmt.Sprintf("Bus %.3d Device %.3d Interface %d Alt %d",
ifaddr.Bus,
ifaddr.Address,
ifaddr.Num,
ifaddr.Alt,
)
}
// UsbIfAddrList represents a list of USB interface addresses
type UsbIfAddrList []UsbIfAddr
// Add UsbIfAddr to UsbIfAddrList
func (list *UsbIfAddrList) Add(addr UsbIfAddr) {
*list = append(*list, addr)
}
// UsbDeviceDesc represents an IPP-over-USB device descriptor
type UsbDeviceDesc struct {
UsbAddr // Device address
Config int // IPP-over-USB configuration
IfAddrs UsbIfAddrList // IPP-over-USB interfaces
IfDescs []UsbIfDesc // Descriptors of all interfaces
}
// GetUsbDeviceInfo obtains UsbDeviceInfo by UsbDeviceDesc
// It may fail, if device cannot be opened
func (desc UsbDeviceDesc) GetUsbDeviceInfo() (UsbDeviceInfo, error) {
dev, err := UsbOpenDevice(desc)
if err == nil {
defer dev.Close()
return dev.UsbDeviceInfo()
}
return UsbDeviceInfo{}, err
}
// UsbIfDesc represents an USB interface descriptor
type UsbIfDesc struct {
Vendor uint16 // USB Vendor ID
Product uint16 // USB Device ID
Config int // Configuration
IfNum int // Interface number
Alt int // Alternate setting
Class int // Class
SubClass int // Subclass
Proto int // Protocol
}
// IsIppOverUsb check if interface is IPP over USB
//
// FIXME. The matching rules must be configurable
func (ifdesc UsbIfDesc) IsIppOverUsb() bool {
switch {
// The classical combination, 7/1/4
case ifdesc.Class == 7 && ifdesc.SubClass == 1 && ifdesc.Proto == 4:
return true
// Some HP devices use non-standard combination, 255/9/1
//
// This is valid at least with the following devices:
// HP LaserJet MFP M426fdn
// HP ColorLaserJet MFP M278-M281
case ifdesc.Vendor == 0x03f0 &&
ifdesc.Class == 255 && ifdesc.SubClass == 9 && ifdesc.Proto == 1:
return true
}
return false
}
// UsbDeviceInfo represents USB device information
type UsbDeviceInfo struct {
// Fields, directly decoded from USB
Vendor uint16 // Vendor ID
Product uint16 // Device ID
SerialNumber string // Device serial number
Manufacturer string // Manufacturer name
ProductName string // Product name
PortNum int // USB port number
BasicCaps UsbIppBasicCaps // Device basic capabilities
// Precomputed fields
MfgAndProduct string // Product with Manufacturer prefix, if needed
}
// UsbIppBasicCaps represents device basic capabilities bits,
// according to the IPP-USB specification, section 4.3
type UsbIppBasicCaps int
// Basic capabilities bits, see IPP-USB specification, section 4.3
const (
UsbIppBasicCapsPrint UsbIppBasicCaps = 1 << iota
UsbIppBasicCapsScan
UsbIppBasicCapsFax
UsbIppBasicCapsOther
UsbIppBasicCapsAnyHTTP
)
// String returns a human-readable representation of UsbAddr
func (caps UsbIppBasicCaps) String() string {
s := []string{}
if caps&UsbIppBasicCapsPrint != 0 {
s = append(s, "print")
}
if caps&UsbIppBasicCapsScan != 0 {
s = append(s, "scan")
}
if caps&UsbIppBasicCapsFax != 0 {
s = append(s, "fax")
}
if caps&UsbIppBasicCapsAnyHTTP != 0 {
s = append(s, "http")
}
return strings.Join(s, ",")
}
// FixUp fixes up precomputed fields
func (info *UsbDeviceInfo) FixUp() {
mfg := strings.TrimSpace(info.Manufacturer)
prod := strings.TrimSpace(info.ProductName)
info.MfgAndProduct = prod
if !strings.HasPrefix(prod, mfg) {
info.MfgAndProduct = mfg + " " + prod
}
}
// Ident returns device identification string, suitable as
// persistent state identifier
func (info UsbDeviceInfo) Ident() string {
id := fmt.Sprintf("%4.4x-%4.4x-%s-%s",
info.Vendor, info.Product, info.SerialNumber, info.MfgAndProduct)
id = strings.Map(func(c rune) rune {
switch {
case '0' <= c && c <= '9':
case 'a' <= c && c <= 'z':
case 'A' <= c && c <= 'Z':
case c == '-' || c == '_':
default:
c = '-'
}
return c
}, id)
return id
}
// DNSSdName generates device DNS-SD name in a case it is not available
// from IPP or eSCL
func (info UsbDeviceInfo) DNSSdName() string {
return info.MfgAndProduct
}
// UUID generates device UUID in a case it is not available
// from IPP or eSCL
func (info UsbDeviceInfo) UUID() string {
hash := sha1.New()
// Arbitrary namespace UUID
const namespace = "fe678de6-f422-467e-9f83-2354e26c3b41"
hash.Write([]byte(namespace))
hash.Write([]byte(info.Ident()))
uuid := hash.Sum(nil)
// UUID.Version = 5: Name-based with SHA1; see RFC4122, 4.1.3.
uuid[6] &= 0x0f
uuid[6] |= 0x5f
// UUID.Variant = 0b10: see RFC4122, 4.1.1.
uuid[8] &= 0x3F
uuid[8] |= 0x80
return fmt.Sprintf(
"%.2x%.2x%.2x%.2x-%.2x%.2x-%.2x%.2x-%.2x%.2x-%.2x%.2x%.2x%.2x%.2x%.2x",
uuid[0], uuid[1], uuid[2], uuid[3],
uuid[4], uuid[5], uuid[6], uuid[7],
uuid[8], uuid[9], uuid[10], uuid[11],
uuid[12], uuid[13], uuid[14], uuid[15])
}
// Comment returns a short comment, describing a device
func (info UsbDeviceInfo) Comment() string {
return info.MfgAndProduct + " serial=" + info.SerialNumber
}
07070100000051000081A400000000000000000000000167D72F5D00000704000000000000000000000000000000000000002100000000ipp-usb-0.9.30/usbcommon_test.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
*
* Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
* See LICENSE for license terms and conditions
*
* Tests for usbcommon.go
*/
package main
import (
"testing"
)
// Check if two UsbAddrList are equal
func equalUsbAddrList(l1, l2 UsbAddrList) bool {
if len(l1) != len(l2) {
return false
}
for i := range l1 {
if l1[i] != l2[i] {
return false
}
}
return true
}
// Make UsbAddrList from individual addresses
func makeUsbAddrList(addrs ...UsbAddr) UsbAddrList {
l := UsbAddrList{}
for _, a := range addrs {
l.Add(a)
}
return l
}
// Test (*UsbAddrList)Add() against (*UsbAddrList)Find()
func TestUsbAddrListAddFind(t *testing.T) {
a1 := UsbAddr{0, 1}
a2 := UsbAddr{0, 2}
a3 := UsbAddr{0, 3}
l1 := makeUsbAddrList(a1, a2)
if l1.Find(a1) < 0 {
t.Fail()
}
if l1.Find(a2) < 0 {
t.Fail()
}
if l1.Find(a3) >= 0 {
t.Fail()
}
}
// Test that (*UsbAddrList)Add() is commutative operation
func TestUsbAddrListAddCommutative(t *testing.T) {
a1 := UsbAddr{0, 1}
a2 := UsbAddr{0, 2}
l1 := UsbAddrList{}
l2 := UsbAddrList{}
l1.Add(a1)
l1.Add(a2)
l2.Add(a2)
l2.Add(a1)
if !equalUsbAddrList(l1, l2) {
t.Fail()
}
}
// Test (*UsbAddrList) Diff()
func TestUsbAddrListDiff(t *testing.T) {
a1 := UsbAddr{0, 1}
a2 := UsbAddr{0, 2}
a3 := UsbAddr{0, 3}
l1 := makeUsbAddrList(a2, a3)
l2 := makeUsbAddrList(a1, a3)
added, removed := l1.Diff(l2)
if !equalUsbAddrList(added, makeUsbAddrList(a1)) {
t.Fail()
}
if !equalUsbAddrList(removed, makeUsbAddrList(a2)) {
t.Fail()
}
added, removed = l2.Diff(l1)
if !equalUsbAddrList(removed, makeUsbAddrList(a1)) {
t.Fail()
}
if !equalUsbAddrList(added, makeUsbAddrList(a2)) {
t.Fail()
}
}
07070100000052000081A400000000000000000000000167D72F5D00005F4D000000000000000000000000000000000000001F00000000ipp-usb-0.9.30/usbio_libusb.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
*
* Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
* See LICENSE for license terms and conditions
*
* USB low-level I/O. Cgo implementation on a top of libusb
*/
package main
import (
"context"
"encoding/binary"
"errors"
"runtime"
"sync"
"sync/atomic"
"time"
"unsafe"
)
// #cgo pkg-config: libusb-1.0
// #include <libusb.h>
//
// int libusbHotplugCallback (libusb_context *ctx, libusb_device *device,
// libusb_hotplug_event event, void *user_data);
// void libusbTransferCallback (struct libusb_transfer *transfer);
//
// typedef struct libusb_device_descriptor libusb_device_descriptor_struct;
// typedef struct libusb_config_descriptor libusb_config_descriptor_struct;
// typedef struct libusb_interface libusb_interface_struct;
// typedef struct libusb_interface_descriptor libusb_interface_descriptor_struct;
// typedef struct libusb_endpoint_descriptor libusb_endpoint_descriptor_struct;
// typedef struct libusb_transfer libusb_transfer_struct;
//
// // Note, libusb_strerror accepts enum libusb_error argument, which
// // unfortunately behaves differently depending on target OS and compiler
// // version (sometimes as C.int, sometimes as int32). Looks like cgo
// // bug. Wrapping this function into this simple wrapper should
// // fix the problem. See #18 for details
// static inline const char*
// libusb_strerror_wrapper (int code) {
// return libusb_strerror(code);
// }
import "C"
// UsbError represents USB error
type UsbError struct {
Func string
Code UsbErrCode
}
// Error describes a libusb error. It implements error interface
func (err UsbError) Error() string {
return err.Func + ": " + err.Code.String()
}
// UsbErrCode represents USB I/O error code
type UsbErrCode int
// UsbErrCode constants
const (
UsbEIO UsbErrCode = C.LIBUSB_ERROR_IO
UsbEInval = C.LIBUSB_ERROR_INVALID_PARAM
UsbEAccess = C.LIBUSB_ERROR_ACCESS
UsbENoDev = C.LIBUSB_ERROR_NO_DEVICE
UsbENotFound = C.LIBUSB_ERROR_NOT_FOUND
UsbEBusy = C.LIBUSB_ERROR_BUSY
UsbETimeout = C.LIBUSB_ERROR_TIMEOUT
UsbEOverflow = C.LIBUSB_ERROR_OVERFLOW
UsbEPipe = C.LIBUSB_ERROR_PIPE
UsbEIntr = C.LIBUSB_ERROR_INTERRUPTED
UsbENomem = C.LIBUSB_ERROR_NO_MEM
UsbENotSupported = C.LIBUSB_ERROR_NOT_SUPPORTED
UsbEOther = C.LIBUSB_ERROR_OTHER
)
// String returns string representation of error code
func (err UsbErrCode) String() string {
return C.GoString(C.libusb_strerror_wrapper(C.int(err)))
}
var (
// libusbContextPtr keeps a pointer to libusb_context.
// It is initialized on demand
libusbContextPtr *C.libusb_context
// libusbContextLock protects libusbContextPtr initialization
// in multithreaded context
libusbContextLock sync.Mutex
// Nonzero, if libusbContextPtr initialized
libusbContextOk int32
// libusbTransferDoneMap contains a map of completion channels,
// associated with each active libusb_transfer.
//
// The libusbTransferCallback uses this map to indicate transfer
// completion
//
// This is required, because CGo is very restrictive in whatever
// can be saved in pointer passed to the C side.
libusbTransferDoneMap = make(map[*C.libusb_transfer_struct]chan struct{})
// libusbTransferDoneLock protects multithreaded access to
// the libusbTransferDoneMap
libusbTransferDoneLock sync.Mutex
// UsbHotPlugChan receives USB hotplug event notifications
UsbHotPlugChan = make(chan struct{}, 1)
)
// UsbInit initializes low-level USB I/O
func UsbInit(nopnp bool) error {
_, err := libusbContext(nopnp)
return err
}
// libusbContext returns libusb_context. It
// initializes context on demand.
func libusbContext(nopnp bool) (*C.libusb_context, error) {
if atomic.LoadInt32(&libusbContextOk) != 0 {
return libusbContextPtr, nil
}
libusbContextLock.Lock()
defer libusbContextLock.Unlock()
// Obtain libusb_context
rc := C.libusb_init(&libusbContextPtr)
if rc != 0 {
return nil, UsbError{"libusb_init", UsbErrCode(rc)}
}
// Subscribe to hotplug events
if !nopnp {
C.libusb_hotplug_register_callback(
libusbContextPtr, // libusb_context
C.LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED| // events mask
C.LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT,
C.LIBUSB_HOTPLUG_NO_FLAGS, // flags
C.LIBUSB_HOTPLUG_MATCH_ANY, // vendor_id
C.LIBUSB_HOTPLUG_MATCH_ANY, // product_id
C.LIBUSB_HOTPLUG_MATCH_ANY, // dev_class
C.libusb_hotplug_callback_fn(unsafe.Pointer(C.libusbHotplugCallback)),
nil, // callback's data
nil, // deregister handle
)
}
// Start libusb thread (required for hotplug and asynchronous I/O)
go func() {
runtime.LockOSThread()
for {
C.libusb_handle_events(libusbContextPtr)
}
}()
atomic.StoreInt32(&libusbContextOk, 1)
return libusbContextPtr, nil
}
// Called by libusb on hotplug event
//
//export libusbHotplugCallback
func libusbHotplugCallback(ctx *C.libusb_context, dev *C.libusb_device,
event C.libusb_hotplug_event, p unsafe.Pointer) C.int {
usbaddr := UsbAddr{
Bus: int(C.libusb_get_bus_number(dev)),
Address: int(C.libusb_get_device_address(dev)),
}
switch event {
case C.LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED:
Log.Debug('+', "HOTPLUG: added %s", usbaddr)
case C.LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT:
Log.Debug('-', "HOTPLUG: removed %s", usbaddr)
}
select {
case UsbHotPlugChan <- struct{}{}:
default:
}
return 0
}
// Called by libusb on libusb_transfer completion
//
//export libusbTransferCallback
func libusbTransferCallback(xfer *C.libusb_transfer_struct) {
// Obtain signaling channel
libusbTransferDoneLock.Lock()
done := libusbTransferDoneMap[xfer]
libusbTransferDoneLock.Unlock()
// Indicate transfer completion by closing the channel
close(done)
}
// libusbTransferStatusDecode decodes libusb_transfer completion status.
//
// It returns either non-negative actual transfer length or error.
//
// When computing an error, it consults context.Context cancellation
// and expiration status.
func libusbTransferStatusDecode(ctx context.Context,
xfer *C.libusb_transfer_struct) (int, error) {
var rc C.int
switch xfer.status {
// Handle special cases
case C.LIBUSB_TRANSFER_COMPLETED:
// Successful completion. Return no error regardless
// of the context.Context status.
return int(xfer.actual_length), nil
case C.LIBUSB_TRANSFER_CANCELLED:
switch {
case ctx.Err() != nil:
return 0, ctx.Err()
default:
rc = C.LIBUSB_ERROR_IO
}
case C.LIBUSB_TRANSFER_TIMED_OUT:
// There may be a race between context.Context
// expiration and libusb timeout. Be consistent
// in returned error.
return 0, context.DeadlineExceeded
// Handle other cases
case C.LIBUSB_TRANSFER_STALL:
rc = C.LIBUSB_ERROR_PIPE
case C.LIBUSB_TRANSFER_OVERFLOW:
rc = C.LIBUSB_ERROR_OVERFLOW
case C.LIBUSB_TRANSFER_NO_DEVICE:
rc = C.LIBUSB_ERROR_NO_DEVICE
case C.LIBUSB_TRANSFER_ERROR:
rc = C.LIBUSB_ERROR_IO
default:
rc = C.LIBUSB_ERROR_OTHER
}
return 0, UsbError{"libusb_submit_transfer", UsbErrCode(rc)}
}
// libusbTransferAlloc allocates a libusb_transfer.
//
// On success, it allocates a completion channel as well and adds
// it into the libusbTransferDoneMap.
func libusbTransferAlloc() (*C.libusb_transfer_struct, chan struct{}, error) {
xfer := C.libusb_alloc_transfer(0)
if xfer == nil {
return nil, nil, UsbError{"libusb_alloc_transfer", UsbENomem}
}
doneChan := make(chan struct{})
libusbTransferDoneLock.Lock()
libusbTransferDoneMap[xfer] = doneChan
libusbTransferDoneLock.Unlock()
return xfer, doneChan, nil
}
// libusbTransferFree removed libusb_transfer from the libusbTransferDoneMap
// and releases its memory.
func libusbTransferFree(xfer *C.libusb_transfer_struct) {
libusbTransferDoneLock.Lock()
delete(libusbTransferDoneMap, xfer)
libusbTransferDoneLock.Unlock()
C.libusb_free_transfer(xfer)
}
// UsbCheckIppOverUsbDevices returns true if there are some IPP-over-USB devices
func UsbCheckIppOverUsbDevices() bool {
descs, _ := UsbGetIppOverUsbDeviceDescs()
return len(descs) != 0
}
// UsbGetIppOverUsbDeviceDescs return list of IPP-over-USB
// device descriptors
func UsbGetIppOverUsbDeviceDescs() (map[UsbAddr]UsbDeviceDesc, error) {
// Obtain libusb context
ctx, err := libusbContext(false)
if err != nil {
return nil, err
}
// Obtain list of devices
var devlist **C.libusb_device
cnt := C.libusb_get_device_list(ctx, &devlist)
if cnt < 0 {
return nil, UsbError{"libusb_get_device_list", UsbErrCode(cnt)}
}
defer C.libusb_free_device_list(devlist, 1)
// Convert devlist to slice.
// See https://github.com/golang/go/wiki/cgo#turning-c-arrays-into-go-slices
devs := (*[1 << 28]*C.libusb_device)(unsafe.Pointer(devlist))[:cnt:cnt]
// Now build list of addresses
descs := make(map[UsbAddr]UsbDeviceDesc)
for _, dev := range devs {
desc, err := libusbBuildUsbDeviceDesc(dev)
// Note, ignore devices, if we don't have
// at least 2 IPP over USB interfaces
// (which should not happen in real life,
// but just in case...
if err == nil && len(desc.IfAddrs) >= 2 {
descs[desc.UsbAddr] = desc
}
}
return descs, nil
}
// libusbBuildUsbDeviceDesc builds device descriptor
func libusbBuildUsbDeviceDesc(dev *C.libusb_device) (UsbDeviceDesc, error) {
var cDesc C.libusb_device_descriptor_struct
var desc UsbDeviceDesc
// Obtain device descriptor
rc := C.libusb_get_device_descriptor(dev, &cDesc)
if rc < 0 {
return desc, UsbError{"libusb_get_device_descriptor", UsbErrCode(rc)}
}
// Decode device descriptor
desc.Bus = int(C.libusb_get_bus_number(dev))
desc.Address = int(C.libusb_get_device_address(dev))
desc.Config = -1
// Roll over configs/interfaces/alt settings/endpoins
for cfgNum := 0; cfgNum < int(cDesc.bNumConfigurations); cfgNum++ {
var conf *C.libusb_config_descriptor_struct
rc = C.libusb_get_config_descriptor(dev, C.uint8_t(cfgNum), &conf)
if rc == 0 {
// Make sure we use the same configuration for all interfaces
if desc.Config >= 0 && desc.Config != int(conf.bConfigurationValue) {
continue
}
ifcnt := conf.bNumInterfaces
ifaces := (*[256]C.libusb_interface_struct)(
unsafe.Pointer(conf._interface))[:ifcnt:ifcnt]
for _, iface := range ifaces {
altcnt := iface.num_altsetting
alts := (*[256]C.libusb_interface_descriptor_struct)(
unsafe.Pointer(iface.altsetting))[:altcnt:altcnt]
for _, alt := range alts {
// Build and append UsbIfDesc
ifdesc := UsbIfDesc{
Vendor: uint16(cDesc.idVendor),
Product: uint16(cDesc.idProduct),
Config: int(conf.bConfigurationValue),
IfNum: int(alt.bInterfaceNumber),
Alt: int(alt.bAlternateSetting),
Class: int(alt.bInterfaceClass),
SubClass: int(alt.bInterfaceSubClass),
Proto: int(alt.bInterfaceProtocol),
}
desc.IfDescs = append(desc.IfDescs, ifdesc)
// We are only interested in IPP-over-USB
// interfaces, i.e., LIBUSB_CLASS_PRINTER,
// SubClass 1, Protocol 4
if ifdesc.IsIppOverUsb() {
epnum := alt.bNumEndpoints
endpoints := (*[256]C.libusb_endpoint_descriptor_struct)(
unsafe.Pointer(alt.endpoint))[:epnum:epnum]
in, out := -1, -1
for _, ep := range endpoints {
num := int(ep.bEndpointAddress & 0xf)
dir := int(ep.bEndpointAddress & 0x80)
switch dir {
case C.LIBUSB_ENDPOINT_IN:
if in == -1 {
in = num
}
case C.LIBUSB_ENDPOINT_OUT:
if out == -1 {
out = num
}
}
}
// Build and append UsbIfAddr
if in >= 0 && out >= 0 {
desc.Config = int(conf.bConfigurationValue)
addr := UsbIfAddr{
UsbAddr: desc.UsbAddr,
Num: int(alt.bInterfaceNumber),
Alt: int(alt.bAlternateSetting),
In: in,
Out: out,
}
desc.IfAddrs.Add(addr)
}
}
}
}
C.libusb_free_config_descriptor(conf)
}
}
return desc, nil
}
// UsbDevHandle represents libusb_device_handle
type UsbDevHandle C.libusb_device_handle
// UsbOpenDevice opens device by device descriptor
func UsbOpenDevice(desc UsbDeviceDesc) (*UsbDevHandle, error) {
// Obtain libusb context
ctx, err := libusbContext(false)
if err != nil {
return nil, err
}
// Obtain list of devices
var devlist **C.libusb_device
cnt := C.libusb_get_device_list(ctx, &devlist)
if cnt < 0 {
return nil, UsbError{"libusb_get_device_list", UsbErrCode(cnt)}
}
defer C.libusb_free_device_list(devlist, 1)
// Convert devlist to slice.
devs := (*[1 << 28]*C.libusb_device)(unsafe.Pointer(devlist))[:cnt:cnt]
// Find and open a device
for _, dev := range devs {
bus := int(C.libusb_get_bus_number(dev))
address := int(C.libusb_get_device_address(dev))
if desc.Bus == bus && desc.Address == address {
// Open device
var devhandle *C.libusb_device_handle
rc := C.libusb_open(dev, &devhandle)
if rc < 0 {
return nil, UsbError{"libusb_open", UsbErrCode(rc)}
}
return (*UsbDevHandle)(devhandle), nil
}
}
return nil, UsbError{"libusb_get_device_list", UsbENotFound}
}
// Configure prepares the device for further work:
// - set proper USB configuration
// - detach kernel driver
func (devhandle *UsbDevHandle) Configure(desc UsbDeviceDesc) error {
// Detach kernel driver
err := (*UsbDevHandle)(devhandle).detachKernelDriver()
if err != nil {
return err
}
// Set configuration
rc := C.libusb_set_configuration(
(*C.libusb_device_handle)(devhandle), C.int(desc.Config))
if rc < 0 {
return UsbError{"libusb_set_configuration", UsbErrCode(rc)}
}
// Printer may require some time to switch configuration
time.Sleep(time.Second / 4)
return nil
}
// detachKernelDriver detaches kernel driver from all interfaces
// of current configuration
func (devhandle *UsbDevHandle) detachKernelDriver() error {
C.libusb_set_auto_detach_kernel_driver(
(*C.libusb_device_handle)(devhandle), 1)
ifnums, err := devhandle.currentInterfaces()
if err != nil {
return err
}
for _, ifnum := range ifnums {
rc := C.libusb_detach_kernel_driver(
(*C.libusb_device_handle)(devhandle), C.int(ifnum))
if rc == C.LIBUSB_ERROR_NOT_FOUND {
rc = 0
}
if rc < 0 {
return UsbError{"libusb_detach_kernel_driver", UsbErrCode(rc)}
}
}
return nil
}
// libusbCurrentInterfaces builds list of interfaces in current configuration
func (devhandle *UsbDevHandle) currentInterfaces() ([]int, error) {
dev := C.libusb_get_device((*C.libusb_device_handle)(devhandle))
// Obtain device descriptor
var cDesc C.libusb_device_descriptor_struct
rc := C.libusb_get_device_descriptor(dev, &cDesc)
if rc < 0 {
return nil, UsbError{"libusb_get_device_descriptor", UsbErrCode(rc)}
}
// Get current configuration
var config C.int
rc = C.libusb_get_configuration((*C.libusb_device_handle)(devhandle), &config)
if rc < 0 {
return nil, UsbError{"libusb_get_configuration", UsbErrCode(rc)}
}
// Get configuration descriptor
var conf *C.libusb_config_descriptor_struct
for cfgNum := 0; cfgNum < int(cDesc.bNumConfigurations); cfgNum++ {
rc = C.libusb_get_config_descriptor(dev, C.uint8_t(cfgNum), &conf)
if rc < 0 {
return nil, UsbError{"libusb_get_configuration", UsbErrCode(rc)}
}
if conf.bConfigurationValue == C.uint8_t(config) {
break
}
C.libusb_free_config_descriptor(conf)
conf = nil
}
if conf == nil {
return nil, errors.New("libusb: unable to find current configuration in device descriptor")
}
defer C.libusb_free_config_descriptor(conf)
// Build list of interface numbers
ifcnt := conf.bNumInterfaces
ifaces := (*[256]C.libusb_interface_struct)(
unsafe.Pointer(conf._interface))[:ifcnt:ifcnt]
ifnumbers := make([]int, 0, ifcnt)
for _, iface := range ifaces {
alt := iface.altsetting
ifnumbers = append(ifnumbers, int(alt.bInterfaceNumber))
}
return ifnumbers, nil
}
// Close a device
func (devhandle *UsbDevHandle) Close() {
C.libusb_close((*C.libusb_device_handle)(devhandle))
}
// Reset a device
func (devhandle *UsbDevHandle) Reset() {
C.libusb_reset_device((*C.libusb_device_handle)(devhandle))
}
// UsbDeviceInfo returns UsbDeviceInfo for the device
func (devhandle *UsbDevHandle) UsbDeviceInfo() (UsbDeviceInfo, error) {
dev := C.libusb_get_device((*C.libusb_device_handle)(devhandle))
var cDesc C.libusb_device_descriptor_struct
var info UsbDeviceInfo
// Obtain device descriptor
rc := C.libusb_get_device_descriptor(dev, &cDesc)
if rc < 0 {
return info, UsbError{"libusb_get_device_descriptor", UsbErrCode(rc)}
}
// Decode device descriptor
info.Vendor = uint16(cDesc.idVendor)
info.Product = uint16(cDesc.idProduct)
info.BasicCaps = devhandle.usbIppBasicCaps()
buf := make([]byte, 256)
strings := []struct {
idx C.uint8_t
str *string
}{
{cDesc.iManufacturer, &info.Manufacturer},
{cDesc.iProduct, &info.ProductName},
{cDesc.iSerialNumber, &info.SerialNumber},
}
for _, s := range strings {
rc := C.libusb_get_string_descriptor_ascii(
(*C.libusb_device_handle)(devhandle),
s.idx,
(*C.uchar)(unsafe.Pointer(&buf[0])),
C.int(len(buf)),
)
if rc > 0 {
*s.str = string(buf[:rc])
}
}
info.PortNum = int(C.libusb_get_port_number(dev))
info.FixUp()
return info, nil
}
// usbIppBasicCaps reads and decodes printer's
// Class-specific Device Info Descriptor to obtain device
// capabilities; see IPP USB specification, section 4.3 for details
//
// This function never fails. In a case of errors, it fall backs
// to the reasonable default
func (devhandle *UsbDevHandle) usbIppBasicCaps() (caps UsbIppBasicCaps) {
// Safe default
caps = UsbIppBasicCapsPrint |
UsbIppBasicCapsScan |
UsbIppBasicCapsFax |
UsbIppBasicCapsAnyHTTP
// Buffer length
const bufLen = 256
// Obtain class-specific Device Info Descriptor
// See IPP USB specification, section 4.3 for details
buf := make([]byte, bufLen)
rc := C.libusb_get_descriptor(
(*C.libusb_device_handle)(devhandle),
0x21, 0,
(*C.uchar)(unsafe.Pointer(&buf[0])),
bufLen)
if rc < 0 {
// Some devices doesn't properly return class-specific
// device descriptor, so ignore an error
return
}
if rc < 10 {
// Malformed response, fall back to default
return
}
// Decode basic capabilities bits
bits := binary.LittleEndian.Uint16(buf[6:8])
if bits == 0 {
// Paranoia. If no caps, return default
return
}
return UsbIppBasicCaps(bits)
}
// OpenUsbInterface opens an interface
func (devhandle *UsbDevHandle) OpenUsbInterface(addr UsbIfAddr,
quirks Quirks) (*UsbInterface, error) {
// Claim the interface
rc := C.libusb_claim_interface(
(*C.libusb_device_handle)(devhandle),
C.int(addr.Num),
)
if rc < 0 {
return nil, UsbError{"libusb_claim_interface", UsbErrCode(rc)}
}
// Activate alternate setting
rc = C.libusb_set_interface_alt_setting(
(*C.libusb_device_handle)(devhandle),
C.int(addr.Num),
C.int(addr.Alt),
)
if rc < 0 {
C.libusb_release_interface(
(*C.libusb_device_handle)(devhandle),
C.int(addr.Num),
)
return nil, UsbError{"libusb_set_interface_alt_setting", UsbErrCode(rc)}
}
return &UsbInterface{
devhandle: devhandle,
addr: addr,
quirks: quirks,
}, nil
}
// UsbInterface represents IPP-over-USB interface
type UsbInterface struct {
devhandle *UsbDevHandle // Device handle
addr UsbIfAddr // Interface address
quirks Quirks // Device quirks
}
// Close the interface
func (iface *UsbInterface) Close() {
C.libusb_release_interface(
(*C.libusb_device_handle)(iface.devhandle),
C.int(iface.addr.Num),
)
}
// SoftReset performs interface soft reset, using class-specific
// SOFT_RESET request
//
// This code was inspired by CUPS, and the original comment follows:
//
// This soft reset is specific to the printer device class and is much less
// invasive than the general USB reset libusb_reset_device(). Especially it
// does never happen that the USB addressing and configuration changes. What
// is actually done is that all buffers get flushed and the bulk IN and OUT
// pipes get reset to their default states. This clears all stall conditions.
// See http://cholla.mmto.org/computers/linux/usb/usbprint11.
func (iface *UsbInterface) SoftReset() error {
rc := C.libusb_control_transfer(
(*C.libusb_device_handle)(iface.devhandle),
C.LIBUSB_REQUEST_TYPE_CLASS|
C.LIBUSB_ENDPOINT_OUT|
C.LIBUSB_RECIPIENT_OTHER,
2, 0, C.ushort(iface.addr.Num), nil, 0, 5000)
if rc < 0 {
rc = C.libusb_control_transfer(
(*C.libusb_device_handle)(iface.devhandle),
C.LIBUSB_REQUEST_TYPE_CLASS|
C.LIBUSB_ENDPOINT_OUT|
C.LIBUSB_RECIPIENT_INTERFACE,
2, 0, C.ushort(iface.addr.Num), nil, 0, 5000)
}
if rc < 0 {
return UsbError{"libusb_control_transfer", UsbErrCode(rc)}
}
return nil
}
// Send data to interface. Returns count of bytes actually transmitted
// and error, if any
func (iface *UsbInterface) Send(ctx context.Context,
data []byte) (n int, err error) {
// Don't even bother to send, if context already expired
if ctx.Err() != nil {
return 0, ctx.Err()
}
// Allocate a libusb_transfer.
xfer, doneChan, err := libusbTransferAlloc()
if err != nil {
return
}
defer libusbTransferFree(xfer)
// Setup bulk transfer
C.libusb_fill_bulk_transfer(
xfer,
(*C.libusb_device_handle)(iface.devhandle),
C.uint8_t(iface.addr.Out|C.LIBUSB_ENDPOINT_OUT),
(*C.uchar)(unsafe.Pointer(&data[0])),
C.int(len(data)),
C.libusb_transfer_cb_fn(unsafe.Pointer(C.libusbTransferCallback)),
nil,
0,
)
if iface.quirks.GetZlpSend() {
xfer.flags |= C.LIBUSB_TRANSFER_ADD_ZERO_PACKET
}
// Submit transfer
rc := C.libusb_submit_transfer(xfer)
if rc < 0 {
return 0, UsbError{"libusb_submit_transfer", UsbErrCode(rc)}
}
// Wait for completion
select {
case <-ctx.Done():
C.libusb_cancel_transfer(xfer)
case <-doneChan:
}
<-doneChan
n, err = libusbTransferStatusDecode(ctx, xfer)
// Introduce inter-URB send delay, if configured
if delay := iface.quirks.GetUsbSendDelay(); delay != 0 {
threshold := int(iface.quirks.GetUsbSendDelayThreshold())
if n > threshold {
time.Sleep(delay)
}
}
return
}
// Recv data from interface. Returns count of bytes actually transmitted
// and error, if any
//
// Note, if data size is not 512-byte aligned, and device has more data,
// that fits the provided buffer, LIBUSB_ERROR_OVERFLOW error may occur
func (iface *UsbInterface) Recv(ctx context.Context,
data []byte) (n int, err error) {
// Don't even bother to recv, if context already expired
if ctx.Err() != nil {
return 0, ctx.Err()
}
// Some versions of Linux kernel don't allow bulk transfers to
// be larger that 16kb per URB, and libusb uses some smart-ass
// mechanism to avoid this limitation.
//
// This mechanism seems not to work very reliable on Raspberry Pi
// (see #3 for details). So just limit bulk reads to 16kb
const MaxBulkRead = 16384
if len(data) > MaxBulkRead {
data = data[0:MaxBulkRead]
}
// Allocate a libusb_transfer.
xfer, doneChan, err := libusbTransferAlloc()
if err != nil {
return
}
defer libusbTransferFree(xfer)
// Setup bulk transfer
C.libusb_fill_bulk_transfer(
xfer,
(*C.libusb_device_handle)(iface.devhandle),
C.uint8_t(iface.addr.In|C.LIBUSB_ENDPOINT_IN),
(*C.uchar)(unsafe.Pointer(&data[0])),
C.int(len(data)),
C.libusb_transfer_cb_fn(unsafe.Pointer(C.libusbTransferCallback)),
nil,
0,
)
// Submit transfer
rc := C.libusb_submit_transfer(xfer)
if rc < 0 {
return 0, UsbError{"libusb_submit_transfer", UsbErrCode(rc)}
}
C.libusb_interrupt_event_handler(libusbContextPtr)
// Wait for completion
select {
case <-ctx.Done():
C.libusb_cancel_transfer(xfer)
case <-doneChan:
}
<-doneChan
n, err = libusbTransferStatusDecode(ctx, xfer)
return
}
// ClearHalt clears "halted" condition of either input or output endpoint
func (iface *UsbInterface) ClearHalt(in bool) error {
var ep C.uint8_t
if in {
ep = C.uint8_t(iface.addr.In | C.LIBUSB_ENDPOINT_IN)
} else {
ep = C.uint8_t(iface.addr.Out | C.LIBUSB_ENDPOINT_OUT)
}
rc := C.libusb_clear_halt(
(*C.libusb_device_handle)(iface.devhandle),
ep)
if rc < 0 {
return UsbError{"libusb_clear_halt", UsbErrCode(rc)}
}
return nil
}
07070100000053000081A400000000000000000000000167D72F5D0000644E000000000000000000000000000000000000001F00000000ipp-usb-0.9.30/usbtransport.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
*
* Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
* See LICENSE for license terms and conditions
*
* USB transport for HTTP
*/
package main
import (
"bufio"
"bytes"
"context"
"fmt"
"io"
"io/ioutil"
"math"
"net/http"
"os"
"sort"
"strconv"
"sync/atomic"
"time"
"github.com/OpenPrinting/goipp"
)
// UsbTransport implements HTTP transport functionality over USB
type UsbTransport struct {
addr UsbAddr // Device address
info UsbDeviceInfo // USB device info
log *Logger // Device's own logger
dev *UsbDevHandle // Underlying USB device
connPool chan *usbConn // Pool of idle connections
connList []*usbConn // List of all connections
connReleased chan struct{} // Signalled when connection released
shutdown chan struct{} // Closed by Shutdown()
connstate *usbConnState // Connections state tracker
quirks Quirks // Device quirks
timeout time.Duration // Timeout for requests (0 is none)
timeoutExpired uint32 // Atomic non-zero, if timeout expired
}
// NewUsbTransport creates new http.RoundTripper backed by IPP-over-USB
func NewUsbTransport(desc UsbDeviceDesc) (*UsbTransport, error) {
// Open the device
dev, err := UsbOpenDevice(desc)
if err != nil {
return nil, err
}
// Create UsbTransport
transport := &UsbTransport{
addr: desc.UsbAddr,
log: NewLogger(),
dev: dev,
connReleased: make(chan struct{}, 1),
shutdown: make(chan struct{}),
}
// Obtain device info
transport.info, err = dev.UsbDeviceInfo()
if err != nil {
dev.Close()
return nil, err
}
transport.log.Cc(Console)
transport.log.ToDevFile(transport.info)
transport.log.SetLevels(Conf.LogDevice)
// Setup quirks
transport.quirks = Conf.Quirks.MatchByModelName(
transport.info.MfgAndProduct)
// Write device info to the log
log := transport.log.Begin().
Nl(LogDebug).
Debug(' ', "===============================").
Info('+', "%s: opened %s", transport.addr, transport.info.ProductName).
Debug(' ', "Device info:").
Debug(' ', " USB Port: %d", transport.info.PortNum).
Debug(' ', " Ident: %s", transport.info.Ident()).
Debug(' ', " Manufacturer: %s", transport.info.Manufacturer).
Debug(' ', " Product: %s", transport.info.ProductName).
Debug(' ', " SerialNumber: %s", transport.info.SerialNumber).
Debug(' ', " MfgAndProduct: %s", transport.info.MfgAndProduct).
Debug(' ', " BasicCaps: %s", transport.info.BasicCaps).
Nl(LogDebug)
transport.dumpQuirks(log)
log.Nl(LogDebug)
transport.dumpUSBparams(log)
log.Nl(LogDebug)
log.Debug(' ', "USB interfaces:")
log.Debug(' ', " Config Interface Alt Class SubClass Proto")
for _, ifdesc := range desc.IfDescs {
prefix := byte(' ')
if ifdesc.IsIppOverUsb() {
prefix = '*'
}
log.Debug(prefix,
" %-3d %-3d %-3d %-3d %-3d %-3d",
ifdesc.Config, ifdesc.IfNum,
ifdesc.Alt, ifdesc.Class, ifdesc.SubClass, ifdesc.Proto)
}
log.Nl(LogDebug)
log.Commit()
var maxconn uint
// Check for blacklisted device
if transport.quirks.GetBlacklist() {
err = ErrBlackListed
goto ERROR
}
// Hard-reset the device, if needed
if transport.quirks.GetInitReset() == QuirkResetHard {
transport.log.Debug(' ', "Doing USB HARD RESET")
dev.Reset()
}
// Configure the device
err = dev.Configure(desc)
if err != nil {
goto ERROR
}
// Open connections
maxconn = transport.quirks.GetUsbMaxInterfaces()
if maxconn == 0 {
maxconn = math.MaxUint32
}
for i, ifaddr := range desc.IfAddrs {
var conn *usbConn
conn, err = transport.openUsbConn(i, ifaddr, transport.quirks)
if err != nil {
goto ERROR
}
transport.connList = append(transport.connList, conn)
maxconn--
if maxconn == 0 {
break
}
}
transport.connPool = make(chan *usbConn, len(transport.connList))
transport.connstate = newUsbConnState(len(desc.IfAddrs))
for _, conn := range transport.connList {
transport.connPool <- conn
}
return transport, nil
// Error: cleanup and exit
ERROR:
for _, conn := range transport.connList {
conn.destroy()
}
dev.Close()
return nil, err
}
// Dump quirks to the UsbTransport's log
func (transport *UsbTransport) dumpQuirks(log *LogMessage) {
log.Debug(' ', "Device quirks:")
prevMatch := ""
for _, q := range transport.quirks.All() {
val := q.RawValue
if _, isStr := q.Parsed.(string); isStr {
val = strconv.Quote(val)
}
if q.Match != prevMatch {
prevMatch = q.Match
log.Debug(' ', " [%s]", q.Match)
}
log.Debug(' ', " ; (%s)", q.Origin)
log.Debug(' ', " %s = %s", q.Name, val)
}
}
// Dump USB stack parameters to the UsbTransport's log
func (transport *UsbTransport) dumpUSBparams(log *LogMessage) {
const usbParamsDir = "/sys/module/usbcore/parameters"
// Obtain list of parameter names (file names)
dir, err := os.Open(usbParamsDir)
if err != nil {
return
}
files, err := dir.Readdirnames(-1)
dir.Close()
if err != nil {
return
}
sort.Strings(files)
if len(files) == 0 {
return
}
// Compute max width of parameter names
wid := 0
for _, file := range files {
if wid < len(file) {
wid = len(file)
}
}
wid++
// Write the table
log.Debug(' ', "USB stack parameters")
for _, file := range files {
p, _ := ioutil.ReadFile(usbParamsDir + "/" + file)
if p == nil {
p = []byte("-")
} else {
p = bytes.TrimSpace(p)
}
log.Debug(' ', " %*s %s", -wid, file+":", p)
}
}
// Get count of connections still in use
func (transport *UsbTransport) connInUse() int {
return cap(transport.connPool) - len(transport.connPool)
}
// SetTimeout sets the timeout for all subsequent requests.
//
// This is useful only at initialization time and if some requests
// were failed due to timeout, device reset is required, because
// at this case synchronization with device will probably be lost.
//
// A zero value for t means no timeout
func (transport *UsbTransport) SetTimeout(t time.Duration) {
transport.timeout = t
}
// TimeoutExpired returns true if one or more of the preceding HTTP request
// has failed due to timeout.
func (transport *UsbTransport) TimeoutExpired() bool {
return atomic.LoadUint32(&transport.timeoutExpired) != 0
}
// closeShutdownChan closes the transport.shutdown, which effectively
// disables connections allocation (usbConnGet will return ErrShutdown)
//
// This function can be safely called multiple times (only the first
// call closes the channel)
//
// Note, this function cannot be called simultaneously from
// different threads. However, it's not a problem, because it
// is only called from (*UsbTransport) Shutdown() and
// (*UsbTransport) Close(), and both of these functions are
// only called from the PnP thread context.
func (transport *UsbTransport) closeShutdownChan() {
select {
case <-transport.shutdown:
// Channel already closed
default:
close(transport.shutdown)
}
}
// Shutdown gracefully shuts down the transport. If provided
// context expires before shutdown completion, Shutdown
// returns the Context's error
func (transport *UsbTransport) Shutdown(ctx context.Context) error {
transport.closeShutdownChan()
for {
n := transport.connInUse()
if n == 0 {
break
}
transport.log.Info('-', "%s: shutdown: %d connections still in use",
transport.addr, n)
select {
case <-transport.connReleased:
case <-ctx.Done():
transport.log.Error('-', "%s: %s: shutdown timeout expired",
transport.addr, transport.info.ProductName)
return ctx.Err()
}
}
return nil
}
// Close the transport
func (transport *UsbTransport) Close(reset bool) {
// Reset the device, if required
if transport.connInUse() > 0 || reset {
transport.log.Info('-', "%s: resetting %s",
transport.addr, transport.info.ProductName)
transport.dev.Reset()
}
// Wait until all connections become inactive
transport.Shutdown(context.Background())
// Destroy all connections and close the USB device
for _, conn := range transport.connList {
conn.destroy()
}
transport.dev.Close()
transport.log.Info('-', "%s: closed %s",
transport.addr, transport.info.ProductName)
}
// Log returns device's own logger
func (transport *UsbTransport) Log() *Logger {
return transport.log
}
// UsbDeviceInfo returns USB device information for the device
// behind the transport
func (transport *UsbTransport) UsbDeviceInfo() UsbDeviceInfo {
return transport.info
}
// Quirks returns device's quirks
func (transport *UsbTransport) Quirks() Quirks {
return transport.quirks
}
// RoundTrip implements http.RoundTripper interface
func (transport *UsbTransport) RoundTrip(r *http.Request) (
*http.Response, error) {
session := int(atomic.AddInt32(&httpSessionID, 1)-1) % 1000
return transport.RoundTripWithSession(session, r)
}
// RoundTripWithSession executes a single HTTP transaction, returning
// a Response for the provided Request. Session number, for logging,
// provided as a separate parameter
func (transport *UsbTransport) RoundTripWithSession(session int,
rq *http.Request) (*http.Response, error) {
// Log the request
transport.log.HTTPRqParams(LogDebug, '>', session, rq)
// Prevent request from being canceled from outside
// We cannot do it on USB: closing USB connection
// doesn't drain buffered data that server is
// about to send to client
outreq := rq.WithContext(context.Background())
outreq.Cancel = nil
// Remove Expect: 100-continue, if any
outreq.Header.Del("Expect")
// Apply quirks
for name, value := range transport.quirks.HTTPHeaders {
if value != "" {
outreq.Header.Set(name, value)
} else {
outreq.Header.Del(name)
}
}
// Don't let Go's stdlib to add Connection: close header
// automatically
outreq.Close = false
// Add User-Agent, if missed. It is just cosmetic
if _, found := outreq.Header["User-Agent"]; !found {
outreq.Header["User-Agent"] = []string{"ipp-usb"}
}
// Wrap request body
if outreq.Body != nil {
outreq.Body = &usbRequestBodyWrapper{
log: transport.log,
session: session,
body: outreq.Body,
}
}
// Prepare to correctly handle HTTP transaction, in a case
// client drops request in a middle of reading body
switch {
case outreq.ContentLength <= 0:
// Nothing to do
if outreq.ContentLength < 0 {
transport.log.HTTPDebug('>', session,
"body is chunked, sending as is")
} else {
transport.log.HTTPDebug('>', session,
"body is empty, sending as is")
}
case outreq.ContentLength < 16384:
// Body is small, prefetch it before sending to USB
buf := &bytes.Buffer{}
_, err := io.CopyN(buf, outreq.Body, outreq.ContentLength)
if err != nil {
return nil, err
}
outreq.Body.Close()
outreq.Body = ioutil.NopCloser(buf)
transport.log.HTTPDebug('>', session,
"body is small (%d bytes), prefetched before sending",
buf.Len())
default:
// Force chunked encoding, so if client drops request,
// we still be able to correctly handle HTTP transaction
transport.log.HTTPDebug('>', session,
"body is large (%d bytes), sending as chunked",
outreq.ContentLength)
outreq.ContentLength = -1
}
// Log request details
transport.log.Begin().
HTTPRequest(LogTraceHTTP, '>', session, outreq).
Commit()
// Allocate USB connection
conn, err := transport.usbConnGet(rq.Context())
if err != nil {
return nil, err
}
transport.log.HTTPDebug(' ', session, "connection %d allocated", conn.index)
// Make an inter-request (or initial) delay, if needed
if delay := conn.delayUntil.Sub(time.Now()); delay > 0 {
transport.log.HTTPDebug(' ', session, "Pausing for %s", delay)
time.Sleep(delay)
}
// Set read/write Context. This effectively sets request timeout.
//
// This is important that context is is set after inter-request
// or initial delay is already done, so we don't need to bother
// with adjusting the timeout.
//
// The context cancel function is called from many places and
// not always used, so for simplicity I'd better initialize it
// to the dummy function rather that to compare it with nil
// every time it is called.
rwctx := context.Background()
cleanupCtx := context.CancelFunc(func() {})
if transport.timeout != 0 {
rwctx, cleanupCtx = context.WithTimeout(rwctx,
transport.timeout)
}
conn.setRWCtx(rwctx)
// Send request and receive a response
err = outreq.Write(conn)
if err != nil {
transport.log.HTTPError('!', session, "%s", err)
conn.put()
cleanupCtx()
return nil, err
}
resp, err := http.ReadResponse(conn.reader, outreq)
if err != nil {
// If the latest conn.Read has returned io.EOF, the only
// reason it could happen is that the zlp-recv-hack
// quirk has triggered.
//
// The stdlib HTTP stack will wrap the io.EOF error into
// its own error message. Here we force error condition
// back to io.EOF so it cleanly can be detected and handled
// by the initialization retry logic at the upper level
if conn.EOFSeen() {
err = io.EOF
}
transport.log.HTTPError('!', session, "%s", err)
conn.put()
cleanupCtx()
return nil, err
}
// Wrap response body
resp.Body = &usbResponseBodyWrapper{
log: transport.log,
session: session,
body: resp.Body,
conn: conn,
cleanupCtx: cleanupCtx,
}
// Optionally sanitize IPP response
if transport.quirks.GetBuggyIppRsp() == QuirkBuggyIppRspSanitize &&
resp.Header.Get("Content-Type") == "application/ipp" {
transport.sanitizeIppResponse(session, resp)
}
// Log the response
if resp != nil {
transport.log.Begin().
HTTPRspStatus(LogDebug, '<', session, outreq, resp).
HTTPResponse(LogTraceHTTP, '<', session, resp).
Commit()
}
return resp, nil
}
// sanitizeIppResponse attempts to sanitize IPP response from device
func (transport *UsbTransport) sanitizeIppResponse(session int,
resp *http.Response) {
// Try to prefetch IPP part of message
buf := &bytes.Buffer{}
buf2 := &bytes.Buffer{}
tee := io.TeeReader(resp.Body, buf)
msg := goipp.Message{}
err := msg.DecodeEx(tee, goipp.DecoderOptions{EnableWorkarounds: true})
if err != nil {
transport.log.HTTPDebug(' ', session,
"IPP sanitize: decode: %s", err)
goto REPLACE
}
// If backup copy decodes without any options, no need to sanitize
if msg2 := (goipp.Message{}); msg2.DecodeBytes(buf.Bytes()) == nil {
transport.log.HTTPDebug(' ', session,
"IPP sanitize: not needed")
goto REPLACE
}
// Re-encode the message correctly
err = msg.Encode(buf2)
if err != nil {
transport.log.HTTPDebug(' ', session,
"IPP sanitize: encode: %s", err)
goto REPLACE
}
// Replace buffer, adjust resp.ContentLength
if resp.ContentLength != -1 {
resp.ContentLength += int64(buf2.Len() - buf.Len())
resp.Header.Set("Content-Length",
strconv.FormatInt(resp.ContentLength, 10))
transport.log.HTTPDebug(' ', session,
"IPP sanitize: %d bytes replaced with %d",
buf.Len(), buf2.Len())
}
buf = buf2
// Replace consumed part of message with re-coded or
// saved backup copy
REPLACE:
wrap := resp.Body.(*usbResponseBodyWrapper)
wrap.preBody = buf
}
// usbRequestBodyWrapper wraps http.Request.Body, adding
// data path instrumentation
type usbRequestBodyWrapper struct {
log *Logger // Device's logger
session int // HTTP session, for logging
count int // Total count of received bytes
body io.ReadCloser // Request.body
drained bool // EOF or error has been seen
}
// Read from usbRequestBodyWrapper
func (wrap *usbRequestBodyWrapper) Read(buf []byte) (int, error) {
n, err := wrap.body.Read(buf)
wrap.count += n
if err != nil {
wrap.log.HTTPDebug('>', wrap.session,
"request body: got %d bytes; %s", wrap.count, err)
err = io.EOF
wrap.drained = true
}
return n, err
}
// Close usbRequestBodyWrapper
func (wrap *usbRequestBodyWrapper) Close() error {
if !wrap.drained {
wrap.log.HTTPDebug('>', wrap.session,
"request body: got %d bytes; closed", wrap.count)
}
return wrap.body.Close()
}
// usbResponseBodyWrapper wraps http.Response.Body and guarantees
// that connection will be always drained before closed
type usbResponseBodyWrapper struct {
log *Logger // Device's logger
session int // HTTP session, for logging
preBody *bytes.Buffer // Data inserted before body, if not nil
body io.ReadCloser // Response.body
conn *usbConn // Underlying USB connection
count int // Total count of received bytes
drained bool // EOF or error has been seen
cleanupCtx context.CancelFunc // Cancel function for I/O Context
}
// Read from usbResponseBodyWrapper
func (wrap *usbResponseBodyWrapper) Read(buf []byte) (int, error) {
if wrap.preBody != nil && wrap.preBody.Len() > 0 {
return wrap.preBody.Read(buf)
}
n, err := wrap.body.Read(buf)
wrap.count += n
if err != nil {
wrap.log.HTTPDebug('<', wrap.session,
"response body: got %d bytes; %s", wrap.count, err)
wrap.drained = true
}
return n, err
}
// Close usbResponseBodyWrapper
func (wrap *usbResponseBodyWrapper) Close() error {
// If EOF or error seen, we can close synchronously
if wrap.drained {
wrap.cleanup()
return nil
}
// Otherwise, we need to drain USB connection
wrap.log.HTTPDebug('<', wrap.session, "client has gone; draining response from USB")
go func() {
defer func() {
v := recover()
if v != nil {
Log.Panic(v)
}
}()
io.Copy(ioutil.Discard, wrap.body)
wrap.cleanup()
}()
return nil
}
// cleanup performs the final cleanup of the usbResponseBodyWrapper
// after use.
func (wrap *usbResponseBodyWrapper) cleanup() {
wrap.body.Close()
wrap.conn.put()
// Cleanup I/O context.Context, if any
if wrap.cleanupCtx != nil {
wrap.cleanupCtx()
}
wrap.log.HTTPDebug('<', wrap.session, "done with response body")
}
// usbConn implements an USB connection
type usbConn struct {
transport *UsbTransport // Transport that owns the connection
index int // Connection index (for logging)
iface *UsbInterface // Underlying interface
reader *bufio.Reader // For http.ReadResponse
rwctx context.Context // For usbConn.Read and usbConn.Write
delayUntil time.Time // Delay till this time before next request
delayInterval time.Duration // Pause between requests
cntRecv int // Total bytes received
cntSent int // Total bytes sent
eofSeen bool // Last usbConn.Read has returned io.EOF
}
// Open usbConn
func (transport *UsbTransport) openUsbConn(
index int, ifaddr UsbIfAddr, quirks Quirks) (*usbConn, error) {
dev := transport.dev
transport.log.Debug(' ', "USB[%d]: open: %s", index, ifaddr)
// Initialize connection structure
conn := &usbConn{
transport: transport,
index: index,
delayUntil: time.Now().Add(quirks.GetInitDelay()),
delayInterval: quirks.GetRequestDelay(),
}
conn.reader = bufio.NewReader(conn)
// Obtain interface
var err error
conn.iface, err = dev.OpenUsbInterface(ifaddr, quirks)
if err != nil {
goto ERROR
}
// Soft-reset interface, if needed
if quirks.GetInitReset() == QuirkResetSoft {
transport.log.Debug(' ', "USB[%d]: doing SOFT_RESET", index)
err = conn.iface.SoftReset()
if err != nil {
// Don't treat it too seriously
transport.log.Info('?', "USB[%d]: SOFT_RESET: %s", index, err)
}
}
return conn, nil
// Error: cleanup and exit
ERROR:
transport.log.Error('!', "USB[%d]: %s", index, err)
if conn.iface != nil {
conn.iface.Close()
}
return nil, err
}
// setRWCtx sets context.Context for subsequent Read and Write operations
func (conn *usbConn) setRWCtx(ctx context.Context) {
conn.rwctx = ctx
}
// Read from USB
func (conn *usbConn) Read(b []byte) (int, error) {
conn.transport.connstate.beginRead(conn)
defer conn.transport.connstate.doneRead(conn)
// Drop conn.eofSeenn flag
conn.eofSeen = false
// Note, to avoid LIBUSB_TRANSFER_OVERFLOW errors
// from libusb, input buffer size must always
// be aligned by 1024 bytes for USB 3.0, 512 bytes
// for USB 2.0, so 1024 bytes alignment is safe for
// both
//
// However if caller requests less that 1024 bytes, we
// can't align here simply by shrinking the buffer,
// because it will result a zero-size buffer. At
// this case we assume caller knows what it is
// doing (actually bufio never behaves this way)
if n := len(b); n >= 1024 {
n &= ^1023
b = b[0:n]
}
// zlp-recv-hack handling
zlpRecvHack := conn.transport.quirks.GetZlpRecvHack()
zlpRecv := false
// Setup deadline
backoff := time.Millisecond * 10
for {
n, err := conn.iface.Recv(conn.rwctx, b)
conn.cntRecv += n
conn.transport.log.Add(LogTraceHTTP, '<',
"USB[%d]: read: wanted %d got %d total %d",
conn.index, len(b), n, conn.cntRecv)
conn.transport.log.HexDump(LogTraceUSB, '<', b[:n])
if err != nil {
conn.transport.log.Error('!',
"USB[%d]: recv: %s", conn.index, err)
if err == context.DeadlineExceeded {
// If we've got read timeout preceded
// by the zero-length packet, interpret
// is as body EOF condition
if zlpRecvHack && zlpRecv {
conn.eofSeen = true
return 0, io.EOF
}
atomic.StoreUint32(
&conn.transport.timeoutExpired, 1)
}
}
if n != 0 || err != nil {
return n, err
}
zlpRecv = true
conn.transport.log.Debug(' ',
"USB[%d]: zero-size read", conn.index)
time.Sleep(backoff)
backoff += backoff / 4 // The same as backoff *= 1.25
if backoff > time.Millisecond*1000 {
backoff = time.Millisecond * 1000
}
}
}
// Write to USB
func (conn *usbConn) Write(b []byte) (int, error) {
conn.transport.connstate.beginWrite(conn)
defer conn.transport.connstate.doneWrite(conn)
n, err := conn.iface.Send(conn.rwctx, b)
conn.cntSent += n
conn.transport.log.Add(LogTraceHTTP, '>',
"USB[%d]: write: wanted %d sent %d total %d",
conn.index, len(b), n, conn.cntSent)
conn.transport.log.HexDump(LogTraceUSB, '>', b[:n])
if err != nil {
conn.transport.log.Error('!',
"USB[%d]: send: %s", conn.index, err)
if err == context.DeadlineExceeded {
atomic.StoreUint32(
&conn.transport.timeoutExpired, 1)
}
}
return n, err
}
// EOFSeen reports of the latest usbConn.Read has returned io.EOF
func (conn *usbConn) EOFSeen() bool {
return conn.eofSeen
}
// Allocate a connection
func (transport *UsbTransport) usbConnGet(ctx context.Context) (*usbConn, error) {
select {
case <-transport.shutdown:
return nil, ErrShutdown
case <-ctx.Done():
return nil, ctx.Err()
case conn := <-transport.connPool:
transport.connstate.gotConn(conn)
transport.log.Debug(' ', "USB[%d]: connection allocated, %s",
conn.index, transport.connstate)
return conn, nil
}
}
// Release the connection
func (conn *usbConn) put() {
transport := conn.transport
conn.reader.Reset(conn)
conn.delayUntil = time.Now().Add(conn.delayInterval)
conn.cntRecv = 0
conn.cntSent = 0
transport.connstate.putConn(conn)
transport.log.Debug(' ', "USB[%d]: connection released, %s",
conn.index, transport.connstate)
transport.connPool <- conn
select {
case transport.connReleased <- struct{}{}:
default:
}
}
// Destroy USB connection
func (conn *usbConn) destroy() {
conn.transport.log.Debug(' ', "USB[%d]: closed", conn.index)
conn.iface.Close()
}
// usbConnState tracks connections state, for logging
type usbConnState struct {
alloc []int32 // Per-connection "allocated" flag
read []int32 // Per-connection "reading" flag
write []int32 // Per-connection "writing" flag
}
// newUsbConnState creates a new usbConnState for given
// number of connections
func newUsbConnState(cnt int) *usbConnState {
return &usbConnState{
alloc: make([]int32, cnt),
read: make([]int32, cnt),
write: make([]int32, cnt),
}
}
// gotConn notifies usbConnState, that connection is allocated
func (state *usbConnState) gotConn(conn *usbConn) {
atomic.AddInt32(&state.alloc[conn.index], 1)
}
// putConn notifies usbConnState, that connection is released
func (state *usbConnState) putConn(conn *usbConn) {
atomic.AddInt32(&state.alloc[conn.index], -1)
}
// beginRead notifies usbConnState, that read is started
func (state *usbConnState) beginRead(conn *usbConn) {
atomic.AddInt32(&state.read[conn.index], 1)
}
// doneRead notifies usbConnState, that read is done
func (state *usbConnState) doneRead(conn *usbConn) {
atomic.AddInt32(&state.read[conn.index], -1)
}
// beginWrite notifies usbConnState, that write is started
func (state *usbConnState) beginWrite(conn *usbConn) {
atomic.AddInt32(&state.write[conn.index], 1)
}
// doneWrite notifies usbConnState, that write is done
func (state *usbConnState) doneWrite(conn *usbConn) {
atomic.AddInt32(&state.write[conn.index], -1)
}
// String returns a string, representing connections state
func (state *usbConnState) String() string {
buf := make([]byte, 0, 64)
used := 0
for i := range state.alloc {
a := atomic.LoadInt32(&state.alloc[i])
r := atomic.LoadInt32(&state.read[i])
w := atomic.LoadInt32(&state.write[i])
if len(buf) != 0 {
buf = append(buf, ' ')
}
if a|r|w == 0 {
buf = append(buf, '-', '-', '-')
} else {
used++
if a != 0 {
buf = append(buf, 'a')
} else {
buf = append(buf, '-')
}
if r != 0 {
buf = append(buf, 'r')
} else {
buf = append(buf, '-')
}
if w != 0 {
buf = append(buf, 'w')
} else {
buf = append(buf, '-')
}
}
}
return fmt.Sprintf("%d in use: %s", used, buf)
}
07070100000054000081A400000000000000000000000167D72F5D00000429000000000000000000000000000000000000001700000000ipp-usb-0.9.30/uuid.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
*
* Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
* See LICENSE for license terms and conditions
*
* UUID normalizer
*/
package main
import (
"bytes"
)
// UUIDNormalize parses an UUID and then reformats it into
// the standard form (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)
//
// If input is not a valid UUID, it returns an empty string
// Many standard formats of UUIDs are recognized
func UUIDNormalize(uuid string) string {
var buf [32]byte
var cnt int
in := bytes.ToLower([]byte(uuid))
if bytes.HasPrefix(in, []byte("urn:")) {
in = in[4:]
}
if bytes.HasPrefix(in, []byte("uuid:")) {
in = in[5:]
}
for len(in) != 0 {
c := in[0]
in = in[1:]
if '0' <= c && c <= '9' || 'a' <= c && c <= 'f' {
if cnt == 32 {
return ""
}
buf[cnt] = c
cnt++
}
}
if cnt != 32 {
return ""
}
return string(buf[0:8]) + "-" +
string(buf[8:12]) + "-" +
string(buf[12:16]) + "-" +
string(buf[16:20]) + "-" +
string(buf[20:32])
}
07070100000055000081A400000000000000000000000167D72F5D0000046E000000000000000000000000000000000000001C00000000ipp-usb-0.9.30/uuid_test.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
*
* Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
* See LICENSE for license terms and conditions
*
* UUID normalizer test
*/
package main
import (
"testing"
)
// Don't forget to update testData when ipp-ini.conf changes
var testDataUUID = []struct{ in, out string }{
{"01234567-89ab-cdef-0123-456789abcdef", "01234567-89ab-cdef-0123-456789abcdef"},
{"01234567-89ab-cdef-0123-456789abcde", ""},
{"01234567-89ab-cdef-0123-456789abcdef0", ""},
{"urn:01234567-89ab-cdef-0123-456789abcdef", "01234567-89ab-cdef-0123-456789abcdef"},
{"urn:uuid:01234567-89ab-cdef-0123-456789abcdef", "01234567-89ab-cdef-0123-456789abcdef"},
{"0123456789abcdef0123456789abcdef", "01234567-89ab-cdef-0123-456789abcdef"},
{"{0123456789abcdef0123456789abcdef}", "01234567-89ab-cdef-0123-456789abcdef"},
}
// Test .INI reader
func TestUUIDNormalize(t *testing.T) {
for _, data := range testDataUUID {
uuid := UUIDNormalize(data.in)
if uuid != data.out {
t.Errorf("UUIDNormalize(%q): expected %q, got %q", data.in, data.out, uuid)
}
}
}
07070100000056000041ED00000000000000000000000267D72F5D00000000000000000000000000000000000000000000001600000000ipp-usb-0.9.30/vendor07070100000057000041ED00000000000000000000000267D72F5D00000000000000000000000000000000000000000000002100000000ipp-usb-0.9.30/vendor/github.com07070100000058000041ED00000000000000000000000267D72F5D00000000000000000000000000000000000000000000002E00000000ipp-usb-0.9.30/vendor/github.com/OpenPrinting07070100000059000041ED00000000000000000000000267D72F5D00000000000000000000000000000000000000000000003400000000ipp-usb-0.9.30/vendor/github.com/OpenPrinting/goipp0707010000005A000081A400000000000000000000000167D72F5D00000013000000000000000000000000000000000000003F00000000ipp-usb-0.9.30/vendor/github.com/OpenPrinting/goipp/.gitignoreipp-usb
tags
*.swp
0707010000005B000081A400000000000000000000000167D72F5D0000052F000000000000000000000000000000000000003C00000000ipp-usb-0.9.30/vendor/github.com/OpenPrinting/goipp/LICENSEBSD 2-Clause License
Copyright (c) 2020, Alexander Pevzner
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
0707010000005C000081A400000000000000000000000167D72F5D00000034000000000000000000000000000000000000003D00000000ipp-usb-0.9.30/vendor/github.com/OpenPrinting/goipp/Makefileall:
-gotags -R . > tags
go build
test:
go test
0707010000005D000081A400000000000000000000000167D72F5D0000035A000000000000000000000000000000000000003E00000000ipp-usb-0.9.30/vendor/github.com/OpenPrinting/goipp/README.md# goipp
[](http://godoc.org/github.com/OpenPrinting/goipp)

[](https://goreportcard.com/report/github.com/OpenPrinting/goipp)
The goipp library is fairly complete implementation of IPP core protocol in
pure Go. Essentially, it is IPP messages parser/composer. Transport is
not implemented here, because Go standard library has an excellent built-in
HTTP client, and it doesn't make a lot of sense to wrap it here.
High-level requests, like "print a file" are also not implemented, only the
low-level stuff.
All documentation is on godoc.org -- follow the link above. Pull requests
are welcomed, assuming they don't break existing API.
0707010000005E000081A400000000000000000000000167D72F5D0000001D000000000000000000000000000000000000004000000000ipp-usb-0.9.30/vendor/github.com/OpenPrinting/goipp/_config.ymltheme: jekyll-theme-architect0707010000005F000081A400000000000000000000000167D72F5D00000811000000000000000000000000000000000000003C00000000ipp-usb-0.9.30/vendor/github.com/OpenPrinting/goipp/attr.go/* Go IPP - IPP core protocol implementation in pure Go
*
* Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
* See LICENSE for license terms and conditions
*
* Message attributes
*/
package goipp
import (
"fmt"
)
// Attributes represents a slice of attributes
type Attributes []Attribute
// Add Attribute to Attributes
func (attrs *Attributes) Add(attr Attribute) {
*attrs = append(*attrs, attr)
}
// Equal checks that attrs and attrs2 are equal
func (attrs Attributes) Equal(attrs2 Attributes) bool {
if len(attrs) != len(attrs2) {
return false
}
for i, attr := range attrs {
attr2 := attrs2[i]
if !attr.Equal(attr2) {
return false
}
}
return true
}
// Attribute represents a single attribute, which consist of
// the Name and one or more Values
type Attribute struct {
Name string // Attribute name
Values Values // Slice of values
}
// MakeAttribute makes Attribute with single value
func MakeAttribute(name string, tag Tag, value Value) Attribute {
attr := Attribute{Name: name}
attr.Values.Add(tag, value)
return attr
}
// Equal checks that Attribute is equal to another Attribute
// (i.e., names are the same and values are equal)
func (a Attribute) Equal(a2 Attribute) bool {
return a.Name == a2.Name && a.Values.Equal(a2.Values)
}
// Unpack attribute value from its wire representation
func (a *Attribute) unpack(tag Tag, value []byte) error {
var err error
var val Value
switch tag.Type() {
case TypeVoid, TypeCollection:
val = Void{}
case TypeInteger:
val = Integer(0)
case TypeBoolean:
val = Boolean(false)
case TypeString:
val = String("")
case TypeDateTime:
val = Time{}
case TypeResolution:
val = Resolution{}
case TypeRange:
val = Range{}
case TypeTextWithLang:
val = TextWithLang{}
case TypeBinary:
val = Binary(nil)
default:
panic(fmt.Sprintf("(Attribute) uppack(): tag=%s type=%s", tag, tag.Type()))
}
val, err = val.decode(value)
if err == nil {
a.Values.Add(tag, val)
} else {
err = fmt.Errorf("%s: %s", tag, err)
}
return err
}
07070100000060000081A400000000000000000000000167D72F5D0000019D000000000000000000000000000000000000003D00000000ipp-usb-0.9.30/vendor/github.com/OpenPrinting/goipp/const.go/* Go IPP - IPP core protocol implementation in pure Go
*
* Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
* See LICENSE for license terms and conditions
*
* Various constants
*/
package goipp
const (
// ContentType is the HTTP content type for IPP messages
ContentType = "application/ipp"
// msgPrintIndent used for indentation by message pretty-printer
msgPrintIndent = " "
)
07070100000061000081A400000000000000000000000167D72F5D00002614000000000000000000000000000000000000003F00000000ipp-usb-0.9.30/vendor/github.com/OpenPrinting/goipp/decoder.go/* Go IPP - IPP core protocol implementation in pure Go
*
* Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
* See LICENSE for license terms and conditions
*
* IPP Message decoder
*/
package goipp
import (
"encoding/binary"
"errors"
"fmt"
"io"
)
// DecoderOptions represents message decoder options
type DecoderOptions struct {
// EnableWorkarounds, if set to true, enables various workarounds
// for decoding IPP messages that violate IPP protocol specification
//
// Currently it includes the following workarounds:
// * Pantum M7300FDW violates collection encoding rules.
// Instead of using TagMemberName, it uses named attributes
// within the collection
//
// The list of implemented workarounds may grow in the
// future
EnableWorkarounds bool
}
// messageDecoder represents Message decoder
type messageDecoder struct {
in io.Reader // Input stream
off int // Offset of last read
cnt int // Count of read bytes
opt DecoderOptions // Options
}
// Decode the message
func (md *messageDecoder) decode(m *Message) error {
// Wire format:
//
// 2 bytes: Version
// 2 bytes: Code (Operation or Status)
// 4 bytes: RequestID
// variable: attributes
// 1 byte: TagEnd
// Parse message header
var err error
m.Version, err = md.decodeVersion()
if err == nil {
m.Code, err = md.decodeCode()
}
if err == nil {
m.RequestID, err = md.decodeU32()
}
// Now parse attributes
done := false
var group *Attributes
var attr Attribute
var prev *Attribute
for err == nil && !done {
var tag Tag
tag, err = md.decodeTag()
if err != nil {
break
}
if tag.IsDelimiter() {
prev = nil
}
if tag.IsGroup() {
m.Groups.Add(Group{tag, nil})
}
switch tag {
case TagZero:
err = errors.New("Invalid tag 0")
case TagEnd:
done = true
case TagOperationGroup:
group = &m.Operation
case TagJobGroup:
group = &m.Job
case TagPrinterGroup:
group = &m.Printer
case TagUnsupportedGroup:
group = &m.Unsupported
case TagSubscriptionGroup:
group = &m.Subscription
case TagEventNotificationGroup:
group = &m.EventNotification
case TagResourceGroup:
group = &m.Resource
case TagDocumentGroup:
group = &m.Document
case TagSystemGroup:
group = &m.System
case TagFuture11Group:
group = &m.Future11
case TagFuture12Group:
group = &m.Future12
case TagFuture13Group:
group = &m.Future13
case TagFuture14Group:
group = &m.Future14
case TagFuture15Group:
group = &m.Future15
default:
// Decode attribute
if tag == TagMemberName || tag == TagEndCollection {
err = fmt.Errorf("Unexpected tag %s", tag)
} else {
attr, err = md.decodeAttribute(tag)
}
if err == nil && tag == TagBeginCollection {
attr.Values[0].V, err = md.decodeCollection()
}
// If everything is OK, save attribute
switch {
case err != nil:
case attr.Name == "":
if prev != nil {
prev.Values.Add(attr.Values[0].T, attr.Values[0].V)
// Append value to the last Attribute of the
// last Group in the m.Groups
//
// Note, if we are here, this last Attribute definitely exists,
// because:
// * prev != nil
// * prev is set when new named attribute is added
// * prev is reset when delimiter tag is encountered
gLast := &m.Groups[len(m.Groups)-1]
aLast := &gLast.Attrs[len(gLast.Attrs)-1]
aLast.Values.Add(attr.Values[0].T, attr.Values[0].V)
} else {
err = errors.New("Additional value without preceding attribute")
}
case group != nil:
group.Add(attr)
prev = &(*group)[len(*group)-1]
m.Groups[len(m.Groups)-1].Add(attr)
default:
err = errors.New("Attribute without a group")
}
}
}
if err != nil {
err = fmt.Errorf("%s at 0x%x", err, md.off)
}
return err
}
// Decode a Collection
//
// Collection is like a nested object - an attribute which value is a sequence
// of named attributes. Collections can be nested.
//
// Wire format:
// ATTR: Tag = TagBeginCollection, - the outer attribute that
// Name = "name", value - ignored contains the collection
//
// ATTR: Tag = TagMemberName, name = "", - member name \
// value - string, name of the next |
// member | repeated for
// | each member
// ATTR: Tag = any attribute tag, name = "", - repeated for |
// value = member value multi-value /
// members
//
// ATTR: Tag = TagEndCollection, name = "",
// value - ignored
//
// The format looks a bit baroque, but please note that it was added
// in the IPP 2.0. For IPP 1.x collection looks like a single multi-value
// TagBeginCollection attribute (attributes without names considered
// next value for the previously defined named attributes) and so
// 1.x parser silently ignores collections and doesn't get confused
// with them.
func (md *messageDecoder) decodeCollection() (Collection, error) {
collection := make(Collection, 0)
memberName := ""
for {
tag, err := md.decodeTag()
if err != nil {
return nil, err
}
// Delimiter cannot be inside a collection
if tag.IsDelimiter() {
err = fmt.Errorf("Collection: unexpected tag %s", tag)
return nil, err
}
// Check for TagMemberName without the subsequent value attribute
if (tag == TagMemberName || tag == TagEndCollection) && memberName != "" {
err = fmt.Errorf("Collection: unexpected %s, expected value tag", tag)
return nil, err
}
// Fetch next attribute
attr, err := md.decodeAttribute(tag)
if err != nil {
return nil, err
}
// Process next attribute
switch tag {
case TagEndCollection:
return collection, nil
case TagMemberName:
memberName = string(attr.Values[0].V.(String))
if memberName == "" {
err = fmt.Errorf("Collection: %s value is empty", tag)
return nil, err
}
case TagBeginCollection:
// Decode nested collection
attr.Values[0].V, err = md.decodeCollection()
if err != nil {
return nil, err
}
fallthrough
default:
if md.opt.EnableWorkarounds &&
memberName == "" && attr.Name != "" {
// Workaround for: Pantum M7300FDW
//
// This device violates collection encoding rules.
// Instead of using TagMemberName, it uses named
// attributes within the collection
memberName = attr.Name
}
if memberName != "" {
attr.Name = memberName
collection = append(collection, attr)
memberName = ""
} else if len(collection) > 0 {
l := len(collection)
collection[l-1].Values.Add(tag, attr.Values[0].V)
} else {
// We've got a value without preceding TagMemberName
err = fmt.Errorf("Collection: unexpected %s, expected %s", tag, TagMemberName)
return nil, err
}
}
}
}
// Decode a tag
func (md *messageDecoder) decodeTag() (Tag, error) {
t, err := md.decodeU8()
return Tag(t), err
}
// Decode a Version
func (md *messageDecoder) decodeVersion() (Version, error) {
code, err := md.decodeU16()
return Version(code), err
}
// Decode a Code
func (md *messageDecoder) decodeCode() (Code, error) {
code, err := md.decodeU16()
return Code(code), err
}
// Decode a single attribute
//
// Wire format:
// 1 byte: Tag
// 2+N bytes: Name length (2 bytes) + name string
// 2+N bytes: Value length (2 bytes) + value bytes
//
// For the extended tag format, Tag is encoded as TagExtension and
// 4 bytes of the actual tag value prepended to the value bytes
func (md *messageDecoder) decodeAttribute(tag Tag) (Attribute, error) {
var attr Attribute
var value []byte
var err error
// Obtain attribute name and raw value
attr.Name, err = md.decodeString()
if err != nil {
goto ERROR
}
value, err = md.decodeBytes()
if err != nil {
goto ERROR
}
// Handle TagExtension
if tag == TagExtension {
if len(value) < 4 {
err = errors.New("Extension tag truncated")
goto ERROR
}
t := binary.BigEndian.Uint32(value[:4])
value = value[4:]
if t > 0x7fffffff {
err = errors.New("Extension tag out of range")
goto ERROR
}
tag = Tag(t)
}
// Unpack value
err = attr.unpack(tag, value)
if err != nil {
goto ERROR
}
return attr, nil
// Return a error
ERROR:
return Attribute{}, err
}
// Decode a 8-bit integer
func (md *messageDecoder) decodeU8() (uint8, error) {
buf := make([]byte, 1)
err := md.read(buf)
return buf[0], err
}
// Decode a 16-bit integer
func (md *messageDecoder) decodeU16() (uint16, error) {
buf := make([]byte, 2)
err := md.read(buf)
return binary.BigEndian.Uint16(buf[:]), err
}
// Decode a 32-bit integer
func (md *messageDecoder) decodeU32() (uint32, error) {
buf := make([]byte, 4)
err := md.read(buf)
return binary.BigEndian.Uint32(buf[:]), err
}
// Decode sequence of bytes
func (md *messageDecoder) decodeBytes() ([]byte, error) {
length, err := md.decodeU16()
if err != nil {
return nil, err
}
data := make([]byte, length)
err = md.read(data)
if err != nil {
return nil, err
}
return data, nil
}
// Decode string
func (md *messageDecoder) decodeString() (string, error) {
data, err := md.decodeBytes()
if err != nil {
return "", err
}
return string(data), nil
}
// Read a piece of raw data from input stream
func (md *messageDecoder) read(data []byte) error {
md.off = md.cnt
for len(data) > 0 {
n, err := md.in.Read(data)
if n > 0 {
md.cnt += n
data = data[n:]
} else {
md.off = md.cnt
if err == nil || err == io.EOF {
err = errors.New("Message truncated")
}
return err
}
}
return nil
}
07070100000062000081A400000000000000000000000167D72F5D00001240000000000000000000000000000000000000003B00000000ipp-usb-0.9.30/vendor/github.com/OpenPrinting/goipp/doc.go/* Go IPP - IPP core protocol implementation in pure Go
/*
* Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
* See LICENSE for license terms and conditions
*
* Package documentation
*/
/*
Package goipp implements IPP core protocol, as defined by RFC 8010
It doesn't implement high-level operations, such as "print a document",
"cancel print job" and so on. It's scope is limited to proper generation
and parsing of IPP requests and responses.
IPP protocol uses the following simple model:
1. Send a request
2. Receive a response
Request and response both has a similar format, represented here
by type Message, with the only difference, that Code field of
that Message is the Operation code in request and Status code
in response. So most of operations are common for request and
response messages
# Example (Get-Printer-Attributes):
package main
import (
"bytes"
"net/http"
"os"
"github.com/OpenPrinting/goipp"
)
const uri = "http://192.168.1.102:631"
// Build IPP OpGetPrinterAttributes request
func makeRequest() ([]byte, error) {
m := goipp.NewRequest(goipp.DefaultVersion, goipp.OpGetPrinterAttributes, 1)
m.Operation.Add(goipp.MakeAttribute("attributes-charset",
goipp.TagCharset, goipp.String("utf-8")))
m.Operation.Add(goipp.MakeAttribute("attributes-natural-language",
goipp.TagLanguage, goipp.String("en-US")))
m.Operation.Add(goipp.MakeAttribute("printer-uri",
goipp.TagURI, goipp.String(uri)))
m.Operation.Add(goipp.MakeAttribute("requested-attributes",
goipp.TagKeyword, goipp.String("all")))
return m.EncodeBytes()
}
// Check that there is no error
func check(err error) {
if err != nil {
panic(err)
}
}
func main() {
request, err := makeRequest()
check(err)
resp, err := http.Post(uri, goipp.ContentType, bytes.NewBuffer(request))
check(err)
var respMsg goipp.Message
err = respMsg.Decode(resp.Body)
check(err)
respMsg.Print(os.Stdout, false)
}
# Example (Print PDF file):
package main
import (
"bytes"
"errors"
"fmt"
"io"
"net/http"
"os"
"github.com/OpenPrinting/goipp"
)
const (
PrinterURL = "http://192.168.1.102:631/ipp/print"
TestPage = "onepage-a4.pdf"
)
type T int
// checkErr checks for an error. If err != nil, it prints error
// message and exits
func checkErr(err error, format string, args ...interface{}) {
if err != nil {
msg := fmt.Sprintf(format, args...)
fmt.Fprintf(os.Stderr, "%s: %s\n", msg, err)
os.Exit(1)
}
}
// ExamplePrintPDF demo
func main() {
// Build and encode IPP request
req := goipp.NewRequest(goipp.DefaultVersion, goipp.OpPrintJob, 1)
req.Operation.Add(goipp.MakeAttribute("attributes-charset",
goipp.TagCharset, goipp.String("utf-8")))
req.Operation.Add(goipp.MakeAttribute("attributes-natural-language",
goipp.TagLanguage, goipp.String("en-US")))
req.Operation.Add(goipp.MakeAttribute("printer-uri",
goipp.TagURI, goipp.String(PrinterURL)))
req.Operation.Add(goipp.MakeAttribute("requesting-user-name",
goipp.TagName, goipp.String("John Doe")))
req.Operation.Add(goipp.MakeAttribute("job-name",
goipp.TagName, goipp.String("job name")))
req.Operation.Add(goipp.MakeAttribute("document-format",
goipp.TagMimeType, goipp.String("application/pdf")))
payload, err := req.EncodeBytes()
checkErr(err, "IPP encode")
// Open document file
file, err := os.Open(TestPage)
checkErr(err, "Open document file")
defer file.Close()
// Build HTTP request
body := io.MultiReader(bytes.NewBuffer(payload), file)
httpReq, err := http.NewRequest(http.MethodPost, PrinterURL, body)
checkErr(err, "HTTP")
httpReq.Header.Set("content-type", goipp.ContentType)
httpReq.Header.Set("accept", goipp.ContentType)
httpReq.Header.Set("accept-encoding", "gzip, deflate, identity")
// Execute HTTP request
httpRsp, err := http.DefaultClient.Do(httpReq)
if httpRsp != nil {
defer httpRsp.Body.Close()
}
checkErr(err, "HTTP")
if httpRsp.StatusCode/100 != 2 {
checkErr(errors.New(httpRsp.Status), "HTTP")
}
// Decode IPP response
rsp := &goipp.Message{}
err = rsp.Decode(httpRsp.Body)
checkErr(err, "IPP decode")
if goipp.Status(rsp.Code) != goipp.StatusOk {
err = errors.New(goipp.Status(rsp.Code).String())
checkErr(err, "IPP")
}
}
*/
package goipp
07070100000063000081A400000000000000000000000167D72F5D00001334000000000000000000000000000000000000003F00000000ipp-usb-0.9.30/vendor/github.com/OpenPrinting/goipp/encoder.go/* Go IPP - IPP core protocol implementation in pure Go
*
* Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
* See LICENSE for license terms and conditions
*
* IPP Message encoder
*/
package goipp
import (
"errors"
"fmt"
"io"
"math"
)
// Type messageEncoder represents Message encoder
type messageEncoder struct {
out io.Writer // Output stream
}
// Encode the message
func (me *messageEncoder) encode(m *Message) error {
// Wire format:
//
// 2 bytes: Version
// 2 bytes: Code (Operation or Status)
// 4 bytes: RequestID
// variable: attributes
// 1 byte: TagEnd
// Encode message header
var err error
err = me.encodeU16(uint16(m.Version))
if err == nil {
err = me.encodeU16(uint16(m.Code))
}
if err == nil {
err = me.encodeU32(uint32(m.RequestID))
}
// Encode attributes
for _, grp := range m.attrGroups() {
err = me.encodeTag(grp.Tag)
if err == nil {
for _, attr := range grp.Attrs {
if attr.Name == "" {
err = errors.New("Attribute without name")
} else {
err = me.encodeAttr(attr, true)
}
}
}
if err != nil {
break
}
}
if err == nil {
err = me.encodeTag(TagEnd)
}
return err
}
// Encode attribute
func (me *messageEncoder) encodeAttr(attr Attribute, checkTag bool) error {
// Wire format
// 1 byte: Tag
// 2 bytes: len(Name)
// variable: name
// 2 bytes: len(Value)
// variable Value
//
// And each additional value comes as attribute
// without name
if len(attr.Values) == 0 {
return errors.New("Attribute without value")
}
name := attr.Name
for _, val := range attr.Values {
tag := val.T
if checkTag {
if tag.IsDelimiter() || tag == TagMemberName || tag == TagEndCollection {
return fmt.Errorf("Tag %s cannot be used with value", tag)
}
if uint(tag)&0x80000000 != 0 {
return fmt.Errorf("Tag %s exceeds extension tag range", tag)
}
}
var err error
if tag >= 0x100 {
err = me.encodeTag(TagExtension)
} else {
err = me.encodeTag(tag)
}
if err != nil {
return err
}
err = me.encodeName(name)
if err != nil {
return err
}
err = me.encodeValue(val.T, val.V)
if err != nil {
return err
}
name = "" // Each additional value comes without name
}
return nil
}
// Encode 8-bit integer
func (me *messageEncoder) encodeU8(v uint8) error {
return me.write([]byte{v})
}
// Encode 16-bit integer
func (me *messageEncoder) encodeU16(v uint16) error {
return me.write([]byte{byte(v >> 8), byte(v)})
}
// Encode 32-bit integer
func (me *messageEncoder) encodeU32(v uint32) error {
return me.write([]byte{byte(v >> 24), byte(v >> 16), byte(v >> 8), byte(v)})
}
// Encode Tag
func (me *messageEncoder) encodeTag(tag Tag) error {
return me.encodeU8(byte(tag))
}
// Encode Attribute name
func (me *messageEncoder) encodeName(name string) error {
if len(name) > math.MaxInt16 {
return fmt.Errorf("Attribute name exceeds %d bytes",
math.MaxInt16)
}
err := me.encodeU16(uint16(len(name)))
if err == nil {
err = me.write([]byte(name))
}
return err
}
// Encode Attribute value
func (me *messageEncoder) encodeValue(tag Tag, v Value) error {
// Check Value type vs the Tag
tagType := tag.Type()
if tagType == TypeVoid {
v = Void{} // Ignore supplied value
} else if tagType != v.Type() {
return fmt.Errorf("Tag %s: %s value required, %s present",
tag, tagType, v.Type())
}
// Encode the value
//
// If tag >= 0x100, tag is replaced with TagExtension, and actual
// tag value prepended to the data bytes. See RFC 8010, 3.5.2 for
// details
data, err := v.encode()
if err != nil {
return err
}
valueLen := len(data)
if tag >= 0x100 {
valueLen += 4 // Prepend extension tag value to the data
}
if valueLen > math.MaxInt16 {
return fmt.Errorf("Attribute value exceeds %d bytes",
math.MaxInt16)
}
err = me.encodeU16(uint16(valueLen))
if err == nil && tag >= 0x100 {
err = me.encodeU32(uint32(tag))
}
if err == nil {
err = me.write(data)
}
// Handle collection
if collection, ok := v.(Collection); ok {
return me.encodeCollection(tag, collection)
}
return err
}
// Encode collection
func (me *messageEncoder) encodeCollection(tag Tag, collection Collection) error {
for _, attr := range collection {
if attr.Name == "" {
return errors.New("Collection member without name")
}
attrName := MakeAttribute("", TagMemberName, String(attr.Name))
err := me.encodeAttr(attrName, false)
if err == nil {
err = me.encodeAttr(Attribute{Name: "", Values: attr.Values}, true)
}
if err != nil {
return err
}
}
return me.encodeAttr(MakeAttribute("", TagEndCollection, Void{}), false)
}
// Write a piece of raw data to output stream
func (me *messageEncoder) write(data []byte) error {
for len(data) > 0 {
n, err := me.out.Write(data)
if err != nil {
return err
}
data = data[n:]
}
return nil
}
07070100000064000081A400000000000000000000000167D72F5D0000002E000000000000000000000000000000000000003B00000000ipp-usb-0.9.30/vendor/github.com/OpenPrinting/goipp/go.modmodule github.com/OpenPrinting/goipp
go 1.11
07070100000065000081A400000000000000000000000167D72F5D000004B4000000000000000000000000000000000000003D00000000ipp-usb-0.9.30/vendor/github.com/OpenPrinting/goipp/group.go/* Go IPP - IPP core protocol implementation in pure Go
*
* Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
* See LICENSE for license terms and conditions
*
* Groups of attributes
*/
package goipp
// Group represents a group of attributes.
//
// Since 1.1.0
type Group struct {
Tag Tag // Group tag
Attrs Attributes // Group attributes
}
// Groups represents a sequence of groups
//
// The primary purpose of this type is to represent
// messages with repeated groups with the same group tag
//
// See Message type documentation for more details
//
// Since 1.1.0
type Groups []Group
// Add Attribute to the Group
func (g *Group) Add(attr Attribute) {
g.Attrs.Add(attr)
}
// Equal checks that groups g and g2 are equal
func (g Group) Equal(g2 Group) bool {
return g.Tag == g2.Tag && g.Attrs.Equal(g2.Attrs)
}
// Add Group to Groups
func (groups *Groups) Add(g Group) {
*groups = append(*groups, g)
}
// Equal checks that groups and groups2 are equal
func (groups Groups) Equal(groups2 Groups) bool {
if len(groups) != len(groups2) {
return false
}
for i, g := range groups {
g2 := groups2[i]
if !g.Equal(g2) {
return false
}
}
return true
}
07070100000066000081A400000000000000000000000167D72F5D00000352000000000000000000000000000000000000003D00000000ipp-usb-0.9.30/vendor/github.com/OpenPrinting/goipp/index.md# goipp
[](http://godoc.org/github.com/OpenPrinting/goipp)

The goipp library is fairly complete implementation of IPP core protocol in
pure Go. Essentially, it is IPP messages parser/composer. Transport is
not implemented here, because Go standard library has an excellent built-in
HTTP client, and it doesn't make a lot of sense to wrap it here.
High-level requests, like "print a file" are also not implemented, only the
low-level stuff.
All documentation is on godoc.org -- follow the link above. Pull requests
are welcomed, assuming they don't break existing API.
For more information and software downloads, please visit the
[Project's page at GitHub](https://github.com/OpenPrinting/sane-airscan)
07070100000067000081A400000000000000000000000167D72F5D00001E50000000000000000000000000000000000000003F00000000ipp-usb-0.9.30/vendor/github.com/OpenPrinting/goipp/message.go/* Go IPP - IPP core protocol implementation in pure Go
*
* Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
* See LICENSE for license terms and conditions
*
* IPP protocol messages
*/
package goipp
import (
"bytes"
"fmt"
"io"
)
// Code represents Op(operation) or Status codes
type Code uint16
// Version represents a protocol version. It consist
// of Major and Minor version codes, packed into a single
// 16-bit word
type Version uint16
// DefaultVersion is the default IPP version (2.0 for now)
const DefaultVersion Version = 0x0200
// MakeVersion makes version from major and minor parts
func MakeVersion(major, minor uint8) Version {
return Version(major)<<8 | Version(minor)
}
// Major returns a major part of version
func (v Version) Major() uint8 {
return uint8(v >> 8)
}
// Minor returns a minor part of version
func (v Version) Minor() uint8 {
return uint8(v)
}
// String() converts version to string (i.e., "2.0")
func (v Version) String() string {
return fmt.Sprintf("%d.%d", v.Major(), v.Minor())
}
// Message represents a single IPP message, which may be either
// client request or server response
type Message struct {
// Common header
Version Version // Protocol version
Code Code // Operation for request, status for response
RequestID uint32 // Set in request, returned in response
// Groups of Attributes
//
// This field allows to represent messages with repeated
// groups of attributes with the same group tag. The most
// noticeable use case is the Get-Jobs response which uses
// multiple Job groups, one per returned job. See RFC 8011,
// 4.2.6.2. for more details
//
// See also the following discussions which explain the demand
// to implement this interface:
// https://github.com/OpenPrinting/goipp/issues/2
// https://github.com/OpenPrinting/goipp/pull/3
//
// With respect to backward compatibility, the following
// behavior is implemented here:
// 1. (*Message).Decode() fills both Groups and named per-group
// fields (i.e., Operation, Job etc)
// 2. (*Message).Encode() and (*Message) Print, if Groups != nil,
// uses Groups and ignores named per-group fields. Otherwise,
// named fields are used as in 1.0.0
// 3. (*Message) Equal(), for each message uses Groups if
// it is not nil or named per-group fields otherwise.
// In another words, Equal() compares messages as if
// they were encoded
//
// Since 1.1.0
Groups Groups
// Attributes, by group
Operation Attributes // Operation attributes
Job Attributes // Job attributes
Printer Attributes // Printer attributes
Unsupported Attributes // Unsupported attributes
Subscription Attributes // Subscription attributes
EventNotification Attributes // Event Notification attributes
Resource Attributes // Resource attributes
Document Attributes // Document attributes
System Attributes // System attributes
Future11 Attributes // \
Future12 Attributes // \
Future13 Attributes // | Reserved for future extensions
Future14 Attributes // /
Future15 Attributes // /
}
// NewRequest creates a new request message
//
// Use DefaultVersion as a first argument, if you don't
// have any specific needs
func NewRequest(v Version, op Op, id uint32) *Message {
return &Message{
Version: v,
Code: Code(op),
RequestID: id,
}
}
// NewResponse creates a new response message
//
// Use DefaultVersion as a first argument, if you don't
func NewResponse(v Version, status Status, id uint32) *Message {
return &Message{
Version: v,
Code: Code(status),
RequestID: id,
}
}
// Equal checks that two messages are equal
func (m Message) Equal(m2 Message) bool {
if m.Version != m2.Version ||
m.Code != m2.Code ||
m.RequestID != m2.RequestID {
return false
}
groups := m.attrGroups()
groups2 := m2.attrGroups()
return groups.Equal(groups2)
}
// Reset the message into initial state
func (m *Message) Reset() {
*m = Message{}
}
// Encode message
func (m *Message) Encode(out io.Writer) error {
me := messageEncoder{
out: out,
}
return me.encode(m)
}
// EncodeBytes encodes message to byte slice
func (m *Message) EncodeBytes() ([]byte, error) {
var buf bytes.Buffer
err := m.Encode(&buf)
return buf.Bytes(), err
}
// Decode reads message from io.Reader
func (m *Message) Decode(in io.Reader) error {
return m.DecodeEx(in, DecoderOptions{})
}
// DecodeEx reads message from io.Reader
//
// It is extended version of the Decode method, with additional
// DecoderOptions parameter
func (m *Message) DecodeEx(in io.Reader, opt DecoderOptions) error {
md := messageDecoder{
in: in,
opt: opt,
}
m.Reset()
return md.decode(m)
}
// DecodeBytes decodes message from byte slice
func (m *Message) DecodeBytes(data []byte) error {
return m.Decode(bytes.NewBuffer(data))
}
// DecodeBytesEx decodes message from byte slice
//
// It is extended version of the DecodeBytes method, with additional
// DecoderOptions parameter
func (m *Message) DecodeBytesEx(data []byte, opt DecoderOptions) error {
return m.DecodeEx(bytes.NewBuffer(data), opt)
}
// Print pretty-prints the message. The 'request' parameter affects
// interpretation of Message.Code: it is interpreted either
// as Op or as Status
func (m *Message) Print(out io.Writer, request bool) {
out.Write([]byte("{\n"))
fmt.Fprintf(out, msgPrintIndent+"VERSION %s\n", m.Version)
if request {
fmt.Fprintf(out, msgPrintIndent+"OPERATION %s\n", Op(m.Code))
} else {
fmt.Fprintf(out, msgPrintIndent+"STATUS %s\n", Status(m.Code))
}
for _, grp := range m.attrGroups() {
fmt.Fprintf(out, "\n"+msgPrintIndent+"GROUP %s\n", grp.Tag)
for _, attr := range grp.Attrs {
m.printAttribute(out, attr, 1)
out.Write([]byte("\n"))
}
}
out.Write([]byte("}\n"))
}
// Pretty-print an attribute. Handles Collection attributes
// recursively
func (m *Message) printAttribute(out io.Writer, attr Attribute, indent int) {
m.printIndent(out, indent)
fmt.Fprintf(out, "ATTR %q", attr.Name)
tag := TagZero
for _, val := range attr.Values {
if val.T != tag {
fmt.Fprintf(out, " %s:", val.T)
tag = val.T
}
if collection, ok := val.V.(Collection); ok {
out.Write([]byte(" {\n"))
for _, attr2 := range collection {
m.printAttribute(out, attr2, indent+1)
out.Write([]byte("\n"))
}
m.printIndent(out, indent)
out.Write([]byte("}"))
} else {
fmt.Fprintf(out, " %s", val.V)
}
}
}
// Print indentation
func (m *Message) printIndent(out io.Writer, indent int) {
for i := 0; i < indent; i++ {
out.Write([]byte(msgPrintIndent))
}
}
// Get attributes by group. Groups with nil Attributes are skipped,
// but groups with non-nil are not, even if len(Attributes) == 0
//
// This is a helper function for message encoder and pretty-printer
func (m *Message) attrGroups() Groups {
// If m.Groups is set, use it
if m.Groups != nil {
return m.Groups
}
// Initialize slice of groups
groups := Groups{
{TagOperationGroup, m.Operation},
{TagJobGroup, m.Job},
{TagPrinterGroup, m.Printer},
{TagUnsupportedGroup, m.Unsupported},
{TagSubscriptionGroup, m.Subscription},
{TagEventNotificationGroup, m.EventNotification},
{TagResourceGroup, m.Resource},
{TagDocumentGroup, m.Document},
{TagSystemGroup, m.System},
{TagFuture11Group, m.Future11},
{TagFuture12Group, m.Future12},
{TagFuture13Group, m.Future13},
{TagFuture14Group, m.Future14},
{TagFuture15Group, m.Future15},
}
// Skip all empty groups
out := 0
for in := 0; in < len(groups); in++ {
if groups[in].Attrs != nil {
groups[out] = groups[in]
out++
}
}
return groups[:out]
}
07070100000068000081A400000000000000000000000167D72F5D00004482000000000000000000000000000000000000003A00000000ipp-usb-0.9.30/vendor/github.com/OpenPrinting/goipp/op.go/* Go IPP - IPP core protocol implementation in pure Go
*
* Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
* See LICENSE for license terms and conditions
*
* IPP Operation Codes
*/
package goipp
import (
"fmt"
)
// Op represents an IPP Operation Code
type Op Code
// Op codes
const (
OpPrintJob Op = 0x0002 // Print-Job: Print a single file
OpPrintURI Op = 0x0003 // Print-URI: Print a single URL
OpValidateJob Op = 0x0004 // Validate-Job: Validate job values prior to submission
OpCreateJob Op = 0x0005 // Create-Job: Create an empty print job
OpSendDocument Op = 0x0006 // Send-Document: Add a file to a job
OpSendURI Op = 0x0007 // Send-URI: Add a URL to a job
OpCancelJob Op = 0x0008 // Cancel-Job: Cancel a job
OpGetJobAttributes Op = 0x0009 // Get-Job-Attribute: Get information about a job
OpGetJobs Op = 0x000a // Get-Jobs: Get a list of jobs
OpGetPrinterAttributes Op = 0x000b // Get-Printer-Attributes: Get information about a printer
OpHoldJob Op = 0x000c // Hold-Job: Hold a job for printing
OpReleaseJob Op = 0x000d // Release-Job: Release a job for printing
OpRestartJob Op = 0x000e // Restart-Job: Reprint a job
OpPausePrinter Op = 0x0010 // Pause-Printer: Stop a printer
OpResumePrinter Op = 0x0011 // Resume-Printer: Start a printer
OpPurgeJobs Op = 0x0012 // Purge-Jobs: Delete all jobs
OpSetPrinterAttributes Op = 0x0013 // Set-Printer-Attributes: Set printer values
OpSetJobAttributes Op = 0x0014 // Set-Job-Attributes: Set job values
OpGetPrinterSupportedValues Op = 0x0015 // Get-Printer-Supported-Values: Get supported values
OpCreatePrinterSubscriptions Op = 0x0016 // Create-Printer-Subscriptions: Create one or more printer subscriptions
OpCreateJobSubscriptions Op = 0x0017 // Create-Job-Subscriptions: Create one of more job subscriptions
OpGetSubscriptionAttributes Op = 0x0018 // Get-Subscription-Attributes: Get subscription information
OpGetSubscriptions Op = 0x0019 // Get-Subscriptions: Get list of subscriptions
OpRenewSubscription Op = 0x001a // Renew-Subscription: Renew a printer subscription
OpCancelSubscription Op = 0x001b // Cancel-Subscription: Cancel a subscription
OpGetNotifications Op = 0x001c // Get-Notifications: Get notification events
OpSendNotifications Op = 0x001d // Send-Notifications: Send notification events
OpGetResourceAttributes Op = 0x001e // Get-Resource-Attributes: Get resource information
OpGetResourceData Op = 0x001f // Get-Resource-Data: Get resource data
OpGetResources Op = 0x0020 // Get-Resources: Get list of resources
OpGetPrintSupportFiles Op = 0x0021 // Get-Printer-Support-Files: Get printer support files
OpEnablePrinter Op = 0x0022 // Enable-Printer: Accept new jobs for a printer
OpDisablePrinter Op = 0x0023 // Disable-Printer: Reject new jobs for a printer
OpPausePrinterAfterCurrentJob Op = 0x0024 // Pause-Printer-After-Current-Job: Stop printer after the current job
OpHoldNewJobs Op = 0x0025 // Hold-New-Jobs: Hold new jobs
OpReleaseHeldNewJobs Op = 0x0026 // Release-Held-New-Jobs: Release new jobs that were previously held
OpDeactivatePrinter Op = 0x0027 // Deactivate-Printer: Stop a printer and do not accept jobs
OpActivatePrinter Op = 0x0028 // Activate-Printer: Start a printer and accept jobs
OpRestartPrinter Op = 0x0029 // Restart-Printer: Restart a printer
OpShutdownPrinter Op = 0x002a // Shutdown-Printer: Turn a printer off
OpStartupPrinter Op = 0x002b // Startup-Printer: Turn a printer on
OpReprocessJob Op = 0x002c // Reprocess-Job: Reprint a job
OpCancelCurrentJob Op = 0x002d // Cancel-Current-Job: Cancel the current job
OpSuspendCurrentJob Op = 0x002e // Suspend-Current-Job: Suspend the current job
OpResumeJob Op = 0x002f // Resume-Job: Resume the current job
OpPromoteJob Op = 0x0030 // Promote-Job: Promote a job to print sooner
OpScheduleJobAfter Op = 0x0031 // Schedule-Job-After: Schedule a job to print after another
OpCancelDocument Op = 0x0033 // Cancel-Document: Cancel a document
OpGetDocumentAttributes Op = 0x0034 // Get-Document-Attributes: Get document information
OpGetDocuments Op = 0x0035 // Get-Documents: Get a list of documents in a job
OpDeleteDocument Op = 0x0036 // Delete-Document: Delete a document
OpSetDocumentAttributes Op = 0x0037 // Set-Document-Attributes: Set document values
OpCancelJobs Op = 0x0038 // Cancel-Jobs: Cancel all jobs (administrative)
OpCancelMyJobs Op = 0x0039 // Cancel-My-Jobs: Cancel a user's jobs
OpResubmitJob Op = 0x003a // Resubmit-Job: Copy and reprint a job
OpCloseJob Op = 0x003b // Close-Job: Close a job and start printing
OpIdentifyPrinter Op = 0x003c // Identify-Printer: Make the printer beep, flash, or display a message for identification
OpValidateDocument Op = 0x003d // Validate-Document: Validate document values prior to submission
OpAddDocumentImages Op = 0x003e // Add-Document-Images: Add image(s) from the specified scanner source
OpAcknowledgeDocument Op = 0x003f // Acknowledge-Document: Acknowledge processing of a document
OpAcknowledgeIdentifyPrinter Op = 0x0040 // Acknowledge-Identify-Printer: Acknowledge action on an Identify-Printer request
OpAcknowledgeJob Op = 0x0041 // Acknowledge-Job: Acknowledge processing of a job
OpFetchDocument Op = 0x0042 // Fetch-Document: Fetch a document for processing
OpFetchJob Op = 0x0043 // Fetch-Job: Fetch a job for processing
OpGetOutputDeviceAttributes Op = 0x0044 // Get-Output-Device-Attributes: Get printer information for a specific output device
OpUpdateActiveJobs Op = 0x0045 // Update-Active-Jobs: Update the list of active jobs that a proxy has processed
OpDeregisterOutputDevice Op = 0x0046 // Deregister-Output-Device: Remove an output device
OpUpdateDocumentStatus Op = 0x0047 // Update-Document-Status: Update document values
OpUpdateJobStatus Op = 0x0048 // Update-Job-Status: Update job values
OpupdateOutputDeviceAttributes Op = 0x0049 // Update-Output-Device-Attributes: Update output device values
OpGetNextDocumentData Op = 0x004a // Get-Next-Document-Data: Scan more document data
OpAllocatePrinterResources Op = 0x004b // Allocate-Printer-Resources: Use resources for a printer
OpCreatePrinter Op = 0x004c // Create-Printer: Create a new service
OpDeallocatePrinterResources Op = 0x004d // Deallocate-Printer-Resources: Stop using resources for a printer
OpDeletePrinter Op = 0x004e // Delete-Printer: Delete an existing service
OpGetPrinters Op = 0x004f // Get-Printers: Get a list of services
OpShutdownOnePrinter Op = 0x0050 // Shutdown-One-Printer: Shutdown a service
OpStartupOnePrinter Op = 0x0051 // Startup-One-Printer: Start a service
OpCancelResource Op = 0x0052 // Cancel-Resource: Uninstall a resource
OpCreateResource Op = 0x0053 // Create-Resource: Create a new (empty) resource
OpInstallResource Op = 0x0054 // Install-Resource: Install a resource
OpSendResourceData Op = 0x0055 // Send-Resource-Data: Upload the data for a resource
OpSetResourceAttributes Op = 0x0056 // Set-Resource-Attributes: Set resource object attributes
OpCreateResourceSubscriptions Op = 0x0057 // Create-Resource-Subscriptions: Create event subscriptions for a resource
OpCreateSystemSubscriptions Op = 0x0058 // Create-System-Subscriptions: Create event subscriptions for a system
OpDisableAllPrinters Op = 0x0059 // Disable-All-Printers: Stop accepting new jobs on all services
OpEnableAllPrinters Op = 0x005a // Enable-All-Printers: Start accepting new jobs on all services
OpGetSystemAttributes Op = 0x005b // Get-System-Attributes: Get system object attributes
OpGetSystemSupportedValues Op = 0x005c // Get-System-Supported-Values: Get supported values for system object attributes
OpPauseAllPrinters Op = 0x005d // Pause-All-Printers: Stop all services immediately
OpPauseAllPrintersAfterCurrentJob Op = 0x005e // Pause-All-Printers-After-Current-Job: Stop all services after processing the current jobs
OpRegisterOutputDevice Op = 0x005f // Register-Output-Device: Register a remote service
OpRestartSystem Op = 0x0060 // Restart-System: Restart all services
OpResumeAllPrinters Op = 0x0061 // Resume-All-Printers: Start job processing on all services
OpSetSystemAttributes Op = 0x0062 // Set-System-Attributes: Set system object attributes
OpShutdownAllPrinters Op = 0x0063 // Shutdown-All-Printers: Shutdown all services
OpStartupAllPrinters Op = 0x0064 // Startup-All-Printers: Startup all services
OpCupsGetDefault Op = 0x4001 // CUPS-Get-Default: Get the default printer
OpCupsGetPrinters Op = 0x4002 // CUPS-Get-Printers: Get a list of printers and/or classes
OpCupsAddModifyPrinter Op = 0x4003 // CUPS-Add-Modify-Printer: Add or modify a printer
OpCupsDeletePrinter Op = 0x4004 // CUPS-Delete-Printer: Delete a printer
OpCupsGetClasses Op = 0x4005 // CUPS-Get-Classes: Get a list of classes
OpCupsAddModifyClass Op = 0x4006 // CUPS-Add-Modify-Class: Add or modify a class
OpCupsDeleteClass Op = 0x4007 // CUPS-Delete-Class: Delete a class
OpCupsAcceptJobs Op = 0x4008 // CUPS-Accept-Jobs: Accept new jobs on a printer
OpCupsRejectJobs Op = 0x4009 // CUPS-Reject-Jobs: Reject new jobs on a printer
OpCupsSetDefault Op = 0x400a // CUPS-Set-Default: Set the default printer
OpCupsGetDevices Op = 0x400b // CUPS-Get-Devices: Get a list of supported devices
OpCupsGetPpds Op = 0x400c // CUPS-Get-PPDs: Get a list of supported drivers
OpCupsMoveJob Op = 0x400d // CUPS-Move-Job: Move a job to a different printer
OpCupsAuthenticateJob Op = 0x400e // CUPS-Authenticate-Job: Authenticate a job
OpCupsGetPpd Op = 0x400f // CUPS-Get-PPD: Get a PPD file
OpCupsGetDocument Op = 0x4027 // CUPS-Get-Document: Get a document file
OpCupsCreateLocalPrinter Op = 0x4028 // CUPS-Create-Local-Printer: Create a local (temporary) printer
)
// String() returns a Status name, as defined by RFC 8010
func (op Op) String() string {
if int(op) < len(opNames) {
if s := opNames[op]; s != "" {
return s
}
}
return fmt.Sprintf("0x%4.4x", int(op))
}
var opNames = [...]string{
OpPrintJob: "Print-Job",
OpPrintURI: "Print-URI",
OpValidateJob: "Validate-Job",
OpCreateJob: "Create-Job",
OpSendDocument: "Send-Document",
OpSendURI: "Send-URI",
OpCancelJob: "Cancel-Job",
OpGetJobAttributes: "Get-Job-Attribute",
OpGetJobs: "Get-Jobs",
OpGetPrinterAttributes: "Get-Printer-Attributes",
OpHoldJob: "Hold-Job",
OpReleaseJob: "Release-Job",
OpRestartJob: "Restart-Job",
OpPausePrinter: "Pause-Printer",
OpResumePrinter: "Resume-Printer",
OpPurgeJobs: "Purge-Jobs",
OpSetPrinterAttributes: "Set-Printer-Attributes",
OpSetJobAttributes: "Set-Job-Attributes",
OpGetPrinterSupportedValues: "Get-Printer-Supported-Values",
OpCreatePrinterSubscriptions: "Create-Printer-Subscriptions",
OpCreateJobSubscriptions: "Create-Job-Subscriptions",
OpGetSubscriptionAttributes: "Get-Subscription-Attributes",
OpGetSubscriptions: "Get-Subscriptions",
OpRenewSubscription: "Renew-Subscription",
OpCancelSubscription: "Cancel-Subscription",
OpGetNotifications: "Get-Notifications",
OpSendNotifications: "Send-Notifications",
OpGetResourceAttributes: "Get-Resource-Attributes",
OpGetResourceData: "Get-Resource-Data",
OpGetResources: "Get-Resources",
OpGetPrintSupportFiles: "Get-Printer-Support-Files",
OpEnablePrinter: "Enable-Printer",
OpDisablePrinter: "Disable-Printer",
OpPausePrinterAfterCurrentJob: "Pause-Printer-After-Current-Job",
OpHoldNewJobs: "Hold-New-Jobs",
OpReleaseHeldNewJobs: "Release-Held-New-Jobs",
OpDeactivatePrinter: "Deactivate-Printer",
OpActivatePrinter: "Activate-Printer",
OpRestartPrinter: "Restart-Printer",
OpShutdownPrinter: "Shutdown-Printer",
OpStartupPrinter: "Startup-Printer",
OpReprocessJob: "Reprocess-Job",
OpCancelCurrentJob: "Cancel-Current-Job",
OpSuspendCurrentJob: "Suspend-Current-Job",
OpResumeJob: "Resume-Job",
OpPromoteJob: "Promote-Job",
OpScheduleJobAfter: "Schedule-Job-After",
OpCancelDocument: "Cancel-Document",
OpGetDocumentAttributes: "Get-Document-Attributes",
OpGetDocuments: "Get-Documents",
OpDeleteDocument: "Delete-Document",
OpSetDocumentAttributes: "Set-Document-Attributes",
OpCancelJobs: "Cancel-Jobs",
OpCancelMyJobs: "Cancel-My-Jobs",
OpResubmitJob: "Resubmit-Job",
OpCloseJob: "Close-Job",
OpIdentifyPrinter: "Identify-Printer",
OpValidateDocument: "Validate-Document",
OpAddDocumentImages: "Add-Document-Images",
OpAcknowledgeDocument: "Acknowledge-Document",
OpAcknowledgeIdentifyPrinter: "Acknowledge-Identify-Printer",
OpAcknowledgeJob: "Acknowledge-Job",
OpFetchDocument: "Fetch-Document",
OpFetchJob: "Fetch-Job",
OpGetOutputDeviceAttributes: "Get-Output-Device-Attributes",
OpUpdateActiveJobs: "Update-Active-Jobs",
OpDeregisterOutputDevice: "Deregister-Output-Device",
OpUpdateDocumentStatus: "Update-Document-Status",
OpUpdateJobStatus: "Update-Job-Status",
OpupdateOutputDeviceAttributes: "Update-Output-Device-Attributes",
OpGetNextDocumentData: "Get-Next-Document-Data",
OpAllocatePrinterResources: "Allocate-Printer-Resources",
OpCreatePrinter: "Create-Printer",
OpDeallocatePrinterResources: "Deallocate-Printer-Resources",
OpDeletePrinter: "Delete-Printer",
OpGetPrinters: "Get-Printers",
OpShutdownOnePrinter: "Shutdown-One-Printer",
OpStartupOnePrinter: "Startup-One-Printer",
OpCancelResource: "Cancel-Resource",
OpCreateResource: "Create-Resource",
OpInstallResource: "Install-Resource",
OpSendResourceData: "Send-Resource-Data",
OpSetResourceAttributes: "Set-Resource-Attributes",
OpCreateResourceSubscriptions: "Create-Resource-Subscriptions",
OpCreateSystemSubscriptions: "Create-System-Subscriptions",
OpDisableAllPrinters: "Disable-All-Printers",
OpEnableAllPrinters: "Enable-All-Printers",
OpGetSystemAttributes: "Get-System-Attributes",
OpGetSystemSupportedValues: "Get-System-Supported-Values",
OpPauseAllPrinters: "Pause-All-Printers",
OpPauseAllPrintersAfterCurrentJob: "Pause-All-Printers-After-Current-Job",
OpRegisterOutputDevice: "Register-Output-Device",
OpRestartSystem: "Restart-System",
OpResumeAllPrinters: "Resume-All-Printers",
OpSetSystemAttributes: "Set-System-Attributes",
OpShutdownAllPrinters: "Shutdown-All-Printers",
OpStartupAllPrinters: "Startup-All-Printers",
OpCupsGetDefault: "CUPS-Get-Default",
OpCupsGetPrinters: "CUPS-Get-Printers",
OpCupsAddModifyPrinter: "CUPS-Add-Modify-Printer",
OpCupsDeletePrinter: "CUPS-Delete-Printer",
OpCupsGetClasses: "CUPS-Get-Classes",
OpCupsAddModifyClass: "CUPS-Add-Modify-Class",
OpCupsDeleteClass: "CUPS-Delete-Class",
OpCupsAcceptJobs: "CUPS-Accept-Jobs",
OpCupsRejectJobs: "CUPS-Reject-Jobs",
OpCupsSetDefault: "CUPS-Set-Default",
OpCupsGetDevices: "CUPS-Get-Devices",
OpCupsGetPpds: "CUPS-Get-PPDs",
OpCupsMoveJob: "CUPS-Move-Job",
OpCupsAuthenticateJob: "CUPS-Authenticate-Job",
OpCupsGetPpd: "CUPS-Get-PPD",
OpCupsGetDocument: "CUPS-Get-Document",
OpCupsCreateLocalPrinter: "CUPS-Create-Local-Printer",
}
07070100000069000081A400000000000000000000000167D72F5D000026C6000000000000000000000000000000000000003E00000000ipp-usb-0.9.30/vendor/github.com/OpenPrinting/goipp/status.go/* Go IPP - IPP core protocol implementation in pure Go
*
* Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
* See LICENSE for license terms and conditions
*
* IPP Status Codes
*/
package goipp
import (
"fmt"
)
// Status represents an IPP Status Code
type Status Code
// Status codes
const (
StatusOk Status = 0x0000 // successful-ok
StatusOkIgnoredOrSubstituted Status = 0x0001 // successful-ok-ignored-or-substituted-attributes
StatusOkConflicting Status = 0x0002 // successful-ok-conflicting-attributes
StatusOkIgnoredSubscriptions Status = 0x0003 // successful-ok-ignored-subscriptions
StatusOkIgnoredNotifications Status = 0x0004 // successful-ok-ignored-notifications
StatusOkTooManyEvents Status = 0x0005 // successful-ok-too-many-events
StatusOkButCancelSubscription Status = 0x0006 // successful-ok-but-cancel-subscription
StatusOkEventsComplete Status = 0x0007 // successful-ok-events-complete
StatusRedirectionOtherSite Status = 0x0200 // redirection-other-site
StatusCupsSeeOther Status = 0x0280 // cups-see-other
StatusErrorBadRequest Status = 0x0400 // client-error-bad-request
StatusErrorForbidden Status = 0x0401 // client-error-forbidden
StatusErrorNotAuthenticated Status = 0x0402 // client-error-not-authenticated
StatusErrorNotAuthorized Status = 0x0403 // client-error-not-authorized
StatusErrorNotPossible Status = 0x0404 // client-error-not-possible
StatusErrorTimeout Status = 0x0405 // client-error-timeout
StatusErrorNotFound Status = 0x0406 // client-error-not-found
StatusErrorGone Status = 0x0407 // client-error-gone
StatusErrorRequestEntity Status = 0x0408 // client-error-request-entity-too-large
StatusErrorRequestValue Status = 0x0409 // client-error-request-value-too-long
StatusErrorDocumentFormatNotSupported Status = 0x040a // client-error-document-format-not-supported
StatusErrorAttributesOrValues Status = 0x040b // client-error-attributes-or-values-not-supported
StatusErrorURIScheme Status = 0x040c // client-error-uri-scheme-not-supported
StatusErrorCharset Status = 0x040d // client-error-charset-not-supported
StatusErrorConflicting Status = 0x040e // client-error-conflicting-attributes
StatusErrorCompressionNotSupported Status = 0x040f // client-error-compression-not-supported
StatusErrorCompressionError Status = 0x0410 // client-error-compression-error
StatusErrorDocumentFormatError Status = 0x0411 // client-error-document-format-error
StatusErrorDocumentAccess Status = 0x0412 // client-error-document-access-error
StatusErrorAttributesNotSettable Status = 0x0413 // client-error-attributes-not-settable
StatusErrorIgnoredAllSubscriptions Status = 0x0414 // client-error-ignored-all-subscriptions
StatusErrorTooManySubscriptions Status = 0x0415 // client-error-too-many-subscriptions
StatusErrorIgnoredAllNotifications Status = 0x0416 // client-error-ignored-all-notifications
StatusErrorPrintSupportFileNotFound Status = 0x0417 // client-error-print-support-file-not-found
StatusErrorDocumentPassword Status = 0x0418 // client-error-document-password-error
StatusErrorDocumentPermission Status = 0x0419 // client-error-document-permission-error
StatusErrorDocumentSecurity Status = 0x041a // client-error-document-security-error
StatusErrorDocumentUnprintable Status = 0x041b // client-error-document-unprintable-error
StatusErrorAccountInfoNeeded Status = 0x041c // client-error-account-info-needed
StatusErrorAccountClosed Status = 0x041d // client-error-account-closed
StatusErrorAccountLimitReached Status = 0x041e // client-error-account-limit-reached
StatusErrorAccountAuthorizationFailed Status = 0x041f // client-error-account-authorization-failed
StatusErrorNotFetchable Status = 0x0420 // client-error-not-fetchable
StatusErrorInternal Status = 0x0500 // server-error-internal-error
StatusErrorOperationNotSupported Status = 0x0501 // server-error-operation-not-supported
StatusErrorServiceUnavailable Status = 0x0502 // server-error-service-unavailable
StatusErrorVersionNotSupported Status = 0x0503 // server-error-version-not-supported
StatusErrorDevice Status = 0x0504 // server-error-device-error
StatusErrorTemporary Status = 0x0505 // server-error-temporary-error
StatusErrorNotAcceptingJobs Status = 0x0506 // server-error-not-accepting-jobs
StatusErrorBusy Status = 0x0507 // server-error-busy
StatusErrorJobCanceled Status = 0x0508 // server-error-job-canceled
StatusErrorMultipleJobsNotSupported Status = 0x0509 // server-error-multiple-document-jobs-not-supported
StatusErrorPrinterIsDeactivated Status = 0x050a // server-error-printer-is-deactivated
StatusErrorTooManyJobs Status = 0x050b // server-error-too-many-jobs
StatusErrorTooManyDocuments Status = 0x050c // server-error-too-many-documents
)
// String() returns a Status name, as defined by RFC 8010
func (status Status) String() string {
if int(status) < len(statusNames) {
if s := statusNames[status]; s != "" {
return s
}
}
return fmt.Sprintf("0x%4.4x", int(status))
}
var statusNames = [...]string{
StatusOk: "successful-ok",
StatusOkIgnoredOrSubstituted: "successful-ok-ignored-or-substituted-attributes",
StatusOkConflicting: "successful-ok-conflicting-attributes",
StatusOkIgnoredSubscriptions: "successful-ok-ignored-subscriptions",
StatusOkIgnoredNotifications: "successful-ok-ignored-notifications",
StatusOkTooManyEvents: "successful-ok-too-many-events",
StatusOkButCancelSubscription: "successful-ok-but-cancel-subscription",
StatusOkEventsComplete: "successful-ok-events-complete",
StatusRedirectionOtherSite: "redirection-other-site",
StatusCupsSeeOther: "cups-see-other",
StatusErrorBadRequest: "client-error-bad-request",
StatusErrorForbidden: "client-error-forbidden",
StatusErrorNotAuthenticated: "client-error-not-authenticated",
StatusErrorNotAuthorized: "client-error-not-authorized",
StatusErrorNotPossible: "client-error-not-possible",
StatusErrorTimeout: "client-error-timeout",
StatusErrorNotFound: "client-error-not-found",
StatusErrorGone: "client-error-gone",
StatusErrorRequestEntity: "client-error-request-entity-too-large",
StatusErrorRequestValue: "client-error-request-value-too-long",
StatusErrorDocumentFormatNotSupported: "client-error-document-format-not-supported",
StatusErrorAttributesOrValues: "client-error-attributes-or-values-not-supported",
StatusErrorURIScheme: "client-error-uri-scheme-not-supported",
StatusErrorCharset: "client-error-charset-not-supported",
StatusErrorConflicting: "client-error-conflicting-attributes",
StatusErrorCompressionNotSupported: "client-error-compression-not-supported",
StatusErrorCompressionError: "client-error-compression-error",
StatusErrorDocumentFormatError: "client-error-document-format-error",
StatusErrorDocumentAccess: "client-error-document-access-error",
StatusErrorAttributesNotSettable: "client-error-attributes-not-settable",
StatusErrorIgnoredAllSubscriptions: "client-error-ignored-all-subscriptions",
StatusErrorTooManySubscriptions: "client-error-too-many-subscriptions",
StatusErrorIgnoredAllNotifications: "client-error-ignored-all-notifications",
StatusErrorPrintSupportFileNotFound: "client-error-print-support-file-not-found",
StatusErrorDocumentPassword: "client-error-document-password-error",
StatusErrorDocumentPermission: "client-error-document-permission-error",
StatusErrorDocumentSecurity: "client-error-document-security-error",
StatusErrorDocumentUnprintable: "client-error-document-unprintable-error",
StatusErrorAccountInfoNeeded: "client-error-account-info-needed",
StatusErrorAccountClosed: "client-error-account-closed",
StatusErrorAccountLimitReached: "client-error-account-limit-reached",
StatusErrorAccountAuthorizationFailed: "client-error-account-authorization-failed",
StatusErrorNotFetchable: "client-error-not-fetchable",
StatusErrorInternal: "server-error-internal-error",
StatusErrorOperationNotSupported: "server-error-operation-not-supported",
StatusErrorServiceUnavailable: "server-error-service-unavailable",
StatusErrorVersionNotSupported: "server-error-version-not-supported",
StatusErrorDevice: "server-error-device-error",
StatusErrorTemporary: "server-error-temporary-error",
StatusErrorNotAcceptingJobs: "server-error-not-accepting-jobs",
StatusErrorBusy: "server-error-busy",
StatusErrorJobCanceled: "server-error-job-canceled",
StatusErrorMultipleJobsNotSupported: "server-error-multiple-document-jobs-not-supported",
StatusErrorPrinterIsDeactivated: "server-error-printer-is-deactivated",
StatusErrorTooManyJobs: "server-error-too-many-jobs",
StatusErrorTooManyDocuments: "server-error-too-many-documents",
}
0707010000006A000081A400000000000000000000000167D72F5D00001735000000000000000000000000000000000000003B00000000ipp-usb-0.9.30/vendor/github.com/OpenPrinting/goipp/tag.go/* Go IPP - IPP core protocol implementation in pure Go
*
* Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
* See LICENSE for license terms and conditions
*
* IPP Tags
*/
package goipp
import (
"fmt"
)
// Tag represents a tag used in a binary representation
// of the IPP message
type Tag int
// Tag values
const (
// Delimiter tags
TagZero Tag = 0x00 // Zero tag - used for separators
TagOperationGroup Tag = 0x01 // Operation group
TagJobGroup Tag = 0x02 // Job group
TagEnd Tag = 0x03 // End-of-attributes
TagPrinterGroup Tag = 0x04 // Printer group
TagUnsupportedGroup Tag = 0x05 // Unsupported attributes group
TagSubscriptionGroup Tag = 0x06 // Subscription group
TagEventNotificationGroup Tag = 0x07 // Event group
TagResourceGroup Tag = 0x08 // Resource group
TagDocumentGroup Tag = 0x09 // Document group
TagSystemGroup Tag = 0x0a // System group
TagFuture11Group Tag = 0x0b // Future group 11
TagFuture12Group Tag = 0x0c // Future group 12
TagFuture13Group Tag = 0x0d // Future group 13
TagFuture14Group Tag = 0x0e // Future group 14
TagFuture15Group Tag = 0x0f // Future group 15
// Value tags
TagUnsupportedValue Tag = 0x10 // Unsupported value
TagDefault Tag = 0x11 // Default value
TagUnknown Tag = 0x12 // Unknown value
TagNoValue Tag = 0x13 // No-value value
TagNotSettable Tag = 0x15 // Not-settable value
TagDeleteAttr Tag = 0x16 // Delete-attribute value
TagAdminDefine Tag = 0x17 // Admin-defined value
TagInteger Tag = 0x21 // Integer value
TagBoolean Tag = 0x22 // Boolean value
TagEnum Tag = 0x23 // Enumeration value
TagString Tag = 0x30 // Octet string value
TagDateTime Tag = 0x31 // Date/time value
TagResolution Tag = 0x32 // Resolution value
TagRange Tag = 0x33 // Range value
TagBeginCollection Tag = 0x34 // Beginning of collection value
TagTextLang Tag = 0x35 // Text-with-language value
TagNameLang Tag = 0x36 // Name-with-language value
TagEndCollection Tag = 0x37 // End of collection value
TagText Tag = 0x41 // Text value
TagName Tag = 0x42 // Name value
TagReservedString Tag = 0x43 // Reserved for future string value
TagKeyword Tag = 0x44 // Keyword value
TagURI Tag = 0x45 // URI value
TagURIScheme Tag = 0x46 // URI scheme value
TagCharset Tag = 0x47 // Character set value
TagLanguage Tag = 0x48 // Language value
TagMimeType Tag = 0x49 // MIME media type value
TagMemberName Tag = 0x4a // Collection member name value
TagExtension Tag = 0x7f // Extension point for 32-bit tags
)
// IsDelimiter returns true for delimiter tags
func (tag Tag) IsDelimiter() bool {
return uint(tag) < 0x10
}
// IsGroup returns true for group tags
func (tag Tag) IsGroup() bool {
return tag.IsDelimiter() && tag != TagZero && tag != TagEnd
}
// Type returns Type of Value that corresponds to the tag
func (tag Tag) Type() Type {
if tag.IsDelimiter() {
return TypeInvalid
}
switch tag {
case TagInteger, TagEnum:
return TypeInteger
case TagBoolean:
return TypeBoolean
case TagUnsupportedValue, TagDefault, TagUnknown, TagNotSettable,
TagDeleteAttr, TagAdminDefine:
// These tags not expected to have value
return TypeVoid
case TagText, TagName, TagReservedString, TagKeyword, TagURI, TagURIScheme,
TagCharset, TagLanguage, TagMimeType, TagMemberName:
return TypeString
case TagDateTime:
return TypeDateTime
case TagResolution:
return TypeResolution
case TagRange:
return TypeRange
case TagTextLang, TagNameLang:
return TypeTextWithLang
case TagBeginCollection:
return TypeCollection
case TagEndCollection:
return TypeVoid
default:
return TypeBinary
}
}
// String() returns a tag name, as defined by RFC 8010
func (tag Tag) String() string {
if 0 <= tag && int(tag) < len(tagNames) {
if s := tagNames[tag]; s != "" {
return s
}
}
if tag < 0x100 {
return fmt.Sprintf("0x%2.2x", uint(tag))
}
return fmt.Sprintf("0x%8.8x", uint(tag))
}
var tagNames = [...]string{
// Delimiter tags
TagZero: "zero",
TagOperationGroup: "operation-attributes-tag",
TagJobGroup: "job-attributes-tag",
TagEnd: "end-of-attributes-tag",
TagPrinterGroup: "printer-attributes-tag",
TagUnsupportedGroup: "unsupported-attributes-tag",
TagSubscriptionGroup: "subscription-attributes-tag",
TagEventNotificationGroup: "event-notification-attributes-tag",
TagResourceGroup: "resource-attributes-tag",
TagDocumentGroup: "document-attributes-tag",
TagSystemGroup: "system-attributes-tag",
// Value tags
TagUnsupportedValue: "unsupported",
TagDefault: "default",
TagUnknown: "unknown",
TagNoValue: "no-value",
TagNotSettable: "not-settable",
TagDeleteAttr: "delete-attribute",
TagAdminDefine: "admin-define",
TagInteger: "integer",
TagBoolean: "boolean",
TagEnum: "enum",
TagString: "octetString",
TagDateTime: "dateTime",
TagResolution: "resolution",
TagRange: "rangeOfInteger",
TagBeginCollection: "collection",
TagTextLang: "textWithLanguage",
TagNameLang: "nameWithLanguage",
TagEndCollection: "endCollection",
TagText: "textWithoutLanguage",
TagName: "nameWithoutLanguage",
TagKeyword: "keyword",
TagURI: "uri",
TagURIScheme: "uriScheme",
TagCharset: "charset",
TagLanguage: "naturalLanguage",
TagMimeType: "mimeMediaType",
TagMemberName: "memberAttrName",
}
0707010000006B000081A400000000000000000000000167D72F5D000005CE000000000000000000000000000000000000003C00000000ipp-usb-0.9.30/vendor/github.com/OpenPrinting/goipp/type.go/* Go IPP - IPP core protocol implementation in pure Go
*
* Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
* See LICENSE for license terms and conditions
*
* Enumeration of value types
*/
package goipp
import (
"fmt"
)
// Type enumerates all possible value types
type Type int
// Type values
const (
TypeInvalid Type = -1 // Invalid Value type
TypeVoid Type = iota // Value is Void
TypeInteger // Value is Integer
TypeBoolean // Value is Boolean
TypeString // Value is String
TypeDateTime // Value is Time
TypeResolution // Value is Resolution
TypeRange // Value is Range
TypeTextWithLang // Value is TextWithLang
TypeBinary // Value is Binary
TypeCollection // Value is Collection
)
// String converts Type to string, for debugging
func (t Type) String() string {
if t == TypeInvalid {
return "Invalid"
}
if 0 <= t && int(t) < len(typeNames) {
if s := typeNames[t]; s != "" {
return s
}
}
return fmt.Sprintf("0x%4.4x", uint(t))
}
var typeNames = [...]string{
TypeVoid: "Void",
TypeInteger: "Integer",
TypeBoolean: "Boolean",
TypeString: "String",
TypeDateTime: "DateTime",
TypeResolution: "Resolution",
TypeRange: "Range",
TypeTextWithLang: "TextWithLang",
TypeBinary: "Binary",
TypeCollection: "Collection",
}
0707010000006C000081A400000000000000000000000167D72F5D00003806000000000000000000000000000000000000003D00000000ipp-usb-0.9.30/vendor/github.com/OpenPrinting/goipp/value.go/* Go IPP - IPP core protocol implementation in pure Go
*
* Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
* See LICENSE for license terms and conditions
*
* Values for message attributes
*/
package goipp
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"math"
"time"
)
// Values represents a sequence of values with tags.
// Usually Values used as a "payload" of Attribute
type Values []struct {
T Tag // The tag
V Value // The value
}
// Add Value to Values
func (values *Values) Add(t Tag, v Value) {
*values = append(*values, struct {
T Tag
V Value
}{t, v})
}
// String converts Values to string
func (values Values) String() string {
if len(values) == 1 {
return values[0].V.String()
}
var buf bytes.Buffer
buf.Write([]byte("["))
for i, v := range values {
if i != 0 {
buf.Write([]byte(","))
}
buf.Write([]byte(v.V.String()))
}
buf.Write([]byte("]"))
return buf.String()
}
// Equal performs deep check of equality of two Values
func (values Values) Equal(values2 Values) bool {
if len(values) != len(values2) {
return false
}
for i, v := range values {
v2 := values2[i]
if v.T != v2.T || !ValueEqual(v.V, v2.V) {
return false
}
}
return true
}
// Value represents an attribute value
//
// IPP uses typed values, and type of each value is unambiguously
// defined by the attribute tag
type Value interface {
String() string
Type() Type
encode() ([]byte, error)
decode([]byte) (Value, error)
}
// ValueEqual checks if two values are equal
//
// Equality means that types and values are equal. For structured
// values, like Collection, deep comparison is performed
func ValueEqual(v1, v2 Value) bool {
if v1.Type() != v2.Type() {
return false
}
switch v1.Type() {
case TypeDateTime:
return v1.(Time).Equal(v2.(Time).Time)
case TypeBinary:
return bytes.Equal(v1.(Binary), v2.(Binary))
case TypeCollection:
c1 := Attributes(v1.(Collection))
c2 := Attributes(v2.(Collection))
return c1.Equal(c2)
}
return v1 == v2
}
// Void is the Value that represents "no value"
//
// Use with: TagUnsupportedValue, TagDefault, TagUnknown,
// TagNotSettable, TagDeleteAttr, TagAdminDefine
type Void struct{}
// String converts Void Value to string
func (Void) String() string { return "" }
// Type returns type of Value (TypeVoid for Void)
func (Void) Type() Type { return TypeVoid }
// Encode Void Value into wire format
func (v Void) encode() ([]byte, error) {
return []byte{}, nil
}
// Decode Void Value from wire format
func (Void) decode([]byte) (Value, error) {
return Void{}, nil
}
// Integer is the Value that represents 32-bit signed int
//
// Use with: TagInteger, TagEnum
type Integer int32
// String converts Integer value to string
func (v Integer) String() string { return fmt.Sprintf("%d", int32(v)) }
// Type returns type of Value (TypeInteger for Integer)
func (Integer) Type() Type { return TypeInteger }
// Encode Integer Value into wire format
func (v Integer) encode() ([]byte, error) {
return []byte{byte(v >> 24), byte(v >> 16), byte(v >> 8), byte(v)}, nil
}
// Decode Integer Value from wire format
func (Integer) decode(data []byte) (Value, error) {
if len(data) != 4 {
return nil, errors.New("value must be 4 bytes")
}
return Integer(binary.BigEndian.Uint32(data)), nil
}
// Boolean is the Value that contains true of false
//
// Use with: TagBoolean
type Boolean bool
// String converts Boolean value to string
func (v Boolean) String() string { return fmt.Sprintf("%t", bool(v)) }
// Type returns type of Value (TypeBoolean for Boolean)
func (Boolean) Type() Type { return TypeBoolean }
// Encode Boolean Value into wire format
func (v Boolean) encode() ([]byte, error) {
if v {
return []byte{1}, nil
}
return []byte{0}, nil
}
// Decode Boolean Value from wire format
func (Boolean) decode(data []byte) (Value, error) {
if len(data) != 1 {
return nil, errors.New("value must be 1 byte")
}
return Boolean(data[0] != 0), nil
}
// String is the Value that represents string of text
//
// Use with: TagText, TagName, TagReservedString, TagKeyword, TagURI,
// TagURIScheme, TagCharset, TagLanguage, TagMimeType, TagMemberName
type String string
// String converts String value to string
func (v String) String() string { return string(v) }
// Type returns type of Value (TypeString for String)
func (String) Type() Type { return TypeString }
// Encode String Value into wire format
func (v String) encode() ([]byte, error) {
return []byte(v), nil
}
// Decode String Value from wire format
func (String) decode(data []byte) (Value, error) {
return String(data), nil
}
// Time is the Value that represents DataTime
//
// Use with: TagTime
type Time struct{ time.Time }
// String converts Time value to string
func (v Time) String() string { return v.Time.Format(time.RFC3339) }
// Type returns type of Value (TypeDateTime for Time)
func (Time) Type() Type { return TypeDateTime }
// Encode Time Value into wire format
func (v Time) encode() ([]byte, error) {
// From RFC2579:
//
// field octets contents range
// ----- ------ -------- -----
// 1 1-2 year* 0..65536
// 2 3 month 1..12
// 3 4 day 1..31
// 4 5 hour 0..23
// 5 6 minutes 0..59
// 6 7 seconds 0..60
// (use 60 for leap-second)
// 7 8 deci-seconds 0..9
// 8 9 direction from UTC '+' / '-'
// 9 10 hours from UTC* 0..13
// 10 11 minutes from UTC 0..59
//
// * Notes:
// - the value of year is in network-byte order
// - daylight saving time in New Zealand is +13
year := v.Year()
_, zone := v.Zone()
dir := byte('+')
if zone < 0 {
zone = -zone
dir = '-'
}
return []byte{
byte(year >> 8), byte(year),
byte(v.Month()),
byte(v.Day()),
byte(v.Hour()),
byte(v.Minute()),
byte(v.Second()),
byte(v.Nanosecond() / 100000000),
dir,
byte(zone / 3600),
byte((zone / 60) % 60),
}, nil
}
// Decode Time Value from wire format
func (Time) decode(data []byte) (Value, error) {
// Check size
if len(data) != 11 {
return nil, errors.New("value must be 11 bytes")
}
// Validate ranges
var err error
switch {
case data[2] < 1 || data[2] > 12:
err = fmt.Errorf("bad month %d", data[2])
case data[3] < 1 || data[3] > 31:
err = fmt.Errorf("bad day %d", data[3])
case data[4] > 23:
err = fmt.Errorf("bad hours %d", data[4])
case data[5] > 59:
err = fmt.Errorf("bad minutes %d", data[5])
case data[6] > 60:
err = fmt.Errorf("bad seconds %d", data[6])
case data[7] > 9:
err = fmt.Errorf("bad deciseconds %d", data[7])
case data[8] != '+' && data[8] != '-':
return nil, errors.New("bad UTC sign")
case data[9] > 11:
err = fmt.Errorf("bad UTC hours %d", data[9])
case data[10] > 59:
err = fmt.Errorf("bad UTC minutes %d", data[10])
}
if err != nil {
return Time{}, err
}
// Decode time zone
tzName := fmt.Sprintf("UTC%c%d", data[8], data[9])
if data[10] != 0 {
tzName += fmt.Sprintf(":%d", data[10])
}
tzOff := 3600*int(data[9]) + 60*int(data[10])
if data[8] == '-' {
tzOff = -tzOff
}
tz := time.FixedZone(tzName, tzOff)
// Decode time
t := time.Date(
int(binary.BigEndian.Uint16(data[0:2])), // year
time.Month(data[2]), // month
int(data[3]), // day
int(data[4]), // hour
int(data[5]), // min
int(data[6]), // sec
int(data[7])*100000000, // nsec
tz, // time zone
)
return Time{t}, nil
}
// Resolution is the Value that represents image resolution.
//
// Use with: TagResolution
type Resolution struct {
Xres, Yres int // X/Y resolutions
Units Units // Resolution units
}
// String converts Resolution value to string
func (v Resolution) String() string {
return fmt.Sprintf("%dx%d%s", v.Xres, v.Yres, v.Units)
}
// Type returns type of Value (TypeResolution for Resolution)
func (Resolution) Type() Type { return TypeResolution }
// Encode Resolution Value into wire format
func (v Resolution) encode() ([]byte, error) {
// Wire format
// 4 bytes: Xres
// 4 bytes: Yres
// 1 byte: Units
x, y := v.Xres, v.Yres
return []byte{
byte(x >> 24), byte(x >> 16), byte(x >> 8), byte(x),
byte(y >> 24), byte(y >> 16), byte(y >> 8), byte(y),
byte(v.Units),
}, nil
}
// Decode Resolution Value from wire format
func (Resolution) decode(data []byte) (Value, error) {
if len(data) != 9 {
return nil, errors.New("value must be 9 bytes")
}
return Resolution{
Xres: int(binary.BigEndian.Uint32(data[0:4])),
Yres: int(binary.BigEndian.Uint32(data[4:8])),
Units: Units(data[8]),
}, nil
}
// Units represents resolution units
type Units uint8
// Resolution units codes
const (
UnitsDpi Units = 3 // Dots per inch
UnitsDpcm Units = 4 // Dots per cm
)
// String converts Units to string
func (u Units) String() string {
switch u {
case UnitsDpi:
return "dpi"
case UnitsDpcm:
return "dpcm"
default:
return fmt.Sprintf("0x%2.2x", uint8(u))
}
}
// Range is the Value that represents a range of 32-bit signed integers
//
// Use with: TagRange
type Range struct {
Lower, Upper int // Lower/upper bounds
}
// String converts Range value to string
func (v Range) String() string {
return fmt.Sprintf("%d-%d", v.Lower, v.Upper)
}
// Type returns type of Value (TypeRange for Range)
func (Range) Type() Type { return TypeRange }
// Encode Range Value into wire format
func (v Range) encode() ([]byte, error) {
// Wire format
// 4 bytes: Lower
// 4 bytes: Upper
l, u := v.Lower, v.Upper
return []byte{
byte(l >> 24), byte(l >> 16), byte(l >> 8), byte(l),
byte(u >> 24), byte(u >> 16), byte(u >> 8), byte(u),
}, nil
}
// Decode Range Value from wire format
func (Range) decode(data []byte) (Value, error) {
if len(data) != 8 {
return nil, errors.New("value must be 8 bytes")
}
return Range{
Lower: int(binary.BigEndian.Uint32(data[0:4])),
Upper: int(binary.BigEndian.Uint32(data[4:8])),
}, nil
}
// TextWithLang is the Value that represents a combination
// of two strings:
// * text on some natural language (i.e., "hello")
// * name of that language (i.e., "en")
//
// Use with: TagTextLang, TagNameLang
type TextWithLang struct {
Lang, Text string // Language and text
}
// String converts TextWithLang value to string
func (v TextWithLang) String() string { return v.Text + " [" + v.Lang + "]" }
// Type returns type of Value (TypeTextWithLang for TextWithLang)
func (TextWithLang) Type() Type { return TypeTextWithLang }
// Encode TextWithLang Value into wire format
func (v TextWithLang) encode() ([]byte, error) {
// Wire format
// 2 bytes: len(Lang)
// variable: Lang
// 2 bytes: len(Text)
// variable: Text
lang := []byte(v.Lang)
text := []byte(v.Text)
if len(lang) > math.MaxUint16 {
return nil, fmt.Errorf("Lang exceeds %d bytes", math.MaxUint16)
}
if len(text) > math.MaxUint16 {
return nil, fmt.Errorf("Text exceeds %d bytes", math.MaxUint16)
}
data := make([]byte, 2+2+len(lang)+len(text))
binary.BigEndian.PutUint16(data, uint16(len(lang)))
copy(data[2:], []byte(lang))
data2 := data[2+len(lang):]
binary.BigEndian.PutUint16(data2, uint16(len(text)))
copy(data2[2:], []byte(text))
return data, nil
}
// Decode TextWithLang Value from wire format
func (TextWithLang) decode(data []byte) (Value, error) {
var langLen, textLen int
var lang, text string
// Unpack language length
if len(data) < 2 {
return nil, errors.New("truncated language length")
}
langLen = int(binary.BigEndian.Uint16(data[0:2]))
data = data[2:]
// Unpack language value
if len(data) < langLen {
return nil, errors.New("truncated language name")
}
lang = string(data[:langLen])
data = data[langLen:]
// Unpack text length
if len(data) < 2 {
return nil, errors.New("truncated text length")
}
textLen = int(binary.BigEndian.Uint16(data[0:2]))
data = data[2:]
// Unpack text value
if len(data) < textLen {
return nil, errors.New("truncated text string")
}
text = string(data[:textLen])
data = data[textLen:]
// We must have consumed all bytes at this point
if len(data) != 0 {
return nil, fmt.Errorf("extra %d bytes at the end of value",
len(data))
}
// Return a value
return TextWithLang{Lang: lang, Text: text}, nil
}
// Binary is the Value that represents a raw binary data
type Binary []byte
// String converts Binary value to string
func (v Binary) String() string {
return fmt.Sprintf("%x", []byte(v))
}
// Type returns type of Value (TypeBinary for Binary)
func (Binary) Type() Type { return TypeBinary }
// Encode TextWithLang Value into wire format
func (v Binary) encode() ([]byte, error) {
return []byte(v), nil
}
// Decode Binary Value from wire format
func (Binary) decode(data []byte) (Value, error) {
return Binary(data), nil
}
// Collection is the Value that represents collection of attributes
//
// Use with: TagBeginCollection
type Collection Attributes
// Add Attribute to Attributes
func (v *Collection) Add(attr Attribute) {
*v = append(*v, attr)
}
// Equal checks that two collections are equal
func (v Collection) Equal(v2 Attributes) bool {
return Attributes(v).Equal(Attributes(v2))
}
// String converts Collection to string
func (v Collection) String() string {
var buf bytes.Buffer
buf.Write([]byte("{"))
for i, attr := range v {
if i > 0 {
buf.Write([]byte(" "))
}
fmt.Fprintf(&buf, "%s=%s", attr.Name, attr.Values)
}
buf.Write([]byte("}"))
return buf.String()
}
// Type returns type of Value (TypeCollection for Collection)
func (Collection) Type() Type { return TypeCollection }
// Encode Collection Value into wire format
func (Collection) encode() ([]byte, error) {
// Note, TagBeginCollection attribute contains
// no data, collection itself handled the different way
return []byte{}, nil
}
// Decode Collection Value from wire format
func (Collection) decode(data []byte) (Value, error) {
panic("internal error")
}
0707010000006D000081A400000000000000000000000167D72F5D00000045000000000000000000000000000000000000002200000000ipp-usb-0.9.30/vendor/modules.txt# github.com/OpenPrinting/goipp v1.1.0
github.com/OpenPrinting/goipp
07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!827 blocks