From 0479a78d16e03bcffc342703d6312fab526e4ef1 Mon Sep 17 00:00:00 2001 From: Cirakg Date: Mon, 14 Apr 2025 23:33:52 +0200 Subject: feat: Move static file handling to nginx config and create script.js. Remove unnecesary code from paste-cgi.py --- README.md | 38 +++++++--- get.html | 131 ++-------------------------------- index.html | 143 ++----------------------------------- paste-cgi.py | 142 +++++++++--------------------------- script.js | 230 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 302 insertions(+), 382 deletions(-) create mode 100644 script.js diff --git a/README.md b/README.md index d82d685..76581d8 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Paste-cgi ## Description -This is a dirty pastebin done in CGI. It is completely client-side encrypted and server only stored encrypted paste. +This is a dirty pastebin done in CGI. It is completely client-side encrypted and server only stores encrypted paste(Title is not encrypted). ## Images ![Index](docs/Index.png) @@ -10,24 +10,44 @@ This is a dirty pastebin done in CGI. It is completely client-side encrypted and ## Nginx Config ``` server { + server_name paste.example.com; + root /example/root/dir; - server_name SERVERURL; - root ROOTFOLDER; + # Global basic Auth + auth_basic "Restricted Content"; + auth_basic_user_file /etc/nginx/.htpasswd; - location / { - auth_basic "Restricted Content"; # Basic Auth - auth_basic_user_file /etc/nginx/.htpasswd; # Basic Auth + # Server static files + location = / { + try_files /index.html =404; + } + + location = /get { + try_files /get.html =404; + } + + location ~* \.(css|js|svg)$ { + try_files $uri =404; + access_log off; + expires 30d; + } + + # Handle cgi routes + location ~ ^/(paste|submit)$ { include fastcgi_params; - fastcgi_param SCRIPT_FILENAME PATH TO SCRIPT; + fastcgi_param SCRIPT_FILENAME /example/path/to/cgi; fastcgi_param PATH_INFO $uri; fastcgi_param QUERY_STRING $args; fastcgi_param HTTP_HOST $server_name; fastcgi_param CONTENT_LENGTH $content_length; fastcgi_param CONTENT_TYPE $content_type; - fastcgi_param ALLOWED_DIR PATH WHERE THE ALLOWED DIRECTORY IS TO CHANGE FILES; + fastcgi_param ALLOWED_DIR /example/allowed/dir; fastcgi_pass unix:/run/fcgiwrap.socket; } - + # Deny everything else + location / { + return 403; + } listen [::]:443 ssl; # managed by Certbot listen 443 ssl; # managed by Certbot diff --git a/get.html b/get.html index 9799eba..37768b0 100644 --- a/get.html +++ b/get.html @@ -3,15 +3,16 @@ - - Secure Paste + paste-cgi
-

paste-cgi

+ +

paste-cgi

+

Title

@@ -30,129 +31,7 @@
- - + \ No newline at end of file diff --git a/index.html b/index.html index f4fe7a3..cafb087 100644 --- a/index.html +++ b/index.html @@ -3,15 +3,16 @@ - - Secure Paste + paste-cgi
-

paste-cgi

+ +

paste-cgi

