summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-cli/src/meshbay_cli/client/invite.py
blob: 6b26c9706c15d70487955834cf21a8343bae5b31 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
"""
Invitation links: `https://<hub>/#/invite?v=1&g=<group>&t=<ticket>&n=<node key>&c=<CODE>`.

The shape `static/invite-link.js` reads and `api/invite_links.py` writes
(docs/MESHBAY_DESIGN.md §3.4). Two secrets: the ticket is for the hub, which
makes the first account to redeem it a member; the code is for the node the
link names, and goes to no other node and never to the hub. Every field is
checked for its exact shape, as the application checks it.
"""

import re
from dataclasses import dataclass
from urllib.parse import parse_qs, urlsplit

_GROUP = re.compile(r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$")
_TICKET = re.compile(r"^[A-Za-z0-9_-]{22}$")
_NODE_PK = re.compile(r"^[A-Za-z0-9_-]{43}$")
_CODE = re.compile(r"^[0-9A-Za-z]{4}-[0-9A-Za-z]{4}$")


@dataclass
class InviteLink:
    origin: str        # the hub the link was made on, '' when only the fragment was given
    group_id: str
    ticket: str
    node_pk: str       # standard base64, as the hub lists node keys
    code: str


def node_pk_from_link(n: str) -> str:
    """The link's URL-safe, unpadded key, as the hub and the transport write it."""
    b64 = n.replace("-", "+").replace("_", "/")
    return b64 + "=" * (-len(b64) % 4)


def parse_link(text: str) -> InviteLink | None:
    """The link in `text`, or None when it holds none (or a malformed one)."""
    s = text.strip()
    at = s.find("/invite?")
    if at < 0:
        return None
    q = {k: v[0] for k, v in parse_qs(s[at + len("/invite?"):]).items()}
    g, t, n = q.get("g", ""), q.get("t", ""), q.get("n", "")
    c = q.get("c", "").upper()
    if q.get("v") != "1" or not (_GROUP.match(g) and _TICKET.match(t)
                                 and _NODE_PK.match(n) and _CODE.match(c)):
        return None
    parts = urlsplit(s)
    origin = f"{parts.scheme}://{parts.netloc}" if parts.scheme and parts.netloc else ""
    return InviteLink(origin, g, t, node_pk_from_link(n), c)