Skip to content

Latest commit

 

History

14 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

hidws - WebSocket/USB HID gateway

hidws is a lightweight C daemon that bridges WebSocket clients to USB HID devices, letting web apps talk to HID devices remotely over the network without direct USB access.

It also ships hid-list, a small diagnostic tool that lists all USB HID devices and probes the reports they support.

Both programs link against the hidapi libusb backend and do not require kernel HID support (/dev/hidraw*), so they work even on systems without a HID/INPUT kernel subsystem.

Prerequisites (Ubuntu/Debian)

sudo apt install libhidapi-dev libwebsockets-dev

The programs link with -lhidapi-libusb at build time. libhidapi-dev must ship the precompiled libhidapi-libusb.so backend (usbfs, libusb).

USB device permissions

To read/write HID devices as a regular user:

1. Add user to plugdev group

sudo usermod -aG plugdev $USER

Log out and back in for the change to take effect.

2. Udev rules

Create /etc/udev/rules.d/99-hid.rules:

# Permissions for HID and USB devices (covers both hidraw and libusb backends)
SUBSYSTEM=="hidraw", MODE="0666"
SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", MODE="0664", GROUP="plugdev"

Then reload:

sudo udevadm control --reload-rules && sudo udevadm trigger

Build & Install

make
sudo make install          # PREFIX=/usr/local

This builds both hidws and hid-list.

MIPS/uClibc note: on the Freetz-EVO MIPS toolchain (GCC 13.4.0, uClibc-ng, -march=34kc -msoft-float), compiling hidws with -O1 or higher miscompiles the reader thread and crashes with a NULL deref inside hid_read_timeout (only -O0 is stable there). On other platforms the default flags are fine. If you hit a Segmentation fault right after [hid] Reader thread started, rebuild with -O0.

Usage

hidws - WebSocket/HID bridge

./hidws [port] [--cert FILE] [--key FILE] [--no-ssl] [--config FILE] [--token SECRET] [--user USER --password PASS] [--allow ip/cidr,...]

Default port is 9001. Point your WebSocket client to ws://<host>:9001. The HID device is released automatically when the client disconnects.

Credentials and the IP allowlist may also be provided via the config file (--config FILE / $HIDWS_CONFIG) or the environment variables HIDWS_TOKEN, HIDWS_USER, HIDWS_PASSWORD, HIDWS_ALLOW — see Access control.

TLS / WSS support (same port as WS)

By default (./hidws [port]) hidws serves both plain ws:// and encrypted wss:// on the same port, using a temporary self-signed certificate kept only in memory (no file, no disk writes, regenerated on every start). A warning is printed to the log and shown on the diagnostic page.

./hidws 9001                      # ws:// + wss://, temporary in-memory cert
./hidws 9001 --no-ssl             # ws:// only (no TLS at all)
./hidws 9001 --cert server.crt --key server.key   # persistent cert
  • --cert FILE [--key FILE]: use a persistent certificate. If the cert file does not exist yet and hidws was built with SSL support, a self-signed certificate/key pair is generated automatically at first start (RSA-2048, valid 10 years, CN/SAN fritz.box). --key is optional: if omitted, it is derived from the cert path by replacing the extension with .key (server.crt -> server.key).
  • --no-ssl: force plain ws:// only (no TLS). Useful to keep an explicit "no TLS" mode, e.g. from an init script.
  • Both schemes are served by the same listening socket, so ws://<host>:9001 and wss://<host>:9001 work side by side. This is handy on HTTPS-hosted web apps (e.g. GitHub Pages) which block plain ws:// connections: the app can fall back to wss://.

Note: the self-signed certificate is not trusted by clients, so browsers must be told to accept it (a one-time warning) or the app must be configured to allow self-signed certificates. With a temporary in-memory cert the exception is re-asked on every service restart; with a persistent cert (--cert) the exception stays valid across restarts.

Access control (optional)

All access control is disabled by default.

