The problem
Previously, I wrote about using named pipes for IPC to
allow controlling a process by another process running on the same
computer possibly as a different user, with the access control set
by file permissions. But I observed that the restricted unidirectional
communication mechanism limited how useful it could be, suggesting
another design might be better in settings where bidirectional communication
including confirmation of commands may be useful.
Is there a good general solution to this problem without losing the
convenience of access control via file permissions?
The solution
Let's use everyone's favorite RPC mechanism: HTTP. But
HTTP normally runs over TCP, and even if we bind to
localhost
, the HTTP server would still be accessible to
any user on the same computer and require selecting a port number that's
not already in use. Instead, we can bind the HTTP server to a
Unix socket, which similar to named pipes, look a lot like
a file, but allow communication like a network socket.
Python's built-in HTTP server doesn't directly support binding to a Unix
socket, but the following is slightly modified from
an example I found of how to get it to:
import http.server
import json
import os
import socket
import sys
import traceback
def process_cmd(cmd, *args):
print(f"In process_cmd({cmd}, {args})...")
class HTTPHandler(http.server.BaseHTTPRequestHandler):
def do_POST(self):
size = int(self.headers.get('Content-Length', 0))
body = self.rfile.read(size)
args = json.loads(body) if body else []
try:
result = process_cmd(self.path[1:], *args)
self.send(200, result or 'Success')
except Exception:
self.send(500, str(traceback.format_exc()))
def do_GET(self):
self.do_POST()
def send(self, code, reply):
# avoid exception in server.py address_string()
self.client_address = ('',)
self.send_response(code)
self.end_headers()
self.wfile.write(reply.encode('utf-8'))
sock_file = sys.argv[1]
try:
os.remove(sock_file)
except OSError:
pass
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.bind(sock_file)
sock.listen(0)
server = http.server.HTTPServer(sock_file, HTTPHandler,
False)
server.socket = sock
server.serve_forever()
sock.shutdown(socket.SHUT_RDWR)
sock.close()
os.remove(sock_file)
Then you can query the server using curl
:
$ ./server.py http.socket &
$ curl --unix-socket http.socket http://anyhostname/foo
[GET response...]
$ curl --unix-socket http.socket http://anyhostname/foo \
--json '["some", "args"]'
[POST reponse...]
or in Python, using requests-unixsocket
:
import requests_unixsocket
session = requests_unixsocket.Session()
host = "http+unix//http.socket/"
r = session.get(host + "foo")
# Inspect r.status_code and r.text or r.json()
r = session.post(host + "foo", json=["some", "args"])
# Inspect r.status_code and r.text or r.json()
The details