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()
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()
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
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
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)
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
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
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
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
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')
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