diff --git a/fpaste b/fpaste index dfdf2ea..96b8d98 100755 --- a/fpaste +++ b/fpaste @@ -227,57 +227,82 @@ def cmd_create(args, config): if not args.quiet: print(" done", file=sys.stderr) - headers = {} + # Build base headers (without PoW) + base_headers = {} if config["cert_sha1"]: - headers["X-SSL-Client-SHA1"] = config["cert_sha1"] + base_headers["X-SSL-Client-SHA1"] = config["cert_sha1"] # Add burn-after-read header if args.burn: - headers["X-Burn-After-Read"] = "true" + base_headers["X-Burn-After-Read"] = "true" # Add custom expiry header if args.expiry: - headers["X-Expiry"] = str(args.expiry) + base_headers["X-Expiry"] = str(args.expiry) # Add password header if args.password: - headers["X-Paste-Password"] = args.password - - # 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(" done", file=sys.stderr) - headers["X-PoW-Token"] = challenge["token"] - headers["X-PoW-Solution"] = str(solution) + base_headers["X-Paste-Password"] = args.password url = config["server"].rstrip("/") + "/" - status, body, _ = request( - url, method="POST", data=content, headers=headers, ssl_context=config.get("ssl_context") - ) + max_retries = 5 + last_error = None - if status == 201: - data = json.loads(body) - # Append encryption key to URL fragment if encrypted - key_fragment = "" - if encryption_key: - key_fragment = "#" + encode_key(encryption_key) + for attempt in range(max_retries): + headers = dict(base_headers) - if args.raw: - print(config["server"].rstrip("/") + data["raw"] + key_fragment) - elif args.quiet: - print(data["id"] + key_fragment) - else: - print(config["server"].rstrip("/") + data["url"] + key_fragment) - else: + # Get and solve PoW challenge if required + challenge = get_challenge(config) + if challenge: + if attempt > 0 and not args.quiet: + print(f"retry {attempt}/{max_retries - 1}...", file=sys.stderr) + if not args.quiet: + diff = challenge["difficulty"] + print(f"solving pow (difficulty={diff})...", end="", file=sys.stderr) + solution = solve_pow(challenge["nonce"], challenge["difficulty"]) + if not args.quiet: + print(" done", file=sys.stderr) + headers["X-PoW-Token"] = challenge["token"] + headers["X-PoW-Solution"] = str(solution) + + status, body, _ = request( + url, method="POST", data=content, headers=headers, ssl_context=config.get("ssl_context") + ) + + if status == 201: + data = json.loads(body) + # Append encryption key to URL fragment if encrypted + key_fragment = "" + if encryption_key: + key_fragment = "#" + encode_key(encryption_key) + + if args.raw: + print(config["server"].rstrip("/") + data["raw"] + key_fragment) + elif args.quiet: + print(data["id"] + key_fragment) + else: + print(config["server"].rstrip("/") + data["url"] + key_fragment) + return + + # Parse error try: - err = json.loads(body).get("error", body.decode()) + last_error = json.loads(body).get("error", body.decode()) except (json.JSONDecodeError, UnicodeDecodeError): - err = body.decode(errors="replace") - die(f"create failed ({status}): {err}") + last_error = body.decode(errors="replace") + + # Check if PoW-related error (worth retrying) + err_lower = last_error.lower() + is_pow_error = status == 400 and ("pow" in err_lower or "proof-of-work" in err_lower) + if not is_pow_error: + # Non-PoW error, don't retry + die(f"create failed ({status}): {last_error}") + + # PoW error, will retry if attempts remain + if not args.quiet: + print(f"pow rejected: {last_error}", file=sys.stderr) + + # All retries exhausted + die(f"create failed after {max_retries} attempts: {last_error}") def cmd_get(args, config):