Пример #1
0
    def update_delegation(self, child_domain: Domain):
        child_subname, child_domain_name = child_domain._partitioned_name
        if self.name != child_domain_name:
            raise ValueError(
                'Cannot update delegation of %s as it is not an immediate child domain of %s.'
                % (child_domain.name, self.name))

        if child_domain.pk:
            # Domain real: set delegation
            child_keys = child_domain.keys
            if not child_keys:
                raise APIException(
                    'Cannot delegate %s, as it currently has no keys.' %
                    child_domain.name)

            RRset.objects.create(domain=self,
                                 subname=child_subname,
                                 type='NS',
                                 ttl=3600,
                                 contents=settings.DEFAULT_NS)
            RRset.objects.create(
                domain=self,
                subname=child_subname,
                type='DS',
                ttl=300,
                contents=[ds for k in child_keys for ds in k['ds']])
            metrics.get('desecapi_autodelegation_created').inc()
        else:
            # Domain not real: remove delegation
            for rrset in self.rrset_set.filter(subname=child_subname,
                                               type__in=['NS', 'DS']):
                rrset.delete()
            metrics.get('desecapi_autodelegation_deleted').inc()
Пример #2
0
    def allow_request(self, request, view):
        # We can only determine the scope once we're called by the view.
        self.scope = getattr(view, self.scope_attr, None)

        # If a view does not have a `throttle_scope` always allow the request
        if not self.scope:
            return True

        # Determine the allowed request rate as we normally would during
        # the `__init__` call.
        self.rate = self.get_rate()
        if self.rate is None:
            return True

        self.now = self.timer()
        self.num_requests, self.duration = zip(*self.parse_rate(self.rate))
        self.key = self.get_cache_key(request, view)
        self.history = {key: [] for key in self.key}
        self.history.update(self.cache.get_many(self.key))

        for num_requests, duration, key in zip(self.num_requests, self.duration, self.key):
            history = self.history[key]
            # Drop any requests from the history which have now passed the
            # throttle duration
            while history and history[-1] <= self.now - duration:
                history.pop()
            if len(history) >= num_requests:
                # Prepare variables used by the Throttle's wait() method that gets called by APIView.check_throttles()
                self.num_requests, self.duration, self.key, self.history = num_requests, duration, key, history
                response = self.throttle_failure()
                metrics.get('desecapi_throttle_failure').labels(request.method, self.scope, request.user.pk).inc()
                return response
            self.history[key] = history
        return self.throttle_success()
Пример #3
0
def decrypt(token, *, context, ttl=None):
    key = retrieve_key(label=b'crypt', context=context)
    try:
        value = Fernet(key=key).decrypt(token, ttl=ttl)
        metrics.get('desecapi_key_decryption_success').labels(context).inc()
        return value
    except InvalidToken:
        raise ValueError
Пример #4
0
 def many_init(cls, *args, **kwargs):
     domain = kwargs.pop('domain')
     # Note: We are not yet deciding the value of the child's "partial" attribute, as its value depends on whether
     # the RRSet is created (never partial) or not (partial if PATCH), for each given item (RRset) individually.
     kwargs['child'] = cls(domain=domain)
     serializer = RRsetListSerializer(*args, **kwargs)
     metrics.get('desecapi_rrset_list_serializer').inc()
     return serializer
Пример #5
0
def exception_handler(exc, context):
    """
    desecapi specific exception handling. If no special treatment is applied,
    we default to restframework's exception handling. See also
    https://www.django-rest-framework.org/api-guide/exceptions/#custom-exception-handling
    """
    def _log():
        logger = logging.getLogger('django.request')
        logger.error('{} Supplementary Information'.format(exc.__class__),
                     exc_info=exc,
                     stack_info=False)

    def _500():
        _log()

        # Let clients know that there is a problem
        response = Response({'detail': 'Internal Server Error. We\'re on it!'},
                            status=status.HTTP_500_INTERNAL_SERVER_ERROR)
        return response

    def _503():
        _log()

        # Let clients know that there is a temporary problem
        response = Response({'detail': 'Please try again later.'},
                            status=status.HTTP_503_SERVICE_UNAVAILABLE)
        return response

    # Catch DB exception and log an extra error for additional context
    if (isinstance(exc, OperationalError)
            and isinstance(exc.args, (list, dict, tuple)) and exc.args
            and exc.args[0] in (
                2002,  # Connection refused (Socket)
                2003,  # Connection refused (TCP)
                2005,  # Unresolved host name
                2007,  # Server protocol mismatch
                2009,  # Wrong host info
                2026,  # SSL connection error
            )):
        metrics.get('desecapi_database_unavailable').inc()
        return _503()

    # OSError happens on system-related errors, like full disk or getaddrinfo() failure.
    if isinstance(exc, OSError):
        # TODO add metrics
        return _500()

    # The PSL encountered an unsupported rule
    if isinstance(exc, UnsupportedRule):
        # TODO add metrics
        return _500()

    # nslord/nsmaster returned an error
    if isinstance(exc, PDNSException):
        # TODO add metrics
        return _500()

    return drf_exception_handler(exc, context)
