diff options
author | Ismaël Bouya <ismael.bouya@normalesup.org> | 2021-09-14 02:28:09 +0200 |
---|---|---|
committer | Ismaël Bouya <ismael.bouya@normalesup.org> | 2021-09-30 00:36:14 +0200 |
commit | a9f52ec521e45204ad9363dd143b32ac9910b6b3 (patch) | |
tree | 6257e8385c240890e1f8c443e8aa886de09ad523 /flakes | |
parent | e4e0de77cd6c9882fa7ff7c3cdd0ed9fce8a59d8 (diff) | |
download | Nix-a9f52ec521e45204ad9363dd143b32ac9910b6b3.tar.gz Nix-a9f52ec521e45204ad9363dd143b32ac9910b6b3.tar.zst Nix-a9f52ec521e45204ad9363dd143b32ac9910b6b3.zip |
Add flask app paste
Diffstat (limited to 'flakes')
-rw-r--r-- | flakes/paste/flake.lock | 42 | ||||
-rw-r--r-- | flakes/paste/flake.nix | 138 | ||||
-rw-r--r-- | flakes/paste/paste/paste.py | 124 | ||||
-rw-r--r-- | flakes/private/paste/flake.lock | 72 | ||||
-rw-r--r-- | flakes/private/paste/flake.nix | 23 |
5 files changed, 399 insertions, 0 deletions
diff --git a/flakes/paste/flake.lock b/flakes/paste/flake.lock new file mode 100644 index 0000000..559e64b --- /dev/null +++ b/flakes/paste/flake.lock | |||
@@ -0,0 +1,42 @@ | |||
1 | { | ||
2 | "nodes": { | ||
3 | "flake-utils": { | ||
4 | "locked": { | ||
5 | "lastModified": 1631561581, | ||
6 | "narHash": "sha256-3VQMV5zvxaVLvqqUrNz3iJelLw30mIVSfZmAaauM3dA=", | ||
7 | "owner": "numtide", | ||
8 | "repo": "flake-utils", | ||
9 | "rev": "7e5bf3925f6fbdfaf50a2a7ca0be2879c4261d19", | ||
10 | "type": "github" | ||
11 | }, | ||
12 | "original": { | ||
13 | "owner": "numtide", | ||
14 | "repo": "flake-utils", | ||
15 | "type": "github" | ||
16 | } | ||
17 | }, | ||
18 | "nixpkgs": { | ||
19 | "locked": { | ||
20 | "lastModified": 1631570365, | ||
21 | "narHash": "sha256-vc6bfo0hijpicdUDiui2DvZXmpIP2iqOFZRcpMOuYPo=", | ||
22 | "owner": "NixOS", | ||
23 | "repo": "nixpkgs", | ||
24 | "rev": "df7113c0727881519248d4c7d080324e0ee3327b", | ||
25 | "type": "github" | ||
26 | }, | ||
27 | "original": { | ||
28 | "owner": "NixOS", | ||
29 | "repo": "nixpkgs", | ||
30 | "type": "github" | ||
31 | } | ||
32 | }, | ||
33 | "root": { | ||
34 | "inputs": { | ||
35 | "flake-utils": "flake-utils", | ||
36 | "nixpkgs": "nixpkgs" | ||
37 | } | ||
38 | } | ||
39 | }, | ||
40 | "root": "root", | ||
41 | "version": 7 | ||
42 | } | ||
diff --git a/flakes/paste/flake.nix b/flakes/paste/flake.nix new file mode 100644 index 0000000..08d0681 --- /dev/null +++ b/flakes/paste/flake.nix | |||
@@ -0,0 +1,138 @@ | |||
1 | { | ||
2 | inputs.flake-utils.url = "github:numtide/flake-utils"; | ||
3 | inputs.nixpkgs.url = "github:NixOS/nixpkgs"; | ||
4 | |||
5 | description = "Pastebin-like service"; | ||
6 | |||
7 | outputs = { self, flake-utils, nixpkgs }: flake-utils.lib.eachDefaultSystem (system: | ||
8 | let | ||
9 | pkgs = import nixpkgs { inherit system; overlays = []; }; | ||
10 | in rec { | ||
11 | hydraJobs = checks; | ||
12 | checks = pkgs.lib.optionalAttrs (builtins.elem system pkgs.lib.systems.doubles.linux) { | ||
13 | test = | ||
14 | let testing = import (nixpkgs + "/nixos/lib/testing-python.nix") { inherit system; }; | ||
15 | in testing.makeTest { | ||
16 | nodes = { | ||
17 | server = { pkgs, ... }: { | ||
18 | imports = [ self.nixosModule ]; | ||
19 | config = { | ||
20 | environment.systemPackages = [ pkgs.curl ]; | ||
21 | services.httpd = { | ||
22 | enable = true; | ||
23 | adminAddr = "foo@example.org"; | ||
24 | extraConfig = '' | ||
25 | ProxyPass / unix:///run/paste/gunicorn.sock|http://localhost/ | ||
26 | ProxyPassReverse / unix:///run/paste/gunicorn.sock|http://localhost/ | ||
27 | ''; | ||
28 | }; | ||
29 | services.paste.enable = true; | ||
30 | }; | ||
31 | }; | ||
32 | }; | ||
33 | testScript = '' | ||
34 | start_all() | ||
35 | server.wait_for_unit("httpd.service") | ||
36 | server.wait_for_unit("paste.service") | ||
37 | server.wait_until_succeeds("[ -S /run/paste/gunicorn.sock ]", 10) | ||
38 | server.succeed("curl -f http://localhost/") | ||
39 | server.succeed("curl -f http://localhost/ | grep -q 'Get the source'") | ||
40 | ''; | ||
41 | }; | ||
42 | }; | ||
43 | }) // rec { | ||
44 | nixosModule = { config, lib, pkgs, ... }: | ||
45 | let | ||
46 | cfg = config.services.paste; | ||
47 | in { | ||
48 | options = { | ||
49 | services.paste = { | ||
50 | enable = lib.mkOption { | ||
51 | type = lib.types.bool; | ||
52 | default = false; | ||
53 | description = "Whether to enable the pastebin service"; | ||
54 | }; | ||
55 | |||
56 | dataDir = lib.mkOption { | ||
57 | type = lib.types.path; | ||
58 | default = "/var/lib/paste"; | ||
59 | description = '' | ||
60 | The directory where Paste stores its data. | ||
61 | ''; | ||
62 | }; | ||
63 | |||
64 | socketsDir = lib.mkOption { | ||
65 | type = lib.types.str; | ||
66 | default = "/run/paste"; | ||
67 | description = "Socket which is used for communication with Paste."; | ||
68 | }; | ||
69 | |||
70 | webDirectory = lib.mkOption { | ||
71 | type = lib.types.nullOr lib.types.str; | ||
72 | default = null; | ||
73 | description = "Subdirectory url to which the app will be served"; | ||
74 | }; | ||
75 | |||
76 | # Output variables | ||
77 | systemdStateDirectory = lib.mkOption { | ||
78 | type = lib.types.str; | ||
79 | # Use ReadWritePaths= instead if varDir is outside of /var/lib | ||
80 | default = assert lib.strings.hasPrefix "/var/lib/" cfg.dataDir; | ||
81 | lib.strings.removePrefix "/var/lib/" cfg.dataDir; | ||
82 | description = '' | ||
83 | Adjusted paste data directory for systemd | ||
84 | ''; | ||
85 | readOnly = true; | ||
86 | }; | ||
87 | systemdRuntimeDirectory = lib.mkOption { | ||
88 | type = lib.types.str; | ||
89 | # Use ReadWritePaths= instead if socketsDir is outside of /run | ||
90 | default = assert lib.strings.hasPrefix "/run/" cfg.socketsDir; | ||
91 | lib.strings.removePrefix "/run/" cfg.socketsDir; | ||
92 | description = '' | ||
93 | Adjusted paste sockets directory for systemd | ||
94 | ''; | ||
95 | readOnly = true; | ||
96 | }; | ||
97 | sockets = lib.mkOption { | ||
98 | type = lib.types.attrsOf lib.types.path; | ||
99 | default = { | ||
100 | pid = "${cfg.socketsDir}/gunicorn.pid"; | ||
101 | gunicorn = "${cfg.socketsDir}/gunicorn.sock"; | ||
102 | }; | ||
103 | readOnly = true; | ||
104 | description = '' | ||
105 | Paste sockets | ||
106 | ''; | ||
107 | }; | ||
108 | }; | ||
109 | }; | ||
110 | |||
111 | config = lib.mkIf cfg.enable { | ||
112 | systemd.services.paste = { | ||
113 | description = "Pastebin like service"; | ||
114 | after = [ "network.target" ]; | ||
115 | wantedBy = [ "multi-user.target" ]; | ||
116 | |||
117 | serviceConfig = { | ||
118 | Environment = pkgs.lib.optionals (cfg.webDirectory != null) [ "SCRIPT_NAME=${cfg.webDirectory}" ]; | ||
119 | Type = "simple"; | ||
120 | User = config.services.httpd.user; | ||
121 | ExecStart = let | ||
122 | python = pkgs.python3.withPackages (p: [p.gunicorn p.flask p.pygments p.python_magic ]); | ||
123 | in | ||
124 | "${python}/bin/gunicorn -w4 -p ${cfg.sockets.pid} -e PASTE_DIRECTORY=${cfg.dataDir} --bind unix:${cfg.sockets.gunicorn} --chdir ${./paste} paste:app"; | ||
125 | Restart = "always"; | ||
126 | RestartSec = "5s"; | ||
127 | PIDFile = cfg.sockets.pid; | ||
128 | RuntimeDirectory = cfg.systemdRuntimeDirectory; | ||
129 | StateDirectory = cfg.systemdStateDirectory; | ||
130 | StandardOutput = "journal"; | ||
131 | StandardError = "inherit"; | ||
132 | }; | ||
133 | |||
134 | }; | ||
135 | }; | ||
136 | }; | ||
137 | }; | ||
138 | } | ||
diff --git a/flakes/paste/paste/paste.py b/flakes/paste/paste/paste.py new file mode 100644 index 0000000..86666b8 --- /dev/null +++ b/flakes/paste/paste/paste.py | |||
@@ -0,0 +1,124 @@ | |||
1 | import os | ||
2 | import secrets | ||
3 | from flask import Flask, abort, request, Response, url_for | ||
4 | from pygments.formatters.html import HtmlFormatter | ||
5 | from pygments import highlight | ||
6 | import pygments.lexers as lexers | ||
7 | import base64 | ||
8 | import magic | ||
9 | import mimetypes | ||
10 | |||
11 | magic = magic.Magic(mime=True) | ||
12 | |||
13 | config = { | ||
14 | "directory": os.environ["PASTE_DIRECTORY"], | ||
15 | "self_paste_id": "abcd123", | ||
16 | "max_content_length": 16 * 1000 * 1000 | ||
17 | } | ||
18 | |||
19 | app = Flask(__name__) | ||
20 | app.config['MAX_CONTENT_LENGTH'] = config["max_content_length"] | ||
21 | |||
22 | def file_location(paste_id): | ||
23 | if paste_id == config["self_paste_id"]: | ||
24 | return os.path.realpath(__file__) | ||
25 | else: | ||
26 | return os.path.join(config['directory'], paste_id + ".dat") | ||
27 | |||
28 | def read_paste(paste_id): | ||
29 | file = file_location(paste_id) | ||
30 | if os.path.isfile(file): | ||
31 | content = open(file, "rb").read() | ||
32 | mime = magic.from_buffer(content) | ||
33 | if mime.startswith("text/x-script."): | ||
34 | mime="text/plain" | ||
35 | return (content, mime) | ||
36 | else: | ||
37 | abort(404) | ||
38 | |||
39 | def generate_paste_id(n=3, attempts=0): | ||
40 | path = secrets.token_hex(n)[:n] | ||
41 | file = file_location(path) | ||
42 | if os.path.isfile(file): | ||
43 | attempts = attempts + 1 | ||
44 | if attempts > 5: | ||
45 | n = n + 1 | ||
46 | return generate_paste_id(n, attempts) | ||
47 | return path | ||
48 | |||
49 | @app.route('/', methods=["GET"]) | ||
50 | def index(): | ||
51 | return Response('''<pre> | ||
52 | $ curl -X POST --data-binary @{self} {host} | ||
53 | {paste} | ||
54 | |||
55 | -> GET {paste} | ||
56 | guesses mimetype | ||
57 | -> GET {paste}/raw | ||
58 | text/plain | ||
59 | -> GET {paste}/bin | ||
60 | application/octet-stream | ||
61 | -> GET {paste}/b64 | ||
62 | base64 encoded | ||
63 | -> GET {paste}/ub64 | ||
64 | tries to decode base64 | ||
65 | -> GET {paste}/python | ||
66 | pretty-print python (replace with anything known by pygments) | ||
67 | -> GET {paste}/guess | ||
68 | pretty-print (language guessed by pygments) | ||
69 | -> GET {paste}/download | ||
70 | force download of file | ||
71 | </pre> | ||
72 | <a href="{paste}/py">Get the source</a> | ||
73 | '''.format(host=url_for('post_paste', _external=True, _scheme="https"), | ||
74 | paste=url_for('get_paste', _external=True, _scheme="https", paste_id=config["self_paste_id"]), | ||
75 | self=os.path.basename(__file__) | ||
76 | ), mimetype="text/html") | ||
77 | |||
78 | @app.route('/', methods=["POST"]) | ||
79 | def post_paste(): | ||
80 | content = request.get_data() | ||
81 | paste_id = generate_paste_id() | ||
82 | with open(file_location(paste_id), "wb") as f: | ||
83 | f.write(content) | ||
84 | return url_for('get_paste', _external=True, _scheme="https", paste_id=paste_id) + "\n" | ||
85 | |||
86 | |||
87 | @app.route('/<paste_id>', methods=["GET"]) | ||
88 | def get_paste(paste_id): | ||
89 | content, mime = read_paste(paste_id) | ||
90 | return Response(content, mimetype=mime) | ||
91 | |||
92 | @app.route('/<paste_id>/<lang>', methods=["GET"]) | ||
93 | def get_paste_with(paste_id, lang): | ||
94 | content, mime = read_paste(paste_id) | ||
95 | if lang == "raw": | ||
96 | return Response(content, mimetype="text/plain") | ||
97 | elif lang == "bin": | ||
98 | return Response(content, mimetype="application/octet-stream") | ||
99 | elif lang == "b64": | ||
100 | return Response(base64.encodebytes(content), mimetype="text/plain") | ||
101 | elif lang == "download": | ||
102 | extension = mimetypes.guess_extension(mime, strict=False) | ||
103 | if extension is None: | ||
104 | cd = "attachment" | ||
105 | else: | ||
106 | cd = 'attachment; filename="{}{}"'.format(paste_id, extension) | ||
107 | return Response(content, mimetype=mime, | ||
108 | headers={"Content-Disposition": cd}) | ||
109 | elif lang == "ub64": | ||
110 | try: | ||
111 | return base64.b64decode(content) | ||
112 | except: | ||
113 | abort(400) | ||
114 | try: | ||
115 | if lang == "guess": | ||
116 | lexer = lexers.guess_lexer(content) | ||
117 | else: | ||
118 | lexer = lexers.find_lexer_class_by_name(lang)() | ||
119 | except: | ||
120 | abort(400) | ||
121 | fmter = HtmlFormatter( | ||
122 | noclasses=True, full=True, style="colorful", linenos="table") | ||
123 | |||
124 | return Response(highlight(content, lexer, fmter), mimetype="text/html") | ||
diff --git a/flakes/private/paste/flake.lock b/flakes/private/paste/flake.lock new file mode 100644 index 0000000..939d589 --- /dev/null +++ b/flakes/private/paste/flake.lock | |||
@@ -0,0 +1,72 @@ | |||
1 | { | ||
2 | "nodes": { | ||
3 | "flake-utils": { | ||
4 | "locked": { | ||
5 | "lastModified": 1631561581, | ||
6 | "narHash": "sha256-3VQMV5zvxaVLvqqUrNz3iJelLw30mIVSfZmAaauM3dA=", | ||
7 | "owner": "numtide", | ||
8 | "repo": "flake-utils", | ||
9 | "rev": "7e5bf3925f6fbdfaf50a2a7ca0be2879c4261d19", | ||
10 | "type": "github" | ||
11 | }, | ||
12 | "original": { | ||
13 | "owner": "numtide", | ||
14 | "repo": "flake-utils", | ||
15 | "type": "github" | ||
16 | } | ||
17 | }, | ||
18 | "nix-lib": { | ||
19 | "locked": { | ||
20 | "lastModified": 1631655525, | ||
21 | "narHash": "sha256-8U7zAdbjNItXo6eqI/rhtOa3LUPGD6yE9PTZQkrSGHo=", | ||
22 | "owner": "NixOS", | ||
23 | "repo": "nixpkgs", | ||
24 | "rev": "cf0caf529c33c140863ebfa43691f7b69fe2233c", | ||
25 | "type": "github" | ||
26 | }, | ||
27 | "original": { | ||
28 | "owner": "NixOS", | ||
29 | "repo": "nixpkgs", | ||
30 | "type": "github" | ||
31 | } | ||
32 | }, | ||
33 | "nixpkgs": { | ||
34 | "locked": { | ||
35 | "lastModified": 1631570365, | ||
36 | "narHash": "sha256-vc6bfo0hijpicdUDiui2DvZXmpIP2iqOFZRcpMOuYPo=", | ||
37 | "owner": "NixOS", | ||
38 | "repo": "nixpkgs", | ||
39 | "rev": "df7113c0727881519248d4c7d080324e0ee3327b", | ||
40 | "type": "github" | ||
41 | }, | ||
42 | "original": { | ||
43 | "owner": "NixOS", | ||
44 | "repo": "nixpkgs", | ||
45 | "type": "github" | ||
46 | } | ||
47 | }, | ||
48 | "paste": { | ||
49 | "inputs": { | ||
50 | "flake-utils": "flake-utils", | ||
51 | "nixpkgs": "nixpkgs" | ||
52 | }, | ||
53 | "locked": { | ||
54 | "narHash": "sha256-oSabBrUGIkY8lKktXlIM4uYSVYI54wKnIjjVZwMOd70=", | ||
55 | "path": "../../paste", | ||
56 | "type": "path" | ||
57 | }, | ||
58 | "original": { | ||
59 | "path": "../../paste", | ||
60 | "type": "path" | ||
61 | } | ||
62 | }, | ||
63 | "root": { | ||
64 | "inputs": { | ||
65 | "nix-lib": "nix-lib", | ||
66 | "paste": "paste" | ||
67 | } | ||
68 | } | ||
69 | }, | ||
70 | "root": "root", | ||
71 | "version": 7 | ||
72 | } | ||
diff --git a/flakes/private/paste/flake.nix b/flakes/private/paste/flake.nix new file mode 100644 index 0000000..71314e8 --- /dev/null +++ b/flakes/private/paste/flake.nix | |||
@@ -0,0 +1,23 @@ | |||
1 | { | ||
2 | inputs.paste = { | ||
3 | path = "../../paste"; | ||
4 | type = "path"; | ||
5 | }; | ||
6 | inputs.nix-lib.url = "github:NixOS/nixpkgs"; | ||
7 | |||
8 | description = "Private configuration for paste"; | ||
9 | outputs = { self, nix-lib, paste }: | ||
10 | let | ||
11 | cfg = name': { config, lib, pkgs, name, ... }: { | ||
12 | config = lib.mkIf (name == name') { | ||
13 | services.paste = { | ||
14 | enable = true; | ||
15 | webDirectory = "/paste"; | ||
16 | }; | ||
17 | }; | ||
18 | }; | ||
19 | in | ||
20 | paste.outputs // | ||
21 | { nixosModules = paste.nixosModules or {} // nix-lib.lib.genAttrs ["eldiron"] cfg; }; | ||
22 | } | ||
23 | |||