pax_global_header00006660000000000000000000000064146567447010014530gustar00rootroot0000000000000052 comment=4b39c447f5ab9be4a6626cd56539d6742a48db01 ipp-usb-0.9.28/000077500000000000000000000000001465674470100132075ustar00rootroot00000000000000ipp-usb-0.9.28/.gitignore000066400000000000000000000000321465674470100151720ustar00rootroot00000000000000ipp-usb tags *.swp *.orig ipp-usb-0.9.28/LICENSE000066400000000000000000000024571465674470100142240ustar00rootroot00000000000000BSD 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. ipp-usb-0.9.28/Makefile000066400000000000000000000014411465674470100146470ustar00rootroot00000000000000MANDIR = /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 ipp-usb-0.9.28/README.md000066400000000000000000000266111465674470100144740ustar00rootroot00000000000000# ipp-usb ![GitHub](https://img.shields.io/github/license/OpenPrinting/ipp-usb) [![Go Report Card](https://goreportcard.com/badge/github.com/OpenPrinting/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. ## 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. ipp-usb-0.9.28/addpdl_test.go000066400000000000000000000037741465674470100160400ustar00rootroot00000000000000/* 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: extected %q, got %q", i+1, data.out, txt[0].Value) } } } ipp-usb-0.9.28/auth.go000066400000000000000000000176271465674470100145140ustar00rootroot00000000000000/* 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 = "No auth rules don 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 } ipp-usb-0.9.28/avahi/000077500000000000000000000000001465674470100142775ustar00rootroot00000000000000ipp-usb-0.9.28/avahi/avahi-localhost.patch000066400000000000000000000054721465674470100204060ustar00rootroot00000000000000diff --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 #include #include +#include #include #include @@ -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, ipp-usb-0.9.28/conf.go000066400000000000000000000122741465674470100144710ustar00rootroot00000000000000/* 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 } ipp-usb-0.9.28/const.go000066400000000000000000000014161465674470100146660ustar00rootroot00000000000000/* 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 ) ipp-usb-0.9.28/ctrlsock.go000066400000000000000000000055271465674470100153730ustar00rootroot00000000000000/* 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 } ipp-usb-0.9.28/daemon.go000066400000000000000000000041541465674470100150050ustar00rootroot00000000000000/* 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 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 } ipp-usb-0.9.28/device.go000066400000000000000000000132101465674470100147720ustar00rootroot00000000000000/* 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" "time" ) // 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 // Create USB transport dev.UsbTransport, err = NewUsbTransport(desc) if err != nil { goto ERROR } // Obtain device's logger info = dev.UsbTransport.UsbDeviceInfo() dev.Log = dev.UsbTransport.Log() // 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 } // Create HTTP server dev.UsbTransport.SetDeadline( time.Now(). Add(DevInitTimeout). Add(dev.UsbTransport.Quirks().GetInitDelay())) dev.HTTPProxy = NewHTTPProxy(dev.Log, listener, dev.UsbTransport) // Obtain DNS-SD info for IPP log = dev.Log.Begin() defer log.Commit() ippinfo, err = IppService(log, &dnssdServices, dev.State.HTTPPort, info, dev.UsbTransport.Quirks(), dev.HTTPClient) if err != nil { dev.Log.Error('!', "IPP: %s", err) } log.Flush() if dev.UsbTransport.DeadlineExpired() { 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 err = EsclService(log, &dnssdServices, dev.State.HTTPPort, info, ippinfo, dev.HTTPClient) if err != nil { dev.Log.Error('!', "ESCL: %s", err) } log.Flush() if dev.UsbTransport.DeadlineExpired() { 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 } // 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.SetDeadline(time.Time{}) 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 { dev.UsbTransport.Close(true) } 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 } } ipp-usb-0.9.28/devstate.go000066400000000000000000000135011465674470100153550ustar00rootroot00000000000000/* 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...) } ipp-usb-0.9.28/dnssd.go000066400000000000000000000171051465674470100146550ustar00rootroot00000000000000/* 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) } } } ipp-usb-0.9.28/dnssd_avahi.go000066400000000000000000000232431465674470100160250ustar00rootroot00000000000000// +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 // #include // #include // #include // #include // // 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) } ipp-usb-0.9.28/err.go000066400000000000000000000013561465674470100143330ustar00rootroot00000000000000/* 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" ) // 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") ) ipp-usb-0.9.28/escl.go000066400000000000000000000153701465674470100144720ustar00rootroot00000000000000/* 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) (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() 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: 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{}{} } } ipp-usb-0.9.28/flock_unix.go000066400000000000000000000025641465674470100157060ustar00rootroot00000000000000// +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 #include 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) } ipp-usb-0.9.28/glob.go000066400000000000000000000030131465674470100144560ustar00rootroot00000000000000/* 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 } ipp-usb-0.9.28/glob_test.go000066400000000000000000000015531465674470100155240ustar00rootroot00000000000000/* 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) } } } ipp-usb-0.9.28/go.mod000066400000000000000000000001361465674470100143150ustar00rootroot00000000000000module github.com/OpenPrinting/ipp-usb go 1.11 require github.com/OpenPrinting/goipp v1.1.0 ipp-usb-0.9.28/go.sum000066400000000000000000000002611465674470100143410ustar00rootroot00000000000000github.com/OpenPrinting/goipp v1.1.0 h1:AK19DwnuvCaqbF6ckT2ICe/Hc1o1sVSS+UzE59z4Dx0= github.com/OpenPrinting/goipp v1.1.0/go.mod h1:ot2iw+QF7fVLaX+55JUNlF5YSDNiXVo2LRAv21iGcQI= ipp-usb-0.9.28/http.go000066400000000000000000000147631465674470100145300ustar00rootroot00000000000000/* 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 } } ipp-usb-0.9.28/inifile.go000066400000000000000000000341601465674470100151610ustar00rootroot00000000000000/* 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 } // LoadQuirksResetMethod loads QuirksResetMethod value // The destination remains untouched in a case of an error func (rec *IniRecord) LoadQuirksResetMethod(out *QuirksResetMethod) error { switch rec.Value { case "none": *out = QuirksResetNone return nil case "soft": *out = QuirksResetSoft return nil case "hard": *out = QuirksResetHard return nil default: return rec.errBadValue("must be none, soft or hard") } } // LoadQuirksBuggyIppRsp loads QuirksBuggyIppRsp value // The destination remains untouched in a case of an error func (rec *IniRecord) LoadQuirksBuggyIppRsp(out *QuirksBuggyIppRsp) error { switch rec.Value { case "allow": *out = QuirksBuggyIppRspAllow return nil case "reject": *out = QuirksBuggyIppRspReject return nil case "sanitize": *out = QuirksBuggyIppRspSanitize return nil default: return rec.errBadValue("must be allow, reject or sanitize") } } // 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) } ipp-usb-0.9.28/inifile_test.go000066400000000000000000000032121465674470100162120ustar00rootroot00000000000000/* 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) } } ipp-usb-0.9.28/ipp-usb-quirks/000077500000000000000000000000001465674470100161025ustar00rootroot00000000000000ipp-usb-0.9.28/ipp-usb-quirks/Canon.conf000066400000000000000000000004521465674470100200100ustar00rootroot00000000000000# 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 ipp-usb-0.9.28/ipp-usb-quirks/HP.conf000066400000000000000000000022261465674470100172620ustar00rootroot00000000000000# 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 ipp-usb-0.9.28/ipp-usb-quirks/Pantum.conf000066400000000000000000000012561465674470100202210ustar00rootroot00000000000000# 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 ipp-usb-0.9.28/ipp-usb-quirks/README000066400000000000000000000036501465674470100167660ustar00rootroot00000000000000This directory contains a collection of quirks files for various devices. Each file consist of sections, each section contains various parameters: [Device Name] http-xxx = yyy blacklist = false | true When searching for quirks for a particular device, device name is matched against section names. Section names may contain a glob-style wildcards (* or ?). If device name must contain one of these characters, use \ as escape. All matching sections are taken in consideration and sorted in priority order. Priority is ordered by amount of matched non-wildcard characters. The more non-wildcard characters are matched, the more the priority If some parameter was found in multiple sections, the value for most prioritized section is taken The following parameters are recognized: blacklist = true | false - blacklist or not the matching devices buggy-ipp-responses = reject | - how to handle buggy IPP responses allow | sanitize disable-fax = true | false - disable fax capability, even if present http-xxx = yyy - set HTTP header Xxx: yyy http-xxx = "" - drop HTTP header Xxx ignore-ipp-status = true|false - If enabled, ignore IPP status of requests that ipp-usb has sent by itself. It affects only ipp-usb initialization init-delay = NNN - delay, in milliseconds, between device is opened and, optionally, reset and the first request is sent to device init-reset = none | soft | hard - how to reset device during initialization, default is none request-delay = NNN - delay, in milliseconds, between subsequent requests usb-max-interfaces = N - use no more that N first USB interfaces See `man ipp-usb` for details ipp-usb-0.9.28/ipp-usb-quirks/blacklist.conf000066400000000000000000000004241465674470100207210ustar00rootroot00000000000000# 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 ipp-usb-0.9.28/ipp-usb-quirks/default.conf000066400000000000000000000001451465674470100203750ustar00rootroot00000000000000# ipp-usb quirks file -- defaults [*] # Drop Connection: header by default http-connection = "" ipp-usb-0.9.28/ipp-usb.8000066400000000000000000000354701465674470100146700ustar00rootroot00000000000000.\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 .TH "IPP\-USB" "8" "June 2024" "" "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 "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 = NNN\fR .br Delay, in milliseconds, between device is opened and, optionally, reset, and the first request is sent to device .IP "\(bu" 4 \fBinit\-reset = none | soft | hard\fR .br How to reset device during initialization\. Default is \fBnone\fR .IP "\(bu" 4 \fBrequest\-delay\fR = NNN .br Delay, in milliseconds, between subsequent requests .IP "\(bu" 4 \fBusb\-max\-interfaces = N\fR .br Don't use more that N USB interfaces, even if more is available .IP "" 0 .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/\.log\fR: per\-device log files .IP "\(bu" 4 \fB/var/ipp\-usb/dev/\.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 ipp-usb-0.9.28/ipp-usb.8.md000066400000000000000000000352451465674470100152670ustar00rootroot00000000000000ipp-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. ## 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`
If `true`, the matching device is ignored by the `ipp-usb` * `buggy-ipp-responses = reject | allow | sanitize`
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`
If `true`, the matching device's fax capability is ignored * `http-XXX = YYY`
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`
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 = NNN`
Delay, in milliseconds, between device is opened and, optionally, reset, and the first request is sent to device * `init-reset = none | soft | hard`
How to reset device during initialization. Default is `none` * `request-delay` = NNN
Delay, in milliseconds, between subsequent requests * `usb-max-interfaces = N`
Don't use more that N USB interfaces, even if more is available 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/.log`: per-device log files * `/var/ipp-usb/dev/.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)
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 ipp-usb-0.9.28/ipp-usb.conf000066400000000000000000000064731465674470100154470ustar00rootroot00000000000000# 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 ipp-usb-0.9.28/ipp.go000066400000000000000000000326201465674470100143310ustar00rootroot00000000000000/* 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 QuirksSet, c *http.Client) (ippinfo *IppPrinterInfo, err error) { // Query printer attributes uri := fmt.Sprintf("http://localhost:%d/ipp/print", port) msg, err := ippGetPrinterAttributes(log, c, quirks, uri) if err != nil { return } // Decode IPP service info attrs := newIppDecoder(msg) ippinfo, ippScv := 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("http://localhost:%d/ipp/faxout", port) if _, err2 := ippGetPrinterAttributes(log, c, quirks, uri); 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 { ippScv.Txt.Add("Fax", "T") ippScv.Txt.Add("rfo", "ipp/faxout") } else { ippScv.Txt.Add("Fax", "F") } // Construct LPD info. Per Apple spec, we MUST advertise // LPD with zero port, even if we don't support it lpdScv := DNSSdSvcInfo{ Type: "_printer._tcp", Port: 0, Txt: nil, } // Pack it all together ippScv.Port = port services.Add(lpdScv) ippinfo.IppSvcIndex = len(*services) services.Add(ippScv) 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 QuirksSet, uri string) (msg *goipp.Message, 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 { err = fmt.Errorf("HTTP: %s", err) return } defer resp.Body.Close() // Check HTTP status if resp.StatusCode/100 != 2 { 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() == QuirksBuggyIppRspAllow { 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: // "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 } ipp-usb-0.9.28/linewriter.go000066400000000000000000000035151465674470100157260ustar00rootroot00000000000000/* 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() } ipp-usb-0.9.28/listener.go000066400000000000000000000033741465674470100153720ustar00rootroot00000000000000/* 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 } } ipp-usb-0.9.28/logger.go000066400000000000000000000433601465674470100150230ustar00rootroot00000000000000/* 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 } ipp-usb-0.9.28/logger_unix.go000066400000000000000000000020671465674470100160650ustar00rootroot00000000000000// +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 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)) } ipp-usb-0.9.28/loopback.go000066400000000000000000000012221465674470100153250ustar00rootroot00000000000000/* 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) } ipp-usb-0.9.28/main.go000066400000000000000000000165241465674470100144720ustar00rootroot00000000000000/* 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 } } ipp-usb-0.9.28/paper.go000066400000000000000000000040161465674470100146460ustar00rootroot00000000000000/* 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 // "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 "isoC-A2") } // HP LaserJet MFP M28 testPaperSizeClassify(t, PaperSize{21590, 29692}, "legal-A4") } ipp-usb-0.9.28/paths.go000066400000000000000000000023341465674470100146570ustar00rootroot00000000000000/* 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" ) ipp-usb-0.9.28/pnp.go000066400000000000000000000074731465674470100143460ustar00rootroot00000000000000/* 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]) StatusSet(addr, devDescs[addr], 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]) StatusSet(addr, devDescs[addr], 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 } ipp-usb-0.9.28/quirks.go000066400000000000000000000256321465674470100150640ustar00rootroot00000000000000/* 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" "strings" "time" ) // Quirks represents device-specific quirks type Quirks struct { Origin string // file:line of definition Model string // Device model name HTTPHeaders map[string]string // HTTP header override Blacklist bool // Blacklist the device BuggyIppRsp QuirksBuggyIppRsp // Handling of buggy IPP responses DisableFax bool // Disable fax for device IgnoreIppStatus bool // Ignore IPP status InitDelay time.Duration // Delay before 1st IPP-USB request InitReset QuirksResetMethod // Device reset method RequestDelay time.Duration // Delay between IPP-USB requests UsbMaxInterfaces uint // Max number of USB interfaces Index int // Incremented in order of loading } // QuirksResetMethod represents how to reset a device // during initialization type QuirksResetMethod int // QuirksResetUnset - reset method not specified // QuirksResetNone - don't reset device at all // QuirksResetSoft - use class-specific soft reset // QuirksResetHard - use USB hard reset const ( QuirksResetUnset QuirksResetMethod = iota QuirksResetNone QuirksResetSoft QuirksResetHard ) // String returns textual representation of QuirksResetMethod func (m QuirksResetMethod) String() string { switch m { case QuirksResetUnset: return "unset" case QuirksResetNone: return "none" case QuirksResetSoft: return "soft" case QuirksResetHard: return "hard" } return fmt.Sprintf("unknown (%d)", int(m)) } // QuirksBuggyIppRsp defines, how to handle buggy IPP responses type QuirksBuggyIppRsp int // QuirksBuggyIppRspUnset - handling of bad IPP responses is not specified // QuirksBuggyIppRspAllow - ipp-usb will allow bad IPP responses // QuirksBuggyIppRspReject - ipp-usb will reject bad IPP responses // QuirksBuggyIppRspSanitize - bad ipp responses will be sanitized (fixed) const ( QuirksBuggyIppRspUnset QuirksBuggyIppRsp = iota QuirksBuggyIppRspAllow QuirksBuggyIppRspReject QuirksBuggyIppRspSanitize ) // String returns textual representation of QuirksBuggyIppRsp func (m QuirksBuggyIppRsp) String() string { switch m { case QuirksBuggyIppRspUnset: return "unset" case QuirksBuggyIppRspAllow: return "allow" case QuirksBuggyIppRspReject: return "reject" case QuirksBuggyIppRspSanitize: return "sanitize" } return fmt.Sprintf("unknown (%d)", int(m)) } // empty returns true, if Quirks are actually empty func (q *Quirks) empty() bool { return !q.Blacklist && len(q.HTTPHeaders) == 0 && !q.Blacklist && q.BuggyIppRsp == QuirksBuggyIppRspUnset && !q.DisableFax && !q.IgnoreIppStatus && q.InitDelay == 0 && q.InitReset == QuirksResetUnset && q.RequestDelay == 0 && q.UsbMaxInterfaces == 0 } // 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 q *Quirks for err == nil { var rec *IniRecord rec, err = ini.Next() if err != nil { break } // Get Quirks structure if rec.Type == IniRecordSection { q = &Quirks{ Origin: fmt.Sprintf("%s:%d", rec.File, rec.Line), Model: rec.Section, HTTPHeaders: make(map[string]string), Index: len(*qset), } qset.Add(q) continue } else if q == nil { err = fmt.Errorf("%s:%d: %q = %q out of any section", rec.File, rec.Line, rec.Key, rec.Value) break } // Update Quirks data if strings.HasPrefix(rec.Key, "http-") { key := http.CanonicalHeaderKey(rec.Key[5:]) q.HTTPHeaders[key] = rec.Value continue } switch rec.Key { case "blacklist": err = rec.LoadBool(&q.Blacklist) case "buggy-ipp-responses": err = rec.LoadQuirksBuggyIppRsp(&q.BuggyIppRsp) case "disable-fax": err = rec.LoadBool(&q.DisableFax) case "ignore-ipp-status": err = rec.LoadBool(&q.IgnoreIppStatus) case "init-delay": err = rec.LoadDuration(&q.InitDelay) case "init-reset": err = rec.LoadQuirksResetMethod(&q.InitReset) case "request-delay": err = rec.LoadDuration(&q.RequestDelay) case "usb-max-interfaces": err = rec.LoadUintRange(&q.UsbMaxInterfaces, 1, math.MaxUint32) } } if err == io.EOF { err = nil } return err } // Add appends Quirks to QuirksSet func (qset *QuirksSet) Add(q *Quirks) { *qset = append(*qset, q) } // ByModelName returns a subset of quirks, applicable for // specific device, matched by model name // // In a case of multiple match, quirks are returned in // the from most prioritized to least prioritized order // // Duplicates are removed: if some parameter is set by // more prioritized entry, it is removed from the less // prioritized entries. Entries, that in result become // empty, are removed at all func (qset QuirksSet) ByModelName(model string) QuirksSet { type item struct { q *Quirks matchlen int } var list []item // Get list of matching quirks for _, q := range qset { matchlen := GlobMatch(model, q.Model) if matchlen >= 0 { list = append(list, item{q, matchlen}) } } // Sort the list by matchlen, in decreasing order sort.Slice(list, func(i, j int) bool { if list[i].matchlen != list[j].matchlen { return list[i].matchlen > list[j].matchlen } return list[i].q.Index < list[j].q.Index }) // Rebuild it into the slice of *Quirks quirks := make(QuirksSet, len(list)) for i := range list { quirks[i] = list[i].q } // Remove duplicates and empty entries httpHeaderSeen := make(map[string]struct{}) out := 0 for in, q := range quirks { // Note, here we avoid modification of the HTTPHeaders // map in the original Quirks structure // // Unfortunately, Golang misses immutable types, // so we must be very careful here q2 := &Quirks{} *q2 = *q q2.HTTPHeaders = make(map[string]string) for name, value := range quirks[in].HTTPHeaders { if _, seen := httpHeaderSeen[name]; !seen { httpHeaderSeen[name] = struct{}{} q2.HTTPHeaders[name] = value } } if !q2.empty() { quirks[out] = q2 out++ } } quirks = quirks[:out] return quirks } // GetBlacklist returns effective "blacklist" parameter, // taking the whole set into consideration func (qset QuirksSet) GetBlacklist() bool { v, _ := qset.GetBlacklistOrigin() return v } // GetBlacklistOrigin returns effective "blacklist" parameter // and its origin func (qset QuirksSet) GetBlacklistOrigin() (bool, *Quirks) { for _, q := range qset { if q.Blacklist { return true, q } } return false, nil } // GetBuggyIppRsp returns effective "buggy-ipp-responses" parameter // taking the whole set into consideration func (qset QuirksSet) GetBuggyIppRsp() QuirksBuggyIppRsp { v, _ := qset.GetBuggyIppRspOrigin() return v } // GetBuggyIppRspOrigin returns effective "buggy-ipp-responses" parameter // and its origin func (qset QuirksSet) GetBuggyIppRspOrigin() (QuirksBuggyIppRsp, *Quirks) { for _, q := range qset { if q.BuggyIppRsp != QuirksBuggyIppRspUnset { return q.BuggyIppRsp, q } } return QuirksBuggyIppRspUnset, nil } // GetDisableFax returns effective "disable-fax" parameter, // taking the whole set into consideration func (qset QuirksSet) GetDisableFax() bool { v, _ := qset.GetDisableFaxOrigin() return v } // GetDisableFaxOrigin returns effective "disable-fax" parameter // and its origin func (qset QuirksSet) GetDisableFaxOrigin() (bool, *Quirks) { for _, q := range qset { if q.DisableFax { return true, q } } return false, nil } // GetIgnoreIppStatus returns effective "ignore-ipp-status" parameter, // taking the whole set into consideration func (qset QuirksSet) GetIgnoreIppStatus() bool { v, _ := qset.GetIgnoreIppStatusOrigin() return v } // GetIgnoreIppStatusOrigin returns effective "ignore-ipp-status" parameter, // and its origin func (qset QuirksSet) GetIgnoreIppStatusOrigin() (bool, *Quirks) { for _, q := range qset { if q.IgnoreIppStatus { return true, q } } return false, nil } // GetInitDelay returns effective "init-delay" parameter // taking the whole set into consideration func (qset QuirksSet) GetInitDelay() time.Duration { v, _ := qset.GetInitDelayOrigin() return v } // GetInitDelayOrigin returns effective "init-delay" parameter // and its origin func (qset QuirksSet) GetInitDelayOrigin() (time.Duration, *Quirks) { for _, q := range qset { if q.InitDelay != 0 { return q.InitDelay, q } } return 0, nil } // GetInitReset returns effective "init-reset" parameter // taking the whole set into consideration func (qset QuirksSet) GetInitReset() QuirksResetMethod { v, _ := qset.GetInitResetOrigin() return v } // GetInitResetOrigin returns effective "init-reset" parameter // and its origin func (qset QuirksSet) GetInitResetOrigin() (QuirksResetMethod, *Quirks) { for _, q := range qset { if q.InitReset != QuirksResetUnset { return q.InitReset, q } } return QuirksResetNone, nil } // GetRequestDelay returns effective "request-delay" parameter // taking the whole set into consideration func (qset QuirksSet) GetRequestDelay() time.Duration { v, _ := qset.GetRequestDelayOrigin() return v } // GetRequestDelayOrigin returns effective "request-delay" parameter // and its origin func (qset QuirksSet) GetRequestDelayOrigin() (time.Duration, *Quirks) { for _, q := range qset { if q.RequestDelay != 0 { return q.RequestDelay, q } } return 0, nil } // GetUsbMaxInterfaces returns effective "usb-max-interfaces" parameter, // taking the whole set into consideration func (qset QuirksSet) GetUsbMaxInterfaces() uint { v, _ := qset.GetUsbMaxInterfacesOrigin() return v } // GetUsbMaxInterfacesOrigin returns effective "usb-max-interfaces" parameter, // and its origin func (qset QuirksSet) GetUsbMaxInterfacesOrigin() (uint, *Quirks) { for _, q := range qset { if q.UsbMaxInterfaces != 0 { return q.UsbMaxInterfaces, q } } return 0, nil } ipp-usb-0.9.28/quirks_test.go000066400000000000000000000024411465674470100161140ustar00rootroot00000000000000/* 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 ( "testing" ) // Test quirls loading and lookup func TestQuirksSetLoadAndLookup(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 qset, err := LoadQuirksSet(path) if err != nil { t.Fatalf("LoadQuirksSet(%q): %s", path, err) } // Test default quirks quirks := qset.ByModelName("unknown device") if quirks == nil { t.Fatalf("default quirls: missed") } if len(quirks) != 1 { t.Fatalf("default quirls: expected 1, got %d", len(quirks)) } // Test quirks for some known device device := "HP LaserJet MFP M28-M31" quirks = qset.ByModelName(device) if quirks == nil { t.Fatalf("%q quirls: missed", device) } if len(quirks) != 1 { // default overridden by this device t.Fatalf("%q quirls: expected 1, got %d", device, len(quirks)) } if quirks[0].Model != device { t.Fatalf("%q quirls: wrong ordering of returned quirks", device) } } ipp-usb-0.9.28/snap/000077500000000000000000000000001465674470100141505ustar00rootroot00000000000000ipp-usb-0.9.28/snap/local/000077500000000000000000000000001465674470100152425ustar00rootroot00000000000000ipp-usb-0.9.28/snap/local/run-ipp-usb000077500000000000000000000013471465674470100173560ustar00rootroot00000000000000#!/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 "$@" ipp-usb-0.9.28/snap/local/run-ipp-usb-server000077500000000000000000000032551465674470100206620ustar00rootroot00000000000000#!/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 ipp-usb-0.9.28/snap/snapcraft.yaml000066400000000000000000000051211465674470100170140ustar00rootroot00000000000000name: 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 # 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 build-packages: - golang-go override-prime: "" ipp-usb: plugin: go source: . source-type: git 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] ipp-usb-0.9.28/status.go000066400000000000000000000053031465674470100150620ustar00rootroot00000000000000/* 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" "sync" ) // statusOfDevice represents a status of the particular device type statusOfDevice struct { desc UsbDeviceDesc // Device descriptor init error // Initialization error, nil if none } 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 } 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 Model\n") for i, status := range devs { info, _ := status.desc.GetUsbDeviceInfo() fmt.Fprintf(buf, " %3d. %s %4.4x:%.4x %q\n", i+1, status.desc.UsbAddr, info.Vendor, info.Product, info.MfgAndProduct) s := "OK" if status.init != nil { s = devs[i].init.Error() } fmt.Fprintf(buf, " status: %s", 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, init error) { statusLock.Lock() statusTable[addr] = &statusOfDevice{ desc: desc, init: init, } statusLock.Unlock() } // StatusDel deletes device from the status table func StatusDel(addr UsbAddr) { statusLock.Lock() delete(statusTable, addr) statusLock.Unlock() } ipp-usb-0.9.28/systemd-udev/000077500000000000000000000000001465674470100156405ustar00rootroot00000000000000ipp-usb-0.9.28/systemd-udev/71-ipp-usb.rules000066400000000000000000000011651465674470100205230ustar00rootroot00000000000000# 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" ipp-usb-0.9.28/systemd-udev/ipp-usb.service000066400000000000000000000003171465674470100206020ustar00rootroot00000000000000[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 ipp-usb-0.9.28/tcpuid_linux.go000066400000000000000000000102031465674470100162410ustar00rootroot00000000000000/* 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 // #include // #include // #include // #include // #include // #include // // 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 } ipp-usb-0.9.28/tcpuid_other.go000066400000000000000000000017221465674470100162310ustar00rootroot00000000000000// +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") } ipp-usb-0.9.28/tcpuid_test.go000066400000000000000000000046151465674470100160730ustar00rootroot00000000000000/* 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) } ipp-usb-0.9.28/testdata/000077500000000000000000000000001465674470100150205ustar00rootroot00000000000000ipp-usb-0.9.28/testdata/ipp-usb.conf000066400000000000000000000031401465674470100172440ustar00rootroot00000000000000# 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 ipp-usb-0.9.28/testdata/quirks/000077500000000000000000000000001465674470100163365ustar00rootroot00000000000000ipp-usb-0.9.28/testdata/quirks/HP.conf000066400000000000000000000002341465674470100175130ustar00rootroot00000000000000# ipp-usb quirks file -- quirks for HP devices [HP LaserJet MFP M28-M31] http-connection = keep-alive [HP OfficeJet Pro 8730] http-connection = close ipp-usb-0.9.28/testdata/quirks/blacklist.conf000066400000000000000000000003011465674470100211470ustar00rootroot00000000000000# 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 ipp-usb-0.9.28/testdata/quirks/default.conf000066400000000000000000000001451465674470100206310ustar00rootroot00000000000000# ipp-usb quirks file -- defaults [*] # Drop Connection: header by default http-connection = "" ipp-usb-0.9.28/usbcommon.go000066400000000000000000000175231465674470100155500ustar00rootroot00000000000000/* 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 } ipp-usb-0.9.28/usbcommon_test.go000066400000000000000000000034041465674470100166000ustar00rootroot00000000000000/* 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() } } ipp-usb-0.9.28/usbio_libusb.go000066400000000000000000000467211465674470100162310ustar00rootroot00000000000000/* 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 ( "encoding/binary" "errors" "runtime" "sync" "sync/atomic" "time" "unsafe" ) // #cgo pkg-config: libusb-1.0 // #include // // int libusbHotplugCallback (libusb_context *ctx, libusb_device *device, // libusb_hotplug_event event, void *user_data); // // 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; // // // 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 ( UsbIO 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 // 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) 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 } // 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) ( *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, }, nil } // UsbInterface represents IPP-over-USB interface type UsbInterface struct { devhandle *UsbDevHandle // Device handle addr UsbIfAddr // Interface address } // 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(data []byte, timeout time.Duration) (n int, err error) { var transferred C.int rc := C.libusb_bulk_transfer( (*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)), &transferred, C.uint(timeout/time.Millisecond), ) if rc < 0 { err = UsbError{"libusb_bulk_transfer", UsbErrCode(rc)} } n = int(transferred) 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(data []byte, timeout time.Duration) (n int, err error) { var transferred C.int // 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] } rc := C.libusb_bulk_transfer( (*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)), &transferred, C.uint(timeout/time.Millisecond), ) if rc == C.LIBUSB_ERROR_PIPE { iface.ClearHalt(true) } if rc < 0 { err = UsbError{"libusb_bulk_transfer", UsbErrCode(rc)} } n = int(transferred) 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 } ipp-usb-0.9.28/usbtransport.go000066400000000000000000000606651465674470100163210ustar00rootroot00000000000000/* 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" "strings" "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 QuirksSet // Device quirks deadline time.Time // Deadline for requests } // 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.ByModelName(transport.info.MfgAndProduct) // Write device info to the log log := transport.log.Begin(). Nl(LogDebug). Debug(' ', "==============================="). Info('+', "%s: added %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() == QuirksResetHard { 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) { type dmp struct { name, val string origin *Quirks } dump := []dmp{} var v interface{} var o *Quirks // Collect parameters v, o = transport.quirks.GetBlacklistOrigin() dump = append(dump, dmp{"blacklist", fmt.Sprintf("%v", v), o}) v, o = transport.quirks.GetBuggyIppRspOrigin() dump = append(dump, dmp{"buggy-ipp-responses", fmt.Sprintf("%s", v), o}) v, o = transport.quirks.GetDisableFaxOrigin() dump = append(dump, dmp{"disable-fax", fmt.Sprintf("%v", v), o}) v, o = transport.quirks.GetIgnoreIppStatusOrigin() dump = append(dump, dmp{"ignore-ipp-status", fmt.Sprintf("%v", v), o}) v, o = transport.quirks.GetInitDelayOrigin() dump = append(dump, dmp{"init-delay", fmt.Sprintf("%v", v), o}) v, o = transport.quirks.GetInitResetOrigin() dump = append(dump, dmp{"init-reset", fmt.Sprintf("%s", v), o}) v, o = transport.quirks.GetRequestDelayOrigin() dump = append(dump, dmp{"request-delay", fmt.Sprintf("%v", v), o}) v, o = transport.quirks.GetUsbMaxInterfacesOrigin() dump = append(dump, dmp{"usb-max-interfaces", fmt.Sprintf("%v", v), o}) // Add HTTP headers for _, q := range transport.quirks { for name, value := range q.HTTPHeaders { d := dmp{ name: "http-" + strings.ToLower(name), val: fmt.Sprintf("%q", value), origin: q, } dump = append(dump, d) } } // Skip no-origin (i.e., actually unset) quirks cnt := 0 for _, d := range dump { if d.origin != nil { dump[cnt] = d cnt++ } } dump = dump[:cnt] // Sort by name sort.Slice(dump, func(i, j int) bool { return dump[i].name < dump[j].name }) // And write to the log log.Debug(' ', "Device quirks:") origin := (*Quirks)(nil) for _, d := range dump { if origin != d.origin { origin = d.origin log.Debug(' ', " from [%s] (%s):", origin.Model, origin.Origin) } log.Debug(' ', " %s = %s", d.name, d.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) } // SetDeadline sets the deadline for all requests, submitted // via RoundTrip and RoundTripWithSession methods // // A deadline is an absolute time after which request processing // will fail instead of blocking // // 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) SetDeadline(t time.Time) { transport.deadline = t } // DeadlineExpired reports if deadline previously set by SetDeadline() // is already expired func (transport *UsbTransport) DeadlineExpired() bool { deadline := transport.deadline return !deadline.IsZero() && time.Until(deadline) <= 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: removed %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() QuirksSet { 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 _, quirks := range transport.quirks { for name, value := range 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) } // Send request and receive a response err = outreq.Write(conn) if err != nil { transport.log.HTTPError('!', session, "%s", err) conn.put() return nil, err } resp, err := http.ReadResponse(conn.reader, outreq) if err != nil { transport.log.HTTPError('!', session, "%s", err) conn.put() return nil, err } // Wrap response body resp.Body = &usbResponseBodyWrapper{ log: transport.log, session: session, body: resp.Body, conn: conn, } // Optionally sanitize IPP response if transport.quirks.GetBuggyIppRsp() == QuirksBuggyIppRspSanitize && 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 } // 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.body.Close() wrap.conn.put() 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.body.Close() wrap.conn.put() }() return nil } // 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 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 } // Open usbConn func (transport *UsbTransport) openUsbConn( index int, ifaddr UsbIfAddr, quirks QuirksSet) (*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) if err != nil { goto ERROR } // Soft-reset interface, if needed if quirks.GetInitReset() == QuirksResetSoft { 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 } // Compute Recv/Send timeout func (conn *usbConn) timeout() (tm time.Duration, expored bool) { deadline := conn.transport.deadline if deadline.IsZero() { return } tm = time.Until(deadline) return tm, tm <= 0 } // Read from USB func (conn *usbConn) Read(b []byte) (int, error) { conn.transport.connstate.beginRead(conn) defer conn.transport.connstate.doneRead(conn) // 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] } backoff := time.Millisecond * 100 for { tm, expired := conn.timeout() if expired { return 0, ErrInitTimedOut } n, err := conn.iface.Recv(b, tm) 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 n != 0 || err != nil { return n, err } conn.transport.log.Error('!', "USB[%d]: zero-size read", conn.index) time.Sleep(backoff) backoff *= 2 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) tm, expired := conn.timeout() if expired { return 0, ErrInitTimedOut } n, err := conn.iface.Send(b, tm) 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) } return n, err } // 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) } ipp-usb-0.9.28/uuid.go000066400000000000000000000020511465674470100145020ustar00rootroot00000000000000/* 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]) } ipp-usb-0.9.28/uuid_test.go000066400000000000000000000021561465674470100155470ustar00rootroot00000000000000/* 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): extected %q, got %q", data.in, data.out, uuid) } } } ipp-usb-0.9.28/vendor/000077500000000000000000000000001465674470100145045ustar00rootroot00000000000000ipp-usb-0.9.28/vendor/github.com/000077500000000000000000000000001465674470100165435ustar00rootroot00000000000000ipp-usb-0.9.28/vendor/github.com/OpenPrinting/000077500000000000000000000000001465674470100211575ustar00rootroot00000000000000ipp-usb-0.9.28/vendor/github.com/OpenPrinting/goipp/000077500000000000000000000000001465674470100222755ustar00rootroot00000000000000ipp-usb-0.9.28/vendor/github.com/OpenPrinting/goipp/.gitignore000066400000000000000000000000231465674470100242600ustar00rootroot00000000000000ipp-usb tags *.swp ipp-usb-0.9.28/vendor/github.com/OpenPrinting/goipp/LICENSE000066400000000000000000000024571465674470100233120ustar00rootroot00000000000000BSD 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. ipp-usb-0.9.28/vendor/github.com/OpenPrinting/goipp/Makefile000066400000000000000000000000641465674470100237350ustar00rootroot00000000000000all: -gotags -R . > tags go build test: go test ipp-usb-0.9.28/vendor/github.com/OpenPrinting/goipp/README.md000066400000000000000000000015321465674470100235550ustar00rootroot00000000000000# goipp [![godoc.org](https://godoc.org/github.com/OpenPrinting/goipp?status.svg)](http://godoc.org/github.com/OpenPrinting/goipp) ![GitHub](https://img.shields.io/github/license/OpenPrinting/goipp) [![Go Report Card](https://goreportcard.com/badge/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. ipp-usb-0.9.28/vendor/github.com/OpenPrinting/goipp/_config.yml000066400000000000000000000000351465674470100244220ustar00rootroot00000000000000theme: jekyll-theme-architectipp-usb-0.9.28/vendor/github.com/OpenPrinting/goipp/attr.go000066400000000000000000000040211465674470100235730ustar00rootroot00000000000000/* 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 } ipp-usb-0.9.28/vendor/github.com/OpenPrinting/goipp/const.go000066400000000000000000000006351465674470100237560ustar00rootroot00000000000000/* 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 = " " ) ipp-usb-0.9.28/vendor/github.com/OpenPrinting/goipp/decoder.go000066400000000000000000000230241465674470100242320ustar00rootroot00000000000000/* 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 } ipp-usb-0.9.28/vendor/github.com/OpenPrinting/goipp/doc.go000066400000000000000000000111001465674470100233620ustar00rootroot00000000000000/* 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 ipp-usb-0.9.28/vendor/github.com/OpenPrinting/goipp/encoder.go000066400000000000000000000114641465674470100242510ustar00rootroot00000000000000/* 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 } ipp-usb-0.9.28/vendor/github.com/OpenPrinting/goipp/go.mod000066400000000000000000000000561465674470100234040ustar00rootroot00000000000000module github.com/OpenPrinting/goipp go 1.11 ipp-usb-0.9.28/vendor/github.com/OpenPrinting/goipp/group.go000066400000000000000000000022641465674470100237640ustar00rootroot00000000000000/* 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 } ipp-usb-0.9.28/vendor/github.com/OpenPrinting/goipp/index.md000066400000000000000000000015221465674470100237260ustar00rootroot00000000000000# goipp [![godoc.org](https://godoc.org/github.com/OpenPrinting/goipp?status.svg)](http://godoc.org/github.com/OpenPrinting/goipp) ![GitHub](https://img.shields.io/github/license/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) ipp-usb-0.9.28/vendor/github.com/OpenPrinting/goipp/message.go000066400000000000000000000171201465674470100242510ustar00rootroot00000000000000/* 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] } ipp-usb-0.9.28/vendor/github.com/OpenPrinting/goipp/op.go000066400000000000000000000422021465674470100232420ustar00rootroot00000000000000/* 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", } ipp-usb-0.9.28/vendor/github.com/OpenPrinting/goipp/status.go000066400000000000000000000233061465674470100241530ustar00rootroot00000000000000/* 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", } ipp-usb-0.9.28/vendor/github.com/OpenPrinting/goipp/tag.go000066400000000000000000000134651465674470100234100ustar00rootroot00000000000000/* 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", } ipp-usb-0.9.28/vendor/github.com/OpenPrinting/goipp/type.go000066400000000000000000000027161465674470100236130ustar00rootroot00000000000000/* 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", } ipp-usb-0.9.28/vendor/github.com/OpenPrinting/goipp/value.go000066400000000000000000000340061465674470100237430ustar00rootroot00000000000000/* 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") } ipp-usb-0.9.28/vendor/modules.txt000066400000000000000000000001051465674470100167110ustar00rootroot00000000000000# github.com/OpenPrinting/goipp v1.1.0 github.com/OpenPrinting/goipp