Пример #6
0
def get_keys(domain):
    """
    Retrieves a dict representation of the DNSSEC key information
    """
    r = _pdns_get(NSLORD, '/zones/%s/cryptokeys' % pdns_id(domain.name))
    metrics.get('desecapi_pdns_keys_fetched').inc()
    return [{k: key[k] for k in ('dnskey', 'ds', 'flags', 'keytype')}
            for key in r.json()
            if key['active'] and key['keytype'] in ['csk', 'ksk']]
    def _perform_handling(name):
        logger = logging.getLogger('django.request')
        logger.error('{} Supplementary Information'.format(name),
                     exc_info=exc,
                     stack_info=False)

        # Gracefully let clients know that we cannot connect to the database
        response = Response({'detail': 'Please try again later.'},
                            status=status.HTTP_503_SERVICE_UNAVAILABLE)
        metrics.get('desecapi_database_unavailable').inc()
        return response
Пример #8
0
def _pdns_request(method, *, server, path, data=None):
    if data is not None:
        data = json.dumps(data)
    if data is not None and len(data) > settings.PDNS_MAX_BODY_SIZE:
        raise RequestEntityTooLarge

    r = requests.request(method, _config[server]['base_url'] + path, data=data, headers=_config[server]['headers'])
    if r.status_code == PDNSValidationError.pdns_code:
        raise PDNSValidationError(response=r)
    elif r.status_code not in range(200, 300):
        raise PDNSException(response=r)
    metrics.get('desecapi_pdns_request_success').labels(method, r.status_code).inc()
    return r
Пример #9
0
def captcha_default_content(kind: str) -> str:
    if kind == Captcha.Kind.IMAGE:
        alphabet = (string.ascii_uppercase + string.digits).translate(
            {ord(c): None
             for c in 'IO0'})
        length = 5
    elif kind == Captcha.Kind.AUDIO:
        alphabet = string.digits
        length = 8
    else:
        raise ValueError(f'Unknown Captcha kind: {kind}')

    content = ''.join([secrets.choice(alphabet) for _ in range(length)])
    metrics.get('desecapi_captcha_content_created').labels(kind).inc()
    return content
Пример #10
0
    def send_email(self, reason, context=None, recipient=None):
        fast_lane = 'email_fast_lane'
        slow_lane = 'email_slow_lane'
        immediate_lane = 'email_immediate_lane'
        lanes = {
            'activate': slow_lane,
            'activate-with-domain': slow_lane,
            'change-email': slow_lane,
            'change-email-confirmation-old-email': fast_lane,
            'password-change-confirmation': fast_lane,
            'reset-password': fast_lane,
            'delete-user': fast_lane,
            'domain-dyndns': fast_lane,
            'renew-domain': immediate_lane,
        }
        if reason not in lanes:
            raise ValueError(
                f'Cannot send email to user {self.pk} without a good reason: {reason}'
            )

        context = context or {}
        context.setdefault(
            'link_expiration_hours',
            settings.VALIDITY_PERIOD_VERIFICATION_SIGNATURE //
            timedelta(hours=1))
        content = get_template(f'emails/{reason}/content.txt').render(context)
        content += f'\nSupport Reference: user_id = {self.pk}\n'
        footer = get_template('emails/footer.txt').render()

        logger.warning(
            f'Queuing email for user account {self.pk} (reason: {reason}, lane: {lanes[reason]})'
        )
        num_queued = EmailMessage(
            subject=get_template(f'emails/{reason}/subject.txt').render(
                context).strip(),
            body=content + footer,
            from_email=get_template('emails/from.txt').render(),
            to=[recipient or self.email],
            connection=get_connection(lane=lanes[reason],
                                      debug={
                                          'user': self.pk,
                                          'reason': reason
                                      })).send()
        metrics.get('desecapi_messages_queued').labels(
            reason, self.pk, lanes[reason]).observe(num_queued)
        return num_queued
Пример #11
0
    def qname(self):
        # hostname parameter
        try:
            if self.request.query_params['hostname'] != 'YES':
                return self.request.query_params['hostname'].lower()
        except KeyError:
            pass

        # host_id parameter
        try:
            return self.request.query_params['host_id'].lower()
        except KeyError:
            pass

        # http basic auth username
        try:
            domain_name = base64.b64decode(
                get_authorization_header(self.request).decode().split(' ')
                [1].encode()).decode().split(':')[0]
            if domain_name and '@' not in domain_name:
                return domain_name.lower()
        except (binascii.Error, IndexError, UnicodeDecodeError):
            pass

        # username parameter
        try:
            return self.request.query_params['username'].lower()
        except KeyError:
            pass

        # only domain associated with this user account
        try:
            return self.request.user.domains.get().name
        except models.Domain.MultipleObjectsReturned:
            raise ValidationError(
                detail={
                    "detail":
                    "Request does not properly specify domain for update.",
                    "code": "domain-unspecified"
                })
        except models.Domain.DoesNotExist:
            metrics.get('desecapi_dynDNS12_domain_not_found').inc()
            raise NotFound('nohost')
Пример #12
0
def encrypt(data, *, context):
    key = retrieve_key(label=b'crypt', context=context)
    value = Fernet(key=key).encrypt(data)
    metrics.get('desecapi_key_encryption_success').labels(context).inc()
    return value