modules/python-http-handlers: init #146

Open
oysteikt wants to merge 2 commits from python-http-handlers into main
3 changed files with 257 additions and 69 deletions
+3 -1
View File
@@ -173,6 +173,7 @@
sops-nix.nixosModules.sops
inputs.roowho2.nixosModules.default
self.nixosModules.rsync-pull-targets
self.nixosModules.python-http-handlers
])
++ modules;
}
@@ -306,13 +307,14 @@
nixosModules = {
bluemap = ./modules/bluemap.nix;
drumknotty = ./modules/drumknotty;
gickup = ./modules/gickup;
matrix-ooye = ./modules/matrix-ooye.nix;
python-http-handlers = ./modules/python-http-handlers.nix;
robots-txt = ./modules/robots-txt.nix;
rsync-pull-targets = ./modules/rsync-pull-targets.nix;
snakeoil-certs = ./modules/snakeoil-certs.nix;
snappymail = ./modules/snappymail.nix;
drumknotty = ./modules/drumknotty;
};
devShells = forAllSystems (system: {
@@ -1,78 +1,40 @@
{ lib, pkgs, ... }:
{
systemd.sockets."gatus-minecraft-checker" = {
wantedBy = [ "sockets.target" ];
services.python-http-handlers."gatus-minecraft-checker" = {
listenStreams = [ "127.0.0.1:1337" ];
socketConfig = {
Accept = false;
};
};
libraries = with pkgs.python3Packages; [
mcstatus
];
handler = ''
import json
from mcstatus import JavaServer
systemd.services."gatus-minecraft-checker" = {
serviceConfig = {
ExecStart = lib.getExe (pkgs.writers.writePython3Bin "gatus-minecraft-checker" {
libraries = with pkgs.python3Packages; [
# sdnotify
mcstatus
];
flakeIgnore = [ "E501" "E704" ];
} ''
import socket
import json
from http.server import HTTPServer, BaseHTTPRequestHandler
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
try:
mcserver = JavaServer.lookup("minecraft.pvv.ntnu.no")
status = mcserver.status()
from mcstatus import JavaServer
body = {
"ok": True,
"players": status.players.online,
"version": status.version.name,
"latency": status.latency,
}
data = json.dumps(body).encode()
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
try:
mcserver = JavaServer.lookup("minecraft.pvv.ntnu.no")
status = mcserver.status()
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
body = {
"ok": True,
"players": status.players.online,
"version": status.version.name,
"latency": status.latency,
}
data = json.dumps(body).encode()
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
except Exception as e:
data = json.dumps({"ok": False, "error": str(e)}).encode()
self.send_response(500)
self.end_headers()
self.wfile.write(data)
class NoBindHTTPServer(HTTPServer):
def server_bind(): pass
def server_activate(): pass
def main():
httpd = NoBindHTTPServer(
("", 0),
Handler,
bind_and_activate=False,
)
httpd.socket = socket.fromfd(3, socket.AF_INET, socket.SOCK_STREAM)
httpd.serve_forever()
if __name__ == '__main__':
main()
'');
NoNewPrivileges = true;
PrivateTmp = true;
};
except Exception as e:
data = json.dumps({"ok": False, "error": str(e)}).encode()
self.send_response(500)
self.end_headers()
self.wfile.write(data)
'';
};
}
+224
View File
@@ -0,0 +1,224 @@
{ config, options, lib, pkgs, utils, ... }:
let
cfg = config.services.python-http-handlers;
in
{
options.services.python-http-handlers = lib.mkOption {
default = { };
type = lib.types.attrsOf (lib.types.submodule ({ name, ... }: {
options = {
enable = lib.mkEnableOption "" // {
default = true;
description = "Whether to enable this python HTTP handler.";
};
name = lib.mkOption {
type = lib.types.str;
default = "phh-${name}";
Review

Consider using a more descriptive name to help troubleshooting and make it more immediately identifyable, e.g. `gatus-http-handler-${name}" or something

Consider using a more descriptive name to help troubleshooting and make it more immediately identifyable, e.g. `gatus-http-handler-${name}" or something
defaultText = "phh-<name>";
description = ''
The name of this HTTP handler.
This will be used for the systemd service among other things.
'';
};
listenStreams = lib.mkOption {
type = with lib.types; listOf str;
default = [
"/run/phh-${name}.socket"
Review

Does this ignore the name option above, and use this naming scheme anyways? If so, why? And if not, will this become /run/phh-phh-name.socket when using the default value in the name option above?

Does this ignore the name option above, and use this naming scheme anyways? If so, why? And if not, will this become `/run/phh-phh-name.socket` when using the default value in the name option above?
];
defaultText = lib.literalExpression ''
[
"/run/phh-<name>.socket"
]
'';
description = ''
For each item in this list, a `ListenStream`
option in the `[Socket]` section will be created.
'';
};
socketConfig = lib.mkOption {
type = utils.systemdUtils.unitOptions.unitOption;
default = { };
description = ''
Config for the systemd socket's `[Socket]` section.
See {manpage}`systemd.socket(5)` for details.
'';
};
serviceConfig = lib.mkOption {
type = utils.systemdUtils.unitOptions.unitOption;
default = { };
description = ''
Config for the systemd service's `[Service]` section.
See {manpage}`systemd.exec(5)` and {manpage}`systemd.service(5)` for details.
'';
};
enableDefaultHardening = lib.mkEnableOption "" // {
Review

Should this option exist? If it is not there, users are incentivized to fix the specific option(s) that they are having problems with, rather than just disabling everything

Should this option exist? If it is not there, users are incentivized to fix the specific option(s) that they are having problems with, rather than just disabling everything
default = true;
description = ''
Whether to enable a set of recommended systemd hardening directives.
If this breaks the service, you can either override the offending directives
through {option}`serviceConfig`, or disable this option altogether.
'';
};
libraries = lib.mkOption {
type = with lib.types; listOf package;
default = [ ];
example = lib.literalExpression ''
with pkgs.python3Packages; [
matplotlib
numpy
pillow
]
'';
description = ''
Extra python libraries to make available to the service.
'';
};
flakeIgnore = lib.mkOption {
type = with lib.types; listOf str;
default = [
"E111" # indentation is not a multiple of four
"E201" # whitespace after (
"E202" # whitespace after )
"E203" # whitespace before :
"E211" # whitespace before (
"E251" # unexpected spaces around keyword / parameter equals
"E301" # expected 1 blank line, found 0
"E302" # expected 2 blank lines, found 0
"E303" # too many blank lines
"E305" # expected 2 blank lines after end of function or class
"E306" # expected 1 blank line before a nested definition
"E501" # max line length
"E704" # multiple statements on one line (def)
];
description = ''
A list of flake8 rules to ignore while linting the python code.
An opinionated list of bogus rules (according to module author) is provided as default.
Review

👀

👀
'';
};
handler = lib.mkOption {
type = lib.types.lines;
default = ''
import json
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
body = {"ok": True}
data = json.dumps(body).encode()
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
'';
description = ''
Python code including the HTTP handler for the server.
You can include as many imports and functions as you'd like here, but you will need
to at least provide a `class Handler(BaseHTTPRequestHandler)` to handle incoming HTTP requests.
'';
};
};
}));
};
config = {
systemd.sockets = lib.pipe cfg [
lib.attrValues
(lib.filter (v: v.enable))
(vs: lib.genAttrs' vs (v: {
inherit (v) name;
value = {
wantedBy = [ "sockets.target" ];
inherit (v) listenStreams;
socketConfig = {
Accept = false;
} // v.socketConfig;
};
}))
];
systemd.services = lib.pipe cfg [
lib.attrValues
(lib.filter (v: v.enable))
(vs: lib.genAttrs' vs (v: {
inherit (v) name;
value = {
serviceConfig = {
Type = "simple";
DynamicUser = true;
ExecStart = let
package = pkgs.writers.writePython3Bin "${v.name}-bin" {
inherit (v) libraries flakeIgnore;
} ''
import socket
from http.server import HTTPServer, BaseHTTPRequestHandler
${v.handler}
class NoBindHTTPServer(HTTPServer):
def server_bind(): pass
def server_activate(): pass
def main():
httpd = NoBindHTTPServer(
("", 0),
Handler,
bind_and_activate=False,
)
httpd.socket = socket.fromfd(3, socket.AF_INET, socket.SOCK_STREAM)
httpd.serve_forever()
if __name__ == '__main__':
main()
'';
in lib.getExe package;
} // (lib.optionalAttrs v.enableDefaultHardening {
AmbientCapabilities = [ "" ];
CapabilityBoundingSet = [ "" ];
DeviceAllow = [ "" ];
LockPersonality = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateTmp = true;
ProcSubset = "pid";
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
ProtectSystem = "strict";
RemoveIPC = true;
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SocketBindDeny = "any";
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
"~@privileged"
"~@resources"
];
UMask = "0077";
}) // v.serviceConfig;
};
}))
];
};
}