pax_global_header00006660000000000000000000000064145336561170014525gustar00rootroot0000000000000052 comment=60f6377ba196853c202aa9ea34a3ef30c27f6ee6 ipp-usb-0.9.24/000077500000000000000000000000001453365611700132005ustar00rootroot00000000000000ipp-usb-0.9.24/.gitignore000066400000000000000000000000321453365611700151630ustar00rootroot00000000000000ipp-usb tags *.swp *.orig ipp-usb-0.9.24/LICENSE000066400000000000000000000024571453365611700142150ustar00rootroot00000000000000BSD 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.24/Makefile000066400000000000000000000014111453365611700146350ustar00rootroot00000000000000MANDIR = /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.24/README.md000066400000000000000000000266111453365611700144650ustar00rootroot00000000000000# ipp-usb ![GitHub](https://img.shields.io/github/license/OpenPrinting/ipp-usb) [![Go Report Card](https://goreportcard.com/badge/github.com/OpenPrinting/ipp-usb)](https://goreportcard.com/badge/github.com/OpenPrinting/ipp-usb) ## Introduction [IPP-over-USB](https://www.usb.org/document-library/ipp-protocol-10) allows using the IPP protocol, normally designed for network printers, to be used with USB printers as well. The idea behind this standard is simple: It allows to send HTTP requests to the device via a USB connection, so enabling IPP, eSCL (AirScan) and web console on devices without Ethernet or WiFi connections. Unfortunately, the naive implementation, which simply relays a TCP connection to USB, does not work. It happens because closing the TCP connection on the client side has a useful side effect of discarding all data sent to this connection from the server side, but it does not happen with USB connections. In the case of USB, all data not received by the client will remain in the USB buffers, and the next time the client connects to the device, it will receive unexpected data, left from the previous abnormally completed request. Actually, it is an obvious flaw in the IPP-over-USB standard, but we have to live with it. So the implementation, once the HTTP request is sent, must read the entire HTTP response, which means that the implementation must understand the HTTP protocol, and effectively implement a HTTP reverse proxy, backed by the IPP-over-USB connection to the device. And this is what the **ipp-usb** program actually does. ## Features in detail * Implements HTTP proxy, backed by USB connection to IPP-over-USB device * Full support of IPP printing, eSCL scanning, and web admin interface * DNS-SD advertising for all supported services * DNS-SD parameters for IPP based on IPP get-printer-attributes query * DNS-SD parameters for eSCL based on parsing GET /eSCL/ScannerCapabilities response * TCP port allocation for device is bound to particular device (combination of VendorID, ProductID and device serial number), so if the user has multiple devices, they will receive the same TCP port when connected. This allocation is persisted on a disk * Automatic DNS-SD name conflict resolution. The finally chosen device's network name is persisted on a disk * Can be started by **UDEV** or run in standalone mode * Can share printer to other computers on a network, or use the loopback interface only * Can generate very detailed logs for possible troubleshooting ## Under the hood Though looks simple, ipp-usb does many non obvious things under the hood * Client-side HTTP connections are completely decoupled from printer-side HTTP-over-USB connections * HTTP requests are sanitized, missed headers are added * HTTP protocol upgraded from 1.0 to 1.1, if needed * Attempts to upgrade HTTP connection to winsock, if unwisely made by web console, are prohibited, because it can steal USB connection for a long time * Client HTTP requests are fairly balanced between all available 2-3 USB connections, regardless of number and persistence of client connections * Dropping connection by client properly handled in all cases, even in a middle of sending. In a worst case, printer may receive truncated document, but HTTP transaction will always be performed correctly ## Memory footprint Being written on Go, ipp-usb has a large executable size. However, its memory consumption is not very high. When single device is connected, ipp-usb RSS is similar or even slightly less in comparison to ippusbxd. And because ipp-usb handles all devices in a single process, it uses noticeably less memory that ippusbxd, when serving 2 or more devices. ## External dependencies This program has very few external dependencies, namely: * `libusb` for USB access * `libavahi-common` and `libavahi-client` for DNS-SD * Running Avahi daemon ## Binary packages Binary packages available for the following Linux distros: * **Debian** (10) * **Fedora** (29, 30, 31 and 32) * **openSUSE** (Tumbleweed) * **Ubuntu** (18.04, 19.04, 19.10 and 20.04) **Linux Mint** users may use Ubuntu packages: * Linux Mint 18.x - use packages for Ubuntu 16.04 * Linux Mint 19.x - use packages for Ubuntu 18.04 Follow this link for downloads: https://download.opensuse.org/repositories/home:/pzz/ ## Not only Linux We are glad to announce that `ipp-usb` was recently included into the FreeBSD ports: https://www.freshports.org/print/ipp-usb/ Hope, NetBSD/OpenBSD support will be added as well, so technology becomes not Linux-only, but UNIX-wide. ## The ipp-usb Snap ipp-usb is also available as a Snap in the Snap Store: https://snapcraft.io/ipp-usb Before you install the Snap, uninstall any already existing installation of ipp-usb. Simply install it via any GUI client for the Snap Store (Like "Ubuntu Software") or via command line: sudo snap install --edge ipp-usb Now you can connect and disconnect IPP-over-USB devices and ipp-usb gets started by the Snap whenever needed. Also devices which are already connected during boot, start, or update of the Snap are considered. You can also use ipp-usb status to check the status of the running ipp-usb daemon (supported device must be connected for the ipp-usb daemon to be running, accesses only the ipp-usb daemon of the Snap) and ipp-usb check to scan the USB for the presence of potentially supported USB devices (7/1/4 interface protocol). This command requires access to the raw USB and therefore on many systems root privileges are required. The Snap is automatically updated when further development on ipp-usb happens. The configuration file is here: /var/snap/ipp-usb/common/etc/ipp-usb.conf You can edit it and afterwards restart the Snap to use the changed configuration. Incompatibilities of particular devices are handled by workarounds defined in the quirk files. You find them here: /var/snap/ipp-usb/common/quirks You can add your own quirk files (but if they solve your problem, please report an issue here, with your quirk file attached, so that others with the same problem will get helped, too). For quick tests you can also edit the existing files, but they will get replaced (and so your changes lost) on the next update of the Snap, as we are changing them on any report of further device incompatibilities. The log file is here /var/snap/ipp-usb/common/var/log and device state files (to assure that each device appears on the same port and with the same DNS-SD service name) are here: /var/snap/ipp-usb/common/var/dev You can also build the Snap locally. This is useful when * You want to modify ipp-usb * You want to learn about snapping Go projects * You want to learn about how to use UDEV from within a Snap (note that a Snap cannot install UDEV rules into the system) To do so, run from the main directory of this source repository snapcraft snap and then install the resulting Snap with sudo snap install --dangerous ipp-usb*.snap An installed Snap from the Snap Store will get overwritten/replaced by your Snap. Some technical notes about this Snap: Snapping the Go project with one Go library taken from upstream (and not from Ubuntu Core) was rather straight-forward. Only observation was that the Go plugin seems not to do "make install". So I had to use an "override-build" to manually install the auxiliary files (ipp-usb.conf, quirk files). I also have adapted the auxiliary file and state directories in paths.go in the "override-build" scriptlet. The real challenge of this Snap was to trigger ipp-usb on the appearing (and also the presence) of IPP-over-USB devices. In the classic installation of ipp-usb (via "make install" or RPM/DEB package installation) a UDEV rules file and a systemd service file (in systemd-udev/) are installed, so that the system automatically triggers the launch of ipp-usb when an appropriate device is connected or already present. A Snap is not able to do so. It cannot install any files into the system. It can only bring its own, static file system and create files only in its own state directory. These locations are not scanned for UDEV rules. So the Snap must discover the devices without its own UDEV rules, but it still can use UDEV. The trick is to do a generic monitoring of UDEV events and filtering out the USB devices with IPP-over-USB interface (7/1/4). If such a device appears, we trigger and ipp-usb launch. We also check on startup of the Snap whether there is such a device already and if so, we also trigger an ipp-usb launch. ipp-usb is run, as in the classic installation, with "udev" argument. This way it stops by itself when there is no device any more (and we do not need to observe the disappearal events of the devices) and it is assured that only one single instance of ipp-usb is running. To do this with low coding effort I use the UDEV command line tool udevadm in a shell script (snap/local/run-ipp-usb). Once it runs in "monitor" mode to observe the UDEV events. Then we parse the output lines to only consider the ones for a device appearing and run "udevadm info -q property" on each device path, to get the properties and filter the 7/1/4 interface. In the beginning we use "udevadm trigger" to find the already passed appearal event of a device which is already present. So the shell script is an auxiliary daemon to start ipp-usb when needed. ## Installation from source You will need to install the following packages (exact name depends of your Linux distro): * libusb development files * libavahi-client and libavahi-common development files * gcc * Go compiler * pkg-config * git, make and so on Building is really simple: git clone https://github.com/OpenPrinting/ipp-usb.git cd ipp-usb make Then you may `make install` or just try to run `./ipp-usb` directly from the build directory ## Avahi Notes (exposing printer to localhost) IPP-over-USB normally exposes printer to localhost only, hence it requires DNS-SD announces to work for localhost. This requires Avahi 0.8.0 or newer. Older Avahi versions do not support announcing to localhost. Some Linux distros (for example recent Ubuntu and Fedora versions) have their Avahi patched to support localhost, others (for example Debian) not. To determine if your Avahi supports localhost, run the following command in one terminal session: ``` avahi-publish -s test _test._tcp 1234 ``` And simultaneously the following command in another terminal session on the same machine: ``` avahi-browse _test._tcp -r ``` If you see localhost in the avahi-browse output, like this: ``` = lo IPv4 test _test._tcp local hostname = [localhost] address = [127.0.0.1] port = [1234] txt = [] ``` your Avahi is OK. Otherwise, update or patching is required. So users of distros that ship a too old Avahi and without the patch have three possibilities: 1. Update Avahi to 0.8.0 or newer 2. Apply the patch by themself, rebuild and reinstall avahi-daemon 3. Configure `ipp-usb` to run on all network interfaces, not only on loopback If you decide to apply the patch, get it as `avahi/avahi-localhost.patch` in this package or [download it here](https://raw.githubusercontent.com/OpenPrinting/ipp-usb/master/avahi/avahi-localhost.patch). The third method is simple to do, just replace `interface = loopback` with `interface = all` in the `ipp-usb.conf` file, but this has the disadvantage of exposing your local USB-connected printer to the entire local network, which can be an unwanted side effect, especially in a big corporative network. ipp-usb-0.9.24/addpdl_test.go000066400000000000000000000037741453365611700160310ustar00rootroot00000000000000/* 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.24/auth.go000066400000000000000000000131041453365611700144670ustar00rootroot00000000000000/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device * * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com) * See LICENSE for license terms and conditions * * Authentication */ package main import ( "errors" "net" "net/http" "os/user" "strconv" "strings" "time" ) // AuthUIDRule represents a single rule for client authentication // based on client UID type AuthUIDRule struct { Name string // @name means group, * means any Allowed AuthOps // Allowed operations } // IsUser tells if rule is a user rule func (rule *AuthUIDRule) IsUser() bool { return !rule.IsGroup() } // IsGroup tells if rule is a group rule func (rule *AuthUIDRule) IsGroup() bool { return strings.HasPrefix(rule.Name, "@") } // MatchUser matches rule against user name func (rule *AuthUIDRule) MatchUser(name string) AuthOps { if rule.IsGroup() { return 0 } if rule.Name == "*" || rule.Name == name { return rule.Allowed } return 0 } // MatchGroup matches rule against group name func (rule *AuthUIDRule) MatchGroup(name string) AuthOps { if !rule.IsGroup() { return 0 } ruleName := rule.Name[1:] // Strip leading '@' if ruleName == "*" || ruleName == name { return rule.Allowed } return 0 } // AuthOps is bitmask of allowed operations type AuthOps int // AuthOps values const ( AuthOpsConfig AuthOps = 1 << iota // Configuration web console AuthOpsFax // Faxing AuthOpsPrint // Printing AuthOpsScan // Scanning // All and None of above AuthOpsAll = AuthOpsConfig | AuthOpsFax | AuthOpsPrint | AuthOpsScan AuthOpsNone AuthOps = 0 ) // authUIDinfo is the resolved and cached UID info, for matching type authUIDinfo struct { usrNames []string // User (numerical and symbolic) names grpNames []string // Group names (numerical and symbolic) expires time.Time // Expiration time } // authUIDinfoCache contains authUIDinfo cache, indexed by UID var authUIDinfoCache = make(map[int]*authUIDinfo) // authUIDinfoCacheTTL is the expiration timeout for authUIDinfoCache const authUIDinfoCacheTTL = 2 * time.Second // authUIDinfoLookup performs authUIDinfo lookup by UID func authUIDinfoLookup(uid int) (*authUIDinfo, error) { // Lookup authUIDinfoCache info := authUIDinfoCache[uid] if info != nil && info.expires.After(time.Now()) { return info, nil } // Resolve user names for matching // Also populates grpIDs with numeric group IDs usrNames := []string{strconv.Itoa(uid)} grpIDs := []string{} if usr, err := user.LookupId(usrNames[0]); err != nil { return nil, err } else { usrNames = append(usrNames, usr.Username) grpIDs = append(grpIDs, usr.Gid) grpids, err := usr.GroupIds() if err != nil { return nil, err } grpIDs = append(grpIDs, grpids...) } // Resolve group IDs to names grpNames := append([]string{}, grpIDs...) for _, gid := range grpIDs { grp, err := user.LookupGroupId(gid) if err != nil { return nil, err } grpNames = append(grpNames, grp.Name) } // Update cache info = &authUIDinfo{ usrNames: usrNames, grpNames: grpNames, expires: time.Now().Add(authUIDinfoCacheTTL), } authUIDinfoCache[uid] = info // Return the answer return info, nil } // AuthUID returns operations allowed to client with given UID // uid == -1 indicates that UID is not available (i.e., external // connection) func AuthUID(uid int) (AuthOps, error) { // Everything is allowed if authentication is not configured if Conf.ConfAuthUID == nil { return AuthOpsAll, nil } // Lookup UID info info, err := authUIDinfoLookup(uid) if err != nil { return 0, err } // Apply rules allowed := AuthOpsNone for _, rule := range Conf.ConfAuthUID { if rule.IsUser() { for _, usr := range info.usrNames { allowed |= rule.MatchUser(usr) } } else { for _, grp := range info.grpNames { allowed |= rule.MatchGroup(grp) } } } return allowed, nil } // AuthHTTPRequest performs authentication for the incoming // HTTP request // // On success, status is http.StatusOK and err is nil. // Otherwise, status is appropriate for HTTP error response, // and err explains the reason func AuthHTTPRequest(client, server *net.TCPAddr, rq *http.Request) ( status int, err error) { // Quess the operation by URL post := rq.Method == "POST" ops := AuthOpsConfig // The default switch { case post && strings.HasPrefix(rq.URL.Path, "/ipp/print"): ops = AuthOpsPrint case post && strings.HasPrefix(rq.URL.Path, "/ipp/faxout"): ops = AuthOpsFax case strings.HasPrefix(rq.URL.Path, "/eSCL"): ops = AuthOpsScan } // Check if client and server addresses are both local addrs, err := net.InterfaceAddrs() if err != nil { return http.StatusInternalServerError, err } clientIsLocal := client.IP.IsLoopback() serverIsLocal := server.IP.IsLoopback() for _, addr := range addrs { if clientIsLocal && serverIsLocal { // Both addresses known to be local, // we don't need to continue break } if ip, ok := addr.(*net.IPNet); ok { if client.IP.Equal(ip.IP) { clientIsLocal = true } if server.IP.Equal(ip.IP) { serverIsLocal = true } } } // Obtain UID uid := -1 if clientIsLocal && serverIsLocal { if TCPClientUIDSupported() { uid, err = TCPClientUID(client, server) if err != nil { return http.StatusInternalServerError, err } } } // Authenticate allowed, err := AuthUID(uid) if err != nil { return http.StatusInternalServerError, err } if ops&allowed != AuthOpsNone { return http.StatusOK, nil } err = errors.New("Operation not allowed. See ipp-usb.conf for details") return http.StatusForbidden, err } ipp-usb-0.9.24/avahi/000077500000000000000000000000001453365611700142705ustar00rootroot00000000000000ipp-usb-0.9.24/avahi/avahi-localhost.patch000066400000000000000000000054721453365611700203770ustar00rootroot00000000000000diff --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.24/conf.go000066400000000000000000000122741453365611700144620ustar00rootroot00000000000000/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device * * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com) * See LICENSE for license terms and conditions * * Program configuration */ package main import ( "errors" "fmt" "io" "os" "path/filepath" "strings" "unicode" ) const ( // ConfFileName defines a name of ipp-usb configuration file ConfFileName = "ipp-usb.conf" ) // Configuration represents a program configuration type Configuration struct { HTTPMinPort int // Starting port number for HTTP to bind to HTTPMaxPort int // Ending port number for HTTP to bind to DNSSdEnable bool // Enable DNS-SD advertising LoopbackOnly bool // Use only loopback interface IPV6Enable bool // Enable IPv6 advertising ConfAuthUID []*AuthUIDRule // [auth uid], parsed LogDevice LogLevel // Per-device LogLevel mask LogMain LogLevel // Main log LogLevel mask LogConsole LogLevel // Console LogLevel mask LogMaxFileSize int64 // Maximum log file size LogMaxBackupFiles uint // Count of files preserved during rotation LogAllPrinterAttrs bool // Get *all* printer attrs, for logging ColorConsole bool // Enable ANSI colors on console Quirks QuirksSet // Device quirks } // Conf contains a global instance of program configuration var Conf = Configuration{ HTTPMinPort: 60000, HTTPMaxPort: 65535, DNSSdEnable: true, LoopbackOnly: true, IPV6Enable: true, ConfAuthUID: nil, LogDevice: LogDebug, LogMain: LogDebug, LogConsole: LogDebug, LogMaxFileSize: 256 * 1024, LogMaxBackupFiles: 5, LogAllPrinterAttrs: false, ColorConsole: true, } // ConfLoad loads the program configuration func ConfLoad() error { // Obtain path to executable directory exepath, err := os.Executable() if err != nil { return fmt.Errorf("conf: %s", err) } exepath = filepath.Dir(exepath) // Build list of configuration files files := []string{ filepath.Join(PathConfDir, ConfFileName), filepath.Join(exepath, ConfFileName), } // Load file by file for _, file := range files { err = confLoadInternal(file) if err != nil { return err } } // Load quirks quirksDirs := []string{ PathQuirksDir, PathConfQuirksDir, filepath.Join(exepath, "ipp-usb-quirks"), } if err == nil { Conf.Quirks, err = LoadQuirksSet(quirksDirs...) } return err } // Load the program configuration -- internal version func confLoadInternal(path string) error { // Open configuration file ini, err := OpenIniFile(path) if err != nil { if os.IsNotExist(err) { err = nil } return err } defer ini.Close() // Extract options for err == nil { var rec *IniRecord rec, err = ini.Next() if err != nil { break } switch { case confMatchName(rec.Section, "network"): switch { case confMatchName(rec.Key, "http-min-port"): err = rec.LoadIPPort(&Conf.HTTPMinPort) case confMatchName(rec.Key, "http-max-port"): err = rec.LoadIPPort(&Conf.HTTPMaxPort) case confMatchName(rec.Key, "dns-sd"): err = rec.LoadNamedBool(&Conf.DNSSdEnable, "disable", "enable") case confMatchName(rec.Key, "interface"): err = rec.LoadNamedBool(&Conf.LoopbackOnly, "all", "loopback") case confMatchName(rec.Key, "ipv6"): err = rec.LoadNamedBool(&Conf.IPV6Enable, "disable", "enable") } case confMatchName(rec.Section, "auth uid"): err = rec.LoadAuthUIDRules(&Conf.ConfAuthUID) case confMatchName(rec.Section, "logging"): switch { case confMatchName(rec.Key, "device-log"): err = rec.LoadLogLevel(&Conf.LogDevice) case confMatchName(rec.Key, "main-log"): err = rec.LoadLogLevel(&Conf.LogMain) case confMatchName(rec.Key, "console-log"): err = rec.LoadLogLevel(&Conf.LogConsole) case confMatchName(rec.Key, "console-color"): err = rec.LoadNamedBool(&Conf.ColorConsole, "disable", "enable") case confMatchName(rec.Key, "max-file-size"): err = rec.LoadSize(&Conf.LogMaxFileSize) case confMatchName(rec.Key, "max-backup-files"): err = rec.LoadUint(&Conf.LogMaxBackupFiles) case confMatchName(rec.Key, "get-all-printer-attrs"): err = rec.LoadBool(&Conf.LogAllPrinterAttrs) } } } if err != nil && err != io.EOF { return err } // Validate configuration if Conf.HTTPMinPort >= Conf.HTTPMaxPort { return errors.New("http-min-port must be less that http-max-port") } return nil } // confMatchName tells if section or key name matches // the pattern // - match is case-insensitive // - difference in amount of free space is ignored // - leading and trailing space is ignored func confMatchName(name, pattern string) bool { name = strings.TrimSpace(name) pattern = strings.TrimSpace(pattern) for name != "" && pattern != "" { c1 := rune(name[0]) c2 := rune(pattern[0]) switch { case unicode.IsSpace(c1): if !unicode.IsSpace(c2) { return false } name = strings.TrimSpace(name) pattern = strings.TrimSpace(pattern) case c1 == c2: name = name[1:] pattern = pattern[1:] default: return false } } return true } ipp-usb-0.9.24/const.go000066400000000000000000000014161453365611700146570ustar00rootroot00000000000000/* 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.24/ctrlsock.go000066400000000000000000000055271453365611700153640ustar00rootroot00000000000000/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device * * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com) * See LICENSE for license terms and conditions * * Control socket handler * * ipp-usb runs a HTTP server on a top of the unix domain control * socket. * * Currently it is only used to obtain a per-device status from the * running daemon. Using HTTP here sounds as overkill, but taking * in account that it costs us virtually nothing and this mechanism * is well-extendable, this is a good choice */ package main import ( "log" "net" "net/http" "os" "syscall" ) var ( // CtrlsockAddr contains control socket address in // a form of the net.UnixAddr structure CtrlsockAddr = &net.UnixAddr{Name: PathControlSocket, Net: "unix"} // ctrlsockServer is a HTTP server that runs on a top of // the status socket ctrlsockServer = http.Server{ Handler: http.HandlerFunc(ctrlsockHandler), ErrorLog: log.New(Log.LineWriter(LogError, '!'), "", 0), } ) // ctrlsockHandler handles HTTP requests that come over the // control socket func ctrlsockHandler(w http.ResponseWriter, r *http.Request) { Log.Debug(' ', "ctrlsock: %s %s", r.Method, r.URL) // Catch panics to log defer func() { v := recover() if v != nil { Log.Panic(v) } }() // Check request method if r.Method != "GET" { http.Error(w, r.Method+": method not supported", http.StatusMethodNotAllowed) return } // Check request path if r.URL.Path != "/status" { http.Error(w, "Not found", http.StatusNotFound) return } // Handle the request w.Header().Set("Content-Type", "text/plain; charset=utf-8") httpNoCache(w) w.WriteHeader(http.StatusOK) w.Write(StatusFormat()) } // CtrlsockStart starts control socket server func CtrlsockStart() error { Log.Debug(' ', "ctrlsock: listening at %q", PathControlSocket) // Listen the socket os.Remove(PathControlSocket) listener, err := net.ListenUnix("unix", CtrlsockAddr) if err != nil { return err } // Make socket accessible to everybody. Error is ignores, // it's not a reason to abort ipp-usb os.Chmod(PathControlSocket, 0777) // Start HTTP server on a top of the listening socket go func() { ctrlsockServer.Serve(listener) }() return nil } // CtrlsockStop stops the control socket server func CtrlsockStop() { Log.Debug(' ', "ctrlsock: shutdown") ctrlsockServer.Close() } // CtrlsockDial connects to the control socket of the running // ipp-usb daemon func CtrlsockDial() (net.Conn, error) { conn, err := net.DialUnix("unix", nil, CtrlsockAddr) if err == nil { return conn, err } if neterr, ok := err.(*net.OpError); ok { if syserr, ok := neterr.Err.(*os.SyscallError); ok { switch syserr.Err { case syscall.ECONNREFUSED, syscall.ENOENT: err = ErrNoIppUsb case syscall.EACCES, syscall.EPERM: err = ErrAccess } } } return conn, err } ipp-usb-0.9.24/daemon.go000066400000000000000000000041541453365611700147760ustar00rootroot00000000000000/* 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.24/debian/000077500000000000000000000000001453365611700144225ustar00rootroot00000000000000ipp-usb-0.9.24/debian/changelog000066400000000000000000000002441453365611700162740ustar00rootroot00000000000000ipp-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.24/debian/compat000066400000000000000000000000031453365611700156210ustar00rootroot0000000000000011 ipp-usb-0.9.24/debian/control000066400000000000000000000017121453365611700160260ustar00rootroot00000000000000Source: 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.24/debian/copyright000066400000000000000000000033551453365611700163630ustar00rootroot00000000000000Format: 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.24/debian/rules000077500000000000000000000002061453365611700155000ustar00rootroot00000000000000#!/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.24/debian/source/000077500000000000000000000000001453365611700157225ustar00rootroot00000000000000ipp-usb-0.9.24/debian/source/format000066400000000000000000000000041453365611700171270ustar00rootroot000000000000001.0 ipp-usb-0.9.24/device.go000066400000000000000000000131161453365611700147700ustar00rootroot00000000000000/* 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.UsbTransport.Quirks(), dev.HTTPClient) if err != nil { dev.Log.Error('!', "IPP: %s", err) } log.Flush() if dev.UsbTransport.DeadlineExpired() { err = ErrInitTimedOut goto ERROR } // Obtain DNS-SD name if ippinfo != nil { dnssdName = ippinfo.DNSSdName } else { dnssdName = info.DNSSdName() } // Update device state, if name changed if dnssdName != dev.State.DNSSdName { dev.State.DNSSdName = dnssdName dev.State.DNSSdOverride = dnssdName dev.State.Save() } // Obtain DNS-SD info for eSCL err = EsclService(log, &dnssdServices, dev.State.HTTPPort, info, ippinfo, dev.HTTPClient) if err != nil { dev.Log.Error('!', "ESCL: %s", err) } log.Flush() if dev.UsbTransport.DeadlineExpired() { err = ErrInitTimedOut goto ERROR } // Update IPP service advertising for scanner presence if ippinfo != nil { if ippSvc := &dnssdServices[ippinfo.IppSvcIndex]; err == nil { ippSvc.Txt.Add("Scan", "T") } else { ippSvc.Txt.Add("Scan", "F") } } // Skip the device, if it cannot do something useful // // Some devices (so far, only HP-rebranded Samsung devices // known to have such a defect) offer 7/1/4 interfaces, but // actually provide no functionality behind these interfaces // and respond with `HTTP 404 Not found` to all the HTTP // requests sent to USB // // ipp-usb ignores such devices to let a chance for // legacy/proprietary drivers to work with them if len(dnssdServices) == 0 { err = ErrUnusable goto ERROR } // Advertise Web service. Assume it always exists dnssdServices.Add(DNSSdSvcInfo{Type: "_http._tcp", Port: dev.State.HTTPPort}) // Advertise service with the following parameters: // Instance: "BBPP", where BB and PP are bus and port numbers in hex // Type: "_ipp-usb._tcp" // // The purpose of this advertising is to help legacy drivers to // easily check for devices, handled by ipp-usb // // See the following for details: // https://github.com/OpenPrinting/ipp-usb/issues/28 dnssdServices.Add(DNSSdSvcInfo{ Instance: fmt.Sprintf("%.2X%.2x", desc.Bus, info.PortNum), Type: "_ipp-usb._tcp", Port: dev.State.HTTPPort, Loopback: true, }) // Enable handling incoming requests dev.UsbTransport.SetDeadline(time.Time{}) dev.HTTPProxy.Enable() // Start DNS-SD publisher for _, svc := range dnssdServices { dev.Log.Debug('>', "%s: %s TXT record:", dnssdName, svc.Type) for _, txt := range svc.Txt { dev.Log.Debug(' ', " %s=%s", txt.Key, txt.Value) } } if Conf.DNSSdEnable { dev.DNSSdPublisher = NewDNSSdPublisher(dev.Log, dev.State, dnssdServices) err = dev.DNSSdPublisher.Publish() if err != nil { goto ERROR } } return dev, nil ERROR: if dev.HTTPProxy != nil { dev.HTTPProxy.Close() } if dev.UsbTransport != nil { dev.UsbTransport.Close(true) } if listener != nil { listener.Close() } return nil, err } // Shutdown gracefully shuts down the device. If provided context // expires before the shutdown is complete, Shutdown returns the // context's error func (dev *Device) Shutdown(ctx context.Context) error { if dev.DNSSdPublisher != nil { dev.DNSSdPublisher.Unpublish() dev.DNSSdPublisher = nil } if dev.HTTPProxy != nil { dev.HTTPProxy.Close() dev.HTTPProxy = nil } if dev.UsbTransport != nil { return dev.UsbTransport.Shutdown(ctx) } return nil } // Close the Device func (dev *Device) Close() { if dev.DNSSdPublisher != nil { dev.DNSSdPublisher.Unpublish() dev.DNSSdPublisher = nil } if dev.HTTPProxy != nil { dev.HTTPProxy.Close() dev.HTTPProxy = nil } if dev.UsbTransport != nil { dev.UsbTransport.Close(false) dev.UsbTransport = nil } } ipp-usb-0.9.24/devstate.go000066400000000000000000000070131453365611700153470ustar00rootroot00000000000000/* 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.24/dnssd.go000066400000000000000000000171051453365611700146460ustar00rootroot00000000000000/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device * * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com) * See LICENSE for license terms and conditions * * DNS-SD publisher: system-independent stuff */ package main import ( "fmt" "strings" "sync" "time" ) // DNSSdTxtItem represents a single TXT record item type DNSSdTxtItem struct { Key, Value string // TXT entry: Key=Value URL bool // It's an URL, hostname must be adjusted } // DNSSdTxtRecord represents a TXT record type DNSSdTxtRecord []DNSSdTxtItem // Add adds regular (non-URL) item to DNSSdTxtRecord func (txt *DNSSdTxtRecord) Add(key, value string) { *txt = append(*txt, DNSSdTxtItem{key, value, false}) } // AddURL adds URL item to DNSSdTxtRecord func (txt *DNSSdTxtRecord) AddURL(key, value string) { *txt = append(*txt, DNSSdTxtItem{key, value, true}) } // AddPDL adds PDL list (list of supported Page Description Languages, i.e., // document formats) to the DNSSdTxtRecord. // // Sometimes the PDL list that comes from device, is too large to fit // TXT record (key=value pair must not exceed 255 bytes). At this case // we take only as much as possible leading entries of the device-supplied // list in hope that firmware is smart enough to place most common PDLs // to the beginning of the list, while more exotic entries goes to the end func (txt *DNSSdTxtRecord) AddPDL(key, value string) { // How many space we have for value? Is it enough? max := 255 - len(key) - 1 if max >= len(value) { txt.Add(key, value) return } // Safety check if max <= 0 { return } // Truncate the value to fit available space value = value[:max+1] i := strings.LastIndexByte(value, ',') if i < 0 { return } value = value[:i] txt.Add(key, value) } // IfNotEmpty adds item to DNSSdTxtRecord if its value is not empty // // It returns true if item was actually added, false otherwise func (txt *DNSSdTxtRecord) IfNotEmpty(key, value string) bool { if value != "" { txt.Add(key, value) return true } return false } // URLIfNotEmpty works as IfNotEmpty, but for URLs func (txt *DNSSdTxtRecord) URLIfNotEmpty(key, value string) bool { if value != "" { txt.AddURL(key, value) return true } return false } // export DNSSdTxtRecord into Avahi format func (txt DNSSdTxtRecord) export() [][]byte { var exported [][]byte // Note, for a some strange reason, Avahi published // TXT record in reverse order, so compensate it here for i := len(txt) - 1; i >= 0; i-- { item := txt[i] exported = append(exported, []byte(item.Key+"="+item.Value)) } return exported } // DNSSdSvcInfo represents a DNS-SD service information type DNSSdSvcInfo struct { Instance string // If not "", override common instance name Type string // Service type, i.e. "_ipp._tcp" SubTypes []string // Service subtypes, if any Port int // TCP port Txt DNSSdTxtRecord // TXT record Loopback bool // Advertise only on loopback interface } // DNSSdServices represents a collection of DNS-SD services type DNSSdServices []DNSSdSvcInfo // Add DNSSdSvcInfo to DNSSdServices func (services *DNSSdServices) Add(srv DNSSdSvcInfo) { *services = append(*services, srv) } // DNSSdPublisher represents a DNS-SD service publisher // One publisher may publish multiple services unser the // same Service Instance Name type DNSSdPublisher struct { Log *Logger // Device's logger DevState *DevState // Device persistent state Services DNSSdServices // Registered services fin chan struct{} // Closed to terminate publisher goroutine finDone sync.WaitGroup // To wait for goroutine termination sysdep *dnssdSysdep // System-dependent stuff } // DNSSdStatus represents DNS-SD publisher status type DNSSdStatus int const ( // DNSSdNoStatus is used to indicate that status is // not known (yet) DNSSdNoStatus DNSSdStatus = iota // DNSSdCollision indicates instance name collision DNSSdCollision // DNSSdFailure indicates publisher failure with any // other reason that listed before DNSSdFailure // DNSSdSuccess indicates successful status DNSSdSuccess ) // String returns human-readable representation of DNSSdStatus func (status DNSSdStatus) String() string { switch status { case DNSSdNoStatus: return "DNSSdNoStatus" case DNSSdCollision: return "DNSSdCollision" case DNSSdFailure: return "DNSSdFailure" case DNSSdSuccess: return "DNSSdSuccess" } return fmt.Sprintf("Unknown DNSSdStatus %d", status) } // NewDNSSdPublisher creates new DNSSdPublisher // // Service instance name comes from the DevState, and if // name changes as result of name collision resolution, // DevState will be updated func NewDNSSdPublisher(log *Logger, devstate *DevState, services DNSSdServices) *DNSSdPublisher { return &DNSSdPublisher{ Log: log, DevState: devstate, Services: services, fin: make(chan struct{}), } } // Publish all services func (publisher *DNSSdPublisher) Publish() error { instance := publisher.instance(0) publisher.sysdep = newDnssdSysdep(publisher.Log, instance, publisher.Services) publisher.Log.Info('+', "DNS-SD: %s: publishing requested", instance) publisher.finDone.Add(1) go publisher.goroutine() return nil } // Unpublish everything func (publisher *DNSSdPublisher) Unpublish() { close(publisher.fin) publisher.finDone.Wait() publisher.sysdep.Halt() publisher.Log.Info('-', "DNS-SD: %s: removed", publisher.instance(0)) } // Build service instance name with optional collision-resolution suffix func (publisher *DNSSdPublisher) instance(suffix int) string { name := publisher.DevState.DNSSdName strSuffix := "" switch { // This happens when we try to resolve name conflict case suffix != 0: strSuffix = fmt.Sprintf(" (USB %d)", suffix) // This happens when we've just initialized or reset DNSSdOverride, // so append "(USB)" suffix case publisher.DevState.DNSSdName == publisher.DevState.DNSSdOverride: strSuffix = " (USB)" // Otherwise, DNSSdOverride contains saved conflict-resolved device name default: name = publisher.DevState.DNSSdOverride } const MaxDNSSDName = 63 if len(name)+len(strSuffix) > MaxDNSSDName { name = name[:MaxDNSSDName-len(strSuffix)] } return name + strSuffix } // Event handling goroutine func (publisher *DNSSdPublisher) goroutine() { // Catch panics to log defer func() { v := recover() if v != nil { Log.Panic(v) } }() defer publisher.finDone.Done() timer := time.NewTimer(time.Hour) timer.Stop() // Not ticking now defer timer.Stop() // And cleanup at return var err error var suffix int instance := publisher.instance(0) for { fail := false select { case <-publisher.fin: return case status := <-publisher.sysdep.Chan(): switch status { case DNSSdSuccess: publisher.Log.Info(' ', "DNS-SD: %s: published", instance) if instance != publisher.DevState.DNSSdOverride { publisher.DevState.DNSSdOverride = instance publisher.DevState.Save() } case DNSSdCollision: publisher.Log.Error(' ', "DNS-SD: %s: name collision", instance) suffix++ fallthrough case DNSSdFailure: publisher.Log.Error(' ', "DNS-SD: %s: publishing failed", instance) fail = true publisher.sysdep.Halt() default: publisher.Log.Error(' ', "DNS-SD: %s: unknown event %s", instance, status) } case <-timer.C: instance = publisher.instance(suffix) publisher.sysdep = newDnssdSysdep(publisher.Log, instance, publisher.Services) if err != nil { publisher.Log.Error('!', "DNS-SD: %s: %s", instance, err) fail = true } } if fail { timer.Reset(DNSSdRetryInterval) } } } ipp-usb-0.9.24/dnssd_avahi.go000066400000000000000000000232431453365611700160160ustar00rootroot00000000000000// +build linux freebsd /* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device * * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com) * See LICENSE for license terms and conditions * * DNS-SD publisher: Avahi-based system-dependent part */ package main // #cgo pkg-config: avahi-client // // #include // #include // #include // #include // #include // // void avahiClientCallback(AvahiClient*, AvahiClientState, void*); // void avahiEntryGroupCallback(AvahiEntryGroup*, AvahiEntryGroupState, void*); import "C" import ( "bytes" "errors" "fmt" "net/url" "sync" "unsafe" ) var ( avahiInitLock sync.Mutex avahiThreadedPoll *C.AvahiThreadedPoll avahiClientMap = make(map[*C.AvahiClient]*dnssdSysdep) avahiEgroupMap = make(map[*C.AvahiEntryGroup]*dnssdSysdep) ) // dnssdSysdep represents a system-dependent DNS-SD advertiser type dnssdSysdep struct { log *Logger // Device's logger instance string // Service Instance Name fqdn string // Host's fully-qualified domain name client *C.AvahiClient // Avahi client egroup *C.AvahiEntryGroup // Avahi entry group statusChan chan DNSSdStatus // Status notifications channel } // dnssdSysdepErr implements error interface on a top of // Avahi error codes type dnssdSysdepErr C.int // Error returns error string for the dnssdSysdepErr func (err dnssdSysdepErr) Error() string { return "Avahi error: " + C.GoString(C.avahi_strerror(C.int(err))) } // newDnssdSysdep creates new dnssdSysdep instance func newDnssdSysdep(log *Logger, instance string, services DNSSdServices) *dnssdSysdep { log.Debug(' ', "DNS-SD: %s: trying", instance) var err error var poll *C.AvahiPoll var rc C.int var proto, iface int sysdep := &dnssdSysdep{ log: log, instance: instance, statusChan: make(chan DNSSdStatus, 10), } // Obtain index of loopback interface loopback, err := Loopback() if err != nil { goto ERROR // Very unlikely to happen } // Obtain AvahiPoll poll, err = avahiGetPoll() if err != nil { goto ERROR } // Synchronize with Avahi thread avahiThreadLock() defer avahiThreadUnlock() // Create Avahi client sysdep.client = C.avahi_client_new( poll, C.AVAHI_CLIENT_NO_FAIL, C.AvahiClientCallback(C.avahiClientCallback), nil, &rc, ) if sysdep.client == nil { goto AVAHI_ERROR } avahiClientMap[sysdep.client] = sysdep sysdep.fqdn = C.GoString(C.avahi_client_get_host_name_fqdn(sysdep.client)) sysdep.log.Debug(' ', "DNS-SD: FQDN: %q", sysdep.fqdn) // Create entry group sysdep.egroup = C.avahi_entry_group_new( sysdep.client, C.AvahiEntryGroupCallback(C.avahiEntryGroupCallback), nil, ) if sysdep.egroup == nil { rc = C.avahi_client_errno(sysdep.client) goto AVAHI_ERROR } avahiEgroupMap[sysdep.egroup] = sysdep // Compute iface and proto, adjust fqdn iface = C.AVAHI_IF_UNSPEC if Conf.LoopbackOnly { iface = loopback old := sysdep.fqdn sysdep.fqdn = "localhost" sysdep.log.Debug(' ', "DNS-SD: FQDN: %q->%q", old, sysdep.fqdn) } proto = C.AVAHI_PROTO_UNSPEC if !Conf.IPV6Enable { proto = C.AVAHI_PROTO_INET } // Populate entry group for _, svc := range services { // Prepare TXT record var cTxt *C.AvahiStringList cTxt, err = sysdep.avahiTxtRecord(svc.Port, svc.Txt) if err != nil { goto ERROR } // Prepare C strings for service instance and type cSvcType := C.CString(svc.Type) var cInstance *C.char if svc.Instance != "" { cInstance = C.CString(svc.Instance) } else { cInstance = C.CString(instance) } // Handle loopback-only mode ifaceInUse := iface if svc.Loopback { ifaceInUse = loopback } // Register service type rc = C.avahi_entry_group_add_service_strlst( sysdep.egroup, C.AvahiIfIndex(ifaceInUse), C.AvahiProtocol(proto), 0, cInstance, cSvcType, nil, // Domain nil, // Host C.uint16_t(svc.Port), cTxt, ) // Register subtypes, if any for _, subtype := range svc.SubTypes { if rc != C.AVAHI_OK { break } sysdep.log.Debug(' ', "DNS-SD: +subtype: %q", subtype) cSubtype := C.CString(subtype) rc = C.avahi_entry_group_add_service_subtype( sysdep.egroup, C.AvahiIfIndex(ifaceInUse), C.AvahiProtocol(proto), 0, cInstance, cSvcType, nil, cSubtype, ) C.free(unsafe.Pointer(cSubtype)) } // Release C memory C.free(unsafe.Pointer(cInstance)) C.free(unsafe.Pointer(cSvcType)) C.avahi_string_list_free(cTxt) // Check for Avahi error if rc != C.AVAHI_OK { goto AVAHI_ERROR } } // Commit changes rc = C.avahi_entry_group_commit(sysdep.egroup) if rc != C.AVAHI_OK { goto AVAHI_ERROR } // Create and return dnssdSysdep return sysdep // Error: cleanup and exit AVAHI_ERROR: err = dnssdSysdepErr(rc) ERROR: // Raise an error event sysdep.log.Error(' ', "DNS-SD: %s: %s", sysdep.instance, err) sysdep.haltLocked() if err == dnssdSysdepErr(C.AVAHI_ERR_COLLISION) { sysdep.notify(DNSSdCollision) } else { sysdep.notify(DNSSdFailure) } return sysdep } // Halt dnssdSysdep // // It cancel all activity related to the dnssdSysdep instance, // but sysdep.Chan() remains valid, though no notifications // will be pushed there anymore func (sysdep *dnssdSysdep) Halt() { avahiThreadLock() sysdep.haltLocked() avahiThreadUnlock() } // Get status change notification channel func (sysdep *dnssdSysdep) Chan() <-chan DNSSdStatus { return sysdep.statusChan } // Halt dnssdSysdep -- internal version // // Must be called under avahiThreadLock // Can be used with semi-constructed dnssdSysdep func (sysdep *dnssdSysdep) haltLocked() { // Free all Avahi stuff if sysdep.egroup != nil { C.avahi_entry_group_free(sysdep.egroup) delete(avahiEgroupMap, sysdep.egroup) sysdep.egroup = nil } if sysdep.client != nil { C.avahi_client_free(sysdep.client) delete(avahiClientMap, sysdep.client) sysdep.client = nil } // Drain status channel for len(sysdep.statusChan) > 0 { <-sysdep.statusChan } } // Push status change notification func (sysdep *dnssdSysdep) notify(status DNSSdStatus) { sysdep.statusChan <- status } // avahiTxtRecord converts DNSSdTxtRecord to AvahiStringList func (sysdep *dnssdSysdep) avahiTxtRecord(port int, txt DNSSdTxtRecord) ( *C.AvahiStringList, error) { var buf bytes.Buffer var list, prev *C.AvahiStringList for _, t := range txt { buf.Reset() buf.WriteString(t.Key) buf.WriteByte('=') if !t.URL || sysdep.fqdn == "" { buf.WriteString(t.Value) } else { value := t.Value if parsed, err := url.Parse(value); err == nil && parsed.IsAbs() { parsed.Host = sysdep.fqdn if port != 0 { parsed.Host += fmt.Sprintf(":%d", port) } value = parsed.String() } buf.WriteString(value) } b := buf.Bytes() prev, list = list, C.avahi_string_list_add_arbitrary( list, (*C.uint8_t)(unsafe.Pointer(&b[0])), C.size_t(len(b)), ) if list == nil { C.avahi_string_list_free(prev) return nil, ErrNoMemory } } return C.avahi_string_list_reverse(list), nil } // avahiClientCallback called by Avahi client to notify us about // client state change // //export avahiClientCallback func avahiClientCallback(client *C.AvahiClient, state C.AvahiClientState, _ unsafe.Pointer) { sysdep := avahiClientMap[client] if sysdep == nil { return } status := DNSSdNoStatus event := "" switch state { case C.AVAHI_CLIENT_S_REGISTERING: event = "AVAHI_CLIENT_S_REGISTERING" case C.AVAHI_CLIENT_S_RUNNING: event = "AVAHI_CLIENT_S_RUNNING" case C.AVAHI_CLIENT_S_COLLISION: // This is host name collision. We can't recover // it here, so lets consider it as DNSSdFailure event = "AVAHI_CLIENT_S_COLLISION" status = DNSSdFailure case C.AVAHI_CLIENT_FAILURE: event = "AVAHI_CLIENT_FAILURE" status = DNSSdFailure case C.AVAHI_CLIENT_CONNECTING: event = "AVAHI_CLIENT_CONNECTING" default: event = fmt.Sprintf("Unknown event %d", state) } sysdep.log.Debug(' ', "DNS-SD: %s: %s", sysdep.instance, event) if status != DNSSdNoStatus { sysdep.notify(status) } } // avahiEntryGroupCallback called by Avahi client to notify us about // entry group state change // //export avahiEntryGroupCallback func avahiEntryGroupCallback(egroup *C.AvahiEntryGroup, state C.AvahiEntryGroupState, _ unsafe.Pointer) { sysdep := avahiEgroupMap[egroup] if sysdep == nil { return } status := DNSSdNoStatus event := "" switch state { case C.AVAHI_ENTRY_GROUP_UNCOMMITED: event = "AVAHI_ENTRY_GROUP_UNCOMMITED" case C.AVAHI_ENTRY_GROUP_REGISTERING: event = "AVAHI_ENTRY_GROUP_REGISTERING" case C.AVAHI_ENTRY_GROUP_ESTABLISHED: event = "AVAHI_ENTRY_GROUP_ESTABLISHED" status = DNSSdSuccess case C.AVAHI_ENTRY_GROUP_COLLISION: event = "AVAHI_ENTRY_GROUP_COLLISION" status = DNSSdCollision case C.AVAHI_ENTRY_GROUP_FAILURE: event = "AVAHI_ENTRY_GROUP_FAILURE" status = DNSSdFailure } sysdep.log.Debug(' ', "DNS-SD: %s: %s", sysdep.instance, event) if status != DNSSdNoStatus { sysdep.notify(status) } } // avahiGetPoll returns pointer to AvahiPoll // Avahi helper thread is created on demand func avahiGetPoll() (*C.AvahiPoll, error) { avahiInitLock.Lock() defer avahiInitLock.Unlock() if avahiThreadedPoll == nil { avahiThreadedPoll = C.avahi_threaded_poll_new() if avahiThreadedPoll == nil { return nil, errors.New("initialization failed, not enough memory") } C.avahi_threaded_poll_start(avahiThreadedPoll) } return C.avahi_threaded_poll_get(avahiThreadedPoll), nil } // Lock Avahi thread func avahiThreadLock() { C.avahi_threaded_poll_lock(avahiThreadedPoll) } // Unlock Avahi thread func avahiThreadUnlock() { C.avahi_threaded_poll_unlock(avahiThreadedPoll) } ipp-usb-0.9.24/err.go000066400000000000000000000013561453365611700143240ustar00rootroot00000000000000/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device * * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com) * See LICENSE for license terms and conditions * * Common errors */ package main import ( "errors" ) // Error values for ipp-usb var ( ErrLockIsBusy = errors.New("Lock is busy") ErrNoMemory = errors.New("Not enough memory") ErrShutdown = errors.New("Shutdown requested") ErrBlackListed = errors.New("Device is blacklisted") ErrInitTimedOut = errors.New("Device initialization timed out") ErrUnusable = errors.New("Device doesn't implement print or scan service") ErrNoIppUsb = errors.New("ipp-usb daemon not running") ErrAccess = errors.New("Access denied") ) ipp-usb-0.9.24/escl.go000066400000000000000000000153701453365611700144630ustar00rootroot00000000000000/* 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.24/flock_unix.go000066400000000000000000000031321453365611700156670ustar00rootroot00000000000000// +build darwin dragonfly freebsd linux nacl netbsd openbsd solaris /* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device * * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com) * See LICENSE for license terms and conditions * * File locking -- UNIX version */ package main /* #include #include static inline int do_lockf (int fd, int cmd, off_t len) { int rc = lockf(fd, cmd, len); if (rc < 0) { rc = -errno; } return rc; } */ import "C" import ( "os" "syscall" ) // FileLockCmd represents set of possible values for the // FileLock argument type FileLockCmd C.int const ( // FileLockWait command used to lock the file; wait if it is busy FileLockWait = C.F_LOCK // FileLockNoWait command used to lock the file without wait. // If file is busy it fails with ErrLockIsBusy error FileLockNoWait = C.F_TLOCK // FileLockTest command used to test the lock. // It returns immediately, with ErrLockIsBusy if file // is busy or without an error if not // // File locking state is not affected in both cases FileLockTest = C.F_TEST // FileLockUnlock command used to unlock the file FileLockUnlock = C.F_ULOCK ) // FileLock manages file lock func FileLock(file *os.File, cmd FileLockCmd) error { rc := C.do_lockf(C.int(file.Fd()), C.int(cmd), 0) if rc == 0 { return nil } var err error = syscall.Errno(-rc) switch err { case syscall.EACCES, syscall.EAGAIN: err = ErrLockIsBusy } return err } // FileUnlock releases file lock func FileUnlock(file *os.File) error { return FileLock(file, FileLockUnlock) } ipp-usb-0.9.24/glob.go000066400000000000000000000030131453365611700144470ustar00rootroot00000000000000/* 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.24/glob_test.go000066400000000000000000000015531453365611700155150ustar00rootroot00000000000000/* 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.24/go.mod000066400000000000000000000001361453365611700143060ustar00rootroot00000000000000module github.com/OpenPrinting/ipp-usb go 1.11 require github.com/OpenPrinting/goipp v1.1.0 ipp-usb-0.9.24/go.sum000066400000000000000000000002611453365611700143320ustar00rootroot00000000000000github.com/OpenPrinting/goipp v1.1.0 h1:AK19DwnuvCaqbF6ckT2ICe/Hc1o1sVSS+UzE59z4Dx0= github.com/OpenPrinting/goipp v1.1.0/go.mod h1:ot2iw+QF7fVLaX+55JUNlF5YSDNiXVo2LRAv21iGcQI= ipp-usb-0.9.24/http.go000066400000000000000000000147461453365611700145220ustar00rootroot00000000000000/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device * * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com) * See LICENSE for license terms and conditions * * HTTP proxy */ package main import ( "context" "errors" "fmt" "io" "log" "net" "net/http" "net/url" "strings" "sync/atomic" ) var ( httpSessionID int32 ) // HTTPProxy represents HTTP protocol proxy backed by the // specified http.RoundTripper. It implements http.Handler // interface type HTTPProxy struct { log *Logger // Logger instance server *http.Server // HTTP server enable bool // Proxy can handle incoming requests transport *UsbTransport // Transport for outgoing requests closeWait chan struct{} // Closed at server close } // NewHTTPProxy creates new HTTP proxy func NewHTTPProxy(logger *Logger, listener net.Listener, transport *UsbTransport) *HTTPProxy { proxy := &HTTPProxy{ log: logger, transport: transport, closeWait: make(chan struct{}), } proxy.server = &http.Server{ Handler: proxy, ErrorLog: log.New(logger.LineWriter(LogError, '!'), "", 0), } go func() { proxy.server.Serve(listener) close(proxy.closeWait) }() return proxy } // Close the proxy func (proxy *HTTPProxy) Close() { proxy.server.Close() <-proxy.closeWait } // Enable indicates that initialization is completed and // incoming requests can be handled func (proxy *HTTPProxy) Enable() { proxy.enable = true } // Handle HTTP request func (proxy *HTTPProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Catch panics to log defer func() { v := recover() if v != nil { Log.Panic(v) } }() session := int(atomic.AddInt32(&httpSessionID, 1)-1) % 1000 // Perform sanity checking if !proxy.enable { proxy.httpError(session, w, r, http.StatusServiceUnavailable, errors.New("ipp-usb is not ready for this device")) return } if r.Method == "CONNECT" { proxy.httpError(session, w, r, http.StatusMethodNotAllowed, errors.New("CONNECT not allowed")) return } if r.Header.Get("Upgrade") != "" { proxy.httpError(session, w, r, http.StatusServiceUnavailable, errors.New("Protocol upgrade is not implemented")) return } if r.URL.IsAbs() { proxy.httpError(session, w, r, http.StatusServiceUnavailable, errors.New("Absolute URL not allowed")) return } // Obtain request's client and server addresses var clientAddr, serverAddr *net.TCPAddr clientAddr, err := net.ResolveTCPAddr("tcp", r.RemoteAddr) if err != nil { proxy.httpError(session, w, r, http.StatusInternalServerError, errors.New("Unable to get client address for request")) return } if v := r.Context().Value(http.LocalAddrContextKey); v != nil { if v != nil { serverAddr, _ = v.(*net.TCPAddr) } } if serverAddr == nil { proxy.httpError(session, w, r, http.StatusInternalServerError, errors.New("Unable to get server address for request")) return } // Authenticate if status, err := AuthHTTPRequest(clientAddr, serverAddr, r); err != nil { proxy.httpError(session, w, r, status, err) return } // Adjust request headers httpRemoveHopByHopHeaders(r.Header) if r.Host == "" { if serverAddr.IP.IsLoopback() { r.Host = fmt.Sprintf("localhost:%d", serverAddr.Port) } else { r.Host = serverAddr.String() } } r.URL.Scheme = "http" r.URL.Host = r.Host // If request is ordered to the loopback address, and r.Host is not // "localhost" or "localhost:port", redirect request to the localhost // // Note, IPP over USB specification requires Host: to be always // "localhost" or "localhost:port". Although most of the printers // accept any syntactically correct Host: header, some of the OKI // printers doesn't, and reject requests that violate this rule // // This redirection fixes compatibility with these printers for // clients that follow redirects (i.e., web browser and sane-airscan; // CUPS unfortunately doesn't follow redirects) if serverAddr.IP.IsLoopback() && (r.Method == "GET" || r.Method == "HEAD") { host := strings.ToLower(r.Host) if host != "localhost" && !strings.HasPrefix(host, "localhost:") { url := *r.URL url.Host = fmt.Sprintf("localhost:%d", serverAddr.Port) proxy.httpRedirect(session, w, r, http.StatusFound, &url) return } } // Send request and obtain response status and header resp, err := proxy.transport.RoundTripWithSession(session, r) if err != nil { proxy.httpError(session, w, r, http.StatusServiceUnavailable, err) return } httpRemoveHopByHopHeaders(resp.Header) httpCopyHeaders(w.Header(), resp.Header) w.WriteHeader(resp.StatusCode) // Obtain response body, if any _, err = io.Copy(w, resp.Body) if err != nil { proxy.log.HTTPError('!', session, "%s", err) } resp.Body.Close() } // Reject request with a error func (proxy *HTTPProxy) httpError(session int, w http.ResponseWriter, r *http.Request, status int, err error) { proxy.log.Begin(). HTTPRqParams(LogDebug, '>', session, r). HTTPRequest(LogTraceHTTP, '>', session, r). Commit() w.Header().Set("Content-Type", "text/plain; charset=utf-8") httpNoCache(w) w.WriteHeader(status) w.Write([]byte(err.Error())) w.Write([]byte("\n")) if err != context.Canceled { proxy.log.HTTPError('!', session, "%s", err.Error()) } else { proxy.log.HTTPDebug(' ', session, "request canceled by impatient client") } } // Respond to request with the HTTP redirect func (proxy *HTTPProxy) httpRedirect(session int, w http.ResponseWriter, r *http.Request, status int, location *url.URL) { proxy.log.Begin(). HTTPRqParams(LogDebug, '>', session, r). HTTPRequest(LogTraceHTTP, '>', session, r). Commit() w.Header().Set("Location", location.String()) w.WriteHeader(status) proxy.log.HTTPDebug(' ', session, "redirected to %s", location) } // Set response headers to disable cacheing func httpNoCache(w http.ResponseWriter) { w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") w.Header().Set("Pragma", "no-cache") w.Header().Set("Expires", "0") } // Remove HTTP hop-by-hop headers, RFC 7230, section 6.1 func httpRemoveHopByHopHeaders(hdr http.Header) { if c := hdr.Get("Connection"); c != "" { for _, f := range strings.Split(c, ",") { if f = strings.TrimSpace(f); f != "" { hdr.Del(f) } } } for _, c := range []string{"Connection", "Keep-Alive", "Proxy-Authenticate", "Proxy-Connection", "Proxy-Authorization", "Te", "Trailer", "Transfer-Encoding"} { hdr.Del(c) } } // Copy HTTP headers func httpCopyHeaders(dst, src http.Header) { for k, v := range src { dst[k] = v } } ipp-usb-0.9.24/inifile.go000066400000000000000000000330461453365611700151540ustar00rootroot00000000000000/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device * * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com) * See LICENSE for license terms and conditions * * .INI file loader */ package main import ( "bufio" "bytes" "fmt" "math" "os" "strconv" "strings" "time" ) // IniFile represents opened .INI file type IniFile struct { file *os.File // Underlying file line int // Line in that file reader *bufio.Reader // Reader on a top of file buf bytes.Buffer // Temporary buffer to speed up things rec IniRecord // Next record withRecType bool // Return records of any type } // IniRecord represents a single .INI file record type IniRecord struct { Section string // Section name Key, Value string // Key and value File string // Origin file Line int // Line in that file Type IniRecordType // Record type } // IniRecordType represents IniRecord type type IniRecordType int // Record types: // [section] <- IniRecordSection // key - value <- IniRecordKeyVal const ( IniRecordSection IniRecordType = iota IniRecordKeyVal ) // IniError represents an .INI file read error type IniError struct { File string // Origin file Line int // Line in that file Message string // Error message } // OpenIniFile opens the .INI file for reading // // If file is opened this way, (*IniFile) Next() returns // records of IniRecordKeyVal type only func OpenIniFile(path string) (ini *IniFile, err error) { f, err := os.Open(path) if err != nil { return nil, err } ini = &IniFile{ file: f, line: 1, reader: bufio.NewReader(f), rec: IniRecord{ File: path, }, } return ini, nil } // OpenIniFileWithRecType opens the .INI file for reading // // If file is opened this way, (*IniFile) Next() returns // records of any type func OpenIniFileWithRecType(path string) (ini *IniFile, err error) { ini, err = OpenIniFile(path) if ini != nil { ini.withRecType = true } return } // 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...), } } // LoadIPPort loads IP port value // The destination remains untouched in a case of an error func (rec *IniRecord) LoadIPPort(out *int) error { port, err := strconv.Atoi(rec.Value) if err == nil && (port < 1 || port > 65535) { err = rec.errBadValue("must be in range 1...65535") } if err != nil { return err } *out = port return nil } // LoadBool loads boolean value // The destination remains untouched in a case of an error func (rec *IniRecord) LoadBool(out *bool) error { return rec.LoadNamedBool(out, "false", "true") } // LoadNamedBool loads boolean value // Names for "true" and "false" values are specified explicitly // The destination remains untouched in a case of an error func (rec *IniRecord) LoadNamedBool(out *bool, vFalse, vTrue string) error { switch rec.Value { case vFalse: *out = false return nil case vTrue: *out = true return nil default: return rec.errBadValue("must be %s or %s", vFalse, vTrue) } } // LoadLogLevel loads LogLevel value // The destination remains untouched in a case of an error func (rec *IniRecord) LoadLogLevel(out *LogLevel) error { var mask LogLevel for _, s := range strings.Split(rec.Value, ",") { s = strings.TrimSpace(s) switch s { case "": case "error": mask |= LogError case "info": mask |= LogInfo | LogError case "debug": mask |= LogDebug | LogInfo | LogError case "trace-ipp": mask |= LogTraceIPP | LogDebug | LogInfo | LogError case "trace-escl": mask |= LogTraceESCL | LogDebug | LogInfo | LogError case "trace-http": mask |= LogTraceHTTP | LogDebug | LogInfo | LogError case "trace-usb": mask |= LogTraceUSB | LogDebug | LogInfo | LogError case "all", "trace-all": mask |= LogAll & ^LogTraceUSB default: return rec.errBadValue("invalid log level %q", s) } } *out = mask return nil } // LoadDuration loads time.Duration value // The destination remains untouched in a case of an error func (rec *IniRecord) LoadDuration(out *time.Duration) error { var ms uint err := rec.LoadUint(&ms) if err == nil { *out = time.Millisecond * time.Duration(ms) } return err } // LoadSize loads size value (returned as int64) // The syntax is following: // 123 - size in bytes // 123K - size in kilobytes, 1K == 1024 // 123M - size in megabytes, 1M == 1024K // The destination remains untouched in a case of an error func (rec *IniRecord) LoadSize(out *int64) error { var units uint64 = 1 if l := len(rec.Value); l > 0 { switch rec.Value[l-1] { case 'k', 'K': units = 1024 case 'm', 'M': units = 1024 * 1024 } if units != 1 { rec.Value = rec.Value[:l-1] } } sz, err := strconv.ParseUint(rec.Value, 10, 64) if err != nil { return rec.errBadValue("%q: invalid size", rec.Value) } if sz > uint64(math.MaxInt64/units) { return rec.errBadValue("size too large") } *out = int64(sz * units) return nil } // LoadUint loads unsigned integer value // The destination remains untouched in a case of an error func (rec *IniRecord) LoadUint(out *uint) error { num, err := strconv.ParseUint(rec.Value, 10, 0) if err != nil { return rec.errBadValue("%q: invalid number", rec.Value) } *out = uint(num) return nil } // LoadUintRange loads unsigned integer value within the range // The destination remains untouched in a case of an error func (rec *IniRecord) LoadUintRange(out *uint, min, max uint) error { var val uint err := rec.LoadUint(&val) if err == nil && (val < min || val > max) { err = rec.errBadValue("must be in range %d...%d", min, max) } if err != nil { return err } *out = val return nil } // LoadAuthUIDRules loads AuthUIDRule-s value and appends them // to the destination // // The destination remains untouched in a case of an error func (rec *IniRecord) LoadAuthUIDRules(out *[]*AuthUIDRule) error { // Parse rec.Key -- it contains list of operations allowed := AuthOpsNone for _, s := range strings.Split(rec.Key, ",") { s = strings.TrimSpace(s) switch s { case "all": allowed |= AuthOpsAll case "config": allowed |= AuthOpsConfig case "fax": allowed |= AuthOpsFax case "print": allowed |= AuthOpsPrint case "scan": allowed |= AuthOpsScan default: return rec.errBadValue("invalid operation: %q", s) } } // Parse rec.Value -- it contains list of users rules := []*AuthUIDRule{} users := make(map[string]struct{}) for _, s := range strings.Split(rec.Value, ",") { s = strings.TrimSpace(s) // Silently ignore empty users and groups if s == "" || s == "@" { continue } // Check for duplicates if _, dup := users[s]; dup { continue } users[s] = struct{}{} // Skip rules that allows nothing if allowed == AuthOpsNone { continue } // Build rules, preserving the order (just in case for now) rule := &AuthUIDRule{ Name: s, Allowed: allowed, } rules = append(rules, rule) } // Save results *out = append(*out, rules...) return nil } // LoadQuirksResetMethod loads QuirksResetMethod value // The destination remains untouched in a case of an error func (rec *IniRecord) LoadQuirksResetMethod(out *QuirksResetMethod) error { switch rec.Value { case "none": *out = QuirksResetNone return nil case "soft": *out = QuirksResetSoft return nil case "hard": *out = QuirksResetHard return nil default: return rec.errBadValue("must be none, soft or hard") } } // errBadValue creates a "bad value" error related to the INI record func (rec *IniRecord) errBadValue(format string, args ...interface{}) error { return &IniError{ File: rec.File, Line: rec.Line, Message: fmt.Sprintf(rec.Key+": "+format, args...), } } // Error implements error interface for the IniError func (err *IniError) Error() string { return fmt.Sprintf("%s:%d: %s", err.File, err.Line, err.Message) } ipp-usb-0.9.24/inifile_test.go000066400000000000000000000032121453365611700162030ustar00rootroot00000000000000/* 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.24/ipp-usb-quirks/000077500000000000000000000000001453365611700160735ustar00rootroot00000000000000ipp-usb-0.9.24/ipp-usb-quirks/Canon.conf000066400000000000000000000004521453365611700200010ustar00rootroot00000000000000# ipp-usb quirks file -- quirks for Canon devices # This device responds to the Get-Printer-Attributes request with the # server-error-internal-error status, but otherwise works correctly # # So we just ignore its returned IPP status as workaround [Canon SELPHY CP1500] ignore-ipp-status = true ipp-usb-0.9.24/ipp-usb-quirks/HP.conf000066400000000000000000000017271453365611700172600ustar00rootroot00000000000000# ipp-usb quirks file -- quirks for HP devices [HP LaserJet MFP M28-M31] http-connection = keep-alive [HP OfficeJet Pro 8730] http-connection = close # eSCL requests hangs on this device, if both USB interfaces are # in use. Limiting number of interfaces to 1 makes scanning # reliable in a cost of making scan cancellation impossible, # as there is no second interface to send cancel request. # (ADF scans still can be canceled between retrieval of # subsequent pages). [HP ScanJet Pro 4500 fn1] usb-max-interfaces = 1 # HP Photosmart 6520 series doesn't implement true faxing, # but instead implements internet-based eFax, # which makes no sense when connected via USB # so can be safely disabled for this kind of devices. [HP Photosmart 6520 series] disable-fax = true # This device sometimes hangs when probing for fax support # See long conversation here for details: # https://github.com/OpenPrinting/ipp-usb/issues/48 [HP ENVY 5530 series] disable-fax = true ipp-usb-0.9.24/ipp-usb-quirks/README000066400000000000000000000021561453365611700167570ustar00rootroot00000000000000This 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 | false - blacklist or not the matching devices disable-fax = true | false - disable fax capability, even if present init-reset = none | soft | hard - should USB reset be performed on start ipp-usb-0.9.24/ipp-usb-quirks/blacklist.conf000066400000000000000000000004241453365611700207120ustar00rootroot00000000000000# ipp-usb quirks file -- blacklisted devices # This device has IPP-over-USB interfaces, but responds HTTP 404 Not found # status to all requests [HP Inc. HP Laser MFP 135a] blacklist = true # And this device has the same problem [HP Inc. HP Laser 107a] blacklist = true ipp-usb-0.9.24/ipp-usb-quirks/default.conf000066400000000000000000000001451453365611700203660ustar00rootroot00000000000000# ipp-usb quirks file -- defaults [*] # Drop Connection: header by default http-connection = "" ipp-usb-0.9.24/ipp-usb.8000066400000000000000000000341411453365611700146530ustar00rootroot00000000000000.\" generated with Ronn-NG/v0.9.1 .\" http://github.com/apjanke/ronn-ng/tree/0.9.1 .TH "IPP\-USB" "8" "November 2023" "" "ipp-usb.8" .SH "NAME" \fBipp\-usb\fR \- Daemon for IPP over USB printer support .SH "DESCRIPTION" \fBipp\-usb\fR daemon enables driver\-less printing and scanning on USB\-only AirPrint\-compatible printers and MFPs\. .P It works by connecting to the device by USB using IPP\-over\-USB protocol, and exposing the device to the network, including DNS\-SD (ZeroConf) advertising\. .P IPP printing, eSCL scanning and web console are fully supported\. .SH "SYNOPSIS" .SS "Usage:" \fBipp\-usb mode [options]\fR .SS "Modes are:" .TP \fBstandalone\fR run forever, automatically discover IPP\-over\-USB devices and serve them all .TP \fBudev\fR like standalone, but exit when last IPP\-over\-USB device is disconnected .TP \fBdebug\fR logs duplicated on console, \-bg option is ignored .TP \fBcheck\fR check configuration and exit\. It also prints a list of all connected devices .TP \fBstatus\fR print status of the running \fBipp\-usb\fR daemon, including information of all connected devices .SS "Options are" .TP \fB\-bg\fR run in background (ignored in debug mode) .SH "NETWORKING" Essentially, \fBipp\-usb\fR makes printer or scanner accessible from the network, converting network\-side HTTP operations to the USB operations\. .P By default, \fBipp\-usb\fR exposes device only to the loopback interface, using the \fBlocalhost\fR address (both \fB127\.0\.0\.1\fR and \fB::1\fR, for IPv4 and IPv6, respectively)\. TCP ports are allocated automatically, and allocation is persisted in the association with the particular device, so the next time the device is plugged on, it will get the same port\. The default port range for TCP ports allocation is \fB60000\-65535\fR\. .P This default behavior can be changed, using configuration file\. See \fBCONFIGURATION\fR section below for details\. .P If you decide to publish your device to the real network, the following things should be taken into consideration: .IP "1." 4 Your \fBprivate\fR device will become \fBpublic\fR and it will become accessible by other computers from the network .IP "2." 4 Firewall rules needs to be updated appropriately\. The \fBipp\-usb\fR daemon will not do it automatically by itself .IP "3." 4 IPP over USB specification explicitly require that the \fBHost\fR field in the HTTP request is set to \fBlocalhost\fR or \fBlocalhost:port\fR\. If device is accessed from the real network, \fBHost\fR header will reflect the real network address\. Most of devices allow it, but some are more restrictive and will not work in this configuration\. .IP "" 0 .SH "DNS\-SD (AVAHI INTEGRATION)" IPP over USB is intended to be used with the automatic device discovery, and for this purpose \fBipp\-usb\fR advertises all devices it handles, using DNS\-SD protocol\. On Linux, DNS\-SD is handled with a help of Avahi daemon\. .P DNS\-SD advertising can be disabled via configuration file\. Also, if Avahi is not installed or not running, \fBipp\-usb\fR will still work correctly, although DNS\-SD advertising will not work\. .P For every device the following services will be advertised: .TS allbox; l l l. Instance Type Subtypes Device name _ipp\._tcp _universal\._sub\._ipp\._tcp Device name _printer\._tcp \~ Device name _uscan\._tcp \~ Device name _http\._tcp \~ BBPP _ipp\-usb\._tcp \~ .TE .P Notes: .IP "\[ci]" 4 \fBDevice name\fR is the name under which device appears in the list of available devices, for example, in the printing dialog (it is DNS\-SD device name, in another words), and for most of devices will match the device\'s model name\. It is appended with the \fB" (USB)"\fR suffix, so if device is connected via network and via USB simultaneously, these two connections can be easily distinguished\. If there are two devices with the same name connected simultaneously, the suffix becomes \fB" (USB NNN)"\fR, with NNN number unique for each device, for disambiguation\. In another words, the single \fB"Kyocera ECOSYS M2040dn"\fR device will be listed as \fB"Kyocera ECOSYS M2040dn (USB)"\fR, and two such a devices will be listed as \fB"Kyocera ECOSYS M2040dn (USB 1)"\fR and \fB"Kyocera ECOSYS M2040dn (USB 2)"\fR .IP "\[ci]" 4 \fB_ipp\._tcp\fR and \fB_printer\._tcp\fR are only advertises for printer devices and MFPs .IP "\[ci]" 4 \fB_uscan\._tcp\fR is only advertised for scanner devices and MFPs .IP "\[ci]" 4 for the \fB_ipp\._tcp\fR service, the \fB_universal\._sub\._ipp\._tcp\fR subtype is also advertised for iOS compatibility .IP "\[ci]" 4 \fB_printer\._tcp\fR is advertised with TCP port set to 0\. Other services are advertised with the actual port number .IP "\[ci]" 4 \fB_http\._tcp\fR is device web\-console\. It is always advertises in assumption it is always exist .IP "\[ci]" 4 \fBBBPP\fR, used for the \fB_ipp\-usb\._tcp\fR service, is the USB bus (BB) and port (PP) numbers in hex\. The purpose of this advertising is to help CUPS and other possible "clients" to guess which devices are handled by the \fBipp\-usb\fR service, to avoid possible conflicts with the legacy USB drivers\. .IP "" 0 .SH "CONFIGURATION" \fBipp\-usb\fR searched for its configuration file in two places: .IP "1." 4 \fB/etc/ipp\-usb/ipp\-usb\.conf\fR .IP "2." 4 \fBipp\-usb\.conf\fR in the directory where executable file is located .IP "" 0 .P Configuration file syntax is very similar to \.INI files syntax\. It consist of named sections, and each section contains a set of named variables\. Comments are started from # or ; characters and continues until end of line: .IP "" 4 .nf # This is a comment [section 1] variable 1 = value 1 ; and another comment variable 2 = value 2 .fi .IP "" 0 .SS "Network parameters" Network parameters are all in the \fB[network]\fR section: .IP "" 4 .nf [network] # TCP ports for HTTP will be automatically allocated in the # following range http\-min\-port = 60000 http\-max\-port = 65535 # Enable or disable DNS\-SD advertisement dns\-sd = enable # enable | disable # Network interface to use\. Set to `all` if you want to expose you # printer to the local network\. This way you can share your printer # with other computers in the network, as well as with iOS and # Android devices\. interface = loopback # all | loopback # Enable or disable IPv6 ipv6 = enable # enable | disable .fi .IP "" 0 .SS "Authentication" By default, \fBipp\-usb\fR exposes locally connected USB printer to all users of the system\. .P Though this is reasonable behavior in most cases, when computer and printer are both in personal use, for bigger installation this approach can be too simple and primitive\. .P \fBipp\-usb\fR provides a mechanism, which allows to control local clients access based on UID the client program runs under\. .P Please note, this mechanism will not work for remote connections (disabled by default but supported)\. Authentication of remote users requires some different mechanism, which is under consideration but is not yet implemented\. .P Authentication parameters are all in the [auth uid] section: .IP "" 4 .nf # Local user authentication by UID/GID [auth uid] # Syntax: # operations = users # # Operations are comma\-separated list of following operations: # all \- all operations # config \- configuration web\-console # fax \- faxing # print \- printing # scan \- scanning # # Users have the following suntax: # user \- user name # @group \- all users that belongs to the group # # Users and groups may be specified either by names or by # numbers\. * means any # # Note, if user/group is not known in the context of request # (for example, in the case of non\-local network connection), # "_" used for matching # # User/group names are resolved at the moment of request # processing (and cached for a couple of seconds), so running # daemon will see changes to the /etc/passwd and /etc/group # # Examples: # fax, print = lp, @lp # Allow CUPS to do its work # scan = * # Allow any user to scan # config = @wheel # Only wheel group members can do that all = * .fi .IP "" 0 .SS "Logging configuration" Logging parameters are all in the \fB[logging]\fR section: .IP "" 4 .nf [logging] # device\-log \- what logs are generated per device # main\-log \- what common logs are generated # console\-log \- what of generated logs goes to console # # parameter contains a comma\-separated list of # the following keywords: # error \- error messages # info \- informative messages # debug \- debug messages # trace\-ipp, trace\-escl, trace\-http \- very detailed # per\-protocol traces # trace\-usb \- hex dump of all USB traffic # all \- all logs # trace\-all \- alias to all # # Note, trace\-* implies debug, debug implies info, info implies # error device\-log = all main\-log = debug console\-log = debug # Log rotation parameters: # log\-file\-size \- max log file before rotation\. Use suffix # M for megabytes or K for kilobytes # log\-backup\-files \- how many backup files to preserve during # rotation # max\-file\-size = 256K max\-backup\-files = 5 # Enable or disable ANSI colors on console console\-color = enable # enable | disable # ipp\-usb queries IPP printer attributes at the initialization time # for its own purposes and writes received attributes to the log\. # By default, only necessary attributes are requested from device\. # # If this parameter is set to true, all printer attributes will # be requested\. Normally, it only affects the logging\. However, # some enterprise\-level HP printers returns such huge amount of # data and do it so slowly, so it can cause initialization timeout\. # This is why this feature is not enabled by default get\-all\-printer\-attrs = false # false | true .fi .IP "" 0 .SS "Quirks" Some devices, due to their firmware bugs, require special handling, called device\-specific \fBquirks\fR\. \fBipp\-usb\fR loads quirks from the \fB/usr/share/ipp\-usb/quirks/*\.conf\fR files and from the \fB/etc/ipp\-usb/quirks/*\.conf\fR files\. The \fB/etc/ipp\-usb/quirks\fR directory is for system quirks overrides or admin changes\. These files have \.INI\-file syntax with the content that looks like this: .IP "" 4 .nf [HP LaserJet MFP M28\-M31] http\-connection = keep\-alive [HP OfficeJet Pro 8730] http\-connection = close [HP Inc\. HP Laser MFP 135a] blacklist = true # Default configuration [*] http\-connection = "" .fi .IP "" 0 .P For each discovered device, its model name is matched against sections of the quirks files\. Section names may contain glob\-style wildcards: \fB*\fR that matches any sequence of characters and \fB?\fR , that matches any single character\. To match one of these characters (\fB*\fR and \fB?\fR) literally, use backslash as escape\. .P Note, the simplest way to guess the exact model name for the particular device is to use \fBipp\-usb check\fR command, which prints a list of all connected devices\. .P All matching sections from all quirks files are taken in consideration, and applied in priority order\. Priority is computed using the following algorithm: .IP "\[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: .IP "\[ci]" 4 \fBblacklist = true | false\fR .br If \fBtrue\fR, the matching device is ignored by the \fBipp\-usb\fR .IP "\[ci]" 4 \fBhttp\-XXX = YYY\fR .br Set XXX header of the HTTP requests forwarded to device to YYY\. If YYY is empty string, XXX header is removed .IP "\[ci]" 4 \fBusb\-max\-interfaces = N\fR .br Don\'t use more that N USB interfaces, even if more is available .IP "\[ci]" 4 \fBdisable\-fax = true | false\fR .br If \fBtrue\fR, the matching device\'s fax capability is ignored .IP "\[ci]" 4 \fBinit\-reset = none | soft | hard\fR .br How to reset device during initialization\. Default is \fBnone\fR .IP "\[ci]" 4 \fBinit\-delay = NNN\fR .br Delay, in milliseconds, between device is opened and, optionally, reset, and the first request is sent to device .IP "\[ci]" 4 \fBrequest\-delay\fR = NNN .br Delay, in milliseconds, between subsequent requests .IP "\[ci]" 4 \fBignore\-ipp\-status = true | false\fR .br If \fBtrue\fR, IPP status of IPP requests sent by the \fBipp\-usb\fR by itself will be ignored\. This quirk is useful, when device correctly handles IPP request but returned status is not reliable\. Affects only \fBipp\-usb\fR initialization\. .IP "" 0 .P If you found out about your device that it needs a quirk to work properly or it does not work with \fBipp\-usb\fR at all, although it provides IPP\-over\-USB interface, please report the issue at https://github\.com/OpenPrinting/ipp\-usb\. It will let us to update our collection of quirks, so helping other owners of such a device\. .SH "FILES" .IP "\[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/var/ipp\-usb/ctrl\fR: \fBipp\-usb\fR control socket\. Currently only used to obtain the per\-device status (printed by \fBipp\-usb status\fR), but its functionality may be extended in a future .IP "\[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, pzz@pzz\.msk\.ru) .br All rights reserved\. .P This program is licensed under 2\-Clause BSD license\. See LICENSE file for details\. .SH "SEE ALSO" \fBcups(1)\fR ipp-usb-0.9.24/ipp-usb.8.md000066400000000000000000000337111453365611700152540ustar00rootroot00000000000000ipp-usb(8) -- Daemon for IPP over USB printer support ===================================================== ## DESCRIPTION `ipp-usb` daemon enables driver-less printing and scanning on USB-only AirPrint-compatible printers and MFPs. It works by connecting to the device by USB using IPP-over-USB protocol, and exposing the device to the network, including DNS-SD (ZeroConf) advertising. IPP printing, eSCL scanning and web console are fully supported. ## SYNOPSIS ### Usage: `ipp-usb mode [options]` ### Modes are: * `standalone`: run forever, automatically discover IPP-over-USB devices and serve them all * `udev`: like standalone, but exit when last IPP-over-USB device is disconnected * `debug`: logs duplicated on console, -bg option is ignored * `check`: check configuration and exit. It also prints a list of all connected devices * `status`: print status of the running `ipp-usb` daemon, including information of all connected devices ### Options are * `-bg`: run in background (ignored in debug mode) ## NETWORKING Essentially, `ipp-usb` makes printer or scanner accessible from the network, converting network-side HTTP operations to the USB operations. By default, `ipp-usb` exposes device only to the loopback interface, using the `localhost` address (both `127.0.0.1` and `::1`, for IPv4 and IPv6, respectively). TCP ports are allocated automatically, and allocation is persisted in the association with the particular device, so the next time the device is plugged on, it will get the same port. The default port range for TCP ports allocation is `60000-65535`. This default behavior can be changed, using configuration file. See `CONFIGURATION` section below for details. If you decide to publish your device to the real network, the following things should be taken into consideration: 1. Your **private** device will become **public** and it will become accessible by other computers from the network 2. Firewall rules needs to be updated appropriately. The `ipp-usb` daemon will not do it automatically by itself 3. IPP over USB specification explicitly require that the `Host` field in the HTTP request is set to `localhost` or `localhost:port`. If device is accessed from the real network, `Host` header will reflect the real network address. Most of devices allow it, but some are more restrictive and will not work in this configuration. ## DNS-SD (AVAHI INTEGRATION) IPP over USB is intended to be used with the automatic device discovery, and for this purpose `ipp-usb` advertises all devices it handles, using DNS-SD protocol. On Linux, DNS-SD is handled with a help of Avahi daemon. DNS-SD advertising can be disabled via configuration file. Also, if Avahi is not installed or not running, `ipp-usb` will still work correctly, although DNS-SD advertising will not work. For every device the following services will be advertised: | Instance | Type | Subtypes | | ----------- | ------------- | ------------------------- | | Device name | _ipp._tcp | _universal._sub._ipp._tcp | | Device name | _printer._tcp | | | Device name | _uscan._tcp | | | Device name | _http._tcp | | | BBPP | _ipp-usb._tcp | | Notes: * `Device name` is the name under which device appears in the list of available devices, for example, in the printing dialog (it is DNS-SD device name, in another words), and for most of devices will match the device's model name. It is appended with the `" (USB)"` suffix, so if device is connected via network and via USB simultaneously, these two connections can be easily distinguished. If there are two devices with the same name connected simultaneously, the suffix becomes `" (USB NNN)"`, with NNN number unique for each device, for disambiguation. In another words, the single `"Kyocera ECOSYS M2040dn"` device will be listed as `"Kyocera ECOSYS M2040dn (USB)"`, and two such a devices will be listed as `"Kyocera ECOSYS M2040dn (USB 1)"` and `"Kyocera ECOSYS M2040dn (USB 2)"` * `_ipp._tcp` and `_printer._tcp` are only advertises for printer devices and MFPs * `_uscan._tcp` is only advertised for scanner devices and MFPs * for the `_ipp._tcp` service, the `_universal._sub._ipp._tcp` subtype is also advertised for iOS compatibility * `_printer._tcp` is advertised with TCP port set to 0. Other services are advertised with the actual port number * `_http._tcp` is device web-console. It is always advertises in assumption it is always exist * `BBPP`, used for the `_ipp-usb._tcp` service, is the USB bus (BB) and port (PP) numbers in hex. The purpose of this advertising is to help CUPS and other possible "clients" to guess which devices are handled by the `ipp-usb` service, to avoid possible conflicts with the legacy USB drivers. ## CONFIGURATION `ipp-usb` searched for its configuration file in two places: 1. `/etc/ipp-usb/ipp-usb.conf` 2. `ipp-usb.conf` in the directory where executable file is located Configuration file syntax is very similar to .INI files syntax. It consist of named sections, and each section contains a set of named variables. Comments are started from # or ; characters and continues until end of line: # This is a comment [section 1] variable 1 = value 1 ; and another comment variable 2 = value 2 ### Network parameters Network parameters are all in the `[network]` section: [network] # TCP ports for HTTP will be automatically allocated in the # following range http-min-port = 60000 http-max-port = 65535 # Enable or disable DNS-SD advertisement dns-sd = enable # enable | disable # Network interface to use. Set to `all` if you want to expose you # printer to the local network. This way you can share your printer # with other computers in the network, as well as with iOS and # Android devices. interface = loopback # all | loopback # Enable or disable IPv6 ipv6 = enable # enable | disable ### Authentication By default, `ipp-usb` exposes locally connected USB printer to all users of the system. Though this is reasonable behavior in most cases, when computer and printer are both in personal use, for bigger installation this approach can be too simple and primitive. `ipp-usb` provides a mechanism, which allows to control local clients access based on UID the client program runs under. Please note, this mechanism will not work for remote connections (disabled by default but supported). Authentication of remote users requires some different mechanism, which is under consideration but is not yet implemented. Authentication parameters are all in the [auth uid] section: # Local user authentication by UID/GID [auth uid] # Syntax: # operations = users # # Operations are comma-separated list of following operations: # all - all operations # config - configuration web-console # fax - faxing # print - printing # scan - scanning # # Users have the following suntax: # user - user name # @group - all users that belongs to the group # # Users and groups may be specified either by names or by # numbers. * means any # # Note, if user/group is not known in the context of request # (for example, in the case of non-local network connection), # "_" used for matching # # User/group names are resolved at the moment of request # processing (and cached for a couple of seconds), so running # daemon will see changes to the /etc/passwd and /etc/group # # Examples: # fax, print = lp, @lp # Allow CUPS to do its work # scan = * # Allow any user to scan # config = @wheel # Only wheel group members can do that all = * ### Logging configuration Logging parameters are all in the `[logging]` section: [logging] # device-log - what logs are generated per device # main-log - what common logs are generated # console-log - what of generated logs goes to console # # parameter contains a comma-separated list of # the following keywords: # error - error messages # info - informative messages # debug - debug messages # trace-ipp, trace-escl, trace-http - very detailed # per-protocol traces # trace-usb - hex dump of all USB traffic # all - all logs # trace-all - alias to all # # Note, trace-* implies debug, debug implies info, info implies # error device-log = all main-log = debug console-log = debug # Log rotation parameters: # log-file-size - max log file before rotation. Use suffix # M for megabytes or K for kilobytes # log-backup-files - how many backup files to preserve during # rotation # max-file-size = 256K max-backup-files = 5 # Enable or disable ANSI colors on console console-color = enable # enable | disable # ipp-usb queries IPP printer attributes at the initialization time # for its own purposes and writes received attributes to the log. # By default, only necessary attributes are requested from device. # # If this parameter is set to true, all printer attributes will # be requested. Normally, it only affects the logging. However, # some enterprise-level HP printers returns such huge amount of # data and do it so slowly, so it can cause initialization timeout. # This is why this feature is not enabled by default get-all-printer-attrs = false # false | true ### Quirks Some devices, due to their firmware bugs, require special handling, called device-specific **quirks**. `ipp-usb` loads quirks from the `/usr/share/ipp-usb/quirks/*.conf` files and from the `/etc/ipp-usb/quirks/*.conf` files. The `/etc/ipp-usb/quirks` directory is for system quirks overrides or admin changes. These files have .INI-file syntax with the content that looks like this: [HP LaserJet MFP M28-M31] http-connection = keep-alive [HP OfficeJet Pro 8730] http-connection = close [HP Inc. HP Laser MFP 135a] blacklist = true # Default configuration [*] http-connection = "" For each discovered device, its model name is matched against sections of the quirks files. Section names may contain glob-style wildcards: `*` that matches any sequence of characters and `?` , that matches any single character. To match one of these characters (`*` and `?`) literally, use backslash as escape. Note, the simplest way to guess the exact model name for the particular device is to use `ipp-usb check` command, which prints a list of all connected devices. All matching sections from all quirks files are taken in consideration, and applied in priority order. Priority is computed using the following algorithm: * When matching model name against section name, amount of non-wildcard matched characters is counted, and the longer match wins * Otherwise, section loaded first wins. Files are loaded in alphabetical order, sections read sequentially If some parameter exist in multiple sections, used its value from the most priority section The following parameters are defined: * `blacklist = true | false`
If `true`, the matching device is ignored by the `ipp-usb` * `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 * `disable-fax = true | false`
If `true`, the matching device's fax capability is ignored * `init-reset = none | soft | hard`
How to reset device during initialization. Default is `none` * `init-delay = NNN`
Delay, in milliseconds, between device is opened and, optionally, reset, and the first request is sent to device * `request-delay` = NNN
Delay, in milliseconds, between subsequent requests * `ignore-ipp-status = true | false`
If `true`, IPP status of IPP requests sent by the `ipp-usb` by itself will be ignored. This quirk is useful, when device correctly handles IPP request but returned status is not reliable. Affects only `ipp-usb` initialization. If you found out about your device that it needs a quirk to work properly or it does not work with `ipp-usb` at all, although it provides IPP-over-USB interface, please report the issue at https://github.com/OpenPrinting/ipp-usb. It will let us to update our collection of quirks, so helping other owners of such a device. ## FILES * `/etc/ipp-usb/ipp-usb.conf`: the daemon configuration file * `/var/log/ipp-usb/main.log`: the main log file * `/var/log/ipp-usb/.log`: per-device log files * `/var/ipp-usb/dev/.state`: device state (HTTP port allocation, DNS-SD name) * `/var/ipp-usb/lock/ipp-usb.lock`: lock file, that helps to prevent multiple copies of daemon to run simultaneously * `/var/ipp-usb/ctrl`: `ipp-usb` control socket. Currently only used to obtain the per-device status (printed by `ipp-usb status`), but its functionality may be extended in a future * `/usr/share/ipp-usb/quirks/*.conf`: device-specific quirks (see above) * `/etc/ipp-usb/quirks/*.conf`: device-specific quirks defined by sysadmin (see above) ## COPYRIGHT Copyright (c) by Alexander Pevzner (pzz@apevzner.com, pzz@pzz.msk.ru)
All rights reserved. This program is licensed under 2-Clause BSD license. See LICENSE file for details. ## SEE ALSO **cups(1)** # vim:ts=8:sw=4:et ipp-usb-0.9.24/ipp-usb.conf000066400000000000000000000064131453365611700154320ustar00rootroot00000000000000# ipp-usb.conf: example configuration file # Networking parameters [network] # TCP ports for HTTP will be automatically allocated in the following range http-min-port = 60000 http-max-port = 65535 # Enable or disable DNS-SD advertisement dns-sd = enable # enable | disable # Network interface to use. Set to `all` if you want to expose you # printer to the local network. This way you can share your printer # with other computers in the network, as well as with iOS and Android # devices. interface = loopback # all | loopback # Enable or disable IPv6 ipv6 = enable # enable | disable # Local user authentication by UID/GID [auth uid] # Syntax: # operations = users # # Operations are comma-separated list of following operations: # all - all operations # config - configuration web-console # fax - faxing # print - printing # scan - scanning # # Users have the following suntax: # user - user name # @group - all users that belongs to the group # # Users and groups may be specified either by names or by # numbers. * means any # # Note, if user/group is not known in the context of request # (for example, in the case of non-local network connection), # "_" used for matching # # User/group names are resolved at the moment of request # processing (and cached for a couple of seconds), so running # daemon will see changes to the /etc/passwd and /etc/group # # Examples: # fax, print = lp, @lp # Allow CUPS to do its work # scan = * # Allow any user to scan # config = @wheel # Only wheel group members can do that all = * # Logging configuration [logging] # device-log - per-device log levels # main-log - main log levels # console-log - console log levels # # parameter contains a comma-separated list of # the following keywords: # error - error messages # info - informative messages # debug - debug messages # trace-ipp, trace-escl, trace-http - very detailed per-protocol traces # trace-usb - hex dump of all USB traffic # all - all logs # trace-all - alias to all # # Note, trace-* implies debug, debug implies info, info implies error device-log = all main-log = debug console-log = debug # Log rotation parameters: # max-file-size - max log file before rotation. Use suffix M # for megabytes or K for kilobytes # max-backup-files - how many backup files to preserve during rotation # max-file-size = 256K max-backup-files = 5 # Enable or disable ANSI colors on console console-color = enable # enable | disable # ipp-usb queries IPP printer attributes at the initialization time # for its own purposes and writes received attributes to the log. # By default, only necessary attributes are requested from device. # # If this parameter is set to true, all printer attributes will # be requested. Normally, it only affects the logging. However, # some enterprise-level HP printers returns such huge amount of # data and do it so slowly, so it can cause initialization timeout. # This is why this feature is not enabled by default get-all-printer-attrs = false # false | true # vim:ts=8:sw=2:et ipp-usb-0.9.24/ipp.go000066400000000000000000000324771453365611700143340ustar00rootroot00000000000000/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device * * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com) * See LICENSE for license terms and conditions * * IPP service registration */ package main import ( "bytes" "fmt" "io/ioutil" "net/http" "strings" "github.com/OpenPrinting/goipp" ) // IppPrinterInfo represents additional printer information, which // is not included into DNS-SD TXT record, but still needed for // other purposes type IppPrinterInfo struct { DNSSdName string // DNS-SD device name UUID string // Device UUID AdminURL string // Admin URL IconURL string // Device icon URL IppSvcIndex int // IPP DNSSdSvcInfo index within array of services } // IppService performs IPP Get-Printer-Attributes query using provided // http.Client and decodes received information into the form suitable // for DNS-SD registration // // Discovered services will be added to the services collection func IppService(log *LogMessage, services *DNSSdServices, port int, usbinfo UsbDeviceInfo, quirks QuirksSet, c *http.Client) (ippinfo *IppPrinterInfo, err error) { // Query printer attributes uri := fmt.Sprintf("http://localhost:%d/ipp/print", port) msg, err := ippGetPrinterAttributes(log, c, quirks, uri) if err != nil { return } // Decode IPP service info attrs := newIppDecoder(msg) ippinfo, ippScv := attrs.decode(usbinfo) // Check for fax support canFax := false if usbinfo.BasicCaps&UsbIppBasicCapsFax != 0 && !quirks.GetDisableFax() { // Note, as device lists Fax on its basic capabilities, // this probe most likely is not needed, but as the // ipp-usb version 0.9.19 and earlier used to guess // for fax support based on the /ipp/faxout probe, // not on device capabilities, lets leave it here // for now, just in case. Firmwares in general are // too buggy, I can't trust them :-( uri = fmt.Sprintf("http://localhost:%d/ipp/faxout", port) if _, err2 := ippGetPrinterAttributes(log, c, quirks, uri); err2 == nil { canFax = true log.Debug(' ', "IPP FaxOut service detected") } else { log.Error('!', "IPP FaxOut probe failed: %s", err2) } } else { log.Debug(' ', "IPP FaxOut service not in capabilities") } if canFax { ippScv.Txt.Add("Fax", "T") ippScv.Txt.Add("rfo", "ipp/faxout") } else { ippScv.Txt.Add("Fax", "F") } // Construct LPD info. Per Apple spec, we MUST advertise // LPD with zero port, even if we don't support it lpdScv := DNSSdSvcInfo{ Type: "_printer._tcp", Port: 0, Txt: nil, } // Pack it all together ippScv.Port = port services.Add(lpdScv) ippinfo.IppSvcIndex = len(*services) services.Add(ippScv) return } // ippGetPrinterAttributes performs GetPrinterAttributes query, // using the specified http.Client and uri // // If this function returns nil error, it means that: // 1) HTTP transaction performed successfully // 2) Received reply successfully decoded // 3) It is not an IPP error response // // Otherwise, the appropriate error is generated and returned func ippGetPrinterAttributes(log *LogMessage, c *http.Client, quirks QuirksSet, uri string) (msg *goipp.Message, err error) { // Query printer attributes msg = goipp.NewRequest(goipp.DefaultVersion, goipp.OpGetPrinterAttributes, 1) msg.Operation.Add(goipp.MakeAttribute("attributes-charset", goipp.TagCharset, goipp.String("utf-8"))) msg.Operation.Add(goipp.MakeAttribute("attributes-natural-language", goipp.TagLanguage, goipp.String("en-US"))) msg.Operation.Add(goipp.MakeAttribute("printer-uri", goipp.TagURI, goipp.String(uri))) rq := goipp.Attribute{Name: "requested-attributes"} if Conf.LogAllPrinterAttrs { rq.Values.Add(goipp.TagKeyword, goipp.String("all")) } else { rq.Values.Add(goipp.TagKeyword, goipp.String("color-supported")) rq.Values.Add(goipp.TagKeyword, goipp.String("document-format-supported")) rq.Values.Add(goipp.TagKeyword, goipp.String("media-size-supported")) rq.Values.Add(goipp.TagKeyword, goipp.String("mopria-certified")) rq.Values.Add(goipp.TagKeyword, goipp.String("printer-device-id")) rq.Values.Add(goipp.TagKeyword, goipp.String("printer-dns-sd-name")) rq.Values.Add(goipp.TagKeyword, goipp.String("printer-icons")) rq.Values.Add(goipp.TagKeyword, goipp.String("printer-info")) rq.Values.Add(goipp.TagKeyword, goipp.String("printer-kind")) rq.Values.Add(goipp.TagKeyword, goipp.String("printer-location")) rq.Values.Add(goipp.TagKeyword, goipp.String("printer-make-and-model")) rq.Values.Add(goipp.TagKeyword, goipp.String("printer-more-info")) rq.Values.Add(goipp.TagKeyword, goipp.String("printer-uuid")) rq.Values.Add(goipp.TagKeyword, goipp.String("sides-supported")) rq.Values.Add(goipp.TagKeyword, goipp.String("urf-supported")) } msg.Operation.Add(rq) log.Add(LogTraceIPP, '>', "IPP request:"). IppRequest(LogTraceIPP, '>', msg). Nl(LogTraceIPP). Flush() req, _ := msg.EncodeBytes() resp, err := c.Post(uri, goipp.ContentType, bytes.NewBuffer(req)) if err != nil { err = fmt.Errorf("HTTP: %s", err) return } defer resp.Body.Close() // Check HTTP status if resp.StatusCode/100 != 2 { err = fmt.Errorf("HTTP: %s", resp.Status) return } // Decode IPP response message respData, err := ioutil.ReadAll(resp.Body) if err != nil { err = fmt.Errorf("HTTP: %s", err) return } err = msg.DecodeBytesEx(respData, goipp.DecoderOptions{EnableWorkarounds: true}) if err != nil { log.Debug(' ', "Failed to decode IPP message: %s", err) log.HexDump(LogTraceIPP, ' ', respData) err = fmt.Errorf("IPP decode: %s", err) return } log.Add(LogTraceIPP, '<', "IPP response:"). IppResponse(LogTraceIPP, '<', msg). Nl(LogTraceIPP). Flush() // Check response status if msg.Code >= 0x100 && !quirks.GetIgnoreIppStatus() { err = fmt.Errorf("IPP: %s", goipp.Status(msg.Code)) return } return } // ippAttrs represents a collection of IPP printer attributes, // enrolled into a map for convenient access type ippAttrs map[string]goipp.Values // Create new ippAttrs func newIppDecoder(msg *goipp.Message) ippAttrs { attrs := make(ippAttrs) // Note, we move from the end of list to the beginning, so // in a case of duplicated attributes, first occurrence wins for i := len(msg.Printer) - 1; i >= 0; i-- { attr := msg.Printer[i] attrs[attr.Name] = attr.Values } return attrs } // Decode printer attributes and build TXT record for IPP service // // This is where information comes from: // // DNS-SD name: "printer-dns-sd-name" with fallback to "printer-info", // "printer-make-and-model" and finally to MfgAndProduct // from the UsbDeviceInfo // // TXT fields: // air: hardcoded as "none" // mopria-certified: "mopria-certified" // rp: hardcoded as "ipp/print" // kind: "printer-kind" // PaperMax: based on decoding "media-size-supported" // URF: "urf-supported" with fallback to // URF extracted from "printer-device-id" // UUID: "printer-uuid", without "urn:uuid:" prefix // Color: "color-supported" // Duplex: search "sides-supported" for strings with // prefix "one" or "two" // note: "printer-location" // qtotal: hardcoded as "1" // usb_MDL: MDL, extracted from "printer-device-id" // usb_MFG: MFG, extracted from "printer-device-id" // usb_CMD: CMD, extracted from "printer-device-id" // ty: "printer-make-and-model" // priority: hardcoded as "50" // product: "printer-make-and-model", in round brackets // pdl: "document-format-supported" // txtvers: hardcoded as "1" // adminurl: "printer-more-info" // func (attrs ippAttrs) decode(usbinfo UsbDeviceInfo) ( ippinfo *IppPrinterInfo, svc DNSSdSvcInfo) { svc = DNSSdSvcInfo{ Type: "_ipp._tcp", SubTypes: []string{"_universal._sub._ipp._tcp"}, } // Obtain IppPrinterInfo ippinfo = &IppPrinterInfo{ AdminURL: attrs.strSingle("printer-more-info"), IconURL: attrs.strSingle("printer-icons"), } // Obtain DNSSdName ippinfo.DNSSdName = attrs.strSingle("printer-dns-sd-name") if ippinfo.DNSSdName == "" { ippinfo.DNSSdName = attrs.strSingle("printer-info") } if ippinfo.DNSSdName == "" { ippinfo.DNSSdName = attrs.strSingle("printer-make-and-model") } if ippinfo.DNSSdName == "" { ippinfo.DNSSdName = usbinfo.MfgAndProduct } // Obtain UUID ippinfo.UUID = attrs.getUUID() if ippinfo.UUID == "" { ippinfo.UUID = usbinfo.UUID() } // Obtain and parse IEEE 1284 device ID devid := make(map[string]string) for _, id := range strings.Split(attrs.strSingle("printer-device-id"), ";") { keyval := strings.SplitN(id, ":", 2) if len(keyval) == 2 { devid[keyval[0]] = keyval[1] } } svc.Txt.Add("air", "none") svc.Txt.IfNotEmpty("mopria-certified", attrs.strSingle("mopria-certified")) svc.Txt.Add("rp", "ipp/print") svc.Txt.Add("priority", "50") svc.Txt.IfNotEmpty("kind", attrs.strJoined("printer-kind")) svc.Txt.IfNotEmpty("PaperMax", attrs.getPaperMax()) if !svc.Txt.IfNotEmpty("URF", attrs.strJoined("urf-supported")) { svc.Txt.IfNotEmpty("URF", devid["URF"]) } svc.Txt.IfNotEmpty("UUID", ippinfo.UUID) svc.Txt.IfNotEmpty("Color", attrs.getBool("color-supported")) svc.Txt.IfNotEmpty("Duplex", attrs.getDuplex()) svc.Txt.Add("note", attrs.strSingle("printer-location")) svc.Txt.Add("qtotal", "1") svc.Txt.IfNotEmpty("usb_MDL", devid["MDL"]) svc.Txt.IfNotEmpty("usb_MFG", devid["MFG"]) svc.Txt.IfNotEmpty("usb_CMD", devid["CMD"]) svc.Txt.IfNotEmpty("ty", attrs.strSingle("printer-make-and-model")) svc.Txt.IfNotEmpty("product", attrs.strBrackets("printer-make-and-model")) svc.Txt.AddPDL("pdl", attrs.strJoined("document-format-supported")) svc.Txt.Add("txtvers", "1") svc.Txt.URLIfNotEmpty("adminurl", ippinfo.AdminURL) return } // getUUID returns printer UUID, or "", if UUID not available func (attrs ippAttrs) getUUID() string { uuid := attrs.strSingle("printer-uuid") return UUIDNormalize(uuid) } // getDuplex returns "T" if printer supports two-sided // printing, "F" if not and "" if it cant' tell func (attrs ippAttrs) getDuplex() string { vals := attrs.getAttr(goipp.TypeString, "sides-supported") one, two := false, false for _, v := range vals { s := string(v.(goipp.String)) switch { case strings.HasPrefix(s, "one"): one = true case strings.HasPrefix(s, "two"): two = true } } if two { return "T" } if one { return "F" } return "" } // getPaperMax returns max paper size, supported by printer // // According to Bonjour Printing Specification, Version 1.2.1, // it can take one of following values: // "isoC-A2" // // If PaperMax cannot be guessed, it returns empty string func (attrs ippAttrs) getPaperMax() string { // Roll over "media-size-supported", extract // max x-dimension and max y-dimension vals := attrs.getAttr(goipp.TypeCollection, "media-size-supported") if vals == nil { return "" } var xDimMax, yDimMax int for _, collection := range vals { var xDimAttr, yDimAttr goipp.Attribute attrs := collection.(goipp.Collection) for i := len(attrs) - 1; i >= 0; i-- { switch attrs[i].Name { case "x-dimension": xDimAttr = attrs[i] case "y-dimension": yDimAttr = attrs[i] } } if len(xDimAttr.Values) > 0 { switch dim := xDimAttr.Values[0].V.(type) { case goipp.Integer: if int(dim) > xDimMax { xDimMax = int(dim) } case goipp.Range: if int(dim.Upper) > xDimMax { xDimMax = int(dim.Upper) } } } if len(yDimAttr.Values) > 0 { switch dim := yDimAttr.Values[0].V.(type) { case goipp.Integer: if int(dim) > yDimMax { yDimMax = int(dim) } case goipp.Range: if int(dim.Upper) > yDimMax { yDimMax = int(dim.Upper) } } } } if xDimMax == 0 || yDimMax == 0 { return "" } // Now classify by paper size return PaperSize{xDimMax, yDimMax}.Classify() } // Get a single-string attribute. func (attrs ippAttrs) strSingle(name string) string { strs := attrs.getStrings(name) if len(strs) == 0 { return "" } return strs[0] } // Get a multi-string attribute, represented as a comma-separated list func (attrs ippAttrs) strJoined(name string) string { strs := attrs.getStrings(name) return strings.Join(strs, ",") } // Get a single string, and put it into brackets func (attrs ippAttrs) strBrackets(name string) string { s := attrs.strSingle(name) if s != "" { s = "(" + s + ")" } return s } // Get attribute's []string value by attribute name func (attrs ippAttrs) getStrings(name string) []string { vals := attrs.getAttr(goipp.TypeString, name) strs := make([]string, len(vals)) for i := range vals { strs[i] = string(vals[i].(goipp.String)) } return strs } // Get boolean attribute. Returns "F" or "T" if attribute is found, // empty string otherwise. func (attrs ippAttrs) getBool(name string) string { vals := attrs.getAttr(goipp.TypeBoolean, name) if vals == nil { return "" } if vals[0].(goipp.Boolean) { return "T" } return "F" } // Get attribute's value by attribute name // Value type is checked and enforced func (attrs ippAttrs) getAttr(t goipp.Type, name string) []goipp.Value { v, ok := attrs[name] if ok && v[0].V.Type() == t { var vals []goipp.Value for i := range v { vals = append(vals, v[i].V) } return vals } return nil } ipp-usb-0.9.24/linewriter.go000066400000000000000000000035151453365611700157170ustar00rootroot00000000000000/* 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.24/listener.go000066400000000000000000000033741453365611700153630ustar00rootroot00000000000000/* 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.24/logger.go000066400000000000000000000436501453365611700150160ustar00rootroot00000000000000/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device * * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com) * See LICENSE for license terms and conditions * * Logging */ package main import ( "bytes" "compress/gzip" "context" "fmt" "io" "io/ioutil" "net/http" "os" "path/filepath" "runtime/debug" "sort" "strings" "sync" "sync/atomic" "time" "github.com/OpenPrinting/goipp" ) const ( // LogMinFileSize specifies a minimum value for the // max-file-size parameter LogMinFileSize = 16 * 1024 ) // Standard loggers var ( // This is the default logger Log = NewLogger().ToMainFile() // Console logger always writes to console Console = NewLogger().ToConsole() // Initlog used only on initialization time // It writes to Stdout or Stderr, depending // on log level InitLog = NewLogger().ToStdOutErr() ) // LogLevel enumerates possible log levels type LogLevel int // LogLevel constants const ( LogError LogLevel = 1 << iota LogInfo LogDebug LogTraceIPP LogTraceESCL LogTraceHTTP LogTraceUSB LogAll = LogError | LogInfo | LogDebug | LogTraceAll LogTraceAll = LogTraceIPP | LogTraceESCL | LogTraceHTTP | LogTraceUSB ) // Adjust LogLevel mask, so more detailed log levels // imply less detailed func (levels *LogLevel) Adjust() { switch { case *levels&LogTraceAll != 0: *levels |= LogDebug | LogInfo | LogError case *levels&LogDebug != 0: *levels |= LogInfo | LogError case *levels&LogInfo != 0: *levels |= LogError } } // loggerMode enumerates possible Logger modes type loggerMode int const ( loggerNoMode loggerMode = iota // Mode not yet set; log is buffered loggerDiscard // Log goes to nowhere loggerConsole // Log goes to console loggerColorConsole // Log goes to console and uses ANSI colors loggerFile // Log goes to disk file ) // Logger implements logging facilities type Logger struct { LogMessage // "Root" log message levels LogLevel // Levels generated by this logger ccLevels LogLevel // Sum of Cc's levels paused int32 // Logger paused, if counter > 0 mode loggerMode // Logger mode lock sync.Mutex // Write lock path string // Path to log file cc []*Logger // Loggers to send carbon copy to out io.Writer // Output stream, may be *os.File outhook func(io.Writer, // Output hook LogLevel, []byte) // Don't reexport these methods from the root message Commit, Flush, Reject struct{} } // NewLogger creates new logger. Logger mode is not set, // so logs written to this logger a buffered until mode // (and direction) is set func NewLogger() *Logger { l := &Logger{ mode: loggerNoMode, levels: LogAll, ccLevels: 0, outhook: func(w io.Writer, _ LogLevel, line []byte) { w.Write(line) }, } l.LogMessage.logger = l return l } // ToNowhere redirects log to nowhere func (l *Logger) ToNowhere() *Logger { l.mode = loggerDiscard l.out = ioutil.Discard return l } // ToConsole redirects log to console func (l *Logger) ToConsole() *Logger { l.mode = loggerConsole l.out = os.Stdout return l } // ToColorConsole redirects log to console with ANSI colors func (l *Logger) ToColorConsole() *Logger { if logIsAtty(os.Stdout) { l.outhook = logColorConsoleWrite } return l.ToConsole() } // ToStdOutErr redirects log to Stdout or Stderr, depending // on LogLevel func (l *Logger) ToStdOutErr() *Logger { l.outhook = func(out io.Writer, level LogLevel, line []byte) { if level == LogError { out = os.Stderr } out.Write(line) } return l.ToConsole() } // ToFile redirects log to arbitrary log file func (l *Logger) ToFile(path string) *Logger { l.path = path l.mode = loggerFile l.out = nil // Will be opened on demand return l } // ToMainFile redirects log to the main log file func (l *Logger) ToMainFile() *Logger { return l.ToFile(PathLogFile) } // ToDevFile redirects log to per-device log file func (l *Logger) ToDevFile(info UsbDeviceInfo) *Logger { return l.ToFile(filepath.Join(PathLogDir, info.Ident()+".log")) } // Cc adds 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, prefix byte, data []byte) *LogMessage { if (msg.logger.levels|msg.logger.ccLevels)&level == 0 { return msg } hex := logLineBufAlloc(0, 0) chr := logLineBufAlloc(0, 0) defer hex.free() defer chr.free() off := 0 for len(data) > 0 { hex.Reset() chr.Reset() sz := len(data) if sz > 16 { sz = 16 } i := 0 for ; i < sz; i++ { c := data[i] fmt.Fprintf(hex, "%2.2x", data[i]) if i%4 == 3 { hex.Write([]byte(":")) } else { hex.Write([]byte(" ")) } if 0x20 <= c && c < 0x80 { chr.WriteByte(c) } else { chr.WriteByte('.') } } for ; i < 16; i++ { hex.WriteString(" ") } msg.Add(level, prefix, "%4.4x: %s %s", off, hex, chr) off += sz data = data[sz:] } return msg } // HTTPRequest dumps HTTP request (except body) to the log message func (msg *LogMessage) HTTPRequest(level LogLevel, prefix byte, session int, rq *http.Request) *LogMessage { if (msg.logger.levels|msg.logger.ccLevels)&level == 0 { return msg } // Clone request, drop body rq = rq.WithContext(context.Background()) rq.Body = struct{ io.ReadCloser }{http.NoBody} // Write it to the log msg.Add(level, prefix, "HTTP[%3.3d]: HTTP request header:", session) buf := &bytes.Buffer{} rq.Write(buf) for _, l := range bytes.Split(buf.Bytes(), []byte("\n")) { if sz := len(l); sz > 0 && l[sz-1] == '\r' { l = l[:sz-1] } msg.Add(level, prefix, " %s", l) if len(l) == 0 { break } } return msg } // HTTPResponse dumps HTTP response (expect body) to the log message func (msg *LogMessage) HTTPResponse(level LogLevel, prefix byte, session int, rsp *http.Response) *LogMessage { if (msg.logger.levels|msg.logger.ccLevels)&level == 0 { return msg } // Clone response header. Avoid rsp.Header.Clone(), // because Go 11 doesn't support it yet hdr := make(http.Header, len(rsp.Header)) for k, v := range rsp.Header { hdr[k] = v } // Go stdlib strips Transfer-Encoding header, so reconstruct it if rsp.TransferEncoding != nil { hdr.Add("Transfer-Encoding", strings.Join(rsp.TransferEncoding, ", ")) } // Write it to the log msg.Add(level, prefix, "HTTP[%3.3d]: HTTP response header:", session) msg.Add(level, prefix, " %s %s", rsp.Proto, rsp.Status) keys := make([]string, 0, len(hdr)) for k := range hdr { keys = append(keys, k) } sort.Strings(keys) for _, k := range keys { msg.Add(level, prefix, " %s: %s", k, hdr.Get(k)) } msg.Add(level, prefix, " ") return msg } // HTTPRqParams dumps HTTP request parameters into the log message func (msg *LogMessage) HTTPRqParams(level LogLevel, prefix byte, session int, rq *http.Request) *LogMessage { msg.Add(level, prefix, "HTTP[%3.3d]: %s %s", session, rq.Method, rq.URL) return msg } // HTTPRspStatus dumps HTTP response status into the log message func (msg *LogMessage) HTTPRspStatus(level LogLevel, prefix byte, session int, rq *http.Request, rsp *http.Response) *LogMessage { msg.Add(level, prefix, "HTTP[%3.3d]: %s %s - %s", session, rq.Method, rq.URL, rsp.Status) return msg } // HTTPError writes HTTP error into the log message func (msg *LogMessage) HTTPError(prefix byte, session int, format string, args ...interface{}) *LogMessage { msg.Error(prefix, "HTTP[%3.3d]: %s", session, fmt.Sprintf(format, args...)) return msg } // HTTPDebug writes HTTP debug line into the log message func (msg *LogMessage) HTTPDebug(prefix byte, session int, format string, args ...interface{}) *LogMessage { msg.Debug(prefix, "HTTP[%3.3d]: %s", session, fmt.Sprintf(format, args...)) return msg } // IppRequest dumps IPP request into the log message func (msg *LogMessage) IppRequest(level LogLevel, prefix byte, m *goipp.Message) *LogMessage { if (msg.logger.levels|msg.logger.ccLevels)&level != 0 { m.Print(msg.LineWriter(level, prefix), true) } return msg } // IppResponse dumps IPP response into the log message func (msg *LogMessage) IppResponse(level LogLevel, prefix byte, m *goipp.Message) *LogMessage { if (msg.logger.levels|msg.logger.ccLevels)&level != 0 { m.Print(msg.LineWriter(level, prefix), false) } return msg } // LineWriter creates a LineWriter that writes to the LogMessage, // using specified LogLevel and prefix func (msg *LogMessage) LineWriter(level LogLevel, prefix byte) *LineWriter { return &LineWriter{ Func: func(line []byte) { msg.addBytes(level, prefix, line) }, } } // Commit message to the log func (msg *LogMessage) Commit() { msg.Flush() msg.free() } // Flush message content to the log // // This is equal to committing the message and starting // the new message, with the exception that old message // pointer remains valid. Message logical atomicity is not // preserved between flushes func (msg *LogMessage) Flush() { // Lock the logger msg.logger.lock.Lock() defer msg.logger.lock.Unlock() // Ignore empty messages if len(msg.lines) == 0 { return } // If message has a parent, simply flush our content there if msg.parent != nil { msg.parent.lines = append(msg.parent.lines, msg.lines...) msg.lines = msg.lines[:0] // If our parent is root, we need to flush root as well if msg.parent.parent == nil { msg = msg.parent } else { return } } // Do nothing, if logger is paused if atomic.LoadInt32(&msg.logger.paused) != 0 { return } // Open log file on demand if msg.logger.out == nil && msg.logger.mode == loggerFile { os.MkdirAll(PathLogDir, 0755) msg.logger.out, _ = os.OpenFile(msg.logger.path, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644) } if msg.logger.out == nil { return } // Rotate now if msg.logger.mode == loggerFile { msg.logger.rotate() } // Prepare to carbon-copy var cclist []struct { levels LogLevel msg *LogMessage } for _, cc := range msg.logger.cc { cclist = append(cclist, struct { levels LogLevel msg *LogMessage }{cc.levels, cc.Begin()}) } // Send message content to the logger buf := msg.logger.fmtTime() defer buf.free() timeLen := buf.Len() for _, l := range msg.lines { l.trim() // Generate own output buf.Truncate(timeLen) if l.level&msg.logger.levels != 0 { if !l.empty() { if timeLen != 0 { buf.WriteByte(' ') } buf.Write(l.Bytes()) } buf.WriteByte('\n') msg.logger.outhook(msg.logger.out, l.level, buf.Bytes()) } // Send carbon copies for _, cc := range cclist { if (cc.levels & l.level) != 0 { cc.msg.addBytes(l.level, 0, l.Bytes()) } } l.free() } // Commit carbon copies for _, cc := range cclist { cc.msg.Commit() } // Reset the message msg.lines = msg.lines[:0] } // Reject the message func (msg *LogMessage) Reject() { msg.free() } // Return message to the logMessagePool func (msg *LogMessage) free() { // Free all lines for _, l := range msg.lines { l.free() } // Reset the message and put it to the pool if len(msg.lines) < 16 { msg.lines = msg.lines[:0] // Keep memory, reset content } else { msg.lines = nil // Drop this large buffer } msg.logger = nil logMessagePool.Put(msg) } // logLineBuf represents a single log line buffer type logLineBuf struct { bytes.Buffer // Underlying buffer level LogLevel // Log level the line was written on } // logLinePool manages a pool of reusable logLines var logLineBufPool = sync.Pool{New: func() interface{} { return &logLineBuf{ Buffer: bytes.Buffer{}, } }} // logLineAlloc() allocates a logLineBuf func logLineBufAlloc(level LogLevel, prefix byte) *logLineBuf { buf := logLineBufPool.Get().(*logLineBuf) buf.level = level if prefix != 0 { buf.Write([]byte{prefix, ' '}) } return buf } // free returns the logLineBuf to the pool func (buf *logLineBuf) free() { if buf.Cap() <= 256 { buf.Reset() logLineBufPool.Put(buf) } } // trim removes trailing spaces func (buf *logLineBuf) trim() { bytes := buf.Bytes() var i int loop: for i = len(bytes); i > 0; i-- { c := bytes[i-1] switch c { case '\t', '\n', '\v', '\f', '\r', ' ', 0x85, 0xA0: default: break loop } } buf.Truncate(i) } // empty returns true if logLineBuf is empty (no text, no prefix) func (buf *logLineBuf) empty() bool { return buf.Len() == 0 } ipp-usb-0.9.24/logger_unix.go000066400000000000000000000020671453365611700160560ustar00rootroot00000000000000// +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.24/loopback.go000066400000000000000000000012221453365611700153160ustar00rootroot00000000000000/* 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.24/main.go000066400000000000000000000165241453365611700144630ustar00rootroot00000000000000/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device * * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com) * See LICENSE for license terms and conditions * * The main function */ package main import ( "bytes" "fmt" "os" "sort" ) const usageText = `Usage: %s mode [options] Modes are: standalone - run forever, automatically discover IPP-over-USB devices and serve them all udev - like standalone, but exit when last IPP-over-USB device is disconnected debug - logs duplicated on console, -bg option is ignored check - check configuration and exit status - print ipp-usb status and exit Options are -bg - run in background (ignored in debug mode) ` // RunMode represents the program run mode type RunMode int // Run modes: // RunStandalone - run forever, automatically discover IPP-over-USB // devices and serve them all // RunUdev - like RunStandalone, but exit when last IPP-over-USB // device is disconnected // RunDebug - logs duplicated on console, -bg option is ignored // RunCheck - check configuration and exit // RunStatus - print ipp-usb status and exit const ( RunDefault RunMode = iota RunStandalone RunUdev RunDebug RunCheck RunStatus ) // String returns RunMode name func (m RunMode) String() string { switch m { case RunDefault: return "default" case RunStandalone: return "standalone" case RunUdev: return "udev" case RunDebug: return "debug" case RunCheck: return "check" case RunStatus: return "status" } return fmt.Sprintf("unknown (%d)", int(m)) } // RunParameters represents the program run parameters type RunParameters struct { Mode RunMode // Run mode Background bool // Run in background } // usage prints detailed usage and exits func usage() { fmt.Printf(usageText, os.Args[0]) os.Exit(0) } // usage_error prints usage error and exits func usageError(format string, args ...interface{}) { if format != "" { fmt.Printf(format+"\n", args...) } fmt.Printf("Try %s -h for more information\n", os.Args[0]) os.Exit(1) } // parseArgv parses program parameters. In a case of usage error, // it prints a error message and exits func parseArgv() (params RunParameters) { // Catch panics to log defer func() { v := recover() if v != nil { Log.Panic(v) } }() // For now, default mode is debug mode. It may change in a future params.Mode = RunDebug modes := 0 for _, arg := range os.Args[1:] { switch arg { case "-h", "-help", "--help": usage() case "standalone": params.Mode = RunStandalone modes++ case "udev": params.Mode = RunUdev modes++ case "debug": params.Mode = RunDebug modes++ case "check": params.Mode = RunCheck modes++ case "status": params.Mode = RunStatus modes++ case "-bg": params.Background = true default: usageError("Invalid argument %s", arg) } } if modes > 1 { usageError("Conflicting run modes") } if params.Mode == RunDebug { params.Background = false } return } // printStatus prints status of running ipp-usb daemon, if any func printStatus() { // Fetch status text, err := StatusRetrieve() if err != nil { InitLog.Info(0, "%s", err) return } // Split into lines text = bytes.Trim(text, "\n") lines := bytes.Split(text, []byte("\n")) // Strip empty lines at the end for len(lines) > 0 && len(lines[len(lines)-1]) == 0 { lines = lines[0 : len(lines)-1] } // Write to log, line by line for _, line := range lines { InitLog.Info(0, "%s", line) } } // The main function func main() { var err error // Parse arguments params := parseArgv() // Load configuration file err = ConfLoad() InitLog.Check(err) // Setup logging if params.Mode != RunDebug && params.Mode != RunCheck && params.Mode != RunStatus { Console.ToNowhere() } else if Conf.ColorConsole { Console.ToColorConsole() } Log.SetLevels(Conf.LogMain) Console.SetLevels(Conf.LogConsole) Log.Cc(Console) // In RunCheck mode, list IPP-over-USB devices if params.Mode == RunCheck { // If we are here, configuration is OK InitLog.Info(0, "Configuration files: OK") var descs map[UsbAddr]UsbDeviceDesc err = UsbInit(true) if err == nil { descs, err = UsbGetIppOverUsbDeviceDescs() } if err != nil { InitLog.Info(0, "Can't read list of USB devices: %s", err) } else if descs == nil || len(descs) == 0 { InitLog.Info(0, "No IPP over USB devices found") } else { // Repack into the sorted list var list []UsbDeviceDesc var buf bytes.Buffer for _, desc := range descs { list = append(list, desc) } sort.Slice(list, func(i, j int) bool { return list[i].UsbAddr.Less(list[j].UsbAddr) }) InitLog.Info(0, "IPP over USB devices:") InitLog.Info(0, " Num Device Vndr:Prod Model") for i, dev := range list { buf.Reset() fmt.Fprintf(&buf, "%3d. %s", i+1, dev.UsbAddr) if info, err := dev.GetUsbDeviceInfo(); err == nil { fmt.Fprintf(&buf, " %4.4x:%.4x %q", info.Vendor, info.Product, info.MfgAndProduct) } InitLog.Info(0, " %s", buf.String()) } } } // In RunStatus mode, print ipp-usb status, and we are done if params.Mode == RunStatus { printStatus() os.Exit(0) } // Check user privileges if os.Geteuid() != 0 { InitLog.Exit(0, "This program requires root privileges") } // If mode is "check", we are done if params.Mode == RunCheck { os.Exit(0) } // If background run is requested, it's time to fork if params.Background { err = Daemon() InitLog.Check(err) os.Exit(0) } // Prevent multiple copies of ipp-usb from being running // in a same time os.MkdirAll(PathLockDir, 0755) lock, err := os.OpenFile(PathLockFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) InitLog.Check(err) defer lock.Close() err = FileLock(lock, FileLockNoWait) if err == ErrLockIsBusy { if params.Mode == RunUdev { // It's not an error in udev mode os.Exit(0) } else { InitLog.Exit(0, "ipp-usb already running") } } InitLog.Check(err) // Write to log that we are here if params.Mode != RunCheck && params.Mode != RunStatus { Log.Info(' ', "===============================") Log.Info(' ', "ipp-usb started in %q mode, pid=%d", params.Mode, os.Getpid()) defer Log.Info(' ', "ipp-usb finished") } // Initialize USB err = UsbInit(false) InitLog.Check(err) // Close stdin/stdout/stderr, unless running in debug mode if params.Mode != RunDebug { err = CloseStdInOutErr() InitLog.Check(err) } // Run PnP manager for { exitReason := PnPStart(params.Mode == RunUdev) // The following race is possible here: // 1) last device disappears, ipp-usb is about to exit // 2) new device connected, new ipp-usb started // 3) new ipp-usp exits, because lock is still held // by the old ipp-usb // 4) old ipp-usb finally exits // // So after releasing a lock, we rescan for IPP-over-USB // devices, and if something was found, we try to reacquire // the lock, and if it succeeds, we continue to serve // these devices instead of exiting if exitReason == PnPIdle && params.Mode == RunUdev { err = FileUnlock(lock) Log.Check(err) if UsbCheckIppOverUsbDevices() && FileLock(lock, FileLockNoWait) == nil { Log.Info(' ', "New IPP-over-USB device found") continue } } break } } ipp-usb-0.9.24/paper.go000066400000000000000000000040161453365611700146370ustar00rootroot00000000000000/* 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.24/paths.go000066400000000000000000000023341453365611700146500ustar00rootroot00000000000000/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device * * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com) * See LICENSE for license terms and conditions * * Common paths */ package main const ( // PathConfDir defines path to configuration directory PathConfDir = "/etc/ipp-usb" // PathConfQuirksDir defines path to quirks files in configuration directory PathConfQuirksDir = "/etc/ipp-usb/quirks" // PathQuirksDir defines path to quirks files PathQuirksDir = "/usr/share/ipp-usb/quirks" // PathProgState defines path to program state directory PathProgState = "/var/ipp-usb" // PathLockDir defines path to directory that contains lock files PathLockDir = PathProgState + "/lock" // PathLockFile defines path to lock file PathLockFile = PathLockDir + "/ipp-usb.lock" // PathControlSocket defines path to the control socket PathControlSocket = PathProgState + "/ctrl" // PathProgStateDev defines path to directory where per-device state // files are saved to PathProgStateDev = PathProgState + "/dev" // PathLogDir defines path to log directory PathLogDir = "/var/log/ipp-usb" // PathLogFile defines path to the main log file PathLogFile = PathLogDir + "/main.log" ) ipp-usb-0.9.24/pnp.go000066400000000000000000000074731453365611700143370ustar00rootroot00000000000000/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device * * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com) * See LICENSE for license terms and conditions * * PnP manager */ package main import ( "context" "os" "os/signal" "sync" "syscall" "time" ) // PnPExitReason explains why PnP manager has exited type PnPExitReason int // PnPExitReason constants const ( PnPIdle PnPExitReason = iota // No more connected devices PnPTerm // Terminating signal received ) // pnpRetryTime returns time of next retry of failed device initialization func pnpRetryTime(err error) time.Time { if err == ErrBlackListed || err == ErrUnusable { // These errors are unrecoverable. // Forget about device for the next million hours :-) return time.Now().Add(time.Hour * 1e6) } return time.Now().Add(DevInitRetryInterval) } // pnpRetryExpired checks if device initialization retry time expired func pnpRetryExpired(tm time.Time) bool { return !time.Now().Before(tm) } // PnPStart start PnP manager // // If exitWhenIdle is true, PnP manager will exit, when there is no more // devices to serve func PnPStart(exitWhenIdle bool) PnPExitReason { devices := UsbAddrList{} devByAddr := make(map[UsbAddr]*Device) retryByAddr := make(map[UsbAddr]time.Time) sigChan := make(chan os.Signal, 1) ticker := time.NewTicker(DevInitRetryInterval / 4) tickerRunning := true signal.Notify(sigChan, os.Signal(syscall.SIGINT), os.Signal(syscall.SIGTERM), os.Signal(syscall.SIGHUP)) // Start control socket server err := CtrlsockStart() if err == nil { defer CtrlsockStop() } // Serve PnP events until terminated loop: for { devDescs, err := UsbGetIppOverUsbDeviceDescs() if err == nil { newdevices := UsbAddrList{} for _, desc := range devDescs { newdevices.Add(desc.UsbAddr) } added, removed := devices.Diff(newdevices) devices = newdevices // Handle added devices for _, addr := range added { Log.Debug('+', "PNP %s: added", addr) dev, err := NewDevice(devDescs[addr]) StatusSet(addr, devDescs[addr], err) if err == nil { devByAddr[addr] = dev } else { Log.Error('!', "PNP %s: %s", addr, err) retryByAddr[addr] = pnpRetryTime(err) } } // Handle removed devices for _, addr := range removed { Log.Debug('-', "PNP %s: removed", addr) delete(retryByAddr, addr) StatusDel(addr) dev, ok := devByAddr[addr] if ok { dev.Close() delete(devByAddr, addr) } } // Handle devices, waiting for retry for addr, tm := range retryByAddr { if !pnpRetryExpired(tm) { continue } Log.Debug('+', "PNP %s: retry", addr) dev, err := NewDevice(devDescs[addr]) StatusSet(addr, devDescs[addr], err) if err == nil { devByAddr[addr] = dev delete(retryByAddr, addr) } else { Log.Error('!', "PNP %s: %s", addr, err) retryByAddr[addr] = pnpRetryTime(err) } } } // Handle exit when idle if exitWhenIdle && len(devices) == 0 { Log.Info(' ', "No IPP-over-USB devices present, exiting") return PnPIdle } // Update ticker switch { case tickerRunning && len(retryByAddr) == 0: ticker.Stop() tickerRunning = false case !tickerRunning && len(retryByAddr) != 0: ticker = time.NewTicker(DevInitRetryInterval / 4) tickerRunning = true } // Wait for the next event select { case <-UsbHotPlugChan: case <-ticker.C: case sig := <-sigChan: Log.Info(' ', "%s signal received, exiting", sig) break loop } } // Close remaining devices ctx, cancel := context.WithTimeout(context.Background(), DevShutdownTimeout) defer cancel() var done sync.WaitGroup for _, dev := range devByAddr { done.Add(1) go func(dev *Device) { dev.Shutdown(ctx) dev.Close() done.Done() }(dev) } done.Wait() return PnPTerm } ipp-usb-0.9.24/quirks.go000066400000000000000000000173361453365611700150570ustar00rootroot00000000000000/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device * * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com) * See LICENSE for license terms and conditions * * Device-specific quirks */ package main import ( "fmt" "io" "io/ioutil" "math" "net/http" "os" "path/filepath" "sort" "strings" "time" ) // Quirks represents device-specific quirks type Quirks struct { Origin string // file:line of definition Model string // Device model name Blacklist bool // Blacklist the device HTTPHeaders map[string]string // HTTP header override UsbMaxInterfaces uint // Max number of USB interfaces DisableFax bool // Disable fax for device ResetMethod QuirksResetMethod // Device reset method InitDelay time.Duration // Delay before 1st IPP-USB request RequestDelay time.Duration // Delay between IPP-USB requests IgnoreIppStatus bool // Ignore IPP status Index int // Incremented in order of loading } // QuirksResetMethod represents how to reset a device // during initialization type QuirksResetMethod int // QuirksResetUnset - reset method not specified // QuirksResetNone - don't reset device at all // QuirksResetSoft - use class-specific soft reset // QuirksResetHard - use USB hard reset const ( QuirksResetUnset QuirksResetMethod = iota QuirksResetNone QuirksResetSoft QuirksResetHard ) // String returns textual representation of QuirksResetMethod func (m QuirksResetMethod) String() string { switch m { case QuirksResetUnset: return "unset" case QuirksResetNone: return "none" case QuirksResetSoft: return "soft" case QuirksResetHard: return "hard" } return fmt.Sprintf("unknown (%d)", int(m)) } // empty returns true, if Quirks are actually empty func (q *Quirks) empty() bool { return !q.Blacklist && len(q.HTTPHeaders) == 0 && q.UsbMaxInterfaces == 0 && !q.DisableFax && q.ResetMethod == QuirksResetUnset && q.InitDelay == 0 && q.RequestDelay == 0 && !q.IgnoreIppStatus } // QuirksSet represents collection of quirks type QuirksSet []*Quirks // LoadQuirksSet creates new QuirksSet and loads its content from a directory func LoadQuirksSet(paths ...string) (QuirksSet, error) { qset := QuirksSet{} for _, path := range paths { err := qset.readDir(path) if err != nil { return nil, err } } return qset, nil } // readDir loads all Quirks from a directory func (qset *QuirksSet) readDir(path string) error { files, err := ioutil.ReadDir(path) if err != nil { if os.IsNotExist(err) { err = nil } return err } for _, file := range files { if file.Mode().IsRegular() && strings.HasSuffix(file.Name(), ".conf") { err = qset.readFile(filepath.Join(path, file.Name())) if err != nil { return err } } } return nil } // readFile reads all Quirks from a file func (qset *QuirksSet) readFile(file string) error { // Open quirks file ini, err := OpenIniFileWithRecType(file) if err != nil { return err } defer ini.Close() // Load all quirks var q *Quirks for err == nil { var rec *IniRecord rec, err = ini.Next() if err != nil { break } // Get Quirks structure if rec.Type == IniRecordSection { q = &Quirks{ Origin: fmt.Sprintf("%s:%d", rec.File, rec.Line), Model: rec.Section, HTTPHeaders: make(map[string]string), Index: len(*qset), } qset.Add(q) continue } else if q == nil { err = fmt.Errorf("%s:%d: %q = %q out of any section", rec.File, rec.Line, rec.Key, rec.Value) break } // Update Quirks data if strings.HasPrefix(rec.Key, "http-") { key := http.CanonicalHeaderKey(rec.Key[5:]) q.HTTPHeaders[key] = rec.Value continue } switch rec.Key { case "blacklist": err = rec.LoadBool(&q.Blacklist) case "usb-max-interfaces": err = rec.LoadUintRange(&q.UsbMaxInterfaces, 1, math.MaxUint32) case "disable-fax": err = rec.LoadBool(&q.DisableFax) case "init-reset": err = rec.LoadQuirksResetMethod(&q.ResetMethod) case "init-delay": err = rec.LoadDuration(&q.InitDelay) case "request-delay": err = rec.LoadDuration(&q.RequestDelay) case "ignore-ipp-status": err = rec.LoadBool(&q.IgnoreIppStatus) } } if err == io.EOF { err = nil } return err } // Add appends Quirks to QuirksSet func (qset *QuirksSet) Add(q *Quirks) { *qset = append(*qset, q) } // ByModelName returns a subset of quirks, applicable for // specific device, matched by model name // // In a case of multiple match, quirks are returned in // the from most prioritized to least prioritized order // // Duplicates are removed: if some parameter is set by // more prioritized entry, it is removed from the less // prioritized entries. Entries, that in result become // empty, are removed at all func (qset QuirksSet) ByModelName(model string) QuirksSet { type item struct { q *Quirks matchlen int } var list []item // Get list of matching quirks for _, q := range qset { matchlen := GlobMatch(model, q.Model) if matchlen >= 0 { list = append(list, item{q, matchlen}) } } // Sort the list by matchlen, in decreasing order sort.Slice(list, func(i, j int) bool { if list[i].matchlen != list[j].matchlen { return list[i].matchlen > list[j].matchlen } return list[i].q.Index < list[j].q.Index }) // Rebuild it into the slice of *Quirks quirks := make(QuirksSet, len(list)) for i := range list { quirks[i] = list[i].q } // Remove duplicates and empty entries httpHeaderSeen := make(map[string]struct{}) out := 0 for in, q := range quirks { // Note, here we avoid modification of the HTTPHeaders // map in the original Quirks structure // // Unfortunately, Golang misses immutable types, // so we must be very careful here q2 := &Quirks{} *q2 = *q q2.HTTPHeaders = make(map[string]string) for name, value := range quirks[in].HTTPHeaders { if _, seen := httpHeaderSeen[name]; !seen { httpHeaderSeen[name] = struct{}{} q2.HTTPHeaders[name] = value } } if !q2.empty() { quirks[out] = q2 out++ } } quirks = quirks[:out] return quirks } // GetBlacklist returns effective Blacklist parameter, // taking the whole set into consideration func (qset QuirksSet) GetBlacklist() bool { for _, q := range qset { if q.Blacklist { return true } } return false } // GetUsbMaxInterfaces returns effective UsbMaxInterfaces parameter, // taking the whole set into consideration func (qset QuirksSet) GetUsbMaxInterfaces() uint { for _, q := range qset { if q.UsbMaxInterfaces != 0 { return q.UsbMaxInterfaces } } return 0 } // GetDisableFax returns effective DisableFax parameter, // taking the whole set into consideration func (qset QuirksSet) GetDisableFax() bool { for _, q := range qset { if q.DisableFax { return true } } return false } // GetResetMethod returns effective ResetMethod parameter func (qset QuirksSet) GetResetMethod() QuirksResetMethod { for _, q := range qset { if q.ResetMethod != QuirksResetUnset { return q.ResetMethod } } return QuirksResetNone } // GetInitDelay returns effective InitDelay parameter func (qset QuirksSet) GetInitDelay() time.Duration { for _, q := range qset { if q.InitDelay != 0 { return q.InitDelay } } return 0 } // GetRequestDelay returns effective RequestDelay parameter func (qset QuirksSet) GetRequestDelay() time.Duration { for _, q := range qset { if q.RequestDelay != 0 { return q.RequestDelay } } return 0 } // GetIgnoreIppStatus returns effective IgnoreIppStatus parameter, // taking the whole set into consideration func (qset QuirksSet) GetIgnoreIppStatus() bool { for _, q := range qset { if q.IgnoreIppStatus { return true } } return false } ipp-usb-0.9.24/quirks_test.go000066400000000000000000000024411453365611700161050ustar00rootroot00000000000000/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device * * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com) * See LICENSE for license terms and conditions * * Tests for device-specific quirks */ package main import ( "testing" ) // Test quirls loading and lookup func TestQuirksSetLoadAndLookup(t *testing.T) { const path = "testdata/quirks" const badPath = path + "-not-exist" // Try non-existent directory _, err := LoadQuirksSet(badPath) if err != nil { t.Fatalf("LoadQuirksSet(%q): %s", badPath, err) } // Try test data qset, err := LoadQuirksSet(path) if err != nil { t.Fatalf("LoadQuirksSet(%q): %s", path, err) } // Test default quirks quirks := qset.ByModelName("unknown device") if quirks == nil { t.Fatalf("default quirls: missed") } if len(quirks) != 1 { t.Fatalf("default quirls: expected 1, got %d", len(quirks)) } // Test quirks for some known device device := "HP LaserJet MFP M28-M31" quirks = qset.ByModelName(device) if quirks == nil { t.Fatalf("%q quirls: missed", device) } if len(quirks) != 1 { // default overridden by this device t.Fatalf("%q quirls: expected 1, got %d", device, len(quirks)) } if quirks[0].Model != device { t.Fatalf("%q quirls: wrong ordering of returned quirks", device) } } ipp-usb-0.9.24/snap/000077500000000000000000000000001453365611700141415ustar00rootroot00000000000000ipp-usb-0.9.24/snap/local/000077500000000000000000000000001453365611700152335ustar00rootroot00000000000000ipp-usb-0.9.24/snap/local/run-ipp-usb000077500000000000000000000013471453365611700173470ustar00rootroot00000000000000#!/bin/sh #set -e -x # Create needed directories # Ignore errors mkdir -p $SNAP_COMMON/etc || : mkdir -p $SNAP_COMMON/var/log || : mkdir -p $SNAP_COMMON/var/lock || : mkdir -p $SNAP_COMMON/var/dev || : mkdir -p $SNAP_COMMON/quirks || : # Put config files in place # # Do not overwrite files and ignore errors, to not reset user configuration # when running as root and to not have ugly error messages when running as # non-root. yes no | cp -i $SNAP/usr/share/ipp-usb/quirks/* $SNAP_COMMON/quirks >/dev/null 2>&1 || : if [ ! -f $SNAP_COMMON/etc/ipp-usb.conf ]; then cp $SNAP/etc/ipp-usb.conf $SNAP_COMMON/etc/ >/dev/null 2>&1 || : fi # Run ipp-usb with the command line arguments with which we were called exec $SNAP/sbin/ipp-usb "$@" ipp-usb-0.9.24/snap/local/run-ipp-usb-server000077500000000000000000000032551453365611700206530ustar00rootroot00000000000000#!/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.24/snap/snapcraft.yaml000066400000000000000000000051211453365611700170050ustar00rootroot00000000000000name: ipp-usb base: core22 version: git summary: IPP-over-USB - Driverless IPP printing on USB-connected printers description: | ipp-usb is a daemon to allow driverless IPP printing on USB-connected printers.It emulates an IPP network printer on the local machine, giving full access to the physical printer: Printing, scanning, fax out, and the admin web interface. grade: stable confinement: strict # Only build on the architectures supported architectures: - build-on: amd64 - build-on: arm64 - build-on: armhf apps: ipp-usb-server: command: scripts/run-ipp-usb-server daemon: simple plugs: [avahi-control, network, network-bind, raw-usb, hardware-observe] ipp-usb: command: scripts/run-ipp-usb plugs: [raw-usb, hardware-observe] parts: goipp: plugin: go source: https://github.com/OpenPrinting/goipp.git source-type: git build-packages: - golang-go override-prime: "" ipp-usb: plugin: go source: . source-type: git override-build: | set -eux # Correct hard-coded paths in paths.go # Not only the config file ipp-usb.conf will be put into a user-editable # space but also the quirks file, so that the user can add and debug # quirks perl -p -i -e 's:/etc/:/var/snap/ipp-usb/common/etc/:' paths.go perl -p -i -e 's:/var/ipp-usb:/var/snap/ipp-usb/common/var:' paths.go perl -p -i -e 's:/usr/share/ipp-usb/quirks:/var/snap/hplip-printer-app/common/quirks:' paths.go perl -p -i -e 's:/var/log/ipp-usb:/var/snap/ipp-usb/common/var/log:' paths.go # Build the executable craftctl default # Place the executable in /sbin, it's a system daemon mv ../install/bin ../install/sbin # Install the config file and the quirk files mkdir -p ../install/etc cp ipp-usb.conf ../install/etc mkdir -p ../install/usr/share/ipp-usb/quirks cp ipp-usb-quirks/* ../install/usr/share/ipp-usb/quirks build-packages: - golang-go - libavahi-client-dev - libavahi-common-dev - libusb-1.0-0-dev - ronn - perl-base stage-packages: - libavahi-client3 - libavahi-common3 - libusb-1.0-0 - udev prime: - etc - -etc/init.d - -etc/udev - sbin - -sbin/systemd-hwdb - lib - -lib/modprobe.d - -lib/systemd - -lib/udev - usr/lib - -usr/lib/tmpfiles.d - usr/share/ipp-usb after: [goipp] scripts: plugin: dump source: snap/local/ organize: run-ipp-usb*: scripts/ prime: - scripts/ after: [ipp-usb] ipp-usb-0.9.24/status.go000066400000000000000000000053031453365611700150530ustar00rootroot00000000000000/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device * * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com) * See LICENSE for license terms and conditions * * ipp-usb status support */ package main import ( "bytes" "fmt" "io/ioutil" "net" "net/http" "sort" "sync" ) // statusOfDevice represents a status of the particular device type statusOfDevice struct { desc UsbDeviceDesc // Device descriptor init error // Initialization error, nil if none } var ( // statusTable maintains a per-device status, // indexed by the UsbAddr statusTable = make(map[UsbAddr]*statusOfDevice) // statusLock protects access to the statusTable statusLock sync.RWMutex ) // StatusRetrieve connects to the running ipp-usb daemon, retrieves // its status and returns retrieved status as a printable text func StatusRetrieve() ([]byte, error) { t := &http.Transport{ Dial: func(network, addr string) (net.Conn, error) { return CtrlsockDial() }, } c := &http.Client{ Transport: t, } rsp, err := c.Get("http://localhost/status") if err != nil { return nil, err } defer rsp.Body.Close() return ioutil.ReadAll(rsp.Body) } // StatusFormat formats ipp-usb status as a text func StatusFormat() []byte { buf := &bytes.Buffer{} // Lock the statusTable statusLock.RLock() defer statusLock.RUnlock() // Dump ipp-usb daemon status. If we are here, we are // definitely running :-) buf.WriteString("ipp-usb daemon: running\n") // Sort devices by address devs := make([]*statusOfDevice, len(statusTable)) i := 0 for _, status := range statusTable { devs[i] = status } sort.Slice(devs, func(i, j int) bool { return devs[i].desc.UsbAddr.Less(devs[j].desc.UsbAddr) }) // Format per-device status buf.WriteString("ipp-usb devices:") if len(statusTable) == 0 { buf.WriteString(" not found\n") } else { buf.WriteString("\n") fmt.Fprintf(buf, " Num Device Vndr:Prod Model\n") for i, status := range devs { info, _ := status.desc.GetUsbDeviceInfo() fmt.Fprintf(buf, " %3d. %s %4.4x:%.4x %q\n", i+1, status.desc.UsbAddr, info.Vendor, info.Product, info.MfgAndProduct) s := "OK" if status.init != nil { s = devs[i].init.Error() } fmt.Fprintf(buf, " status: %s", s) } } return buf.Bytes() } // StatusSet adds device to the status table or updates status // of the already known device func StatusSet(addr UsbAddr, desc UsbDeviceDesc, init error) { statusLock.Lock() statusTable[addr] = &statusOfDevice{ desc: desc, init: init, } statusLock.Unlock() } // StatusDel deletes device from the status table func StatusDel(addr UsbAddr) { statusLock.Lock() delete(statusTable, addr) statusLock.Unlock() } ipp-usb-0.9.24/systemd-udev/000077500000000000000000000000001453365611700156315ustar00rootroot00000000000000ipp-usb-0.9.24/systemd-udev/71-ipp-usb.rules000066400000000000000000000011651453365611700205140ustar00rootroot00000000000000# Standard IPP over USB devices, with Class/SubClass/Protocol = 7/1/4 ACTION=="add", SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", ENV{ID_USB_INTERFACES}=="*:070104:*", OWNER="root", GROUP="lp", MODE="0664", TAG+="systemd", ENV{SYSTEMD_WANTS}+="ipp-usb.service" # Non-standard HP devices with 255/9/1 combination # Tested with following devices: # HP LaserJet MFP M426fdn # HP ColorLaserJet MFP M278-M281 ACTION=="add", SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", ENV{ID_VENDOR_ID}=="03f0", ENV{ID_USB_INTERFACES}=="*:ff0901:*", OWNER="root", GROUP="lp", MODE="0664", TAG+="systemd", ENV{SYSTEMD_WANTS}+="ipp-usb.service" ipp-usb-0.9.24/systemd-udev/ipp-usb.service000066400000000000000000000003171453365611700205730ustar00rootroot00000000000000[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.24/tcpuid_linux.go000066400000000000000000000072261453365611700162450ustar00rootroot00000000000000/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device * * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com) * See LICENSE for license terms and conditions * * UID discovery for TCP connection over loopback -- Linux version */ package main import ( "encoding/binary" "fmt" "net" "syscall" "unsafe" ) // #include // #include // #include // #include // #include // #include // #include // // typedef struct inet_diag_req_v2 inet_diag_req_v2_struct; // typedef struct inet_diag_sockid inet_diag_sockid_struct; // typedef struct nlmsgerr nlmsgerr_struct; // typedef struct inet_diag_msg inet_diag_msg_struct; // // typedef struct { // struct nlmsghdr hdr; // struct inet_diag_req_v2 data; // } sock_diag_request; // import "C" // TCPClientUIDSupported tells if TCPClientUID supported on this platform func TCPClientUIDSupported() bool { return true } // TCPClientUID obtains UID of client process that created // TCP connection over the loopback interface func TCPClientUID(client, server *net.TCPAddr) (int, error) { // Open NETLINK_SOCK_DIAG socket sock, err := sockDiagOpen() if err != nil { return -1, err } defer syscall.Close(sock) // Prepare request rq := C.sock_diag_request{} rq.hdr.nlmsg_len = C.uint32_t(unsafe.Sizeof(rq)) rq.hdr.nlmsg_type = C.uint16_t(C.SOCK_DIAG_BY_FAMILY) rq.hdr.nlmsg_flags = C.uint16_t(C.NLM_F_REQUEST) rq.data.sdiag_family = C.AF_INET6 rq.data.sdiag_protocol = C.IPPROTO_TCP rq.data.idiag_states = 1 << C.TCP_ESTABLISHED rq.data.id.idiag_sport = C.uint16_t(toBE16((uint16(client.Port)))) rq.data.id.idiag_dport = C.uint16_t(toBE16((uint16(server.Port)))) rq.data.id.idiag_cookie[0] = C.INET_DIAG_NOCOOKIE rq.data.id.idiag_cookie[1] = C.INET_DIAG_NOCOOKIE copy((*[16]byte)(unsafe.Pointer(&rq.data.id.idiag_src))[:], client.IP.To16()) copy((*[16]byte)(unsafe.Pointer(&rq.data.id.idiag_dst))[:], server.IP.To16()) // Send request rqData := (*[unsafe.Sizeof(rq)]byte)(unsafe.Pointer(&rq)) rqAddr := &syscall.SockaddrNetlink{Family: syscall.AF_NETLINK} err = syscall.Sendto(sock, rqData[:], 0, rqAddr) if err != nil { return -1, fmt.Errorf("sock_diag: sendto(): %s", err) } // Receive responses buf := make([]byte, syscall.Getpagesize()) for { num, _, err := syscall.Recvfrom(sock, buf, 0) if err != nil { return -1, fmt.Errorf("sock_diag: recvfrom(): %s", err) } msgs, err := syscall.ParseNetlinkMessage(buf[:num]) if err != nil { return -1, fmt.Errorf("sock_diag: can't parse response") } for _, msg := range msgs { data := unsafe.Pointer(&msg.Data[0]) switch msg.Header.Type { case syscall.NLMSG_ERROR: rsp := (*C.nlmsgerr_struct)(data) err = syscall.Errno(-rsp.error) return -1, err case uint16(C.SOCK_DIAG_BY_FAMILY): rsp := (*C.inet_diag_msg_struct)(data) return int(rsp.idiag_uid), nil } } } } // sockDiagOpen opens NETLINK_SOCK_DIAG socket func sockDiagOpen() (int, error) { const stype = syscall.SOCK_DGRAM | syscall.SOCK_CLOEXEC const proto = int(C.NETLINK_SOCK_DIAG) sock, err := syscall.Socket(syscall.AF_NETLINK, stype, proto) if err != nil { return -1, fmt.Errorf("sock_diag: socket(): %s", err) } sa := &syscall.SockaddrNetlink{Family: syscall.AF_NETLINK} err = syscall.Bind(sock, sa) if err != nil { syscall.Close(sock) return -1, fmt.Errorf("sock_diag: bind(): %s", err) } return sock, nil } // toBE16 converts uint16 to big endian func toBE16(in uint16) uint16 { var out uint16 p := (*[2]byte)(unsafe.Pointer(&out)) binary.BigEndian.PutUint16(p[:], in) return out } ipp-usb-0.9.24/tcpuid_other.go000066400000000000000000000017221453365611700162220ustar00rootroot00000000000000// +build !linux /* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device * * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com) * See LICENSE for license terms and conditions * * UID discovery for TCP connection over loopback -- default version * * If you've have added support for yet another platform, please don't * forget to update build tag at the top of this file to exclude your * platform */ package main import ( "net" ) // TCPClientUIDSupported tells if TCPClientUID supported on this platform // // If this function returns false, TCPClientUID should never be called func TCPClientUIDSupported() bool { return false } // TCPClientUID obtains UID of client process that created // TCP connection over the loopback interface func TCPClientUID(client, server *net.TCPAddr) (int, error) { // Note, TCPClientUID should never be called, if // TCPClientUIDSupported returns false panic("TCPClientUID not supported") } ipp-usb-0.9.24/tcpuid_test.go000066400000000000000000000033361453365611700160630ustar00rootroot00000000000000/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device * * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com) * See LICENSE for license terms and conditions * * Tests for TCPClientUID */ package main import ( "net" "os" "testing" ) // doTestTCPClientUID performs TCPClientUID for the specified // network and loopback address func doTestTCPClientUID(t *testing.T, network, loopback string) { // Do nothing if TCPClientUID is not supported by the platform if !TCPClientUIDSupported() { return } // Create loopback listener -- it gives us a port l, err := net.Listen(network, loopback+":") if err != nil { t.Fatalf("net.Listen(%q,%q): %s", network, loopback+":", err) } defer l.Close() // Dial client connection addr := l.Addr() clnt, err := net.Dial("tcp", addr.String()) if err != nil { t.Fatalf("net.Dial(%q,%q): %s", network, addr, err) } defer clnt.Close() // Accept server connection srv, err := l.Accept() if err != nil { t.Fatalf("net.Accept(%q,%q): %s", network, addr, err) } defer srv.Close() // Get and check Client UID uid, err := TCPClientUID(clnt.LocalAddr().(*net.TCPAddr), srv.LocalAddr().(*net.TCPAddr)) if err != nil { t.Fatalf("TCPClientUID(%q,%q): %s", clnt.LocalAddr(), srv.LocalAddr(), err) } if uid != os.Getuid() { t.Fatalf("TCPClientUID(%q,%q): uid mismatch (expected %d, present %d)", clnt.LocalAddr(), srv.LocalAddr(), os.Getuid(), uid) } } // TestTCPClientUIDIp4 performs TCPClientUID test for IPv4 func TestTCPClientUIDIp4(t *testing.T) { doTestTCPClientUID(t, "tcp", "127.0.0.1") } // TestTCPClientUIDIp6 performs TCPClientUID test for IPv6 func TestTCPClientUIDIp6(t *testing.T) { doTestTCPClientUID(t, "tcp6", "[::1]") } ipp-usb-0.9.24/testdata/000077500000000000000000000000001453365611700150115ustar00rootroot00000000000000ipp-usb-0.9.24/testdata/ipp-usb.conf000066400000000000000000000031401453365611700172350ustar00rootroot00000000000000# 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.24/testdata/quirks/000077500000000000000000000000001453365611700163275ustar00rootroot00000000000000ipp-usb-0.9.24/testdata/quirks/HP.conf000066400000000000000000000002341453365611700175040ustar00rootroot00000000000000# 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.24/testdata/quirks/blacklist.conf000066400000000000000000000003011453365611700211400ustar00rootroot00000000000000# 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.24/testdata/quirks/default.conf000066400000000000000000000001451453365611700206220ustar00rootroot00000000000000# ipp-usb quirks file -- defaults [*] # Drop Connection: header by default http-connection = "" ipp-usb-0.9.24/usbcommon.go000066400000000000000000000175231453365611700155410ustar00rootroot00000000000000/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device * * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com) * See LICENSE for license terms and conditions * * Common types for USB */ package main import ( "crypto/sha1" "fmt" "sort" "strings" ) // UsbAddr represents an USB device address type UsbAddr struct { Bus int // The bus on which the device was detected Address int // The address of the device on the bus } // String returns a human-readable representation of UsbAddr func (addr UsbAddr) String() string { return fmt.Sprintf("Bus %.3d Device %.3d", addr.Bus, addr.Address) } // Less returns true, if addr is "less" that addr2, for sorting func (addr UsbAddr) Less(addr2 UsbAddr) bool { return addr.Bus < addr2.Bus || (addr.Bus == addr2.Bus && addr.Address < addr2.Address) } // UsbAddrList represents a list of USB addresses // // For faster lookup and comparable logging, address list // is always sorted in acceding order. To maintain this // invariant, never modify list directly, and use the provided // (*UsbAddrList) Add() function type UsbAddrList []UsbAddr // Add UsbAddr to UsbAddrList func (list *UsbAddrList) Add(addr UsbAddr) { // Find the smallest index of address list // item which is greater or equal to the // newly inserted address // // Note, of "not found" case sort.Search() // returns len(*list) i := sort.Search(len(*list), func(n int) bool { return !(*list)[n].Less(addr) }) // Check for duplicate if i < len(*list) && (*list)[i] == addr { return } // The simple case: all items are less // that newly added, so just append new // address to the end if i == len(*list) { *list = append(*list, addr) return } // Insert item in the middle *list = append(*list, (*list)[i]) (*list)[i] = addr } // Find address in a list. Returns address index, // if address is found, -1 otherwise func (list UsbAddrList) Find(addr UsbAddr) int { i := sort.Search(len(list), func(n int) bool { return !list[n].Less(addr) }) if i < len(list) && list[i] == addr { return i } return -1 } // Diff computes a difference between two address lists, // returning lists of elements to be added and to be removed // to/from the list to convert it to the list2 func (list UsbAddrList) Diff(list2 UsbAddrList) (added, removed UsbAddrList) { // Note, there is no needs to sort added and removed // lists, they are already created sorted for _, a := range list2 { if list.Find(a) < 0 { added.Add(a) } } for _, a := range list { if list2.Find(a) < 0 { removed.Add(a) } } return } // UsbIfAddr represents a full "address" of the USB interface type UsbIfAddr struct { UsbAddr // Device address Num int // Interface number within Config Alt int // Number of alternate setting In, Out int // Input/output endpoint numbers } // String returns a human readable short representation of UsbIfAddr func (ifaddr UsbIfAddr) String() string { return fmt.Sprintf("Bus %.3d Device %.3d Interface %d Alt %d", ifaddr.Bus, ifaddr.Address, ifaddr.Num, ifaddr.Alt, ) } // UsbIfAddrList represents a list of USB interface addresses type UsbIfAddrList []UsbIfAddr // Add UsbIfAddr to UsbIfAddrList func (list *UsbIfAddrList) Add(addr UsbIfAddr) { *list = append(*list, addr) } // UsbDeviceDesc represents an IPP-over-USB device descriptor type UsbDeviceDesc struct { UsbAddr // Device address Config int // IPP-over-USB configuration IfAddrs UsbIfAddrList // IPP-over-USB interfaces IfDescs []UsbIfDesc // Descriptors of all interfaces } // GetUsbDeviceInfo obtains UsbDeviceInfo by UsbDeviceDesc // It may fail, if device cannot be opened func (desc UsbDeviceDesc) GetUsbDeviceInfo() (UsbDeviceInfo, error) { dev, err := UsbOpenDevice(desc) if err == nil { defer dev.Close() return dev.UsbDeviceInfo() } return UsbDeviceInfo{}, err } // UsbIfDesc represents an USB interface descriptor type UsbIfDesc struct { Vendor uint16 // USB Vendor ID Product uint16 // USB Device ID Config int // Configuration IfNum int // Interface number Alt int // Alternate setting Class int // Class SubClass int // Subclass Proto int // Protocol } // IsIppOverUsb check if interface is IPP over USB // // FIXME. The matching rules must be configurable func (ifdesc UsbIfDesc) IsIppOverUsb() bool { switch { // The classical combination, 7/1/4 case ifdesc.Class == 7 && ifdesc.SubClass == 1 && ifdesc.Proto == 4: return true // Some HP devices use non-standard combination, 255/9/1 // // This is valid at least with the following devices: // HP LaserJet MFP M426fdn // HP ColorLaserJet MFP M278-M281 case ifdesc.Vendor == 0x03f0 && ifdesc.Class == 255 && ifdesc.SubClass == 9 && ifdesc.Proto == 1: return true } return false } // UsbDeviceInfo represents USB device information type UsbDeviceInfo struct { // Fields, directly decoded from USB Vendor uint16 // Vendor ID Product uint16 // Device ID SerialNumber string // Device serial number Manufacturer string // Manufacturer name ProductName string // Product name PortNum int // USB port number BasicCaps UsbIppBasicCaps // Device basic capabilities // Precomputed fields MfgAndProduct string // Product with Manufacturer prefix, if needed } // UsbIppBasicCaps represents device basic capabilities bits, // according to the IPP-USB specification, section 4.3 type UsbIppBasicCaps int // Basic capabilities bits, see IPP-USB specification, section 4.3 const ( UsbIppBasicCapsPrint UsbIppBasicCaps = 1 << iota UsbIppBasicCapsScan UsbIppBasicCapsFax UsbIppBasicCapsOther UsbIppBasicCapsAnyHTTP ) // String returns a human-readable representation of UsbAddr func (caps UsbIppBasicCaps) String() string { s := []string{} if caps&UsbIppBasicCapsPrint != 0 { s = append(s, "print") } if caps&UsbIppBasicCapsScan != 0 { s = append(s, "scan") } if caps&UsbIppBasicCapsFax != 0 { s = append(s, "fax") } if caps&UsbIppBasicCapsAnyHTTP != 0 { s = append(s, "http") } return strings.Join(s, ",") } // FixUp fixes up precomputed fields func (info *UsbDeviceInfo) FixUp() { mfg := strings.TrimSpace(info.Manufacturer) prod := strings.TrimSpace(info.ProductName) info.MfgAndProduct = prod if !strings.HasPrefix(prod, mfg) { info.MfgAndProduct = mfg + " " + prod } } // Ident returns device identification string, suitable as // persistent state identifier func (info UsbDeviceInfo) Ident() string { id := fmt.Sprintf("%4.4x-%4.4x-%s-%s", info.Vendor, info.Product, info.SerialNumber, info.MfgAndProduct) id = strings.Map(func(c rune) rune { switch { case '0' <= c && c <= '9': case 'a' <= c && c <= 'z': case 'A' <= c && c <= 'Z': case c == '-' || c == '_': default: c = '-' } return c }, id) return id } // DNSSdName generates device DNS-SD name in a case it is not available // from IPP or eSCL func (info UsbDeviceInfo) DNSSdName() string { return info.MfgAndProduct } // UUID generates device UUID in a case it is not available // from IPP or eSCL func (info UsbDeviceInfo) UUID() string { hash := sha1.New() // Arbitrary namespace UUID const namespace = "fe678de6-f422-467e-9f83-2354e26c3b41" hash.Write([]byte(namespace)) hash.Write([]byte(info.Ident())) uuid := hash.Sum(nil) // UUID.Version = 5: Name-based with SHA1; see RFC4122, 4.1.3. uuid[6] &= 0x0f uuid[6] |= 0x5f // UUID.Variant = 0b10: see RFC4122, 4.1.1. uuid[8] &= 0x3F uuid[8] |= 0x80 return fmt.Sprintf( "%.2x%.2x%.2x%.2x-%.2x%.2x-%.2x%.2x-%.2x%.2x-%.2x%.2x%.2x%.2x%.2x%.2x", uuid[0], uuid[1], uuid[2], uuid[3], uuid[4], uuid[5], uuid[6], uuid[7], uuid[8], uuid[9], uuid[10], uuid[11], uuid[12], uuid[13], uuid[14], uuid[15]) } // Comment returns a short comment, describing a device func (info UsbDeviceInfo) Comment() string { return info.MfgAndProduct + " serial=" + info.SerialNumber } ipp-usb-0.9.24/usbcommon_test.go000066400000000000000000000034041453365611700165710ustar00rootroot00000000000000/* 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.24/usbio_libusb.go000066400000000000000000000467211453365611700162220ustar00rootroot00000000000000/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device * * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com) * See LICENSE for license terms and conditions * * USB low-level I/O. Cgo implementation on a top of libusb */ package main import ( "encoding/binary" "errors" "runtime" "sync" "sync/atomic" "time" "unsafe" ) // #cgo pkg-config: libusb-1.0 // #include // // int libusbHotplugCallback (libusb_context *ctx, libusb_device *device, // libusb_hotplug_event event, void *user_data); // // typedef struct libusb_device_descriptor libusb_device_descriptor_struct; // typedef struct libusb_config_descriptor libusb_config_descriptor_struct; // typedef struct libusb_interface libusb_interface_struct; // typedef struct libusb_interface_descriptor libusb_interface_descriptor_struct; // typedef struct libusb_endpoint_descriptor libusb_endpoint_descriptor_struct; // // // Note, libusb_strerror accepts enum libusb_error argument, which // // unfortunately behaves differently depending on target OS and compiler // // version (sometimes as C.int, sometimes as int32). Looks like cgo // // bug. Wrapping this function into this simple wrapper should // // fix the problem. See #18 for details // static inline const char* // libusb_strerror_wrapper (int code) { // return libusb_strerror(code); // } import "C" // UsbError represents USB error type UsbError struct { Func string Code UsbErrCode } // Error describes a libusb error. It implements error interface func (err UsbError) Error() string { return err.Func + ": " + err.Code.String() } // UsbErrCode represents USB I/O error code type UsbErrCode int // UsbErrCode constants const ( UsbIO UsbErrCode = C.LIBUSB_ERROR_IO UsbEInval = C.LIBUSB_ERROR_INVALID_PARAM UsbEAccess = C.LIBUSB_ERROR_ACCESS UsbENoDev = C.LIBUSB_ERROR_NO_DEVICE UsbENotFound = C.LIBUSB_ERROR_NOT_FOUND UsbEBusy = C.LIBUSB_ERROR_BUSY UsbETimeout = C.LIBUSB_ERROR_TIMEOUT UsbEOverflow = C.LIBUSB_ERROR_OVERFLOW UsbEPipe = C.LIBUSB_ERROR_PIPE UsbEIntr = C.LIBUSB_ERROR_INTERRUPTED UsbENomem = C.LIBUSB_ERROR_NO_MEM UsbENotSupported = C.LIBUSB_ERROR_NOT_SUPPORTED UsbEOther = C.LIBUSB_ERROR_OTHER ) // String returns string representation of error code func (err UsbErrCode) String() string { return C.GoString(C.libusb_strerror_wrapper(C.int(err))) } var ( // libusbContextPtr keeps a pointer to libusb_context. // It is initialized on demand libusbContextPtr *C.libusb_context // libusbContextLock protects libusbContextPtr initialization // in multithreaded context libusbContextLock sync.Mutex // Nonzero, if libusbContextPtr initialized libusbContextOk int32 // UsbHotPlugChan receives USB hotplug event notifications UsbHotPlugChan = make(chan struct{}, 1) ) // UsbInit initializes low-level USB I/O func UsbInit(nopnp bool) error { _, err := libusbContext(nopnp) return err } // libusbContext returns libusb_context. It // initializes context on demand. func libusbContext(nopnp bool) (*C.libusb_context, error) { if atomic.LoadInt32(&libusbContextOk) != 0 { return libusbContextPtr, nil } libusbContextLock.Lock() defer libusbContextLock.Unlock() // Obtain libusb_context rc := C.libusb_init(&libusbContextPtr) if rc != 0 { return nil, UsbError{"libusb_init", UsbErrCode(rc)} } // Subscribe to hotplug events if !nopnp { C.libusb_hotplug_register_callback( libusbContextPtr, // libusb_context C.LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED| // events mask C.LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT, C.LIBUSB_HOTPLUG_NO_FLAGS, // flags C.LIBUSB_HOTPLUG_MATCH_ANY, // vendor_id C.LIBUSB_HOTPLUG_MATCH_ANY, // product_id C.LIBUSB_HOTPLUG_MATCH_ANY, // dev_class C.libusb_hotplug_callback_fn(unsafe.Pointer(C.libusbHotplugCallback)), nil, // callback's data nil, // deregister handle ) } // Start libusb thread (required for hotplug) go func() { runtime.LockOSThread() for { C.libusb_handle_events(libusbContextPtr) } }() atomic.StoreInt32(&libusbContextOk, 1) return libusbContextPtr, nil } // Called by libusb on hotplug event // //export libusbHotplugCallback func libusbHotplugCallback(ctx *C.libusb_context, dev *C.libusb_device, event C.libusb_hotplug_event, p unsafe.Pointer) C.int { usbaddr := UsbAddr{ Bus: int(C.libusb_get_bus_number(dev)), Address: int(C.libusb_get_device_address(dev)), } switch event { case C.LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED: Log.Debug('+', "HOTPLUG: added %s", usbaddr) case C.LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT: Log.Debug('-', "HOTPLUG: removed %s", usbaddr) } select { case UsbHotPlugChan <- struct{}{}: default: } return 0 } // UsbCheckIppOverUsbDevices returns true if there are some IPP-over-USB devices func UsbCheckIppOverUsbDevices() bool { descs, _ := UsbGetIppOverUsbDeviceDescs() return len(descs) != 0 } // UsbGetIppOverUsbDeviceDescs return list of IPP-over-USB // device descriptors func UsbGetIppOverUsbDeviceDescs() (map[UsbAddr]UsbDeviceDesc, error) { // Obtain libusb context ctx, err := libusbContext(false) if err != nil { return nil, err } // Obtain list of devices var devlist **C.libusb_device cnt := C.libusb_get_device_list(ctx, &devlist) if cnt < 0 { return nil, UsbError{"libusb_get_device_list", UsbErrCode(cnt)} } defer C.libusb_free_device_list(devlist, 1) // Convert devlist to slice. // See https://github.com/golang/go/wiki/cgo#turning-c-arrays-into-go-slices devs := (*[1 << 28]*C.libusb_device)(unsafe.Pointer(devlist))[:cnt:cnt] // Now build list of addresses descs := make(map[UsbAddr]UsbDeviceDesc) for _, dev := range devs { desc, err := libusbBuildUsbDeviceDesc(dev) // Note, ignore devices, if we don't have // at least 2 IPP over USB interfaces // (which should not happen in real life, // but just in case... if err == nil && len(desc.IfAddrs) >= 2 { descs[desc.UsbAddr] = desc } } return descs, nil } // libusbBuildUsbDeviceDesc builds device descriptor func libusbBuildUsbDeviceDesc(dev *C.libusb_device) (UsbDeviceDesc, error) { var cDesc C.libusb_device_descriptor_struct var desc UsbDeviceDesc // Obtain device descriptor rc := C.libusb_get_device_descriptor(dev, &cDesc) if rc < 0 { return desc, UsbError{"libusb_get_device_descriptor", UsbErrCode(rc)} } // Decode device descriptor desc.Bus = int(C.libusb_get_bus_number(dev)) desc.Address = int(C.libusb_get_device_address(dev)) desc.Config = -1 // Roll over configs/interfaces/alt settings/endpoins for cfgNum := 0; cfgNum < int(cDesc.bNumConfigurations); cfgNum++ { var conf *C.libusb_config_descriptor_struct rc = C.libusb_get_config_descriptor(dev, C.uint8_t(cfgNum), &conf) if rc == 0 { // Make sure we use the same configuration for all interfaces if desc.Config >= 0 && desc.Config != int(conf.bConfigurationValue) { continue } ifcnt := conf.bNumInterfaces ifaces := (*[256]C.libusb_interface_struct)( unsafe.Pointer(conf._interface))[:ifcnt:ifcnt] for _, iface := range ifaces { altcnt := iface.num_altsetting alts := (*[256]C.libusb_interface_descriptor_struct)( unsafe.Pointer(iface.altsetting))[:altcnt:altcnt] for _, alt := range alts { // Build and append UsbIfDesc ifdesc := UsbIfDesc{ Vendor: uint16(cDesc.idVendor), Product: uint16(cDesc.idProduct), Config: int(conf.bConfigurationValue), IfNum: int(alt.bInterfaceNumber), Alt: int(alt.bAlternateSetting), Class: int(alt.bInterfaceClass), SubClass: int(alt.bInterfaceSubClass), Proto: int(alt.bInterfaceProtocol), } desc.IfDescs = append(desc.IfDescs, ifdesc) // We are only interested in IPP-over-USB // interfaces, i.e., LIBUSB_CLASS_PRINTER, // SubClass 1, Protocol 4 if ifdesc.IsIppOverUsb() { epnum := alt.bNumEndpoints endpoints := (*[256]C.libusb_endpoint_descriptor_struct)( unsafe.Pointer(alt.endpoint))[:epnum:epnum] in, out := -1, -1 for _, ep := range endpoints { num := int(ep.bEndpointAddress & 0xf) dir := int(ep.bEndpointAddress & 0x80) switch dir { case C.LIBUSB_ENDPOINT_IN: if in == -1 { in = num } case C.LIBUSB_ENDPOINT_OUT: if out == -1 { out = num } } } // Build and append UsbIfAddr if in >= 0 && out >= 0 { desc.Config = int(conf.bConfigurationValue) addr := UsbIfAddr{ UsbAddr: desc.UsbAddr, Num: int(alt.bInterfaceNumber), Alt: int(alt.bAlternateSetting), In: in, Out: out, } desc.IfAddrs.Add(addr) } } } } C.libusb_free_config_descriptor(conf) } } return desc, nil } // UsbDevHandle represents libusb_device_handle type UsbDevHandle C.libusb_device_handle // UsbOpenDevice opens device by device descriptor func UsbOpenDevice(desc UsbDeviceDesc) (*UsbDevHandle, error) { // Obtain libusb context ctx, err := libusbContext(false) if err != nil { return nil, err } // Obtain list of devices var devlist **C.libusb_device cnt := C.libusb_get_device_list(ctx, &devlist) if cnt < 0 { return nil, UsbError{"libusb_get_device_list", UsbErrCode(cnt)} } defer C.libusb_free_device_list(devlist, 1) // Convert devlist to slice. devs := (*[1 << 28]*C.libusb_device)(unsafe.Pointer(devlist))[:cnt:cnt] // Find and open a device for _, dev := range devs { bus := int(C.libusb_get_bus_number(dev)) address := int(C.libusb_get_device_address(dev)) if desc.Bus == bus && desc.Address == address { // Open device var devhandle *C.libusb_device_handle rc := C.libusb_open(dev, &devhandle) if rc < 0 { return nil, UsbError{"libusb_open", UsbErrCode(rc)} } return (*UsbDevHandle)(devhandle), nil } } return nil, UsbError{"libusb_get_device_list", UsbENotFound} } // Configure prepares the device for further work: // - set proper USB configuration // - detach kernel driver func (devhandle *UsbDevHandle) Configure(desc UsbDeviceDesc) error { // Detach kernel driver err := (*UsbDevHandle)(devhandle).detachKernelDriver() if err != nil { return err } // Set configuration rc := C.libusb_set_configuration( (*C.libusb_device_handle)(devhandle), C.int(desc.Config)) if rc < 0 { return UsbError{"libusb_set_configuration", UsbErrCode(rc)} } // Printer may require some time to switch configuration time.Sleep(time.Second / 4) return nil } // detachKernelDriver detaches kernel driver from all interfaces // of current configuration func (devhandle *UsbDevHandle) detachKernelDriver() error { C.libusb_set_auto_detach_kernel_driver( (*C.libusb_device_handle)(devhandle), 1) ifnums, err := devhandle.currentInterfaces() if err != nil { return err } for _, ifnum := range ifnums { rc := C.libusb_detach_kernel_driver( (*C.libusb_device_handle)(devhandle), C.int(ifnum)) if rc == C.LIBUSB_ERROR_NOT_FOUND { rc = 0 } if rc < 0 { return UsbError{"libusb_detach_kernel_driver", UsbErrCode(rc)} } } return nil } // libusbCurrentInterfaces builds list of interfaces in current configuration func (devhandle *UsbDevHandle) currentInterfaces() ([]int, error) { dev := C.libusb_get_device((*C.libusb_device_handle)(devhandle)) // Obtain device descriptor var cDesc C.libusb_device_descriptor_struct rc := C.libusb_get_device_descriptor(dev, &cDesc) if rc < 0 { return nil, UsbError{"libusb_get_device_descriptor", UsbErrCode(rc)} } // Get current configuration var config C.int rc = C.libusb_get_configuration((*C.libusb_device_handle)(devhandle), &config) if rc < 0 { return nil, UsbError{"libusb_get_configuration", UsbErrCode(rc)} } // Get configuration descriptor var conf *C.libusb_config_descriptor_struct for cfgNum := 0; cfgNum < int(cDesc.bNumConfigurations); cfgNum++ { rc = C.libusb_get_config_descriptor(dev, C.uint8_t(cfgNum), &conf) if rc < 0 { return nil, UsbError{"libusb_get_configuration", UsbErrCode(rc)} } if conf.bConfigurationValue == C.uint8_t(config) { break } C.libusb_free_config_descriptor(conf) conf = nil } if conf == nil { return nil, errors.New("libusb: unable to find current configuration in device descriptor") } defer C.libusb_free_config_descriptor(conf) // Build list of interface numbers ifcnt := conf.bNumInterfaces ifaces := (*[256]C.libusb_interface_struct)( unsafe.Pointer(conf._interface))[:ifcnt:ifcnt] ifnumbers := make([]int, 0, ifcnt) for _, iface := range ifaces { alt := iface.altsetting ifnumbers = append(ifnumbers, int(alt.bInterfaceNumber)) } return ifnumbers, nil } // Close a device func (devhandle *UsbDevHandle) Close() { C.libusb_close((*C.libusb_device_handle)(devhandle)) } // Reset a device func (devhandle *UsbDevHandle) Reset() { C.libusb_reset_device((*C.libusb_device_handle)(devhandle)) } // UsbDeviceInfo returns UsbDeviceInfo for the device func (devhandle *UsbDevHandle) UsbDeviceInfo() (UsbDeviceInfo, error) { dev := C.libusb_get_device((*C.libusb_device_handle)(devhandle)) var cDesc C.libusb_device_descriptor_struct var info UsbDeviceInfo // Obtain device descriptor rc := C.libusb_get_device_descriptor(dev, &cDesc) if rc < 0 { return info, UsbError{"libusb_get_device_descriptor", UsbErrCode(rc)} } // Decode device descriptor info.Vendor = uint16(cDesc.idVendor) info.Product = uint16(cDesc.idProduct) info.BasicCaps = devhandle.usbIppBasicCaps() buf := make([]byte, 256) strings := []struct { idx C.uint8_t str *string }{ {cDesc.iManufacturer, &info.Manufacturer}, {cDesc.iProduct, &info.ProductName}, {cDesc.iSerialNumber, &info.SerialNumber}, } for _, s := range strings { rc := C.libusb_get_string_descriptor_ascii( (*C.libusb_device_handle)(devhandle), s.idx, (*C.uchar)(unsafe.Pointer(&buf[0])), C.int(len(buf)), ) if rc > 0 { *s.str = string(buf[:rc]) } } info.PortNum = int(C.libusb_get_port_number(dev)) info.FixUp() return info, nil } // usbIppBasicCaps reads and decodes printer's // Class-specific Device Info Descriptor to obtain device // capabilities // // See IPP USB specification, section 4.3 for details // // This function never fails. In a case of errors, it fall backs // to the reasonable default func (devhandle *UsbDevHandle) usbIppBasicCaps() (caps UsbIppBasicCaps) { // Safe default caps = UsbIppBasicCapsPrint | UsbIppBasicCapsScan | UsbIppBasicCapsFax | UsbIppBasicCapsAnyHTTP // Buffer length const bufLen = 256 // Obtain class-specific Device Info Descriptor // See IPP USB specification, section 4.3 for details buf := make([]byte, bufLen) rc := C.libusb_get_descriptor( (*C.libusb_device_handle)(devhandle), 0x21, 0, (*C.uchar)(unsafe.Pointer(&buf[0])), bufLen) if rc < 0 { // Some devices doesn't properly return class-specific // device descriptor, so ignore an error return } if rc < 10 { // Malformed response, fall back to default return } // Decode basic capabilities bits bits := binary.LittleEndian.Uint16(buf[6:8]) if bits == 0 { // Paranoia. If no caps, return default return } return UsbIppBasicCaps(bits) } // OpenUsbInterface opens an interface func (devhandle *UsbDevHandle) OpenUsbInterface(addr UsbIfAddr) ( *UsbInterface, error) { // Claim the interface rc := C.libusb_claim_interface( (*C.libusb_device_handle)(devhandle), C.int(addr.Num), ) if rc < 0 { return nil, UsbError{"libusb_claim_interface", UsbErrCode(rc)} } // Activate alternate setting rc = C.libusb_set_interface_alt_setting( (*C.libusb_device_handle)(devhandle), C.int(addr.Num), C.int(addr.Alt), ) if rc < 0 { C.libusb_release_interface( (*C.libusb_device_handle)(devhandle), C.int(addr.Num), ) return nil, UsbError{"libusb_set_interface_alt_setting", UsbErrCode(rc)} } return &UsbInterface{ devhandle: devhandle, addr: addr, }, nil } // UsbInterface represents IPP-over-USB interface type UsbInterface struct { devhandle *UsbDevHandle // Device handle addr UsbIfAddr // Interface address } // Close the interface func (iface *UsbInterface) Close() { C.libusb_release_interface( (*C.libusb_device_handle)(iface.devhandle), C.int(iface.addr.Num), ) } // SoftReset performs interface soft reset, using class-specific // SOFT_RESET request // // This code was inspired by CUPS, and the original comment follows: // // This soft reset is specific to the printer device class and is much less // invasive than the general USB reset libusb_reset_device(). Especially it // does never happen that the USB addressing and configuration changes. What // is actually done is that all buffers get flushed and the bulk IN and OUT // pipes get reset to their default states. This clears all stall conditions. // See http://cholla.mmto.org/computers/linux/usb/usbprint11. func (iface *UsbInterface) SoftReset() error { rc := C.libusb_control_transfer( (*C.libusb_device_handle)(iface.devhandle), C.LIBUSB_REQUEST_TYPE_CLASS| C.LIBUSB_ENDPOINT_OUT| C.LIBUSB_RECIPIENT_OTHER, 2, 0, C.ushort(iface.addr.Num), nil, 0, 5000) if rc < 0 { rc = C.libusb_control_transfer( (*C.libusb_device_handle)(iface.devhandle), C.LIBUSB_REQUEST_TYPE_CLASS| C.LIBUSB_ENDPOINT_OUT| C.LIBUSB_RECIPIENT_INTERFACE, 2, 0, C.ushort(iface.addr.Num), nil, 0, 5000) } if rc < 0 { return UsbError{"libusb_control_transfer", UsbErrCode(rc)} } return nil } // Send data to interface. Returns count of bytes actually transmitted // and error, if any func (iface *UsbInterface) Send(data []byte, timeout time.Duration) (n int, err error) { var transferred C.int rc := C.libusb_bulk_transfer( (*C.libusb_device_handle)(iface.devhandle), C.uint8_t(iface.addr.Out|C.LIBUSB_ENDPOINT_OUT), (*C.uchar)(unsafe.Pointer(&data[0])), C.int(len(data)), &transferred, C.uint(timeout/time.Millisecond), ) if rc < 0 { err = UsbError{"libusb_bulk_transfer", UsbErrCode(rc)} } n = int(transferred) return } // Recv data from interface. Returns count of bytes actually transmitted // and error, if any // // Note, if data size is not 512-byte aligned, and device has more data, // that fits the provided buffer, LIBUSB_ERROR_OVERFLOW error may occur func (iface *UsbInterface) Recv(data []byte, timeout time.Duration) (n int, err error) { var transferred C.int // Some versions of Linux kernel don't allow bulk transfers to // be larger that 16kb per URB, and libusb uses some smart-ass // mechanism to avoid this limitation. // // This mechanism seems not to work very reliable on Raspberry Pi // (see #3 for details). So just limit bulk reads to 16kb const MaxBulkRead = 16384 if len(data) > MaxBulkRead { data = data[0:MaxBulkRead] } rc := C.libusb_bulk_transfer( (*C.libusb_device_handle)(iface.devhandle), C.uint8_t(iface.addr.In|C.LIBUSB_ENDPOINT_IN), (*C.uchar)(unsafe.Pointer(&data[0])), C.int(len(data)), &transferred, C.uint(timeout/time.Millisecond), ) if rc == C.LIBUSB_ERROR_PIPE { iface.ClearHalt(true) } if rc < 0 { err = UsbError{"libusb_bulk_transfer", UsbErrCode(rc)} } n = int(transferred) return } // ClearHalt clears "halted" condition of either input or output endpoint func (iface *UsbInterface) ClearHalt(in bool) error { var ep C.uint8_t if in { ep = C.uint8_t(iface.addr.In | C.LIBUSB_ENDPOINT_IN) } else { ep = C.uint8_t(iface.addr.Out | C.LIBUSB_ENDPOINT_OUT) } rc := C.libusb_clear_halt( (*C.libusb_device_handle)(iface.devhandle), ep) if rc < 0 { return UsbError{"libusb_clear_halt", UsbErrCode(rc)} } return nil } ipp-usb-0.9.24/usbtransport.go000066400000000000000000000524221453365611700163020ustar00rootroot00000000000000/* 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 QuirksSet // Device quirks deadline time.Time // Deadline for requests } // NewUsbTransport creates new http.RoundTripper backed by IPP-over-USB func NewUsbTransport(desc UsbDeviceDesc) (*UsbTransport, error) { // Open the device dev, err := UsbOpenDevice(desc) if err != nil { return nil, err } // Create UsbTransport transport := &UsbTransport{ addr: desc.UsbAddr, log: NewLogger(), dev: dev, connReleased: make(chan struct{}, 1), shutdown: make(chan struct{}), } // Obtain device info transport.info, err = dev.UsbDeviceInfo() if err != nil { dev.Close() return nil, err } transport.log.Cc(Console) transport.log.ToDevFile(transport.info) transport.log.SetLevels(Conf.LogDevice) // Setup quirks transport.quirks = Conf.Quirks.ByModelName(transport.info.MfgAndProduct) // Write device info to the log log := transport.log.Begin(). Nl(LogDebug). Debug(' ', "==============================="). Info('+', "%s: added %s", transport.addr, transport.info.ProductName). Debug(' ', "Device info:"). Debug(' ', " 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) log.Debug(' ', " disable-fax = %v", quirks.DisableFax) log.Debug(' ', " init-delay = %s", quirks.InitDelay) log.Debug(' ', " request-delay = %s", quirks.RequestDelay) if quirks.ResetMethod != QuirksResetUnset { log.Debug(' ', " init-reset = %s", quirks.ResetMethod) } log.Debug(' ', " ignore-ipp-status= %v", quirks.IgnoreIppStatus) 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 transport.quirks.GetBlacklist() { err = ErrBlackListed goto ERROR } // Hard-reset the device, if needed if transport.quirks.GetResetMethod() == QuirksResetHard { transport.log.Debug(' ', "Doing USB HARD RESET") dev.Reset() } // Configure the device err = dev.Configure(desc) if err != nil { goto ERROR } // Open connections maxconn = transport.quirks.GetUsbMaxInterfaces() if maxconn == 0 { maxconn = math.MaxUint32 } for i, ifaddr := range desc.IfAddrs { var conn *usbConn conn, err = transport.openUsbConn(i, ifaddr, transport.quirks) if err != nil { goto ERROR } transport.connList = append(transport.connList, conn) maxconn-- if maxconn == 0 { break } } transport.connPool = make(chan *usbConn, len(transport.connList)) transport.connstate = newUsbConnState(len(desc.IfAddrs)) for _, conn := range transport.connList { transport.connPool <- conn } return transport, nil // Error: cleanup and exit ERROR: for _, conn := range transport.connList { conn.destroy() } dev.Close() return nil, err } // Dump USB stack parameters to the UsbTransport's log func (transport *UsbTransport) dumpUSBparams(log *LogMessage) { const usbParamsDir = "/sys/module/usbcore/parameters" // Obtain list of parameter names (file names) dir, err := os.Open(usbParamsDir) if err != nil { return } files, err := dir.Readdirnames(-1) dir.Close() if err != nil { return } sort.Strings(files) if len(files) == 0 { return } // Compute max width of parameter names wid := 0 for _, file := range files { if wid < len(file) { wid = len(file) } } wid++ // Write the table log.Debug(' ', "USB stack parameters") for _, file := range files { p, _ := ioutil.ReadFile(usbParamsDir + "/" + file) if p == nil { p = []byte("-") } else { p = bytes.TrimSpace(p) } log.Debug(' ', " %*s %s", -wid, file+":", p) } } // Get count of connections still in use func (transport *UsbTransport) connInUse() int { return cap(transport.connPool) - len(transport.connPool) } // SetDeadline sets the deadline for all requests, submitted // via RoundTrip and RoundTripWithSession methods // // A deadline is an absolute time after which request processing // will fail instead of blocking // // This is useful only at initialization time and if some requests // were failed due to timeout, device reset is required, because // at this case synchronization with device will probably be lost // // A zero value for t means no timeout func (transport *UsbTransport) SetDeadline(t time.Time) { transport.deadline = t } // DeadlineExpired reports if deadline previously set by SetDeadline() // is already expired func (transport *UsbTransport) DeadlineExpired() bool { deadline := transport.deadline return !deadline.IsZero() && time.Until(deadline) <= 0 } // closeShutdownChan closes the transport.shutdown, which effectively // disables connections allocation (usbConnGet will return ErrShutdown) // // This function can be safely called multiple times (only the first // call closes the channel) // // Note, this function cannot be called simultaneously from // different threads. However, it's not a problem, because it // is only called from (*UsbTransport) Shutdown() and // (*UsbTransport) Close(), and both of these functions are // only called from the PnP thread context. func (transport *UsbTransport) closeShutdownChan() { select { case <-transport.shutdown: // Channel already closed default: close(transport.shutdown) } } // Shutdown gracefully shuts down the transport. If provided // context expires before shutdown completion, Shutdown // returns the Context's error func (transport *UsbTransport) Shutdown(ctx context.Context) error { transport.closeShutdownChan() for { n := transport.connInUse() if n == 0 { break } transport.log.Info('-', "%s: shutdown: %d connections still in use", transport.addr, n) select { case <-transport.connReleased: case <-ctx.Done(): transport.log.Error('-', "%s: %s: shutdown timeout expired", transport.addr, transport.info.ProductName) return ctx.Err() } } return nil } // Close the transport func (transport *UsbTransport) Close(reset bool) { // Reset the device, if required if transport.connInUse() > 0 || reset { transport.log.Info('-', "%s: resetting %s", transport.addr, transport.info.ProductName) transport.dev.Reset() } // Wait until all connections become inactive transport.Shutdown(context.Background()) // Destroy all connections and close the USB device for _, conn := range transport.connList { conn.destroy() } transport.dev.Close() transport.log.Info('-', "%s: removed %s", transport.addr, transport.info.ProductName) } // Log returns device's own logger func (transport *UsbTransport) Log() *Logger { return transport.log } // UsbDeviceInfo returns USB device information for the device // behind the transport func (transport *UsbTransport) UsbDeviceInfo() UsbDeviceInfo { return transport.info } // Quirks returns device's quirks func (transport *UsbTransport) Quirks() QuirksSet { return transport.quirks } // RoundTrip implements http.RoundTripper interface func (transport *UsbTransport) RoundTrip(r *http.Request) ( *http.Response, error) { session := int(atomic.AddInt32(&httpSessionID, 1)-1) % 1000 return transport.RoundTripWithSession(session, r) } // RoundTripWithSession executes a single HTTP transaction, returning // a Response for the provided Request. Session number, for logging, // provided as a separate parameter func (transport *UsbTransport) RoundTripWithSession(session int, rq *http.Request) (*http.Response, error) { // Log the request transport.log.HTTPRqParams(LogDebug, '>', session, rq) // Prevent request from being canceled from outside // We cannot do it on USB: closing USB connection // doesn't drain buffered data that server is // about to send to client outreq := rq.WithContext(context.Background()) outreq.Cancel = nil // Remove Expect: 100-continue, if any outreq.Header.Del("Expect") // Apply quirks for _, quirks := range transport.quirks { for name, value := range quirks.HTTPHeaders { if value != "" { outreq.Header.Set(name, value) } else { outreq.Header.Del(name) } } } // Don't let Go's stdlib to add Connection: close header // automatically outreq.Close = false // Add User-Agent, if missed. It is just cosmetic if _, found := outreq.Header["User-Agent"]; !found { outreq.Header["User-Agent"] = []string{"ipp-usb"} } // Wrap request body if outreq.Body != nil { outreq.Body = &usbRequestBodyWrapper{ log: transport.log, session: session, body: outreq.Body, } } // Prepare to correctly handle HTTP transaction, in a case // client drops request in a middle of reading body switch { case outreq.ContentLength <= 0: // Nothing to do transport.log.HTTPDebug('>', session, "body is chunked, sending as is") case outreq.ContentLength < 16384: // Body is small, prefetch it before sending to USB buf := &bytes.Buffer{} _, err := io.CopyN(buf, outreq.Body, outreq.ContentLength) if err != nil { return nil, err } outreq.Body.Close() outreq.Body = ioutil.NopCloser(buf) transport.log.HTTPDebug('>', session, "body is small (%d bytes), prefetched before sending", buf.Len()) default: // Force chunked encoding, so if client drops request, // we still be able to correctly handle HTTP transaction transport.log.HTTPDebug('>', session, "body is large (%d bytes), sending as chunked", outreq.ContentLength) outreq.ContentLength = -1 } // Log request details transport.log.Begin(). HTTPRequest(LogTraceHTTP, '>', session, outreq). Commit() // Allocate USB connection conn, err := transport.usbConnGet(rq.Context()) if err != nil { return nil, err } transport.log.HTTPDebug(' ', session, "connection %d allocated", conn.index) // Make an inter-request (or initial) delay, if needed if delay := conn.delayUntil.Sub(time.Now()); delay > 0 { transport.log.HTTPDebug(' ', session, "Pausing for %s", delay) time.Sleep(delay) } // Send request and receive a response err = outreq.Write(conn) if err != nil { transport.log.HTTPError('!', session, "%s", err) conn.put() return nil, err } resp, err := http.ReadResponse(conn.reader, outreq) if err != nil { transport.log.HTTPError('!', session, "%s", err) conn.put() return nil, err } // Wrap response body resp.Body = &usbResponseBodyWrapper{ log: transport.log, session: session, body: resp.Body, conn: conn, } // 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 delayUntil time.Time // Delay till this time before next request delayInterval time.Duration // Pause between requests cntRecv int // Total bytes received cntSent int // Total bytes sent } // Open usbConn func (transport *UsbTransport) openUsbConn( index int, ifaddr UsbIfAddr, quirks QuirksSet) (*usbConn, error) { dev := transport.dev transport.log.Debug(' ', "USB[%d]: open: %s", index, ifaddr) // Initialize connection structure conn := &usbConn{ transport: transport, index: index, delayUntil: time.Now().Add(quirks.GetInitDelay()), delayInterval: quirks.GetRequestDelay(), } conn.reader = bufio.NewReader(conn) // Obtain interface var err error conn.iface, err = dev.OpenUsbInterface(ifaddr) if err != nil { goto ERROR } // Soft-reset interface, if needed if quirks.GetResetMethod() == QuirksResetSoft { transport.log.Debug(' ', "USB[%d]: doing SOFT_RESET", index) err = conn.iface.SoftReset() if err != nil { // Don't treat it too seriously transport.log.Info('?', "USB[%d]: SOFT_RESET: %s", index, err) } } return conn, nil // Error: cleanup and exit ERROR: transport.log.Error('!', "USB[%d]: %s", index, err) if conn.iface != nil { conn.iface.Close() } return nil, err } // Compute Recv/Send timeout func (conn *usbConn) timeout() (tm time.Duration, expored bool) { deadline := conn.transport.deadline if deadline.IsZero() { return } tm = time.Until(deadline) return tm, tm <= 0 } // Read from USB func (conn *usbConn) Read(b []byte) (int, error) { conn.transport.connstate.beginRead(conn) defer conn.transport.connstate.doneRead(conn) // Note, to avoid LIBUSB_TRANSFER_OVERFLOW errors // from libusb, input buffer size must always // be aligned by 1024 bytes for USB 3.0, 512 bytes // for USB 2.0, so 1024 bytes alignment is safe for // both // // However if caller requests less that 1024 bytes, we // can't align here simply by shrinking the buffer, // because it will result a zero-size buffer. At // this case we assume caller knows what it is // doing (actually bufio never behaves this way) if n := len(b); n >= 1024 { n &= ^1023 b = b[0:n] } backoff := time.Millisecond * 100 for { tm, expired := conn.timeout() if expired { return 0, ErrInitTimedOut } n, err := conn.iface.Recv(b, tm) conn.cntRecv += n conn.transport.log.Add(LogTraceHTTP, '<', "USB[%d]: read: wanted %d got %d total %d", conn.index, len(b), n, conn.cntRecv) conn.transport.log.HexDump(LogTraceUSB, '<', b[:n]) if err != nil { conn.transport.log.Error('!', "USB[%d]: recv: %s", conn.index, err) } if n != 0 || err != nil { return n, err } conn.transport.log.Error('!', "USB[%d]: zero-size read", conn.index) time.Sleep(backoff) backoff *= 2 if backoff > time.Millisecond*1000 { backoff = time.Millisecond * 1000 } } } // Write to USB func (conn *usbConn) Write(b []byte) (int, error) { conn.transport.connstate.beginWrite(conn) defer conn.transport.connstate.doneWrite(conn) tm, expired := conn.timeout() if expired { return 0, ErrInitTimedOut } n, err := conn.iface.Send(b, tm) conn.cntSent += n conn.transport.log.Add(LogTraceHTTP, '>', "USB[%d]: write: wanted %d sent %d total %d", conn.index, len(b), n, conn.cntSent) conn.transport.log.HexDump(LogTraceUSB, '>', b[:n]) if err != nil { conn.transport.log.Error('!', "USB[%d]: send: %s", conn.index, err) } return n, err } // Allocate a connection func (transport *UsbTransport) usbConnGet(ctx context.Context) (*usbConn, error) { select { case <-transport.shutdown: return nil, ErrShutdown case <-ctx.Done(): return nil, ctx.Err() case conn := <-transport.connPool: transport.connstate.gotConn(conn) transport.log.Debug(' ', "USB[%d]: connection allocated, %s", conn.index, transport.connstate) return conn, nil } } // Release the connection func (conn *usbConn) put() { transport := conn.transport conn.reader.Reset(conn) conn.delayUntil = time.Now().Add(conn.delayInterval) conn.cntRecv = 0 conn.cntSent = 0 transport.connstate.putConn(conn) transport.log.Debug(' ', "USB[%d]: connection released, %s", conn.index, transport.connstate) transport.connPool <- conn select { case transport.connReleased <- struct{}{}: default: } } // Destroy USB connection func (conn *usbConn) destroy() { conn.transport.log.Debug(' ', "USB[%d]: closed", conn.index) conn.iface.Close() } // usbConnState tracks connections state, for logging type usbConnState struct { alloc []int32 // Per-connection "allocated" flag read []int32 // Per-connection "reading" flag write []int32 // Per-connection "writing" flag } // newUsbConnState creates a new usbConnState for given // number of connections func newUsbConnState(cnt int) *usbConnState { return &usbConnState{ alloc: make([]int32, cnt), read: make([]int32, cnt), write: make([]int32, cnt), } } // gotConn notifies usbConnState, that connection is allocated func (state *usbConnState) gotConn(conn *usbConn) { atomic.AddInt32(&state.alloc[conn.index], 1) } // putConn notifies usbConnState, that connection is released func (state *usbConnState) putConn(conn *usbConn) { atomic.AddInt32(&state.alloc[conn.index], -1) } // beginRead notifies usbConnState, that read is started func (state *usbConnState) beginRead(conn *usbConn) { atomic.AddInt32(&state.read[conn.index], 1) } // doneRead notifies usbConnState, that read is done func (state *usbConnState) doneRead(conn *usbConn) { atomic.AddInt32(&state.read[conn.index], -1) } // beginWrite notifies usbConnState, that write is started func (state *usbConnState) beginWrite(conn *usbConn) { atomic.AddInt32(&state.write[conn.index], 1) } // doneWrite notifies usbConnState, that write is done func (state *usbConnState) doneWrite(conn *usbConn) { atomic.AddInt32(&state.write[conn.index], -1) } // String returns a string, representing connections state func (state *usbConnState) String() string { buf := make([]byte, 0, 64) used := 0 for i := range state.alloc { a := atomic.LoadInt32(&state.alloc[i]) r := atomic.LoadInt32(&state.read[i]) w := atomic.LoadInt32(&state.write[i]) if len(buf) != 0 { buf = append(buf, ' ') } if a|r|w == 0 { buf = append(buf, '-', '-', '-') } else { used++ if a != 0 { buf = append(buf, 'a') } else { buf = append(buf, '-') } if r != 0 { buf = append(buf, 'r') } else { buf = append(buf, '-') } if w != 0 { buf = append(buf, 'w') } else { buf = append(buf, '-') } } } return fmt.Sprintf("%d in use: %s", used, buf) } ipp-usb-0.9.24/uuid.go000066400000000000000000000020511453365611700144730ustar00rootroot00000000000000/* 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.24/uuid_test.go000066400000000000000000000021561453365611700155400ustar00rootroot00000000000000/* 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) } } }