fix: use mTLS client cert to bypass PoW on flaskpaste
When secrets/flaskpaste/derp.crt and derp.key are present, load them into the SSL context for mutual TLS auth and skip the PoW challenge entirely. Fall back to PoW only when no client cert is available.
This commit is contained in:
@@ -862,9 +862,8 @@ Output format:
|
||||
https://paste.mymx.me/abc12345
|
||||
```
|
||||
|
||||
- PoW challenge (difficulty 20) solved per request
|
||||
- mTLS client cert skips PoW; falls back to PoW challenge if no cert
|
||||
- Content sent as JSON body to FlaskPaste API
|
||||
- No API key needed -- PoW is the auth mechanism
|
||||
- Raw content available at `<paste_url>/raw`
|
||||
|
||||
### `!shorten` -- Shorten URL
|
||||
@@ -882,7 +881,7 @@ https://paste.mymx.me/s/AbCdEfGh
|
||||
```
|
||||
|
||||
- URL must start with `http://` or `https://`
|
||||
- PoW challenge (difficulty 20) solved per request
|
||||
- mTLS client cert skips PoW; falls back to PoW challenge if no cert
|
||||
- Also used internally by `!alert` to shorten announcement URLs
|
||||
|
||||
### FlaskPaste Configuration
|
||||
@@ -892,4 +891,5 @@ https://paste.mymx.me/s/AbCdEfGh
|
||||
url = "https://paste.mymx.me" # or set FLASKPASTE_URL env var
|
||||
```
|
||||
|
||||
TLS: custom CA cert at `secrets/flaskpaste/derp.crt` loaded automatically.
|
||||
Auth: place client cert/key at `secrets/flaskpaste/derp.crt` and `derp.key`
|
||||
for mTLS (bypasses PoW). Without them, PoW challenges are solved per request.
|
||||
|
||||
@@ -29,13 +29,18 @@ def _get_base_url(bot) -> str:
|
||||
return url.rstrip("/")
|
||||
|
||||
|
||||
def _has_client_cert() -> bool:
|
||||
"""Check if mTLS client cert and key are available."""
|
||||
return (_CERT_DIR / "derp.crt").exists() and (_CERT_DIR / "derp.key").exists()
|
||||
|
||||
|
||||
def _ssl_context() -> ssl.SSLContext:
|
||||
"""Build SSL context with custom CA cert if available."""
|
||||
"""Build SSL context, loading client cert for mTLS if available."""
|
||||
ctx = ssl.create_default_context()
|
||||
cert_path = _CERT_DIR / "derp.crt"
|
||||
if cert_path.exists():
|
||||
ctx = ssl.create_default_context(cafile=str(cert_path))
|
||||
else:
|
||||
ctx = ssl.create_default_context()
|
||||
key_path = _CERT_DIR / "derp.key"
|
||||
if cert_path.exists() and key_path.exists():
|
||||
ctx.load_cert_chain(str(cert_path), str(key_path))
|
||||
return ctx
|
||||
|
||||
|
||||
@@ -74,55 +79,45 @@ def _get_challenge(base_url: str) -> dict:
|
||||
return json.loads(resp.read())
|
||||
|
||||
|
||||
def _create_paste(base_url: str, content: str) -> str:
|
||||
"""Challenge + solve + POST / to create a paste. Returns paste URL."""
|
||||
def _pow_headers(base_url: str) -> dict:
|
||||
"""Solve PoW challenge and return auth headers. Empty dict if mTLS."""
|
||||
if _has_client_cert():
|
||||
return {}
|
||||
ch = _get_challenge(base_url)
|
||||
nonce = ch["nonce"]
|
||||
difficulty = ch["difficulty"]
|
||||
token = ch["token"]
|
||||
solution = _solve_pow(nonce, difficulty)
|
||||
solution = _solve_pow(ch["nonce"], ch["difficulty"])
|
||||
return {
|
||||
"X-PoW-Token": ch["token"],
|
||||
"X-PoW-Solution": str(solution),
|
||||
}
|
||||
|
||||
|
||||
def _create_paste(base_url: str, content: str) -> str:
|
||||
"""POST / to create a paste. Uses mTLS or PoW. Returns paste URL."""
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "derp-bot",
|
||||
**_pow_headers(base_url),
|
||||
}
|
||||
data = json.dumps({"content": content}).encode()
|
||||
req = urllib.request.Request(
|
||||
base_url,
|
||||
data=data,
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"X-PoW-Token": token,
|
||||
"X-PoW-Solution": str(solution),
|
||||
"User-Agent": "derp-bot",
|
||||
},
|
||||
)
|
||||
req = urllib.request.Request(base_url, data=data, headers=headers)
|
||||
ctx = _ssl_context()
|
||||
with urllib.request.urlopen(req, timeout=_TIMEOUT, context=ctx) as resp:
|
||||
body = json.loads(resp.read())
|
||||
# Response should contain the paste URL or ID
|
||||
paste_id = body.get("id", "")
|
||||
if paste_id:
|
||||
return f"{base_url}/{paste_id}"
|
||||
# Fallback: check for url field
|
||||
return body.get("url", "")
|
||||
|
||||
|
||||
def _shorten_url(base_url: str, url: str) -> str:
|
||||
"""Challenge + solve + POST /s to shorten a URL. Returns short URL."""
|
||||
ch = _get_challenge(base_url)
|
||||
nonce = ch["nonce"]
|
||||
difficulty = ch["difficulty"]
|
||||
token = ch["token"]
|
||||
solution = _solve_pow(nonce, difficulty)
|
||||
|
||||
"""POST /s to shorten a URL. Uses mTLS or PoW. Returns short URL."""
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "derp-bot",
|
||||
**_pow_headers(base_url),
|
||||
}
|
||||
data = json.dumps({"url": url}).encode()
|
||||
req = urllib.request.Request(
|
||||
f"{base_url}/s",
|
||||
data=data,
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"X-PoW-Token": token,
|
||||
"X-PoW-Solution": str(solution),
|
||||
"User-Agent": "derp-bot",
|
||||
},
|
||||
)
|
||||
req = urllib.request.Request(f"{base_url}/s", data=data, headers=headers)
|
||||
ctx = _ssl_context()
|
||||
with urllib.request.urlopen(req, timeout=_TIMEOUT, context=ctx) as resp:
|
||||
body = json.loads(resp.read())
|
||||
|
||||
Reference in New Issue
Block a user