This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

configs.paste: serve paste, imgur and cyberlocker through fcgiwrap

One python CGI (configs/paste/paste.py) replaces the htgen-paste,
htgen-imgur and htgen-cyberlocker handlers and the paste-form script.
Items keep their names and directories, so old links and the backups stay
as they are; systemd hands the directories to the new paste user. The
upload form now stores directly instead of posting back to p.krebsco.de.

+446 -190
-189
configs/paste.nix
··· 1 - { 2 - config, 3 - lib, 4 - pkgs, 5 - self, 6 - ... 7 - }: 8 - 9 - let 10 - stockholmPkgs = self.inputs.stockholm.packages.${pkgs.stdenv.hostPlatform.system}; 11 - 12 - uploadPage = pkgs.writeText "paste-upload.html" '' 13 - <!doctype html> 14 - <title>p.krebsco.de</title> 15 - <form action="/form" method="post" enctype="multipart/form-data"> 16 - <input type="file" name="file" required> 17 - <button>upload</button> 18 - </form> 19 - <p>or with curl:</p> 20 - <pre> 21 - curl --data-binary @file https://p.krebsco.de 22 - some-command | curl --data-binary @- https://p.krebsco.de 23 - </pre> 24 - ''; 25 - in 26 - { 27 - 28 - services.nginx.virtualHosts.cyberlocker = { 29 - serverAliases = [ "c.r" ]; 30 - locations."/".extraConfig = '' 31 - client_max_body_size 4G; 32 - proxy_set_header Host $host; 33 - proxy_pass http://127.0.0.1:${toString config.krebs.htgen.cyberlocker.port}; 34 - ''; 35 - extraConfig = '' 36 - add_header Access-Control-Allow-Origin * always; 37 - add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS'; 38 - ''; 39 - }; 40 - services.nginx.virtualHosts.paste = { 41 - serverAliases = [ "p.r" ]; 42 - locations."/".extraConfig = '' 43 - client_max_body_size 4G; 44 - proxy_set_header Host $host; 45 - proxy_pass http://127.0.0.1:${toString config.krebs.htgen.paste.port}; 46 - ''; 47 - locations."/image".extraConfig = # nginx 48 - '' 49 - client_max_body_size 40M; 50 - 51 - proxy_set_header Host $host; 52 - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 53 - proxy_set_header X-Forwarded-Proto $scheme; 54 - 55 - proxy_pass http://127.0.0.1:${toString config.krebs.htgen.imgur.port}; 56 - proxy_pass_header Server; 57 - ''; 58 - extraConfig = '' 59 - add_header 'Access-Control-Allow-Origin' '*'; 60 - add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; 61 - ''; 62 - }; 63 - services.nginx.virtualHosts."c.krebsco.de" = { 64 - enableACME = true; 65 - addSSL = true; 66 - serverAliases = [ "c.krebsco.de" ]; 67 - locations."/".extraConfig = '' 68 - if ($request_method != GET) { 69 - return 403; 70 - } 71 - proxy_set_header Host $host; 72 - proxy_pass http://127.0.0.1:${toString config.krebs.htgen.cyberlocker.port}; 73 - ''; 74 - extraConfig = '' 75 - add_header Access-Control-Allow-Origin * always; 76 - add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS' always; 77 - ''; 78 - }; 79 - services.nginx.virtualHosts."p.krebsco.de" = { 80 - enableACME = true; 81 - addSSL = true; 82 - serverAliases = [ "p.krebsco.de" ]; 83 - locations."/".extraConfig = '' 84 - if ($request_method = 'OPTIONS') { 85 - return 204; 86 - } 87 - if ($request_method ~ ^(GET|HEAD)$) { 88 - rewrite ^/$ /_upload last; 89 - } 90 - client_max_body_size 4G; 91 - proxy_set_header Host $host; 92 - proxy_set_header X-Forwarded-Proto $scheme; 93 - proxy_pass http://127.0.0.1:${toString config.krebs.htgen.paste.port}; 94 - ''; 95 - locations."= /_upload".extraConfig = '' 96 - internal; 97 - default_type text/html; 98 - alias ${uploadPage}; 99 - ''; 100 - locations."/form".extraConfig = '' 101 - client_max_body_size 4G; 102 - proxy_set_header Host $host; 103 - proxy_pass http://127.0.0.1:${toString config.krebs.htgen.paste-form.port}; 104 - ''; 105 - locations."/image".extraConfig = '' 106 - proxy_set_header Host $host; 107 - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 108 - proxy_set_header X-Forwarded-Proto $scheme; 109 - 110 - proxy_pass http://127.0.0.1:${toString config.krebs.htgen.imgur.port}; 111 - proxy_pass_header Server; 112 - ''; 113 - extraConfig = '' 114 - add_header Access-Control-Allow-Headers Authorization always; 115 - add_header Access-Control-Allow-Origin * always; 116 - add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS' always; 117 - ''; 118 - }; 119 - 120 - krebs.htgen.paste = { 121 - port = 9081; 122 - script = # sh 123 - '' 124 - (. ${stockholmPkgs.htgen-paste}/bin/htgen-paste) 125 - ''; 126 - }; 127 - 128 - systemd.services.paste-gc = { 129 - startAt = "daily"; 130 - serviceConfig = { 131 - ExecStart = '' 132 - ${pkgs.findutils}/bin/find /var/lib/htgen-paste/items -type f -mtime '+30' -exec rm {} \; 133 - ''; 134 - User = "htgen-paste"; 135 - }; 136 - }; 137 - 138 - krebs.htgen.paste-form = { 139 - port = 7770; 140 - script = # sh 141 - '' 142 - export PATH=${ 143 - lib.makeBinPath [ 144 - pkgs.curl 145 - pkgs.gnused 146 - ] 147 - }:$PATH 148 - (. ${pkgs.writeScript "paste-form" '' 149 - case "$Method" in 150 - 'POST') 151 - # multipart body: part headers up to the first empty line, then the 152 - # file, then "\r\n--$boundary--\r\n" (length of boundary + 8 bytes) 153 - boundary=''${req_content_type-} 154 - boundary=''${boundary#*boundary=} 155 - boundary=''${boundary%%;*} 156 - boundary=''${boundary#\"} 157 - boundary=''${boundary%\"} 158 - ref=$(head -c $req_content_length | sed '0,/^\r$/d' | head -c -$(expr ''${#boundary} + 8) | curl -fSs --data-binary @- https://p.krebsco.de | sed '1d;s/^http:/https:/') 159 - 160 - printf 'HTTP/1.1 200 OK\r\n' 161 - printf 'Content-Type: text/plain; charset=UTF-8\r\n' 162 - printf 'Server: %s\r\n' "$Server" 163 - printf 'Connection: close\r\n' 164 - printf 'Content-Length: %d\r\n' $(expr ''${#ref} + 1) 165 - printf '\r\n' 166 - printf '%s\n' "$ref" 167 - 168 - exit 169 - ;; 170 - esac 171 - ''}) 172 - ''; 173 - }; 174 - krebs.htgen.imgur = { 175 - port = 7771; 176 - script = # sh 177 - '' 178 - (. ${stockholmPkgs.htgen-imgur}/bin/htgen-imgur) 179 - ''; 180 - }; 181 - krebs.htgen.cyberlocker = { 182 - port = 7772; 183 - script = # sh 184 - '' 185 - (. ${stockholmPkgs.htgen-cyberlocker}/bin/htgen-cyberlocker) 186 - ''; 187 - }; 188 - networking.firewall.interfaces.retiolum.allowedTCPPorts = [ 80 ]; 189 - }
+154
configs/paste/default.nix
··· 1 + { 2 + config, 3 + lib, 4 + pkgs, 5 + ... 6 + }: 7 + 8 + let 9 + uploadPage = pkgs.writeText "paste-upload.html" '' 10 + <!doctype html> 11 + <title>p.krebsco.de</title> 12 + <form action="/form" method="post" enctype="multipart/form-data"> 13 + <input type="file" name="file" required> 14 + <button>upload</button> 15 + </form> 16 + <p>or with curl:</p> 17 + <pre> 18 + curl --data-binary @file https://p.krebsco.de 19 + some-command | curl --data-binary @- https://p.krebsco.de 20 + </pre> 21 + ''; 22 + 23 + pasteCgi = pkgs.writers.writePython3 "paste-cgi" { } ( 24 + lib.replaceStrings 25 + [ "@file@" "@exiv2@" ] 26 + [ (lib.getExe pkgs.file) (lib.getExe' pkgs.exiv2 "exiv2") ] 27 + (builtins.readFile ./paste.py) 28 + ); 29 + 30 + # item directories of the former htgen handlers, kept for their contents 31 + # and the backup paths 32 + items = { 33 + paste = "/var/lib/htgen-paste/items"; 34 + imgur = "/var/lib/htgen-imgur/items"; 35 + cyberlocker = "/var/lib/htgen-cyberlocker/items"; 36 + }; 37 + 38 + # systemd chowns them to paste if they still belong to the htgen users 39 + stateDirectories = { 40 + StateDirectory = map (dir: lib.removePrefix "/var/lib/" (dirOf dir)) (lib.attrValues items); 41 + StateDirectoryMode = "0700"; 42 + }; 43 + 44 + cgi = app: itemsDir: '' 45 + include ${config.services.nginx.package}/conf/fastcgi_params; 46 + fastcgi_param SCRIPT_FILENAME ${pasteCgi}; 47 + fastcgi_param PASTE_APP ${app}; 48 + fastcgi_param PASTE_ITEMS ${itemsDir}; 49 + fastcgi_pass unix:${config.services.fcgiwrap.instances.paste.socket.address}; 50 + ''; 51 + in 52 + { 53 + 54 + services.nginx.virtualHosts.cyberlocker = { 55 + serverAliases = [ "c.r" ]; 56 + locations."/".extraConfig = '' 57 + client_max_body_size 4G; 58 + ${cgi "cyberlocker" items.cyberlocker} 59 + ''; 60 + extraConfig = '' 61 + add_header Access-Control-Allow-Origin * always; 62 + add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS'; 63 + ''; 64 + }; 65 + services.nginx.virtualHosts.paste = { 66 + serverAliases = [ "p.r" ]; 67 + locations."/".extraConfig = '' 68 + client_max_body_size 4G; 69 + ${cgi "paste" items.paste} 70 + ''; 71 + locations."/image".extraConfig = '' 72 + client_max_body_size 40M; 73 + ${cgi "imgur" items.imgur} 74 + ''; 75 + extraConfig = '' 76 + add_header 'Access-Control-Allow-Origin' '*'; 77 + add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; 78 + ''; 79 + }; 80 + services.nginx.virtualHosts."c.krebsco.de" = { 81 + enableACME = true; 82 + addSSL = true; 83 + serverAliases = [ "c.krebsco.de" ]; 84 + locations."/".extraConfig = '' 85 + if ($request_method != GET) { 86 + return 403; 87 + } 88 + ${cgi "cyberlocker" items.cyberlocker} 89 + ''; 90 + extraConfig = '' 91 + add_header Access-Control-Allow-Origin * always; 92 + add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS' always; 93 + ''; 94 + }; 95 + services.nginx.virtualHosts."p.krebsco.de" = { 96 + enableACME = true; 97 + addSSL = true; 98 + serverAliases = [ "p.krebsco.de" ]; 99 + locations."/".extraConfig = '' 100 + if ($request_method = 'OPTIONS') { 101 + return 204; 102 + } 103 + if ($request_method ~ ^(GET|HEAD)$) { 104 + rewrite ^/$ /_upload last; 105 + } 106 + client_max_body_size 4G; 107 + ${cgi "paste" items.paste} 108 + ''; 109 + locations."= /_upload".extraConfig = '' 110 + internal; 111 + default_type text/html; 112 + alias ${uploadPage}; 113 + ''; 114 + locations."/form".extraConfig = '' 115 + client_max_body_size 4G; 116 + ${cgi "form" items.paste} 117 + ''; 118 + locations."/image".extraConfig = '' 119 + ${cgi "imgur" items.imgur} 120 + ''; 121 + extraConfig = '' 122 + add_header Access-Control-Allow-Headers Authorization always; 123 + add_header Access-Control-Allow-Origin * always; 124 + add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS' always; 125 + ''; 126 + }; 127 + 128 + services.fcgiwrap.instances.paste = { 129 + # requests at a time; uploads and downloads hold a process each 130 + process.prefork = 8; 131 + process.user = "paste"; 132 + process.group = "paste"; 133 + socket.user = config.services.nginx.user; 134 + socket.group = config.services.nginx.group; 135 + }; 136 + systemd.services.fcgiwrap-paste.serviceConfig = stateDirectories; 137 + users.users.paste = { 138 + isSystemUser = true; 139 + group = "paste"; 140 + }; 141 + users.groups.paste = { }; 142 + 143 + systemd.services.paste-gc = { 144 + startAt = "daily"; 145 + serviceConfig = stateDirectories // { 146 + ExecStart = '' 147 + ${pkgs.findutils}/bin/find ${items.paste} -type f -mtime '+30' -exec rm {} \; 148 + ''; 149 + User = "paste"; 150 + }; 151 + }; 152 + 153 + networking.firewall.interfaces.retiolum.allowedTCPPorts = [ 80 ]; 154 + }
+291
configs/paste/paste.py
··· 1 + """CGI behind fcgiwrap for p.r/p.krebsco.de (paste, form, imgur) and c.r 2 + (cyberlocker). nginx picks the app with PASTE_APP and the item directory with 3 + PASTE_ITEMS. The on-disk layout is the one of the former htgen-paste, 4 + htgen-imgur and htgen-cyberlocker handlers, so existing items keep working. 5 + """ 6 + 7 + import email.utils 8 + import hashlib 9 + import json 10 + import os 11 + import re 12 + import shutil 13 + import subprocess 14 + import sys 15 + import tempfile 16 + import time 17 + import urllib.parse 18 + import uuid 19 + 20 + FILE = "@file@" 21 + EXIV2 = "@exiv2@" 22 + 23 + ENV = os.environ 24 + ITEMS = ENV["PASTE_ITEMS"] 25 + METHOD = ENV.get("REQUEST_METHOD", "GET") 26 + RAW_PATH, _, QUERY = ENV.get("REQUEST_URI", "/").partition("?") 27 + HOST = ENV.get("HTTP_HOST", "") 28 + STDIN = sys.stdin.buffer 29 + OUT = sys.stdout.buffer 30 + 31 + 32 + def respond(status, headers=(), body=b""): 33 + """Send a CGI response; body is bytes or an open file to stream.""" 34 + lines = [f"Status: {status}", *(f"{k}: {v}" for k, v in headers)] 35 + if isinstance(body, str): 36 + body = body.encode() 37 + if isinstance(body, bytes): 38 + lines.append(f"Content-Length: {len(body)}") 39 + OUT.write(("\r\n".join(lines) + "\r\n\r\n").encode()) 40 + if isinstance(body, bytes): 41 + OUT.write(body) 42 + else: 43 + shutil.copyfileobj(body, OUT) 44 + 45 + 46 + def text(status, body): 47 + respond(status, [("Content-Type", "text/plain; charset=UTF-8")], body) 48 + 49 + 50 + def send_file(path, content_type): 51 + with open(path, "rb") as f: 52 + size = os.fstat(f.fileno()).st_size 53 + headers = [("Content-Type", content_type), ("Content-Length", size)] 54 + respond("200 OK", headers, f) 55 + 56 + 57 + def file_type(path): 58 + cmd = [FILE, "-ib", path] 59 + return subprocess.run(cmd, capture_output=True, text=True).stdout.strip() 60 + 61 + 62 + def body_length(): 63 + return int(ENV.get("CONTENT_LENGTH") or 0) 64 + 65 + 66 + def read_body(size, head=b""): 67 + """Yield head and then the next size - len(head) bytes of the body.""" 68 + if head: 69 + yield head 70 + left = size - len(head) 71 + while left > 0: 72 + chunk = STDIN.read(min(left, 1 << 20)) 73 + if not chunk: 74 + break 75 + left -= len(chunk) 76 + yield chunk 77 + 78 + 79 + def save(chunks, name=None): 80 + """Write chunks to ITEMS/name, replacing it, or without a name to the 81 + nix-base32 sha256 of the content, kept if it exists already. 82 + Returns (name, path, created).""" 83 + os.makedirs(ITEMS, exist_ok=True) 84 + digest = hashlib.sha256() 85 + tmp = tempfile.NamedTemporaryFile(dir=ITEMS, prefix=".", delete=False) 86 + try: 87 + with tmp: 88 + for chunk in chunks: 89 + digest.update(chunk) 90 + tmp.write(chunk) 91 + replace = name is not None 92 + name = name or nix32(digest.digest()) 93 + path = os.path.join(ITEMS, name) 94 + created = not os.path.exists(path) 95 + if replace or created: 96 + os.chmod(tmp.name, 0o644) 97 + os.replace(tmp.name, path) 98 + return name, path, created 99 + finally: 100 + if os.path.exists(tmp.name): 101 + os.unlink(tmp.name) 102 + 103 + 104 + NIX32 = "0123456789abcdfghijklmnpqrsvwxyz" 105 + 106 + 107 + def nix32(digest): 108 + """Nix's base32 (nix-hash --to-base32).""" 109 + out = "" 110 + for n in reversed(range((len(digest) * 8 - 1) // 5 + 1)): 111 + byte, bit = divmod(n * 5, 8) 112 + c = digest[byte] >> bit 113 + if byte + 1 < len(digest): 114 + c |= digest[byte + 1] << (8 - bit) 115 + out += NIX32[c & 0x1F] 116 + return out 117 + 118 + 119 + def find(ident): 120 + """The item whose name starts with ident (7+ chars), if exactly one.""" 121 + if len(ident) < 7 or not re.fullmatch("[0-9a-z]+", ident): 122 + return None 123 + try: 124 + names = os.listdir(ITEMS) 125 + except FileNotFoundError: 126 + return None 127 + hits = [n for n in names if n.startswith(ident) and n.isalnum()] 128 + return os.path.join(ITEMS, hits[0]) if len(hits) == 1 else None 129 + 130 + 131 + def short(name): 132 + return name[:7] if find(name[:7]) else None 133 + 134 + 135 + def paste(): 136 + if METHOD == "POST" and RAW_PATH == "/": 137 + name, path, _ = save(read_body(body_length())) 138 + override = ENV.get("HTTP_CONTENT_TYPE_OVERRIDE") 139 + if override: 140 + with open(path + ".content_type", "w") as f: 141 + f.write(override) 142 + refs = [f"http://{HOST}/{n}" for n in (name, short(name)) if n] 143 + return text("200 OK", "".join(ref + "\n" for ref in refs)) 144 + item = find(RAW_PATH[1:]) 145 + if item and METHOD == "GET": 146 + try: 147 + with open(item + ".content_type") as f: 148 + content_type = f.read() 149 + except FileNotFoundError: 150 + content_type = file_type(item) 151 + return send_file(item, content_type) 152 + if item and METHOD == "DELETE": 153 + os.unlink(item) 154 + return respond("200 OK") 155 + respond("404 Not Found") 156 + 157 + 158 + def form(): 159 + """multipart/form-data upload from the p.krebsco.de page: the file is the 160 + first part, after its headers and before "\r\n--boundary--\r\n".""" 161 + if METHOD != "POST": 162 + return respond("404 Not Found") 163 + boundary = ENV.get("CONTENT_TYPE", "").partition("boundary=")[2] 164 + boundary = boundary.partition(";")[0].strip('"') 165 + length = body_length() 166 + head = STDIN.read(min(length, 1 << 16)) 167 + start = head.find(b"\r\n\r\n") + 4 168 + size = length - start - (len(boundary) + 8) 169 + if not boundary or start < 4 or size < 0: 170 + return text("400 Bad Request", "bad multipart upload\n") 171 + name, _, _ = save(read_body(size, head[start:start + size])) 172 + text("200 OK", f"https://{HOST}/{short(name) or name}\n") 173 + 174 + 175 + def imgur_response(status, data=None): 176 + """https://api.imgur.com/models/basic""" 177 + code = int(status.split()[0]) 178 + body = {"data": data, "status": code, "success": 200 <= code <= 299} 179 + body = json.dumps(body, separators=(",", ":")) + "\n" 180 + respond(status, [("Content-Type", "application/json; charset=UTF-8")], 181 + body) 182 + 183 + 184 + def image_info(path): 185 + cmd = [EXIV2, "print", path] 186 + out = subprocess.run(cmd, capture_output=True, text=True).stdout 187 + info = {} 188 + for line in out.splitlines(): 189 + key, sep, value = line.partition(":") 190 + if sep: 191 + info[key.strip()] = value.strip() 192 + size = re.fullmatch(r"([0-9]+)\s*x\s*([0-9]+)", info.get("Image size", "")) 193 + if "MIME type" not in info or not size: 194 + return None 195 + return info["MIME type"], int(size[1]), int(size[2]) 196 + 197 + 198 + def imgur(): 199 + query = urllib.parse.parse_qs(QUERY) 200 + ident = RAW_PATH.rpartition("/")[2] 201 + if METHOD == "POST" and RAW_PATH == "/image": 202 + name, path, created = save(read_body(body_length())) 203 + info = image_info(path) 204 + if not info: 205 + if created: 206 + os.unlink(path) 207 + return imgur_response("400 Bad Request") 208 + content_type, width, height = info 209 + scheme = ENV.get("REQUEST_SCHEME", "http") 210 + deletehash = uuid.uuid4().hex 211 + data = { 212 + "id": name, 213 + "title": query.get("title", [None])[0], 214 + "description": query.get("description", [None])[0], 215 + "datetime": time.time(), 216 + "type": content_type, 217 + "animated": False, 218 + "width": width, 219 + "height": height, 220 + "size": os.path.getsize(path), 221 + "views": 0, 222 + "bandwidth": 0, 223 + "vote": None, 224 + "favorite": False, 225 + "nsfw": None, 226 + "section": None, 227 + "account_url": None, 228 + "acount_id": 0, 229 + "is_ad": False, 230 + "is_most_viral": False, 231 + "tags": [], 232 + "ad_type": 0, 233 + "ad_url": "", 234 + "in_gallery": False, 235 + "deletehash": urllib.parse.quote( 236 + f"{name}?deletehash={deletehash}", safe="" 237 + ), 238 + "name": "", 239 + "link": f"{scheme}://{HOST}/image/{short(name) or name}", 240 + } 241 + os.setxattr(path, "user.deletehash", deletehash.encode()) 242 + os.setxattr(path, "user.data", json.dumps(data).encode()) 243 + return imgur_response("200 OK", data) 244 + item = find(ident) 245 + if item and METHOD == "GET" and RAW_PATH.startswith("/image/"): 246 + try: 247 + content_type = json.loads(os.getxattr(item, "user.data"))["type"] 248 + except (OSError, ValueError, KeyError): 249 + content_type = "application/octet-stream" 250 + return send_file(item, content_type) 251 + if ( 252 + item 253 + and METHOD == "DELETE" 254 + and RAW_PATH.startswith("/image/delete/") 255 + ): 256 + given = query.get("deletehash", [""])[0] 257 + if given.encode() != os.getxattr(item, "user.deletehash"): 258 + return imgur_response("401 Unauthorized") 259 + os.unlink(item) 260 + return imgur_response("200 OK") 261 + respond("404 Not Found") 262 + 263 + 264 + def cyberlocker(): 265 + """Files by path; the raw request path, with repeated slashes collapsed, 266 + percent-encoded into one file name.""" 267 + name = urllib.parse.quote(re.sub("/+", "/", RAW_PATH), safe="") 268 + path = os.path.join(ITEMS, name) 269 + if METHOD in ("POST", "PUT"): 270 + save(read_body(body_length()), name) 271 + return respond("204 No Content") 272 + if not os.path.exists(path): 273 + return respond("404 Not Found") 274 + if METHOD == "GET": 275 + since = ENV.get("HTTP_IF_MODIFIED_SINCE") 276 + try: 277 + since = since and email.utils.parsedate_to_datetime(since) 278 + except (TypeError, ValueError): 279 + since = None 280 + if since and os.path.getmtime(path) < since.timestamp(): 281 + return respond("304 Not Modified") 282 + return send_file(path, file_type(path)) 283 + if METHOD == "DELETE": 284 + os.unlink(path) 285 + return respond("204 No Content") 286 + respond("404 Not Found") 287 + 288 + 289 + {"paste": paste, "form": form, "imgur": imgur, "cyberlocker": cyberlocker}[ 290 + ENV["PASTE_APP"] 291 + ]()
+1 -1
machines/neoprism/config.nix
··· 80 80 ../../configs/realwallpaper.nix 81 81 82 82 # paste + cyberlocker 83 - ../../configs/paste.nix 83 + ../../configs/paste 84 84 85 85 # opencrow matrix bot 86 86 ../../configs/opencrow.nix