diff --git a/app/api/routes.py b/app/api/routes.py index 807e0cf..3eb8931 100644 --- a/app/api/routes.py +++ b/app/api/routes.py @@ -469,6 +469,7 @@ class IndexView(MethodView): f"GET {prefixed_url('/')}": "API information", f"GET {prefixed_url('/health')}": "Health check", f"GET {prefixed_url('/client')}": "Download CLI client", + f"GET {prefixed_url('/solver')}": "Download PoW solver script", f"GET {prefixed_url('/challenge')}": "Get PoW challenge", f"POST {prefixed_url('/')}": "Create paste", f"GET {prefixed_url('/pastes')}": "List your pastes (auth required)", @@ -756,6 +757,91 @@ class ClientView(MethodView): return error_response("Client not available", 404) +class SolverView(MethodView): + """PoW solver script download endpoint.""" + + def get(self) -> Response: + """Serve a Python script for solving PoW challenges.""" + server_url = base_url() + script = f'''#!/usr/bin/env python3 +"""FlaskPaste PoW solver - solves challenges for curl-based uploads.""" +import hashlib +import json +import sys +import urllib.request + +SERVER = "{server_url}" + + +def get_challenge(): + """Fetch a new challenge from the server.""" + with urllib.request.urlopen(f"{{SERVER}}/challenge") as r: + return json.load(r) + + +def solve(nonce: str, difficulty: int) -> int: + """Find solution where sha256(nonce:solution) has sufficient leading zero bits.""" + i = 0 + while True: + h = hashlib.sha256(f"{{nonce}}:{{i}}".encode()).digest() + zero_bits = 0 + for byte in h: + if byte == 0: + zero_bits += 8 + else: + zero_bits += 8 - byte.bit_length() + break + if zero_bits >= difficulty: + return i + i += 1 + + +def main(): + if len(sys.argv) < 2: + print("Usage: fpaste-solver ", file=sys.stderr) + print(" cat data | fpaste-solver -", file=sys.stderr) + sys.exit(1) + + # Read content + if sys.argv[1] == "-": + content = sys.stdin.buffer.read() + else: + with open(sys.argv[1], "rb") as f: + content = f.read() + + # Get and solve challenge + ch = get_challenge() + if not ch.get("enabled", True): + token, solution = "", "" + else: + print(f"Solving PoW (difficulty={{ch['difficulty']}})...", file=sys.stderr) + solution = solve(ch["nonce"], ch["difficulty"]) + token = ch["token"] + print(f"Solved: {{solution}}", file=sys.stderr) + + # Upload + req = urllib.request.Request( + f"{{SERVER}}/", + data=content, + headers={{ + "Content-Type": "application/octet-stream", + "X-PoW-Token": token, + "X-PoW-Solution": str(solution), + }}, + ) + with urllib.request.urlopen(req) as r: + result = json.load(r) + print(result.get("raw", result.get("url", json.dumps(result)))) + + +if __name__ == "__main__": + main() +''' + response = Response(script, mimetype="text/x-python") + response.headers["Content-Disposition"] = "attachment; filename=fpaste-solver" + return response + + class PasteView(MethodView): """Paste metadata operations.""" @@ -1399,6 +1485,7 @@ bp.add_url_rule("/", view_func=IndexView.as_view("index")) bp.add_url_rule("/health", view_func=HealthView.as_view("health")) bp.add_url_rule("/challenge", view_func=ChallengeView.as_view("challenge")) bp.add_url_rule("/client", view_func=ClientView.as_view("client")) +bp.add_url_rule("/solver", view_func=SolverView.as_view("solver")) # Paste operations bp.add_url_rule("/pastes", view_func=PastesListView.as_view("pastes_list"))