Clients must solve a SHA256 hash puzzle before paste creation. Configurable via FLASKPASTE_POW_DIFFICULTY (0 = disabled, 16 = default). Challenge tokens expire after FLASKPASTE_POW_TTL seconds (default 300).
This commit is contained in:
59
fpaste
59
fpaste
@@ -2,6 +2,7 @@
|
||||
"""FlaskPaste command-line client."""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
@@ -54,6 +55,53 @@ def die(msg, code=1):
|
||||
sys.exit(code)
|
||||
|
||||
|
||||
def solve_pow(nonce, difficulty):
|
||||
"""Solve proof-of-work challenge.
|
||||
|
||||
Find a number N such that SHA256(nonce:N) has `difficulty` leading zero bits.
|
||||
"""
|
||||
n = 0
|
||||
target_bytes = (difficulty + 7) // 8 # Bytes to check
|
||||
|
||||
while True:
|
||||
work = f"{nonce}:{n}".encode()
|
||||
hash_bytes = hashlib.sha256(work).digest()
|
||||
|
||||
# Count leading zero bits
|
||||
zero_bits = 0
|
||||
for byte in hash_bytes[:target_bytes + 1]:
|
||||
if byte == 0:
|
||||
zero_bits += 8
|
||||
else:
|
||||
zero_bits += (8 - byte.bit_length())
|
||||
break
|
||||
|
||||
if zero_bits >= difficulty:
|
||||
return n
|
||||
|
||||
n += 1
|
||||
# Progress indicator for high difficulty
|
||||
if n % 100000 == 0:
|
||||
print(f"\rsolving pow: {n} attempts...", end="", file=sys.stderr)
|
||||
|
||||
return n
|
||||
|
||||
|
||||
def get_challenge(config):
|
||||
"""Fetch PoW challenge from server."""
|
||||
url = config["server"].rstrip("/") + "/challenge"
|
||||
status, body, _ = request(url)
|
||||
|
||||
if status != 200:
|
||||
return None
|
||||
|
||||
data = json.loads(body)
|
||||
if not data.get("enabled"):
|
||||
return None
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def cmd_create(args, config):
|
||||
"""Create a new paste."""
|
||||
# Read content from file or stdin
|
||||
@@ -78,6 +126,17 @@ def cmd_create(args, config):
|
||||
if config["cert_sha1"]:
|
||||
headers["X-SSL-Client-SHA1"] = config["cert_sha1"]
|
||||
|
||||
# Get and solve PoW challenge if required
|
||||
challenge = get_challenge(config)
|
||||
if challenge:
|
||||
if not args.quiet:
|
||||
print(f"solving pow (difficulty={challenge['difficulty']})...", end="", file=sys.stderr)
|
||||
solution = solve_pow(challenge["nonce"], challenge["difficulty"])
|
||||
if not args.quiet:
|
||||
print(f" done", file=sys.stderr)
|
||||
headers["X-PoW-Token"] = challenge["token"]
|
||||
headers["X-PoW-Solution"] = str(solution)
|
||||
|
||||
url = config["server"].rstrip("/") + "/"
|
||||
status, body, _ = request(url, method="POST", data=content, headers=headers)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user