import base64
import hashlib
import hmac
import json
import os
import time
from pathlib import Path
import boto3
BUCKET = os.environ["BUCKET"]
TABLE = os.environ["TABLE"]
MAX_PIN_ATTEMPTS = 5
MAX_UPLOAD_ATTEMPTS = 10
MAX_UPLOAD = 512 * 1024 * 1024
_CORS = {"access-control-allow-origin": "*"}
_ddb = boto3.client("dynamodb")
_s3 = boto3.client("s3")
_ssm = boto3.client("ssm")
def _load_gate_secret():
name = os.environ.get("GATE_SECRET_PARAM")
if name:
try:
value = _ssm.get_parameter(Name=name, WithDecryption=True)["Parameter"]["Value"]
return bytes.fromhex(value)
except Exception: return None
raw = os.environ.get("GATE_SECRET")
return bytes.fromhex(raw) if raw else None
GATE_SECRET = _load_gate_secret()
PAGE = (Path(__file__).parent / "share.html").read_text()
REQUEST_PAGE = (Path(__file__).parent / "request.html").read_text()
OG_PNG = (Path(__file__).parent / "og.png").read_bytes()
OG_REQUEST_PNG = (Path(__file__).parent / "og-request.png").read_bytes()
def _png(data):
return {
"statusCode": 200,
"headers": {"content-type": "image/png", "cache-control": "public, max-age=86400"},
"body": base64.b64encode(data).decode(),
"isBase64Encoded": True,
}
def _resp(status, body="", content_type="text/plain; charset=utf-8", extra=None):
headers = {"content-type": content_type}
if extra:
headers.update(extra)
return {"statusCode": status, "headers": headers, "body": body}
def _origin(event):
dom = event.get("requestContext", {}).get("domainName", "")
return f"https://{dom}" if dom else ""
def _valid_id(share_id):
if GATE_SECRET is None:
return True if len(share_id) != 32:
return False
try:
raw = bytes.fromhex(share_id)
except ValueError:
return False
nonce, mac = raw[:8], raw[8:]
expected = hmac.new(GATE_SECRET, nonce, hashlib.sha256).digest()[:8]
return hmac.compare_digest(mac, expected)
def handler(event, _context):
raw = event.get("rawPath", "")
if raw == "/og.png":
return _png(OG_PNG) if raw == "/og-request.png":
return _png(OG_REQUEST_PNG) parts = [p for p in raw.split("/") if p]
if len(parts) < 2:
return _resp(404, "not a dove share link")
route, share_id = parts[0], parts[1]
if route in ("meta", "verify", "dl", "rmeta", "up", "done") and not _valid_id(share_id):
return _resp(403, "not a dove share link")
if route == "d":
page = PAGE.replace("__OGBASE__", _origin(event))
return _resp(200, page, "text/html; charset=utf-8")
if route == "r":
page = REQUEST_PAGE.replace("__OGBASE__", _origin(event))
return _resp(200, page, "text/html; charset=utf-8")
if route == "meta":
return _meta(share_id)
if route == "verify":
params = event.get("queryStringParameters") or {}
return _verify(share_id, params.get("pin"))
if route == "dl":
params = event.get("queryStringParameters") or {}
return _download(share_id, params.get("pin"))
if route == "rmeta":
return _rmeta(share_id)
if route == "up":
params = event.get("queryStringParameters") or {}
return _up(share_id, params.get("pin"))
if route == "done":
params = event.get("queryStringParameters") or {}
body = event.get("body") or ""
if event.get("isBase64Encoded"):
try:
body = base64.b64decode(body).decode()
except Exception: body = ""
return _done(share_id, params.get("pin"), body)
return _resp(404, "not a dove share link")
def _meta(share_id):
item = _ddb.get_item(TableName=TABLE, Key={"id": {"S": share_id}}).get("Item")
if not item:
return _resp(404, json.dumps({"error": "not found"}), "application/json")
size = int(item.get("size", {}).get("N", "0"))
if not size: try:
size = _s3.head_object(Bucket=BUCKET, Key=item["s3_key"]["S"])["ContentLength"]
except Exception: pass
pin_required = "pin_hash" in item
locked = pin_required and int(item.get("pin_attempts", {}).get("N", "0")) >= MAX_PIN_ATTEMPTS
body = json.dumps(
{
"downloads_remaining": int(item["downloads_remaining"]["N"]),
"downloads_total": int(item.get("downloads_total", {}).get("N", "0")),
"expires_at": int(item["expires_at"]["N"]),
"size": size,
"meta": item.get("meta", {}).get("S", ""),
"pin_required": pin_required,
"locked": locked,
}
)
return _resp(200, body, "application/json", {"access-control-allow-origin": "*"})
def _pin_gate(share_id, item, pin):
pin_hash = item.get("pin_hash", {}).get("S")
if not pin_hash:
return None attempts = int(item.get("pin_attempts", {}).get("N", "0"))
if attempts >= MAX_PIN_ATTEMPTS:
return _resp(423, json.dumps({"error": "locked"}), "application/json")
if not pin:
return _resp(401, json.dumps({"error": "pin required"}), "application/json")
if hashlib.sha256(f"{share_id}:{pin}".encode()).hexdigest() == pin_hash:
return None try:
res = _ddb.update_item(
TableName=TABLE,
Key={"id": {"S": share_id}},
UpdateExpression="SET pin_attempts = if_not_exists(pin_attempts, :z) + :one",
ConditionExpression="attribute_not_exists(pin_attempts) OR pin_attempts < :max",
ExpressionAttributeValues={
":one": {"N": "1"},
":z": {"N": "0"},
":max": {"N": str(MAX_PIN_ATTEMPTS)},
},
ReturnValues="ALL_NEW",
)
remaining = max(0, MAX_PIN_ATTEMPTS - int(res["Attributes"]["pin_attempts"]["N"]))
except _ddb.exceptions.ConditionalCheckFailedException:
return _resp(423, json.dumps({"error": "locked"}), "application/json")
status = 423 if remaining == 0 else 401
error = "locked" if remaining == 0 else "wrong pin"
return _resp(
status,
json.dumps({"error": error, "attempts_remaining": remaining}),
"application/json",
)
def _verify(share_id, pin):
now = int(time.time())
item = _ddb.get_item(TableName=TABLE, Key={"id": {"S": share_id}}).get("Item")
if item and item.get("kind", {}).get("S") == "request":
if (
int(item["expires_at"]["N"]) <= now
or int(item.get("uploads_remaining", {}).get("N", "0")) <= 0
or "s3_key" in item
):
return _resp(410, json.dumps({"error": "gone"}), "application/json")
elif not item or int(item["expires_at"]["N"]) <= now or int(item["downloads_remaining"]["N"]) <= 0:
return _resp(410, json.dumps({"error": "gone"}), "application/json")
gate = _pin_gate(share_id, item, pin)
if gate is not None:
return gate
return _resp(200, json.dumps({"ok": True}), "application/json")
def _download(share_id, pin):
now = int(time.time())
item = _ddb.get_item(TableName=TABLE, Key={"id": {"S": share_id}}).get("Item")
if not item:
return _resp(410, "this share has expired or reached its download limit")
if int(item["expires_at"]["N"]) <= now or int(item["downloads_remaining"]["N"]) <= 0:
return _resp(410, "this share has expired or reached its download limit")
gate = _pin_gate(share_id, item, pin)
if gate is not None:
return gate
try:
result = _ddb.update_item(
TableName=TABLE,
Key={"id": {"S": share_id}},
UpdateExpression="SET downloads_remaining = downloads_remaining - :one",
ConditionExpression=(
"attribute_exists(id) "
"AND downloads_remaining > :zero "
"AND expires_at > :now"
),
ExpressionAttributeValues={
":one": {"N": "1"},
":zero": {"N": "0"},
":now": {"N": str(now)},
},
ReturnValues="ALL_NEW",
)
except _ddb.exceptions.ConditionalCheckFailedException:
return _resp(410, "this share has expired or reached its download limit")
except Exception: return _resp(500, "the gate hit an error")
s3_key = result["Attributes"]["s3_key"]["S"]
presigned = _s3.generate_presigned_url(
"get_object",
Params={"Bucket": BUCKET, "Key": s3_key},
ExpiresIn=900,
)
return _resp(302, "", extra={"location": presigned})
def _rmeta(request_id):
now = int(time.time())
item = _ddb.get_item(TableName=TABLE, Key={"id": {"S": request_id}}).get("Item")
if not item:
return _resp(404, json.dumps({"error": "not found"}), "application/json", _CORS)
pin_required = "pin_hash" in item
pin_attempts = int(item.get("pin_attempts", {}).get("N", "0"))
upload_attempts = int(item.get("upload_attempts", {}).get("N", "0"))
expires_at = int(item["expires_at"]["N"])
received = "s3_key" in item
locked = pin_attempts >= MAX_PIN_ATTEMPTS
reason = None
if received:
status = "received"
elif pin_attempts >= MAX_PIN_ATTEMPTS:
status, reason = "failed", "locked"
elif upload_attempts >= MAX_UPLOAD_ATTEMPTS:
status, reason = "failed", "rate-limited"
elif expires_at <= now:
status, reason = "failed", "expired"
else:
status = "waiting"
body = {
"meta": item.get("meta", {}).get("S", ""),
"expires_at": expires_at,
"uploads_remaining": int(item.get("uploads_remaining", {}).get("N", "0")),
"pin_required": pin_required,
"locked": locked,
"status": status,
"reason": reason,
}
if received:
body["name_meta"] = item.get("upload_meta", {}).get("S", "")
body["size"] = int(item.get("size", {}).get("N", "0"))
return _resp(200, json.dumps(body), "application/json", _CORS)
def _up(request_id, pin):
now = int(time.time())
item = _ddb.get_item(TableName=TABLE, Key={"id": {"S": request_id}}).get("Item")
if not item or item.get("kind", {}).get("S") != "request":
return _resp(404, json.dumps({"error": "not found"}), "application/json", _CORS)
if (
int(item["expires_at"]["N"]) <= now
or "s3_key" in item or int(item.get("uploads_remaining", {}).get("N", "0")) <= 0
):
return _resp(410, json.dumps({"error": "gone"}), "application/json", _CORS)
gate = _pin_gate(request_id, item, pin)
if gate is not None:
return gate
try:
_ddb.update_item(
TableName=TABLE,
Key={"id": {"S": request_id}},
UpdateExpression="SET upload_attempts = if_not_exists(upload_attempts, :z) + :one",
ConditionExpression="attribute_not_exists(upload_attempts) OR upload_attempts < :max",
ExpressionAttributeValues={
":one": {"N": "1"},
":z": {"N": "0"},
":max": {"N": str(MAX_UPLOAD_ATTEMPTS)},
},
)
except _ddb.exceptions.ConditionalCheckFailedException:
return _resp(429, json.dumps({"error": "rate-limited"}), "application/json", _CORS)
key = f"req/{request_id}"
post = _s3.generate_presigned_post(
Bucket=BUCKET,
Key=key,
Conditions=[["content-length-range", 0, MAX_UPLOAD]],
ExpiresIn=900,
)
body = json.dumps({"url": post["url"], "fields": post["fields"], "key": key})
return _resp(200, body, "application/json", _CORS)
def _done(request_id, pin, body_meta):
now = int(time.time())
item = _ddb.get_item(TableName=TABLE, Key={"id": {"S": request_id}}).get("Item")
if not item or item.get("kind", {}).get("S") != "request":
return _resp(404, json.dumps({"error": "not found"}), "application/json", _CORS)
gate = _pin_gate(request_id, item, pin)
if gate is not None:
return gate
key = f"req/{request_id}"
try:
head = _s3.head_object(Bucket=BUCKET, Key=key)
except Exception: return _resp(410, json.dumps({"error": "no upload found"}), "application/json", _CORS)
size = int(head.get("ContentLength", 0))
upload_meta = ""
try:
upload_meta = (json.loads(body_meta) or {}).get("upload_meta", "") if body_meta else ""
except Exception: upload_meta = ""
try:
_ddb.update_item(
TableName=TABLE,
Key={"id": {"S": request_id}},
UpdateExpression=(
"SET s3_key = :k, size = :sz, upload_meta = :um, received_at = :now, "
"uploads_remaining = uploads_remaining - :one"
),
ConditionExpression="attribute_not_exists(s3_key)",
ExpressionAttributeValues={
":k": {"S": key},
":sz": {"N": str(size)},
":um": {"S": upload_meta},
":now": {"N": str(now)},
":one": {"N": "1"},
},
)
except _ddb.exceptions.ConditionalCheckFailedException:
return _resp(200, json.dumps({"ok": True}), "application/json", _CORS)
return _resp(200, json.dumps({"ok": True}), "application/json", _CORS)