pax_global_header00006660000000000000000000000064142140444520014512gustar00rootroot0000000000000052 comment=2574c62d67c66a43028ec846669ceb764ee95dde ipp-usb-0.9.20/000077500000000000000000000000001421404445200131615ustar00rootroot00000000000000ipp-usb-0.9.20/.gitignore000066400000000000000000000000321421404445200151440ustar00rootroot00000000000000ipp-usb tags *.swp *.orig ipp-usb-0.9.20/LICENSE000066400000000000000000000024571421404445200141760ustar00rootroot00000000000000BSD 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.20/Makefile000066400000000000000000000014111421404445200146160ustar00rootroot00000000000000MANDIR = /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 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 ipp-usb-0.9.20/README.md000066400000000000000000000253361421404445200144510ustar00rootroot00000000000000# 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/ ## 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. The Snap is also 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.20/addpdl_test.go000066400000000000000000000037741421404445200160120ustar00rootroot00000000000000/* 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.20/avahi/000077500000000000000000000000001421404445200142515ustar00rootroot00000000000000ipp-usb-0.9.20/avahi/avahi-localhost.patch000066400000000000000000000054721421404445200203600ustar00rootroot00000000000000diff --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.20/conf.go000066400000000000000000000145541421404445200144460ustar00rootroot00000000000000/* 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" "math" "os" "path/filepath" "strconv" "strings" ) 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 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 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, LogDevice: LogDebug, LogMain: LogDebug, LogConsole: LogDebug, LogMaxFileSize: 256 * 1024, LogMaxBackupFiles: 5, 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 fmt.Errorf("conf: %s", err) } } // Load quirks quirksDirs := []string{ PathQuirksDir, PathConfQuirksDir, filepath.Join(exepath, "ipp-usb-quirks"), } if err == nil { Conf.Quirks, err = LoadQuirksSet(quirksDirs...) } return err } // Create "bad value" error func confBadValue(rec *IniRecord, format string, args ...interface{}) error { return fmt.Errorf(rec.Key+": "+format, args...) } // 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 rec.Section { case "network": switch rec.Key { case "http-min-port": err = confLoadIPPortKey(&Conf.HTTPMinPort, rec) case "http-max-port": err = confLoadIPPortKey(&Conf.HTTPMaxPort, rec) case "dns-sd": err = confLoadBinaryKey(&Conf.DNSSdEnable, rec, "disable", "enable") case "interface": err = confLoadBinaryKey(&Conf.LoopbackOnly, rec, "all", "loopback") case "ipv6": err = confLoadBinaryKey(&Conf.IPV6Enable, rec, "disable", "enable") } case "logging": switch rec.Key { case "device-log": err = confLoadLogLevelKey(&Conf.LogDevice, rec) case "main-log": err = confLoadLogLevelKey(&Conf.LogMain, rec) case "console-log": err = confLoadLogLevelKey(&Conf.LogConsole, rec) case "console-color": err = confLoadBinaryKey(&Conf.ColorConsole, rec, "disable", "enable") case "max-file-size": err = confLoadSizeKey(&Conf.LogMaxFileSize, rec) case "max-backup-files": err = confLoadUintKey(&Conf.LogMaxBackupFiles, rec) } } } 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 } // Load IP port key func confLoadIPPortKey(out *int, rec *IniRecord) error { port, err := strconv.Atoi(rec.Value) if err == nil && (port < 1 || port > 65535) { err = confBadValue(rec, "must be in range 1...65535") } if err != nil { return err } *out = int(port) return nil } // Load the binary key func confLoadBinaryKey(out *bool, rec *IniRecord, vFalse, vTrue string) error { switch rec.Value { case vFalse: *out = false return nil case vTrue: *out = true return nil default: return confBadValue(rec, "must be %s or %s", vFalse, vTrue) } } // Load LogLevel key func confLoadLogLevelKey(out *LogLevel, rec *IniRecord) error { var mask LogLevel for _, s := range strings.Split(rec.Value, ",") { switch s { 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 "all", "trace-all": mask |= LogAll default: return confBadValue(rec, "invalid log level %q", s) } } *out = mask return nil } // Load size key func confLoadSizeKey(out *int64, rec *IniRecord) error { 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 confBadValue(rec, "%q: invalid size", rec.Value) } if sz > uint64(math.MaxInt64/units) { return confBadValue(rec, "size too large") } *out = int64(sz * units) return nil } // Load unsigned integer key func confLoadUintKey(out *uint, rec *IniRecord) error { num, err := strconv.ParseUint(rec.Value, 10, 0) if err != nil { return confBadValue(rec, "%q: invalid number", rec.Value) } *out = uint(num) return nil } // Load unsigned integer key within the range func confLoadUintKeyRange(out *uint, rec *IniRecord, min, max uint) error { var val uint err := confLoadUintKey(&val, rec) if err == nil && (val < min || val > max) { err = confBadValue(rec, "must be in range %d...%d", min, max) } if err == nil { *out = val } return err } ipp-usb-0.9.20/const.go000066400000000000000000000014161421404445200146400ustar00rootroot00000000000000/* 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.20/daemon.go000066400000000000000000000041541421404445200147570ustar00rootroot00000000000000/* 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.20/debian/000077500000000000000000000000001421404445200144035ustar00rootroot00000000000000ipp-usb-0.9.20/debian/changelog000066400000000000000000000002441421404445200162550ustar00rootroot00000000000000ipp-usb (0.0~git20200206.9c9d40f-1) UNRELEASED; urgency=medium * Initial release -- Alexander Pevzner Thu, 06 Feb 2020 11:23:28 -0500 ipp-usb-0.9.20/debian/compat000066400000000000000000000000031421404445200156020ustar00rootroot0000000000000011 ipp-usb-0.9.20/debian/control000066400000000000000000000017121421404445200160070ustar00rootroot00000000000000Source: ipp-usb Maintainer: Alexander Pevzner Uploaders: Alexander Pevzner Section: comm Priority: optional Build-Depends: debhelper (>= 11), libusb-1.0-0-dev, libavahi-common-dev, libavahi-client-dev, make, pkg-config, golang-go, Standards-Version: 4.2.1 Homepage: https://github.com/OpenPrinting/ipp-usb Package: ipp-usb Architecture: any Conflicts: ippusbxd Breaks: ippusbxd Depends: ${misc:Depends}, ${shlibs:Depends} Description: Daemon for IPP over USB printer support ipp-usb is a userland driver for USB devices (printers, scanners, MFC), supporting the IPP over USB protocol. It enables these USB devices to be seen as regular network printers. . It is designed to be a replacement of ippusbxd daemon, previously used for this purpose. It has a greatly rethought architecture, in comparison with ippusbxd, and fixes all of its major flaws and issues. ipp-usb-0.9.20/debian/copyright000066400000000000000000000033551421404445200163440ustar00rootroot00000000000000Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: ipp-usb Source: https://github.com/alexpevzner/ipp-usb Files: * Copyright: 2020 Alexander Pevzner License: BSD-2-clause Files: debian/* Copyright: 2020 Alexander Pevzner License: BSD-2-clause Comment: Debian packaging is licensed under the same terms as upstream License: BSD-2-clause BSD 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.20/debian/rules000077500000000000000000000002061421404445200154610ustar00rootroot00000000000000#!/usr/bin/make -f %: dh $@ override_dh_auto_build: make override_dh_auto_install: make PREFIX=$(CURDIR)/debian/ipp-usb install ipp-usb-0.9.20/debian/source/000077500000000000000000000000001421404445200157035ustar00rootroot00000000000000ipp-usb-0.9.20/debian/source/format000066400000000000000000000000041421404445200171100ustar00rootroot000000000000001.0 ipp-usb-0.9.20/device.go000066400000000000000000000121001421404445200147410ustar00rootroot00000000000000/* 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)) 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.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") } } // 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.20/devstate.go000066400000000000000000000070131421404445200153300ustar00rootroot00000000000000/* 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" "io/ioutil" "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 func LoadDevState(ident, comment string) *DevState { state := &DevState{ Ident: ident, comment: comment, } state.path = state.devStatePath() // Open state file ini, err := OpenIniFile(state.path) if err == nil { defer ini.Close() } // Extract data 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 != nil && err != io.EOF { if !os.IsNotExist(err) { Log.Error('!', "STATE LOAD: %s", state.error("%s", err)) } state.Save() } return state } // 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 := ioutil.WriteFile(state.path, buf.Bytes(), 0644) if err != nil { err = state.error("%s", err) Log.Error('!', "STATE SAVE: %s", err) } } // 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 for port = Conf.HTTPMinPort; port <= Conf.HTTPMaxPort; port++ { listener, err := NewListener(port) if err == nil { state.HTTPPort = port state.Save() return listener, nil } } 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.20/dnssd.go000066400000000000000000000167551421404445200146410ustar00rootroot00000000000000/* 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 DNSSdStatus = iota // Invalid status DNSSdCollision // Service instance name collision DNSSdFailure // Publisher failed DNSSdSuccess // Services successfully published ) // 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 MAX_DNSSD_NAME = 63 if len(name)+len(strSuffix) > MAX_DNSSD_NAME { name = name[:MAX_DNSSD_NAME-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.20/dnssd_avahi.go000066400000000000000000000232701421404445200157770ustar00rootroot00000000000000// +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 * * 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 c_txt *C.AvahiStringList c_txt, err = sysdep.avahiTxtRecord(svc.Port, svc.Txt) if err != nil { goto ERROR } // Prepare C strings for service instance and type c_svc_type := C.CString(svc.Type) var c_instance *C.char if svc.Instance != "" { c_instance = C.CString(svc.Instance) } else { c_instance = C.CString(instance) } // Handle loopback-only mode iface_in_use := iface if svc.Loopback { iface_in_use = loopback } // Register service type rc = C.avahi_entry_group_add_service_strlst( sysdep.egroup, C.AvahiIfIndex(iface_in_use), C.AvahiProtocol(proto), 0, c_instance, c_svc_type, nil, // Domain nil, // Host C.uint16_t(svc.Port), c_txt, ) // Register subtypes, if any for _, subtype := range svc.SubTypes { if rc != C.AVAHI_OK { break } sysdep.log.Debug(' ', "DNS-SD: +subtype: %q", subtype) c_subtype := C.CString(subtype) rc = C.avahi_entry_group_add_service_subtype( sysdep.egroup, C.AvahiIfIndex(iface_in_use), C.AvahiProtocol(proto), 0, c_instance, c_svc_type, nil, c_subtype, ) C.free(unsafe.Pointer(c_subtype)) } // Release C memory C.free(unsafe.Pointer(c_instance)) C.free(unsafe.Pointer(c_svc_type)) C.avahi_string_list_free(c_txt) // 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.20/err.go000066400000000000000000000010271421404445200143000ustar00rootroot00000000000000/* 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" ) 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") ) ipp-usb-0.9.20/escl.go000066400000000000000000000153701421404445200144440ustar00rootroot00000000000000/* 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.20/flock_unix.go000066400000000000000000000015021421404445200156470ustar00rootroot00000000000000// +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 import ( "os" "syscall" ) // FileLock acquires file lock func FileLock(file *os.File, exclusive, wait bool) error { var how int if exclusive { how = syscall.LOCK_EX } else { how = syscall.LOCK_SH } if !wait { how |= syscall.LOCK_NB } err := syscall.Flock(int(file.Fd()), how) if err == syscall.Errno(syscall.EWOULDBLOCK) { err = ErrLockIsBusy } return err } // FileUnlock releases file lock func FileUnlock(file *os.File) error { return syscall.Flock(int(file.Fd()), syscall.LOCK_UN) } ipp-usb-0.9.20/flock_windows.go000066400000000000000000000025371421404445200163670ustar00rootroot00000000000000/* 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 -- Windows version */ package main /* #define NTDDI_VERSION NTDDI_WIN7 #include #include */ import "C" import ( "os" "runtime" "syscall" ) // FileLock acquires file lock func FileLock(file *os.File, exclusive, wait bool) error { runtime.LockOSThread() defer runtime.UnlockOSThread() var flags C.DWORD if exclusive { flags |= C.LOCKFILE_EXCLUSIVE_LOCK } if !wait { flags |= C.LOCKFILE_FAIL_IMMEDIATELY } var ovp C.OVERLAPPED ok := C.LockFileEx( C.HANDLE(file.Fd()), flags, 0, 0xffffffff, 0xffffffff, &ovp, ) if int(ok) != 0 { return nil } switch errno := C.GetLastError(); errno { case C.NO_ERROR, C.ERROR_LOCK_VIOLATION: return ErrLockIsBusy default: return syscall.Errno(errno) } } // FileUnlock releases file lock func FileUnlock(file *os.File) error { runtime.LockOSThread() defer runtime.UnlockOSThread() var ovp C.OVERLAPPED ok := C.UnlockFileEx( C.HANDLE(file.Fd()), 0, 0xffffffff, 0xffffffff, &ovp, ) if int(ok) != 0 { return nil } switch errno := C.GetLastError(); errno { case C.NO_ERROR: return nil default: return syscall.Errno(errno) } } ipp-usb-0.9.20/glob.go000066400000000000000000000030131421404445200144300ustar00rootroot00000000000000/* 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.20/glob_test.go000066400000000000000000000015531421404445200154760ustar00rootroot00000000000000/* 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.20/go.mod000066400000000000000000000001361421404445200142670ustar00rootroot00000000000000module github.com/OpenPrinting/ipp-usb go 1.11 require github.com/OpenPrinting/goipp v1.0.0 ipp-usb-0.9.20/go.sum000066400000000000000000000002611421404445200143130ustar00rootroot00000000000000github.com/OpenPrinting/goipp v1.0.0 h1:8dEahD/KPQhMQE8+D7lc18OkkvcDC/BUfVeNIA1GW4E= github.com/OpenPrinting/goipp v1.0.0/go.mod h1:ot2iw+QF7fVLaX+55JUNlF5YSDNiXVo2LRAv21iGcQI= ipp-usb-0.9.20/http.go000066400000000000000000000141531421404445200144730ustar00rootroot00000000000000/* 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 our local address the request was ordered to var localAddr *net.TCPAddr if v := r.Context().Value(http.LocalAddrContextKey); v != nil { if v != nil { localAddr, _ = v.(*net.TCPAddr) } } if localAddr == nil { proxy.httpError(session, w, r, http.StatusInternalServerError, errors.New("Unable to get local address for request")) return } // Adjust request headers httpRemoveHopByHopHeaders(r.Header) if r.Host == "" { if localAddr.IP.IsLoopback() { r.Host = fmt.Sprintf("localhost:%d", localAddr.Port) } else { r.Host = localAddr.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 localAddr.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", localAddr.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.20/inifile.go000066400000000000000000000176121421404445200151360ustar00rootroot00000000000000/* 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" "os" ) // 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 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 } // 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 } break case prsString: if c == '\\' { state = prsStringBslash } else if c == '"' { state = prsBody } else { ini.buf.WriteByte(c) } break 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' break case 'b': c = '\b' break case 'e': c = '\x1b' break case 'f': c = '\f' break case 'n': c = '\n' break case 'r': c = '\r' break case 't': c = '\t' break case 'v': c = '\v' break } ini.buf.WriteByte(c) state = prsString } break 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) } break 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) } break case prsComment: break } } // 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...), } } // 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.20/inifile_test.go000066400000000000000000000032121421404445200161640ustar00rootroot00000000000000/* 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.20/ipp-usb-quirks/000077500000000000000000000000001421404445200160545ustar00rootroot00000000000000ipp-usb-0.9.20/ipp-usb-quirks/HP.conf000066400000000000000000000010211421404445200172240ustar00rootroot00000000000000# 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 ipp-usb-0.9.20/ipp-usb-quirks/README000066400000000000000000000017411421404445200167370ustar00rootroot00000000000000This 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: http-xxx = yyy - set HTTP header Xxx: yyy http-xxx = "" - drop HTTP header Xxx blacklist = true - blacklist the matching devices blacklist = false - don't blacklist the matching devices ipp-usb-0.9.20/ipp-usb-quirks/blacklist.conf000066400000000000000000000003011421404445200206650ustar00rootroot00000000000000# 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.20/ipp-usb-quirks/default.conf000066400000000000000000000001451421404445200203470ustar00rootroot00000000000000# ipp-usb quirks file -- defaults [*] # Drop Connection: header by default http-connection = "" ipp-usb-0.9.20/ipp-usb.8000066400000000000000000000150461421404445200146370ustar00rootroot00000000000000.\" generated with Ronn-NG/v0.9.1 .\" http://github.com/apjanke/ronn-ng/tree/0.9.1 .TH "IPP\-USB" "8" "March 2022" "" "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 .SS "Options are" .TP \fB\-bg\fR run in background (ignored in debug mode) .SH "CONFIGURATION" \fBipp\-usb\fR searched for its configuration file in two places: 1\. \fB/etc/ipp\-usb/ipp\-usb\.conf\fR 2\. \fBipp\-usb\.conf\fR in the directory where executable file is located .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 "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 # 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 .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\. The section name contains an exact model name, which contains \fBiManufacturer\fR+\fBiProduct\fR entries from \fBlsusb \-v\fR command output, or it 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 All matching sections from all quirks files are taken in consideration, and applied in priority order\. Priority is computed using the following algorithm: .IP "\[ci]" 4 When matching model name against section name, amount of non\-wildcard matched characters is counted, and the longer match wins .IP "\[ci]" 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: .TP \fBblacklist = true | false\fR If \fBtrue\fR, the matching device is ignored by the \fBipp\-usb\fR .TP \fBhttp\-XXX = YYY\fR Set XXX header of the HTTP requests forwarded to device to YYY\. If YYY is empty string, XXX header is removed .TP \fBusb\-max\-interfaces = N\fR Don\'t use more that N USB interfaces, even if more is available .P In case of you found out about your device 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 isues at https://github\.com/OpenPrinting/ipp\-usb\. The possible quirk for the device can be added to the project itself and fix the situation for all device\'s owners\. .SH "FILES" .IP "\[ci]" 4 \fB/etc/ipp\-usb/ipp\-usb\.conf\fR: the daemon configuration file .IP "\[ci]" 4 \fB/var/log/ipp\-usb/main\.log\fR: the main log file .IP "\[ci]" 4 \fB/var/log/ipp\-usb/\.log\fR: per\-device log files .IP "\[ci]" 4 \fB/var/ipp\-usb/dev/\.state\fR: device state (HTTP port allocation, DNS\-SD name) .IP "\[ci]" 4 \fB/var/ipp\-usb/lock/ipp\-usb\.lock\fR: lock file, that helps to prevent multiple copies of daemon to run simultaneously .IP "\[ci]" 4 \fB/usr/share/ipp\-usb/quirks/*\.conf\fR: device\-specific quirks (see above) .IP "\[ci]" 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) .br All rights reserved\. .P This program is licensed under 2\-Clause BSD license\. See LICENSE file for details\. .SH "SEE ALSO" cups(1) ipp-usb-0.9.20/ipp-usb.8.md000066400000000000000000000144411421404445200152340ustar00rootroot00000000000000ipp-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 ### Options are * `-bg`: run in background (ignored in debug mode) ## 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 ### 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 # 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 ### 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. The section name contains an exact model name, which contains `iManufacturer`+`iProduct` entries from `lsusb -v` command output, or it 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. 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` * `http-XXX = YYY`: Set XXX header of the HTTP requests forwarded to device to YYY. If YYY is empty string, XXX header is removed * `usb-max-interfaces = N`: Don't use more that N USB interfaces, even if more is available In case of you found out about your device 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 isues at https://github.com/OpenPrinting/ipp-usb. The possible quirk for the device can be added to the project itself and fix the situation for all device's owners. ## 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 * `/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)
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.20/ipp-usb.conf000066400000000000000000000031401421404445200154050ustar00rootroot00000000000000# 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.20/ipp.go000066400000000000000000000321151421404445200143020ustar00rootroot00000000000000/* 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, c *http.Client) (ippinfo *IppPrinterInfo, err error) { // Query printer attributes uri := fmt.Sprintf("http://localhost:%d/ipp/print", port) msg, err := ippGetPrinterAttributes(log, c, 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 { // 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, 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, 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"} 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 } err = msg.DecodeBytes(respData) 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 >= 100 { 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 x_dim_max, y_dim_max int for _, collection := range vals { var x_dim_attr, y_dim_attr goipp.Attribute attrs := collection.(goipp.Collection) for i := len(attrs) - 1; i >= 0; i-- { switch attrs[i].Name { case "x-dimension": x_dim_attr = attrs[i] case "y-dimension": y_dim_attr = attrs[i] } } if len(x_dim_attr.Values) > 0 { switch dim := x_dim_attr.Values[0].V.(type) { case goipp.Integer: if int(dim) > x_dim_max { x_dim_max = int(dim) } case goipp.Range: if int(dim.Upper) > x_dim_max { x_dim_max = int(dim.Upper) } } } if len(y_dim_attr.Values) > 0 { switch dim := y_dim_attr.Values[0].V.(type) { case goipp.Integer: if int(dim) > y_dim_max { y_dim_max = int(dim) } case goipp.Range: if int(dim.Upper) > y_dim_max { y_dim_max = int(dim.Upper) } } } } if x_dim_max == 0 || y_dim_max == 0 { return "" } // Now classify by paper size return PaperSize{x_dim_max, y_dim_max}.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.20/linewriter.go000066400000000000000000000035151421404445200157000ustar00rootroot00000000000000/* 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.20/listener.go000066400000000000000000000033741421404445200153440ustar00rootroot00000000000000/* 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.20/logger.go000066400000000000000000000435451421404445200150020ustar00rootroot00000000000000/* 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 const ( LogError LogLevel = 1 << iota LogInfo LogDebug LogTraceIPP LogTraceESCL LogTraceHTTP LogAll = LogError | LogInfo | LogDebug | LogTraceAll LogTraceAll = LogTraceIPP | LogTraceESCL | LogTraceHTTP ) // 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 io.Writer to send "carbon copy" to // The mask parameter filters what lines will included into the carbon copy // // Note: // LogTraceXxx implies LogDebug // LogDebug implies LogInfo // LogInfo implies LogError 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, 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, ' ', "%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.20/logger_unix.go000066400000000000000000000020671421404445200160370ustar00rootroot00000000000000// +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.20/loopback.go000066400000000000000000000012221421404445200152770ustar00rootroot00000000000000/* 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.20/main.go000066400000000000000000000135251421404445200144420ustar00rootroot00000000000000/* 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 Options are -bg - run in background (ignored in debug mode) ` // RunMode represents the program run mode type RunMode int const ( RunDefault RunMode = iota RunStandalone RunUdev RunDebug RunCheck ) // 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" } 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 "-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 } // 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 { 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 { descs, _ := UsbGetIppOverUsbDeviceDescs() 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()) } } } // 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, true, false) 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 { 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() 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, true, false) == nil { Log.Info(' ', "New IPP-over-USB device found") continue } } break } } ipp-usb-0.9.20/paper.go000066400000000000000000000040161421404445200146200ustar00rootroot00000000000000/* 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.20/paths.go000066400000000000000000000021651421404445200146330ustar00rootroot00000000000000/* 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" // 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.20/pnp.go000066400000000000000000000065601421404445200143140ustar00rootroot00000000000000/* 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 const ( PnPIdle PnPExitReason = iota // No more connected devices PnPTerm // Terminating signal received ) // pnpRetryTime returns time of next retry of failed device initialization func pnpRetryTime() time.Time { 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)) // Serve PnP events until terminated loop: for { dev_descs, err := UsbGetIppOverUsbDeviceDescs() if err == nil { newdevices := UsbAddrList{} for _, desc := range dev_descs { 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(dev_descs[addr]) if err == nil { devByAddr[addr] = dev } else { Log.Error('!', "PNP %s: %s", addr, err) retryByAddr[addr] = pnpRetryTime() } } // Handle removed devices for _, addr := range removed { Log.Debug('-', "PNP %s: removed", addr) delete(retryByAddr, 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(dev_descs[addr]) if err == nil { devByAddr[addr] = dev delete(retryByAddr, addr) } else { Log.Error('!', "PNP %s: %s", addr, err) retryByAddr[addr] = pnpRetryTime() } } } // 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 } 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.20/quirks.go000066400000000000000000000111021421404445200150210ustar00rootroot00000000000000/* 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" ) // Quirks represents device-specific quirks type Quirks struct { Origin string // file:line of definition Model string // Device model name Blacklist bool // Blacklist the device HttpHeaders map[string]string // HTTP header override UsbMaxInterfaces uint // Max number of USB interfaces Index int // Incremented in order of loading } // empty returns true, if Quirks are actually empty func (q *Quirks) empty() bool { return !q.Blacklist && len(q.HttpHeaders) == 0 && q.UsbMaxInterfaces == 0 } // QuirksSet represents collection of quirks, indexed by model name 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 = append(*qset, 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 = confLoadBinaryKey(&q.Blacklist, rec, "false", "true") case "usb-max-interfaces": err = confLoadUintKeyRange(&q.UsbMaxInterfaces, rec, 1, math.MaxUint32) } } if err == io.EOF { err = nil } return err } // Get quirks 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) Get(model string) []Quirks { 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([]Quirks, len(list)) for i := range list { quirks[i] = *list[i].q } // If at least one Quirks contains Blacklist == true, // it overrides everything else. // // Note, we check it after building and sorting the entire // list for more accurate logging for _, q := range quirks { if q.Blacklist { return []Quirks{q} } } // Remove duplicates and empty entries httpHeaderSeen := make(map[string]struct{}) out := 0 for in, q := range quirks { q.HttpHeaders = make(map[string]string) for name, value := range quirks[in].HttpHeaders { if _, seen := httpHeaderSeen[name]; !seen { httpHeaderSeen[name] = struct{}{} q.HttpHeaders[name] = value } } if !q.empty() { quirks[out] = q out++ } } quirks = quirks[:out] return quirks } ipp-usb-0.9.20/quirks_test.go000066400000000000000000000024241421404445200160670ustar00rootroot00000000000000/* 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 bad_path = path + "-not-exist" // Try non-existent directory _, err := LoadQuirksSet(bad_path) if err != nil { t.Fatalf("LoadQuirksSet(%q): %s", bad_path, err) } // Try test data qset, err := LoadQuirksSet(path) if err != nil { t.Fatalf("LoadQuirksSet(%q): %s", path, err) } // Test default quirks quirks := qset.Get("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.Get(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.20/snap/000077500000000000000000000000001421404445200141225ustar00rootroot00000000000000ipp-usb-0.9.20/snap/local/000077500000000000000000000000001421404445200152145ustar00rootroot00000000000000ipp-usb-0.9.20/snap/local/run-ipp-usb000077500000000000000000000032551421404445200173300ustar00rootroot00000000000000#!/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.20/snap/snapcraft.yaml000066400000000000000000000046711421404445200167770ustar00rootroot00000000000000name: ipp-usb base: core20 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: command: scripts/run-ipp-usb daemon: simple plugs: [avahi-control, network, network-bind, raw-usb, hardware-observe] parts: goipp: plugin: go source: https://github.com/OpenPrinting/goipp.git source-type: git 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 snapcraftctl build # 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: - 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.20/systemd-udev/000077500000000000000000000000001421404445200156125ustar00rootroot00000000000000ipp-usb-0.9.20/systemd-udev/71-ipp-usb.rules000066400000000000000000000003001421404445200204630ustar00rootroot00000000000000ACTION=="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" ipp-usb-0.9.20/systemd-udev/ipp-usb.service000066400000000000000000000003171421404445200205540ustar00rootroot00000000000000[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.20/testdata/000077500000000000000000000000001421404445200147725ustar00rootroot00000000000000ipp-usb-0.9.20/testdata/ipp-usb.conf000066400000000000000000000031401421404445200172160ustar00rootroot00000000000000# 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.20/testdata/quirks/000077500000000000000000000000001421404445200163105ustar00rootroot00000000000000ipp-usb-0.9.20/testdata/quirks/HP.conf000066400000000000000000000002341421404445200174650ustar00rootroot00000000000000# 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.20/testdata/quirks/blacklist.conf000066400000000000000000000003011421404445200211210ustar00rootroot00000000000000# 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.20/testdata/quirks/default.conf000066400000000000000000000001451421404445200206030ustar00rootroot00000000000000# ipp-usb quirks file -- defaults [*] # Drop Connection: header by default http-connection = "" ipp-usb-0.9.20/usbcommon.go000066400000000000000000000165071421404445200155230ustar00rootroot00000000000000/* 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 { Config int // Configuration IfNum int // Interface number Alt int // Alternate setting Class int // Class SubClass int // Subclass Proto int // Protocol } // UsbIfDesc check if interface is IPP over USB func (ifdesc UsbIfDesc) IsIppOverUsb() bool { return ifdesc.Class == 7 && ifdesc.SubClass == 1 && ifdesc.Proto == 4 } // 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, ",") } // Fix 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.20/usbcommon_test.go000066400000000000000000000034041421404445200165520ustar00rootroot00000000000000/* 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.20/usbio_libusb.go000066400000000000000000000461141421404445200161770ustar00rootroot00000000000000/* 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" "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 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{}) ) // Initialize low-level USB I/O func UsbInit() error { _, err := libusbContext() return err } // libusbContext returns libusb_context. It // initializes context on demand. func libusbContext() (*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 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() { 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() 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) 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 c_desc C.libusb_device_descriptor_struct var desc UsbDeviceDesc // Obtain device descriptor rc := C.libusb_get_device_descriptor(dev, &c_desc) 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(c_desc.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 ifnum, iface := range ifaces { altcnt := iface.num_altsetting alts := (*[256]C.libusb_interface_descriptor_struct)( unsafe.Pointer(iface.altsetting))[:altcnt:altcnt] for altnum, alt := range alts { // Build and append UsbIfDesc ifdesc := UsbIfDesc{ 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: ifnum, Alt: altnum, 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() 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 c_desc C.libusb_device_descriptor_struct rc := C.libusb_get_device_descriptor(dev, &c_desc) 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(c_desc.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 c_desc C.libusb_device_descriptor_struct var info UsbDeviceInfo // Obtain device descriptor rc := C.libusb_get_device_descriptor(dev, &c_desc) if rc < 0 { return info, UsbError{"libusb_get_device_descriptor", UsbErrCode(rc)} } // Decode device descriptor info.Vendor = uint16(c_desc.idVendor) info.Product = uint16(c_desc.idProduct) info.BasicCaps = devhandle.usbIppBasicCaps() buf := make([]byte, 256) strings := []struct { idx C.uint8_t str *string }{ {c_desc.iManufacturer, &info.Manufacturer}, {c_desc.iProduct, &info.ProductName}, {c_desc.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.pdf 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 MAX_BULK_READ = 16384 if len(data) > MAX_BULK_READ { data = data[0:MAX_BULK_READ] } 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 } // Clear "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.20/usbtransport.go000066400000000000000000000500711421404445200162610ustar00rootroot00000000000000/* 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" "strings" "sync/atomic" "time" ) // UsbTransport implements HTTP transport functionality over USB type UsbTransport struct { addr UsbAddr // Device address info UsbDeviceInfo // USB device info log *Logger // Device's own logger dev *UsbDevHandle // Underlying USB device connPool chan *usbConn // Pool of idle connections connList []*usbConn // List of all connections connReleased chan struct{} // Signalled when connection released shutdown chan struct{} // Closed by Shutdown() connstate *usbConnState // Connections state tracker quirks []Quirks // Device quirks 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{}), 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.Get(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(' ', " 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) log.Debug(' ', "Device quirks:") for _, quirks := range transport.quirks { log.Debug(' ', " from [%s] (%s)", quirks.Model, quirks.Origin) log.Debug(' ', " blacklist = %v", quirks.Blacklist) log.Debug(' ', " usb-max-interfaces = %v", quirks.UsbMaxInterfaces) for name, value := range quirks.HttpHeaders { log.Debug(' ', " http-%s = %q", strings.ToLower(name), value) } } 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 len(transport.quirks) > 0 && transport.quirks[0].Blacklist { err = ErrBlackListed goto ERROR } // Configure the device err = dev.Configure(desc) if err != nil { goto ERROR } // Open connections maxconn = math.MaxUint32 for _, quirks := range transport.quirks { if quirks.UsbMaxInterfaces != 0 { maxconn = quirks.UsbMaxInterfaces } } for i, ifaddr := range desc.IfAddrs { var conn *usbConn conn, err = transport.openUsbConn(i, ifaddr) 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 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 } // 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 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) // 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, } // Log the response if resp != nil { transport.log.Begin(). HTTPRspStatus(LogDebug, '<', session, outreq, resp). HTTPResponse(LogTraceHTTP, '<', session, resp). Commit() } return resp, nil } // 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 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) { 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 cntRecv int // Total bytes received cntSent int // Total bytes sent } // Open usbConn func (transport *UsbTransport) openUsbConn( index int, ifaddr UsbIfAddr) (*usbConn, error) { dev := transport.dev transport.log.Debug(' ', "USB[%d]: open: %s", index, ifaddr) // Initialize connection structure conn := &usbConn{ transport: transport, index: index, } conn.reader = bufio.NewReader(conn) // Obtain interface var err error conn.iface, err = dev.OpenUsbInterface(ifaddr) if err != nil { goto ERROR } // Soft-reset interface // // Note, disabled for now, because it causes problems // with EPSON ET-4750 (see #17) // // May be in a future we will enable it conditionally, // for some printer models (based on quirks) // // 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) 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) 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.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.20/uuid.go000066400000000000000000000020511421404445200144540ustar00rootroot00000000000000/* 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.20/uuid_test.go000066400000000000000000000021561421404445200155210ustar00rootroot00000000000000/* 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) } } }