- commit
- 38452867fe097b470d00951fbe0c3ac194ca3322
- parent
- 81569ca7a740b022f6dd92a97818b77830ab5e12
- Author
- Tobias Bengfort <tobias.bengfort@posteo.de>
- Date
- 2026-05-24 14:34
add a simple socket service
Diffstat
| M | PKGBUILD | 1 | + |
| M | system/systemd.service | 1 | + |
| A | system/systemd.socket | 8 | ++++++++ |
| M | xikeyring/__main__.py | 12 | ++++++++++-- |
| M | xikeyring/pidfd.py | 16 | ++++++++++++++++ |
| A | xikeyring/socket_service.py | 114 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
6 files changed, 150 insertions, 2 deletions
diff --git a/PKGBUILD b/PKGBUILD
@@ -18,5 +18,6 @@ package() {
18 18 install -Dm 644 README.md "$pkgdir/usr/share/docs/xi-keyring/README.md"
19 19 install -Dm 644 system/dbus.service "$pkgdir/usr/share/dbus-1/services/org.xi.keyring.service"
20 20 install -Dm 644 system/systemd.service "$pkgdir/usr/lib/systemd/user/xi-keyring.service"
-1 21 install -Dm 644 system/systemd.socket "$pkgdir/usr/lib/systemd/user/xi-keyring.socket"
21 22 install -Dm 644 system/portal "$pkgdir/usr/share/xdg-desktop-portal/portals/xi-keyring.portal"
22 23 }
diff --git a/system/systemd.service b/system/systemd.service
@@ -1,6 +1,7 @@ 1 1 [Unit] 2 2 Description=xi keyring 3 3 PartOf=graphical-session.target -1 4 Requires=xi-keyring.socket 4 5 Requires=dbus.service 5 6 After=dbus.service 6 7
diff --git a/system/systemd.socket b/system/systemd.socket
@@ -0,0 +1,8 @@ -1 1 [Unit] -1 2 Description=xi keyring -1 3 -1 4 [Socket] -1 5 ListenStream=%t/xi.portal.Secret -1 6 -1 7 [Install] -1 8 WantedBy=sockets.target
diff --git a/xikeyring/__main__.py b/xikeyring/__main__.py
@@ -11,6 +11,7 @@ from .dbus import DBusService 11 11 from .dumpable import pr_set 12 12 from .keyring import KeyringProxy 13 13 from .keyring import write_bytes -1 14 from .socket_service import SocketService 14 15 15 16 16 17 def get_data_home(): @@ -45,6 +46,12 @@ def parse_args(): 45 46 parser.add_argument( 46 47 '--bus', '-b', help='bus name', default='org.freedesktop.secrets' 47 48 ) -1 49 parser.add_argument( -1 50 '--socket', -1 51 help='socket path', -1 52 type=Path, -1 53 default=None, -1 54 ) 48 55 return parser.parse_args() 49 56 50 57 @@ -66,5 +73,6 @@ elif args.action == 'change-password': 66 73 write_bytes(args.key, encrypted) 67 74 else: 68 75 with DBusService(keyring).own(args.bus):69 -1 loop = GLib.MainLoop()70 -1 loop.run()-1 76 with SocketService(keyring).listen(args.socket): -1 77 loop = GLib.MainLoop() -1 78 loop.run()
diff --git a/xikeyring/pidfd.py b/xikeyring/pidfd.py
@@ -1,12 +1,28 @@ 1 1 import selectors -1 2 import socket -1 3 import struct 2 4 from pathlib import Path 3 5 -1 6 try: -1 7 SO_PEERPIDFD = socket.SO_PEERPIDFD -1 8 except AttributeError: -1 9 SO_PEERPIDFD = 77 -1 10 4 11 5 12 class PID: 6 13 def __init__(self, pid: int, pidfd: int): 7 14 self.pid = pid 8 15 self.pidfd = pidfd 9 16 -1 17 @classmethod -1 18 def from_socket(cls, sock: socket.socket) -> 'PID': -1 19 cred = sock.getsockopt( -1 20 socket.SOL_SOCKET, socket.SO_PEERCRED, struct.calcsize('3i') -1 21 ) -1 22 pid, _uid, _gid = struct.unpack('3i', cred) -1 23 pidfd = sock.getsockopt(socket.SOL_SOCKET, SO_PEERPIDFD) -1 24 return cls(pid, pidfd) -1 25 10 26 def check_active(self) -> None: 11 27 with selectors.DefaultSelector() as sel: 12 28 sel.register(self.pidfd, selectors.EVENT_READ)
diff --git a/xikeyring/socket_service.py b/xikeyring/socket_service.py
@@ -0,0 +1,114 @@
-1 1 import json
-1 2 import socket
-1 3 import sys
-1 4 from contextlib import contextmanager
-1 5 from pathlib import Path
-1 6
-1 7 from gi.repository import GLib
-1 8
-1 9 from .keyring import AccessDeniedError
-1 10 from .keyring import Keyring
-1 11 from .keyring import NotFoundError
-1 12 from .pidfd import PID
-1 13
-1 14
-1 15 class AmbiguousError(Exception):
-1 16 pass
-1 17
-1 18
-1 19 def watch(sock: socket.socket, callback) -> int:
-1 20 sock.setblocking(False) # noqa
-1 21 chan = GLib.IOChannel.unix_new(sock.fileno())
-1 22 flags = GLib.IO_IN|GLib.IO_HUP|GLib.IO_ERR
-1 23 return GLib.io_add_watch(chan, flags, callback, sock)
-1 24
-1 25
-1 26 class Connection:
-1 27 def __init__(self, sock: socket.socket, keyring: Keyring):
-1 28 self.sock = sock
-1 29 self.keyring = keyring
-1 30 self.pid = PID.from_socket(sock)
-1 31 watch(sock, self.on_io)
-1 32
-1 33 def get_id(self, query):
-1 34 ids = self.keyring.search_items(self.pid, query)
-1 35 if not ids:
-1 36 raise NotFoundError
-1 37 if len(ids) > 1:
-1 38 raise AmbiguousError
-1 39 return ids[0]
-1 40
-1 41 def on_msg(self, msg):
-1 42 if msg['method'] == 'get':
-1 43 id = self.get_id(msg['query'])
-1 44 secret = self.keyring.get_secret(self.pid, id)
-1 45 return {'secret': secret.decode('utf-8')}
-1 46 elif msg['method'] == 'set':
-1 47 secret = msg['secret'].encode('utf-8')
-1 48 try:
-1 49 id = self.get_id(msg['query'])
-1 50 self.keyring.update_secret(self.pid, id, secret)
-1 51 except NotFoundError:
-1 52 self.keyring.create_item(self.pid, msg['query'], secret)
-1 53 return {}
-1 54 elif msg['method'] == 'del':
-1 55 id = self.get_id(msg['query'])
-1 56 self.keyring.delete_item(self.pid, id)
-1 57 return {}
-1 58 else:
-1 59 return {'error': 'invalid request'}
-1 60
-1 61 def on_io(self, source, flags, sock):
-1 62 if flags & GLib.IO_ERR or flags & GLib.IO_HUP:
-1 63 sock.close()
-1 64 return False
-1 65 chunk = sock.recv(1024)
-1 66 if chunk:
-1 67 # TODO buffer
-1 68 try:
-1 69 msg = json.loads(chunk)
-1 70 reply = self.on_msg(msg)
-1 71 except AccessDeniedError:
-1 72 reply = {'error': 'access denied'}
-1 73 except NotFoundError:
-1 74 reply = {'error': 'not found'}
-1 75 except AmbiguousError:
-1 76 reply = {'error': 'ambiguous'}
-1 77 except Exception as e:
-1 78 print(e, file=sys.stderr)
-1 79 reply = {'error': 'server error'}
-1 80 sock.send(json.dumps(reply).encode('utf-8'))
-1 81 return True
-1 82 else:
-1 83 sock.close()
-1 84 return False
-1 85
-1 86
-1 87 class SocketService:
-1 88 def __init__(self, keyring: Keyring):
-1 89 self.keyring = keyring
-1 90
-1 91 def on_con(self, source, flags, sock: socket.socket):
-1 92 if flags & GLib.IO_ERR or flags & GLib.IO_HUP:
-1 93 return False
-1 94 conn, _addr = sock.accept()
-1 95 Connection(conn, self.keyring)
-1 96 return True
-1 97
-1 98 @contextmanager
-1 99 def listen(self, path: Path | None):
-1 100 if path is None: # systemd socket activation
-1 101 with socket.fromfd(3, socket.AF_INET, socket.SOCK_STREAM) as sock:
-1 102 watch(sock, self.on_con)
-1 103 yield
-1 104 else:
-1 105 sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
-1 106 sock.bind(str(path))
-1 107 sock.listen()
-1 108 print(f'listening on {path}')
-1 109 try:
-1 110 watch(sock, self.on_con)
-1 111 yield
-1 112 finally:
-1 113 sock.close()
-1 114 path.unlink()