Credentials and the IP allowlist can be supplied through three sources (lowest to highest precedence):

  1. Config file — --config FILE (or the HIDWS_CONFIG environment variable) reads a simple key=value file (# comments allowed):

    # /etc/hidws.conf
    token=mysecret
    user=alice
    password=s3cret
    allow=192.168.178.0/24,10.0.0.0/8
  2. Environment variables — HIDWS_TOKEN, HIDWS_USER, HIDWS_PASSWORD, HIDWS_ALLOW (comma-separated allowlist):

    HIDWS_TOKEN=mysecret ./hidws 9001
    HIDWS_USER=alice HIDWS_PASSWORD=s3cret ./hidws 9001
    HIDWS_ALLOW=192.168.178.0/24,10.0.0.0/8 ./hidws 9001
  3. Command line — overrides the previous sources:

    ./hidws 9001 --token mysecret                # bearer token
    ./hidws 9001 --user alice --password s3cret  # user/password
    ./hidws 9001 --config /etc/hidws.conf

Prefer a config file or environment variables over command-line options so the secrets don't show up in ps output or shell history.

Token / user+password

./hidws 9001 --token mysecret                # bearer token
./hidws 9001 --user alice --password s3cret  # user/password

When any credential is configured, every client session starts unauthenticated: all HID commands (list, open, send_report, ...) are answered with {"type":"error","message":"not authenticated"} until the client authenticates with one of the following methods:

  1. Application-level (recommended for browser clients; no credential in the URL):

    {"cmd":"auth","token":"mysecret"}
    {"cmd":"auth","user":"alice","password":"s3cret"}

    Success → {"type":"auth_ok"}. Failure → {"type":"error","message":"auth failed"}; after 5 consecutive failures the connection is closed (brute-force guard).

  2. ?token= in the WebSocket URL (handy for quick manual use; the token travels in the request line):

    ws://<host>:9001/?token=mysecret
    wss://<host>:9001/?token=mysecret
    
  3. Authorization header (for non-browser clients, e.g. curl/scripts):

    Authorization: Bearer mysecret
    Authorization: Basic base64(alice:s3cret)
    

If no credential is configured, an auth command is accepted idempotently (always answered auth_ok), so clients that always send it keep working on both open and protected servers.

IP allowlist

./hidws 9001 --allow 192.168.178.0/24,10.0.0.0/8
./hidws 9001 --allow 192.168.178.2
./hidws 9001 --config /etc/hidws.conf       # allow= in the file
HIDWS_ALLOW=192.168.178.0/24 ./hidws 9001   # or env var

--allow takes a comma-separated list of IPv4 addresses and/or CIDR prefixes. Connections from any other address are dropped before the HTTP/WebSocket handshake. With no --allow, every address is accepted.

For exposure beyond the LAN, use wss:// (--cert) on top of the token: without TLS the token travels in clear text.

Diagnostic page (http/https on the same port)

Opening http://<host>:<port>/ or https://<host>:<port>/ in a browser serves a small diagnostic page that confirms the daemon is reachable and shows version, port and the available WebSocket endpoints, plus "Test ws://" / "Test wss://" buttons that open a real WebSocket to the server and report success/failure. When a temporary in-memory certificate is in use, the page shows a warning banner about it.

This is the convenient way to confirm access and (for wss://) to accept the browser's one-time security exception for the self-signed certificate: visit https://<host>:9001/, accept the warning, then reload the page and press "Test wss://".

hid-list - enumerate HID devices

./hid-list

Prints every USB HID device (path, VID/PID, manufacturer, product, serial) and probes its feature and input reports.

Wire Protocol

All messages are JSON over WebSocket.

Client -> Server

Command Payload
auth {"cmd":"auth","token":SECRET} or {"cmd":"auth","user":U,"password":P}
list {"cmd":"list"}
open {"cmd":"open","vendorId":<int>,"productId":<int>}
send_report {"cmd":"send_report","reportId":<int>,"data":[<bytes>]}
send_feature_report {"cmd":"send_feature_report","reportId":<int>,"data":[<bytes>]}
close {"cmd":"close"}

Server -> Client

Type Payload
auth_ok {"type":"auth_ok"}
device_list {"type":"device_list","devices":[{"vendorId":...,"productId":...,"productName":"..."}]}
opened {"type":"opened","vendorId":...,"productId":...,"productName":"...","usagePage":...,"usage":...,"interfaceNumber":...,"busType":...,"releaseNumber":...,"serialNumber":"...","reportDescriptor":[...]}
input_report {"type":"input_report","reportId":<int>,"data":[<bytes>]}
ok {"type":"ok"}
error {"type":"error","message":"..."}
closed {"type":"closed"}

opened also carries the hidapi device info (usagePage, usage, interfaceNumber, busType, releaseNumber, serialNumber) and the raw HID report descriptor as a byte array (reportDescriptor), so clients can render the same report collections (input/output/feature with report IDs and items) as a local WebHID connection.

Examples of Web Applications using hidws

Notes

  • Access control (--token/--user/--password/--allow, env vars HIDWS_TOKEN/HIDWS_USER/HIDWS_PASSWORD/HIDWS_ALLOW, or a config file with token=/user=/password=/allow=) is off by default; it only activates when at least one credential is configured.
  • The KT02H20 family (FiiO JA11 etc.) uses OUTPUT reports; frontends must send data with send_report (report ID 0x02), not feature reports.
  • input_report data received over WebSocket includes the report-ID byte as the first element for numbered input reports; WebHID strips it, so remote frontends must strip it to match.

License

European Union Public Licence 1.2 (EUPL-1.2). See LICENSE.

About

WebSocket - HID bridge

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages