diff --git a/.gitignore b/.gitignore index b6e4761..0e75bdf 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ __pycache__/ *.so # Distribution / packaging +.idea .Python build/ develop-eggs/ diff --git a/remote_evdev/__init__.py b/remote_evdev/__init__.py new file mode 100755 index 0000000..ea6e444 --- /dev/null +++ b/remote_evdev/__init__.py @@ -0,0 +1,5 @@ +#!/usr/bin/env python3 + +from .config import get_config +from .stream import get_streams +from .stream import handle_client diff --git a/remote_evdev/config/__init__.py b/remote_evdev/config/__init__.py new file mode 100644 index 0000000..39d111d --- /dev/null +++ b/remote_evdev/config/__init__.py @@ -0,0 +1,75 @@ +from dataclasses import dataclass +import sys +from os import path + + +@dataclass +class DeviceInfo: + path: str + device_type: str + + +@dataclass +class NetConfig: + is_server: bool + is_host: bool + port: int + ip_address: str + + +def get_config() -> tuple[NetConfig, list[DeviceInfo]]: + args = sys.argv + + cfg = NetConfig( + is_server=False, + is_host=True, + port=64654, + ip_address="auto" + ) + devices_info = [] + + n = 1 + while n < len(args): + match args[n]: + case "-s" | "--server": cfg.is_server = True + case "-c" | "--client": cfg.is_server = False + case "-h" | "--host": cfg.is_host = True + case "-g" | "--guest": cfg.is_host = False + case "-p" | "--port": + n += 1 + try: + cfg.port = int(args[n]) + except ValueError: + raise ValueError(f"Port must be a integer, not {args[n]}") + case "-a" | "--ip-address": + cfg.ip_address = args[n := n+1] + case "-d" | "--device": + devices_info.append(DeviceInfo( + path="", + device_type="other" + )) + case _: + key = "path" + value = "" + match args[n]: + case "--id": value = "/dev/input/by-id/" + case "--path": value = "/dev/input/by-path/" + case "--event": value = "/dev/input/" + case "--full-path": pass + case "--type": + match args[n+1]: + case "pointer" | "keyboard": key = "type" + case _: raise ValueError(f"Invalid device type {args[n+1]}") + n += 1 + match key: + case "path": + value = f"{value}{args[n]}" + if path.exists(value): + devices_info[-1].path = value + case "type": devices_info[-1].device_type = args[n] + n += 1 + + if cfg.ip_address == "auto": + raise ValueError(f"Auto IP address not implemented yet") + + return cfg, devices_info diff --git a/remote_evdev/stream/__init__.py b/remote_evdev/stream/__init__.py new file mode 100644 index 0000000..bc4b3ac --- /dev/null +++ b/remote_evdev/stream/__init__.py @@ -0,0 +1,30 @@ +from socket import socket, AF_INET, SOCK_STREAM, SOL_SOCKET, SO_REUSEADDR +from typing import Generator + +from . import guest +from . import host +from ..config import NetConfig, DeviceInfo + + +def get_streams(cfg: NetConfig) -> Generator[socket, None, None]: + while True: + with socket(AF_INET, SOCK_STREAM) as s: + s.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) + if cfg.is_server: + s.bind((cfg.ip_address, cfg.port)) + s.listen() + while True: + conn, addr = s.accept() + with conn: + print(f"Connection established to {addr}") + yield conn + else: + s.connect((cfg.ip_address, cfg.port)) + yield s + + +def handle_client(s: socket, cfg: NetConfig, devices_info: list[DeviceInfo]): + if cfg.is_host: + host.handle_client(s, devices_info) + else: + guest.handle_client(s) diff --git a/remote_evdev/stream/guest.py b/remote_evdev/stream/guest.py new file mode 100644 index 0000000..c9ddbd4 --- /dev/null +++ b/remote_evdev/stream/guest.py @@ -0,0 +1,36 @@ +from evdev import UInput +from socket import socket +import json +from typing import Generator + + +def receive_devices(s: socket) -> Generator[tuple[int, UInput], None, None]: + while buf_size := int.from_bytes(s.recv(4), byteorder='big'): + fd = int.from_bytes(s.recv(4), byteorder='big') + cap = json.loads(s.recv(buf_size).decode()) + cap = {int(key): cap[key] for key in cap} + del cap[0] + yield fd, UInput(cap, name=f"web-evdev-device-fd{fd}") + + +def receive_event(s: socket) -> tuple[int, tuple[int, int, int]]: + fd = int.from_bytes(s.recv(4), byteorder='big') + event_type = int.from_bytes(s.recv(4), byteorder='big') + code = int.from_bytes(s.recv(4), byteorder='big') + value = int.from_bytes(s.recv(8), byteorder='big', signed=True) + return fd, (event_type, code, value) + + +def handle_client(s: socket): + devices = {} + for fd, device in receive_devices(s): + devices[fd] = device + + while True: + fd, event = receive_event(s) + if fd == 4294967295: + break + elif event[0] == 0: + devices[fd].syn() + else: + devices[fd].write(*event) \ No newline at end of file diff --git a/remote_evdev/stream/host.py b/remote_evdev/stream/host.py new file mode 100644 index 0000000..6015503 --- /dev/null +++ b/remote_evdev/stream/host.py @@ -0,0 +1,38 @@ +from socket import socket +from evdev import InputDevice, InputEvent +from select import select +import json + +from ..config import DeviceInfo + + +def send_device(s: socket, device: InputDevice): + buf = json.dumps(device.capabilities()).encode() + s.sendall(len(buf).to_bytes(4, byteorder='big')) + s.sendall(device.fd.to_bytes(4, byteorder='big')) + s.sendall(buf) + + +def send_input_event(s: socket, fd: int, input_event: InputEvent): + s.sendall(fd.to_bytes(4, byteorder='big')) + s.sendall(input_event.type.to_bytes(4, byteorder='big')) + s.sendall(input_event.code.to_bytes(4, byteorder='big')) + s.sendall(input_event.value.to_bytes(8, byteorder='big', signed=True)) + + +def handle_client(s: socket, devices_info: list[DeviceInfo]): + devices = [] + for device_info in devices_info: + devices.append(InputDevice(device_info.path)) + devices[-1].grab() + send_device(s, devices[-1]) + s.sendall((0).to_bytes(4, byteorder='big')) + devices = {device.fd: device for device in devices} + while True: + r, w, x = select(devices, [], []) + for fd in r: + for event in devices[fd].read(): + send_input_event(s, fd, event) + s.sendall(b'\xff'*20) + for device in devices: + device.ungrab() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..ded4463 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +setuptools +evdev \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..2c78f98 --- /dev/null +++ b/setup.py @@ -0,0 +1,12 @@ +from setuptools import setup + +setup( + name='remote-evdev', + version='1.0', + packages=['remote_evdev', 'remote_evdev.config', 'remote_evdev.stream'], + url='https://github.com/Surferlul/remote-evdev', + license='GNU General Public License v2.0', + author='lu', + author_email='lukasabaumann@gmail.com', + description='Share evdev devices (unix input devices) over network' +)