* Extract suffix generation and validation to a module * Updated tests * Make custom alias use signed suffixes * Added the signature check to the module * Fix invalid route * Move more suffix related stuff * Fix tests Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
163 lines
5.4 KiB
Python
163 lines
5.4 KiB
Python
from __future__ import annotations
|
|
import json
|
|
from dataclasses import asdict, dataclass
|
|
from typing import Optional
|
|
|
|
import itsdangerous
|
|
from app import config
|
|
from app.log import LOG
|
|
from app.models import User
|
|
|
|
|
|
signer = itsdangerous.TimestampSigner(config.CUSTOM_ALIAS_SECRET)
|
|
|
|
|
|
@dataclass
|
|
class AliasSuffix:
|
|
# whether this is a custom domain
|
|
is_custom: bool
|
|
# Suffix
|
|
suffix: str
|
|
# Suffix signature
|
|
signed_suffix: str
|
|
# whether this is a premium SL domain. Not apply to custom domain
|
|
is_premium: bool
|
|
# can be either Custom or SL domain
|
|
domain: str
|
|
# if custom domain, whether the custom domain has MX verified, i.e. can receive emails
|
|
mx_verified: bool = True
|
|
|
|
def serialize(self):
|
|
return json.dumps(asdict(self))
|
|
|
|
@classmethod
|
|
def deserialize(cls, data: str) -> AliasSuffix:
|
|
return AliasSuffix(**json.loads(data))
|
|
|
|
|
|
def check_suffix_signature(signed_suffix: str) -> Optional[str]:
|
|
# hypothesis: user will click on the button in the 600 secs
|
|
try:
|
|
return signer.unsign(signed_suffix, max_age=600).decode()
|
|
except itsdangerous.BadSignature:
|
|
return None
|
|
|
|
|
|
def verify_prefix_suffix(user: User, alias_prefix, alias_suffix) -> bool:
|
|
"""verify if user could create an alias with the given prefix and suffix"""
|
|
if not alias_prefix or not alias_suffix: # should be caught on frontend
|
|
return False
|
|
|
|
user_custom_domains = [cd.domain for cd in user.verified_custom_domains()]
|
|
|
|
# make sure alias_suffix is either .random_word@simplelogin.co or @my-domain.com
|
|
alias_suffix = alias_suffix.strip()
|
|
# alias_domain_prefix is either a .random_word or ""
|
|
alias_domain_prefix, alias_domain = alias_suffix.split("@", 1)
|
|
|
|
# alias_domain must be either one of user custom domains or built-in domains
|
|
if alias_domain not in user.available_alias_domains():
|
|
LOG.e("wrong alias suffix %s, user %s", alias_suffix, user)
|
|
return False
|
|
|
|
# SimpleLogin domain case:
|
|
# 1) alias_suffix must start with "." and
|
|
# 2) alias_domain_prefix must come from the word list
|
|
if (
|
|
alias_domain in user.available_sl_domains()
|
|
and alias_domain not in user_custom_domains
|
|
# when DISABLE_ALIAS_SUFFIX is true, alias_domain_prefix is empty
|
|
and not config.DISABLE_ALIAS_SUFFIX
|
|
):
|
|
|
|
if not alias_domain_prefix.startswith("."):
|
|
LOG.e("User %s submits a wrong alias suffix %s", user, alias_suffix)
|
|
return False
|
|
|
|
else:
|
|
if alias_domain not in user_custom_domains:
|
|
if not config.DISABLE_ALIAS_SUFFIX:
|
|
LOG.e("wrong alias suffix %s, user %s", alias_suffix, user)
|
|
return False
|
|
|
|
if alias_domain not in user.available_sl_domains():
|
|
LOG.e("wrong alias suffix %s, user %s", alias_suffix, user)
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
def get_alias_suffixes(user: User) -> [AliasSuffix]:
|
|
"""
|
|
Similar to as get_available_suffixes() but also return custom domain that doesn't have MX set up.
|
|
"""
|
|
user_custom_domains = user.verified_custom_domains()
|
|
|
|
alias_suffixes: [AliasSuffix] = []
|
|
|
|
# put custom domain first
|
|
# for each user domain, generate both the domain and a random suffix version
|
|
for custom_domain in user_custom_domains:
|
|
if custom_domain.random_prefix_generation:
|
|
suffix = "." + user.get_random_alias_suffix() + "@" + custom_domain.domain
|
|
alias_suffix = AliasSuffix(
|
|
is_custom=True,
|
|
suffix=suffix,
|
|
signed_suffix=signer.sign(suffix).decode(),
|
|
is_premium=False,
|
|
domain=custom_domain.domain,
|
|
mx_verified=custom_domain.verified,
|
|
)
|
|
if user.default_alias_custom_domain_id == custom_domain.id:
|
|
alias_suffixes.insert(0, alias_suffix)
|
|
else:
|
|
alias_suffixes.append(alias_suffix)
|
|
|
|
suffix = "@" + custom_domain.domain
|
|
alias_suffix = AliasSuffix(
|
|
is_custom=True,
|
|
suffix=suffix,
|
|
signed_suffix=signer.sign(suffix).decode(),
|
|
is_premium=False,
|
|
domain=custom_domain.domain,
|
|
mx_verified=custom_domain.verified,
|
|
)
|
|
|
|
# put the default domain to top
|
|
# only if random_prefix_generation isn't enabled
|
|
if (
|
|
user.default_alias_custom_domain_id == custom_domain.id
|
|
and not custom_domain.random_prefix_generation
|
|
):
|
|
alias_suffixes.insert(0, alias_suffix)
|
|
else:
|
|
alias_suffixes.append(alias_suffix)
|
|
|
|
# then SimpleLogin domain
|
|
for sl_domain in user.get_sl_domains():
|
|
suffix = (
|
|
(
|
|
""
|
|
if config.DISABLE_ALIAS_SUFFIX
|
|
else "." + user.get_random_alias_suffix()
|
|
)
|
|
+ "@"
|
|
+ sl_domain.domain
|
|
)
|
|
alias_suffix = AliasSuffix(
|
|
is_custom=False,
|
|
suffix=suffix,
|
|
signed_suffix=signer.sign(suffix).decode(),
|
|
is_premium=sl_domain.premium_only,
|
|
domain=sl_domain.domain,
|
|
mx_verified=True,
|
|
)
|
|
|
|
# put the default domain to top
|
|
if user.default_alias_public_domain_id == sl_domain.id:
|
|
alias_suffixes.insert(0, alias_suffix)
|
|
else:
|
|
alias_suffixes.append(alias_suffix)
|
|
|
|
return alias_suffixes
|