aboutsummaryrefslogtreecommitdiff
path: root/flakes/paste
diff options
context:
space:
mode:
authorIsmaël Bouya <ismael.bouya@normalesup.org>2021-09-14 02:28:09 +0200
committerIsmaël Bouya <ismael.bouya@normalesup.org>2021-09-30 00:36:14 +0200
commita9f52ec521e45204ad9363dd143b32ac9910b6b3 (patch)
tree6257e8385c240890e1f8c443e8aa886de09ad523 /flakes/paste
parente4e0de77cd6c9882fa7ff7c3cdd0ed9fce8a59d8 (diff)
downloadNix-a9f52ec521e45204ad9363dd143b32ac9910b6b3.tar.gz
Nix-a9f52ec521e45204ad9363dd143b32ac9910b6b3.tar.zst
Nix-a9f52ec521e45204ad9363dd143b32ac9910b6b3.zip
Add flask app paste
Diffstat (limited to 'flakes/paste')
-rw-r--r--flakes/paste/flake.lock42
-rw-r--r--flakes/paste/flake.nix138
-rw-r--r--flakes/paste/paste/paste.py124
3 files changed, 304 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 @@
1import os
2import secrets
3from flask import Flask, abort, request, Response, url_for
4from pygments.formatters.html import HtmlFormatter
5from pygments import highlight
6import pygments.lexers as lexers
7import base64
8import magic
9import mimetypes
10
11magic = magic.Magic(mime=True)
12
13config = {
14 "directory": os.environ["PASTE_DIRECTORY"],
15 "self_paste_id": "abcd123",
16 "max_content_length": 16 * 1000 * 1000
17 }
18
19app = Flask(__name__)
20app.config['MAX_CONTENT_LENGTH'] = config["max_content_length"]
21
22def 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
28def 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
39def 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"])
50def 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"])
79def 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"])
88def 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"])
93def 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")