+
@@ -54,141 +55,7 @@
- - + \ No newline at end of file diff --git a/paste-cgi.py b/paste-cgi.py index 88bd802..dd1d9e1 100644 --- a/paste-cgi.py +++ b/paste-cgi.py @@ -46,13 +46,33 @@ def check_working_dir(allowed_dir): sys.exit(0) +def status_405(): + print("Status: 405 Method Not Allowed") + print("Content-Type: text/plain") + print("") + print("405 Method Not Allowed") + sys.exit(0) + + +def status_415(): + print("Status: 415 Unsupported Media Type") + print("Content-Type: text/plain") + print("") + print("415 Unsupported Media Type.") + sys.exit(0) + + +def status_404(): + print("Status: 404 Not Found") + print("Content-Type: text/plain") + print("") + print("File not found") + sys.exit(0) + + def check_method(method): if method not in ["GET", "POST"]: - print("Status: 405 Method Not Allowed") - print("Content-Type: text/plain") - print("") - print("405 Method Not Allowed") - sys.exit(0) + status_405() def get_content_lenght(): @@ -82,59 +102,7 @@ def validate_payload(payload): ): if SubmitConstants.PASTED_TEXT in payload: return - print("Status: 415 Unsupported Media Type") - print("Content-Type: text/plain") - print("") - print("415 Unsupported Media Type: Expected 'application/json'.") - sys.exit(0) - - -def return_index_html(): - try: - with open("index.html", "r") as file: - index_html = file.read() - - print("Content-Type: text/html") - print("") - print(index_html) - - except Exception: - print("Status: 404 Not Found") - print("Content-Type: text/html") - print("") - print("

404 Not Found

") - - -def return_pico_css(): - try: - with open("pico.min.css", "r") as file: - pico_css = file.read() - - print("Content-Type: text/css") - print("") - print(pico_css) - - except Exception: - print("Status: 404 Not Found") - print("Content-Type: text/html") - print("") - print("

404 Not Found

") - - -def return_favicon_svg(): - try: - with open("favicon.svg", "r") as file: - favicon_svg = file.read() - - print("Content-Type: image/svg+xml") - print("") - print(favicon_svg) - - except Exception: - print("Status: 404 Not Found") - print("Content-Type: text/html") - print("") - print("

404 Not Found

") + status_415() def submit(post_data): @@ -203,17 +171,11 @@ def handle_data(data): def return_paste(query_string): if query_string is None or not query_string.startswith("id="): - print("Content-Type: text/plain") - print("") - print(query_string) - sys.exit(0) + status_415() id = query_string.split("=") if len(id) != 2: - print("Content-Type: text/plain") - print("") - print(query_string) - sys.exit(0) + status_415() id = id[1] @@ -222,18 +184,10 @@ def return_paste(query_string): os.chdir(directory) if os.getcwd() != DATABASE_DIRECTORY: - print("Content-Type: text/plain") - print("") - print("NICE TRY") - sys.exit(0) + status_415() if not os.path.exists(full_path): - print("Status: 404 Not Found") - print("Content-Type: text/plain") - print("") - print("File not found") - print(full_path) - sys.exit(0) + status_404() with open(full_path, "r") as file: data = json.load(file) @@ -243,10 +197,7 @@ def return_paste(query_string): if deleted: os.remove(full_path) - print("Status: 404 Not Found") - print("Content-Type: text/plain") - print("") - print("File not found") + status_404() print("Content-Type: application/json") print("") @@ -257,26 +208,7 @@ def return_paste(query_string): sys.exit(0) except Exception: - print("Status: 404 Not Found") - print("Content-Type: text/plain") - print("") - print("File not found") - - -def return_get(): - try: - with open("get.html", "r") as file: - get_html = file.read() - - print("Content-Type: text/html") - print("") - print(get_html) - - except Exception: - print("Status: 404 Not Found") - print("Content-Type: text/html") - print("") - print("

404 Not Found

") + status_404() allowed_dir = os.environ.get("ALLOWED_DIR", None) @@ -293,15 +225,7 @@ content_length = get_content_lenght() if method == "GET": - if script_name == "/": - return_index_html() - elif script_name == "/pico.min.css": - return_pico_css() - elif script_name == "/favicon.svg": - return_favicon_svg() - elif script_name == "/get": - return_get() - elif script_name == "/paste": + if script_name == "/paste": return_paste(query_string) elif method == "POST": post_data = sys.stdin.read(content_length) diff --git a/script.js b/script.js new file mode 100644 index 0000000..995156f --- /dev/null +++ b/script.js @@ -0,0 +1,230 @@ +const pasteTypeConstant = "pasteType"; +const passwordFieldConstant = "passwordField"; +const passwordTypeConstant = "password"; +const plainTypeConstant = "plain"; +const titleConstant = "title"; +const expirationConstant = "expiration"; +const plaintextConstant = "plaintext"; +const pasteUrlConstant = "pasteUrl"; +const pasteUrlSectionConstant = "pasteUrlSection"; +const decryptedOutputConstant = "decryptedOutput"; +const decryptButtonConstant = "decryptBtn"; + +let globalData; + +function copyPaste() { + const copyText = document.getElementById( + document.getElementById(pasteUrlConstant) + ? pasteUrlConstant + : decryptedOutputConstant + ); + copyText.select(); + copyText.setSelectionRange(0, 99999); + navigator.clipboard.writeText(copyText.value); +} + +function togglePasteType() { + const passwordType = document.getElementById(pasteTypeConstant)?.value || globalData?.type; + const passwordField = document.getElementById(passwordFieldConstant); + const decryptButton = document.getElementById(decryptButtonConstant); + + if (document.getElementById(titleConstant) && globalData?.title) { + document.getElementById(titleConstant).textContent = "Title:" + globalData.title; + } + + if (passwordType === passwordTypeConstant) { + passwordField.style.display = "block"; + if (decryptButton) decryptButton.style.display = "block"; + } else { + passwordField.style.display = "none"; + if (decryptButton) decryptButton.style.display = "none"; + if (document.getElementById(decryptedOutputConstant)) { + document.getElementById(decryptedOutputConstant).textContent = globalData?.pasted_text || ""; + } + document.getElementById(pasteUrlSectionConstant).style.display = "block"; + } +} + +async function deriveKey(password, salt) { + const encodedPassword = new TextEncoder().encode(password); + const baseKey = await window.crypto.subtle.importKey( + "raw", + encodedPassword, + { name: "PBKDF2" }, + false, + ["deriveKey"] + ); + + const derivedKey = await window.crypto.subtle.deriveKey( + { + name: "PBKDF2", + salt: salt, + iterations: 600000, + hash: "SHA-256", + }, + baseKey, + { name: "AES-GCM", length: 256 }, + true, + ["encrypt", "decrypt"] + ); + + return derivedKey; +} + +async function encryptData(data, password) { + const salt = window.crypto.getRandomValues(new Uint8Array(16)); + const iv = window.crypto.getRandomValues(new Uint8Array(12)); + const key = await deriveKey(password, salt); + const encodedData = new TextEncoder().encode(data); + + const encryptedContent = await window.crypto.subtle.encrypt( + { + name: "AES-GCM", + iv: iv, + tagLength: 128, + }, + key, + encodedData + ); + + const ciphertext = encryptedContent.slice(0, encryptedContent.byteLength - 16); + const authTag = encryptedContent.slice(encryptedContent.byteLength - 16); + + return { + ciphertext: new Uint8Array(ciphertext), + iv: iv, + authTag: new Uint8Array(authTag), + salt: salt, + }; +} + +async function decryptData(encryptedData, password) { + const { ciphertext, iv, authTag, salt } = encryptedData; + const key = await deriveKey(password, salt); + + const dataWithAuthTag = new Uint8Array(ciphertext.length + authTag.length); + dataWithAuthTag.set(ciphertext, 0); + dataWithAuthTag.set(authTag, ciphertext.length); + + const decryptedContent = await window.crypto.subtle.decrypt( + { name: "AES-GCM", iv: iv, tagLength: 128 }, + key, + dataWithAuthTag + ); + + return new TextDecoder().decode(decryptedContent); +} + +async function handlePaste() { + const type = document.getElementById(pasteTypeConstant).value; + const title = document.getElementById(titleConstant).value; + const expiration = document.getElementById(expirationConstant).value; + const plaintext = document.getElementById(plaintextConstant).value; + const password = document.getElementById(passwordTypeConstant).value; + + let pasted_text = ""; + + if (type === plainTypeConstant) { + if (!plaintext || !title) return alert("Enter title and paste."); + pasted_text = plaintext; + } else { + if (!plaintext || !password || !title) return alert("Enter title, paste and password."); + const encrypted = await encryptData(plaintext, password); + pasted_text = btoa(JSON.stringify(encrypted)); + } + + const currentPath = window.location.origin; + + try { + const response = await fetch(currentPath + "/submit", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + type: type, + title: title, + expiration: expiration, + pasted_text: pasted_text, + }), + }); + + if (response.ok) { + const jsonResponse = await response.json(); + document.getElementById(pasteUrlConstant).value = currentPath + "/get?id=" + jsonResponse.id; + document.getElementById(pasteUrlSectionConstant).style.display = "block"; + } else { + console.error("Failed to submit data. Status:", response.status); + } + } catch (error) { + console.error("Error making the POST request:", error); + } +} + +async function handleDecrypt() { + const base64 = globalData.pasted_text; + const password = document.getElementById(passwordTypeConstant).value; + if (!base64 || !password) return alert("Enter encrypted text and password."); + + try { + const parsed = JSON.parse(atob(base64)); + const encryptedData = { + salt: new Uint8Array(Object.values(parsed.salt)), + iv: new Uint8Array(Object.values(parsed.iv)), + authTag: new Uint8Array(Object.values(parsed.authTag)), + ciphertext: new Uint8Array(Object.values(parsed.ciphertext)), + }; + + const decrypted = await decryptData(encryptedData, password); + document.getElementById(decryptedOutputConstant).value = decrypted; + document.getElementById(pasteUrlSectionConstant).style.display = "block"; + } catch (err) { + document.getElementById(decryptedOutputConstant).value = "❌ Decryption failed."; + } +} + +async function loadPasteFromUrl() { + const urlParams = new URLSearchParams(window.location.search); + const id = urlParams.get("id"); + + if (!id) return; + + const fetchUrl = `${window.location.origin}/paste?id=${id}`; + + try { + const response = await fetch(fetchUrl); + if (!response.ok) throw new Error("Failed to fetch data"); + + globalData = await response.json(); + togglePasteType(); + } catch (error) { + alert("No File Found"); + console.error("Error fetching data:", error); + } +} + +window.onload = async function () { + let path = window.location.pathname; + if (path != "/get") + return; + const urlParams = new URLSearchParams(window.location.search); + const id = urlParams.get('id'); + + const fetchUrl = `${window.location.origin}/paste?id=${id}`; + + try { + const response = await fetch(fetchUrl); + + if (!response.ok) { + throw new Error('Failed to fetch data'); + } + + const data = await response.json(); + + globalData = data; + togglePasteType() + } catch (error) { + alert("No File Found") + console.error('Error fetching data:', error); + } +}; \ No newline at end of file -- cgit v1.2.3