Høst domænenavne med certifikatlogs

I På jagt efter danske domænenavne skrev jeg om en smart metode, jeg har fundet til at finde de .dk-domænenavne, som autoriteterne ikke ville dele med mig.

Måske er der andre end mig, der er interesseret i at holde lidt øje med nye steder på internettet?

Her er i hvert fald en lille opskrift på, hvordan man gør:

1. Hent Axeman

Axeman er et program, der hjælper med at automatisere og parallellisere downloads af certifikatlogs. Jeg måtte rette det en lille smule for at få det til at køre, da det vist ikke vedligeholdes aktivt. “Min” udgave finder du her:

https://github.com/helmstedt/Axeman

2. Download en masse logs

Axeman gemmer certifikatlogs som csv-filer. Skriv fx…

axeman -u 'ct.googleapis.com/logs/argon2022'

…for at hente Googles 2022-logs. Det gik ikke specielt hurtigt hos mig, men det virkede.

3. Gennemgå log-filer for .dk-domæner

Jeg skrev et lille program, der søger logfiler igennem for .dk-domæner (der kommer også nogle andre domæner med engang imellem, hvis der er “.dk” et sted i domænet). Det ser sådan her ud:

import os
from pathlib import Path
import csv
 
csv_dir = 'PATH_TO_LOG_FILES'
paths = Path(csv_dir).iterdir()

for file_path in paths:
    # Open and process csv file
    with open(file_path, 'rt') as csv_input:
        print('Processing: ', file_path)
        reader = csv.reader(csv_input)
        for row in reader:
            host = row[4]
            if '.dk' in host:
                print('Found: ', host)
                with open('PATH_TO_OUTPUT_FILE.CSV', 'a') as dotdk_output:
                    dotdk_output.write(host + '\n')
    # Delete csv file after processing
    os.remove(file_path)

Det eneste lidt obskure i programmet er måske hvad der gemmer sig i row[4]? Den indeholder en liste over de domæner og subdomæner, det enkelte certifikat er udstedt til. Elementerne i listen er adskilt med mellemrum.

4. Filtrer listen, så der kun kommer ægte, unikke .dk-domæner med

Efter grovsorteringen, skrev jeg endnu et lille program, der finsorterer. Det ser sådan her ud:

domains_in_file = set()
with open('PATH_TO_INPUT_FILE.CSV', 'rt') as file_input:
    for index, row in enumerate(file_input):
        items = row.split(' ')
        for item in items:
            item = item.replace('*.','').replace('\n','')
            matches = re.findall(r'([^.]*$)', item)
            dk_domain = ''
            if matches[0] == 'dk':
                dk_domain = item.split('.')
                dk_domain = dk_domain[-2] + '.' + dk_domain[-1]
                dk_domain = dk_domain.lower()
    with open('PATH_TO_OUTPUT_FILE.BIN', 'wb') as unique_domains_file:
        pickle.dump(domains_in_file, unique_domains_file)

Her er koden nok lidt mere obskur. Jeg tager hvert element i hver række fra CSV-filen i sidste trin og:

  • Fjerner evt. wildcard (*.) og linjeskift (\n) fra værtsnavnet
  • Finder domænesuffixet med et regulær udtryk
  • Hvis domænesuffixet er .dk, splitter jeg hvert enkelt element i værstnavnet op i en liste
  • Konstruerer selve domænenavnet ved at sætte det næstsidste (fx helmstedt) og sidste element (.dk) sammen
  • Sørger for en sikkerheds skyld for, at konvertere domænenavnet til små bogstaver

5. Nyd dit kendskab til nye og gamle domænenavne (der har fået udstedt SSL-certifikater)

2-0 til wishlist.dk i kampen mod techgiganten Ønskeskyen

På min ukommercielle, overvågnings- og loginfrie ønskeseddelservice wishlist.dk, tilbyder jeg brugere at importere deres ønskesedler direkte fra min største konkurrent, den overvågningskapitalistiske monopolistiske techgigant Ønskeskyen.

Hvis du tror jeg overdriver det med overvågningen, så se lige dette lille bitte udsnit af vejen fra at klikke på et link til en såkaldt “Airfryer” hos Ønskeskyen og komme hen hos Elgiganten og kunne købe den… GDPR much?

Med wishlist.dks gigantiske scale-up-unicorn-succes-vækst vidste jeg, at det kun var et spørgsmål om tid, før Ønskeskyen ville begynde at bruge ufine metoder for at hindre den frie konkurrence (og brugernes ret til dataportabilitet):

Et let aflæseligt diagram over den eksplosive udvikling i brugere på wishlist.dk over tid.

Pludselig holdt min import-funktion op med at virke!

For et par dage siden holdt min import-funktion fra Ønskeskyen op med at virke.

Jeg undersøgte straks min kode og fyrede op for mit lokale testmiljø på min hjemmecomputer. Måske havde Ønskeskyen ændret på sit interne API, eller også havde jeg bare skrevet noget dårligt kode?

Alt fungerede præcis som det skulle i testmiljøet.

Så forsøgte jeg at sende en forespørgsel til ønskeskyen.dk fra min ydmyge start-up-webserver, der er placeret i Tyskland et sted. Forbindelsen timede ud.

Der var altså ingen kontakt overhovedet mellem min maskine og den maskine hos Ønskeskyen, hvis data brugerne af wishlist.dk forsøgte at rekvirere, for at undslippe overvågningskapitalismens skarpe kløer!

Har Ønskeskyen blokeret wishlist.dk?

Da jeg havde konstateret, at jeg slet ingen kontakt havde til Ønskeskyens server, gik det op for mig:

Måske føler Ønskeskyen sig truet og har blokeret for, at deres brugere emigrerer til wishlist.dk?

Jeg kontaktede Ønskeskyen:

Ønskeskyen føler sig slet ikke truet af wishlist.dks eksplosive vækst og roser endda min “fine” hjemmeside.

Ud over at blive glad for det hjertevarme “rigtig god dag”, følte jeg mig betrygget af mailen fra Ønskeskyens flinke supportperson.

Ønskeskyen er da alt for store og ædle til at blokere for lille iværksætter-wishlist.dk…

Plottet tykner!

Eller er de?

Med min nyfundte optimisme og tro på den fri konkurrence sendte jeg en opfølgende mail til Ønskeskyen. Jeg spurgte, om de ville undersøge deres teknik for fejl? Hvis de ikke havde blokeret wishlist.dk, var der måske noget i deres ende, der ikke fungerede som det skulle?

Når man har ambitioner om at erobre hele verden dur det jo ikke, at man ved et uheld kommer til at lukke for forespørgsler fra en server placeret hos Danmarks vigtigste handelspartner:

THE LARGEST WISH LIST APP IN THE WORLD!!!111!

Her er min opfølgende mail til Ønskeskyen:

Jeg har gjort et menneske fortræd ved at beskrive forretningsmodellen for personens arbejdsplads.

Mailen var nedtrykkende:

Desværre kan Ønskeskyens teknikere ikke hjælpe med mit problem, da de, (ligesom jeg selv), har travlt med at bygge en verdensdominerende ønskeseddelservice og slet ikke har tid til at beskæftige sig med negative vibes fra konkurrenter.

Æv! Og så fik jeg oven i købet gjort supporteren ked af det med min kritik.

Der er noget i tonen i svaret fra Ønskeskyen, der alligevel får mig til at konkludere:

Ønskeskyen har blokeret wishlist.dk!

Hvad gjorde jeg så?

Jeg spurgte de sociale medier om råd, og fik anbefalet noget, der hedder Tor, der gør det muligt at hente indhold fra internettet anonymt ved at sende sine forespørgsler gennem andre menneskers computere.

At installere Tor på min server tog ca. 10 sekunder, og at tilføjere et par linjers kode til mit Python-script, der importerer ønsker fra Ønskeskyen, tog ikke meget længere.

Nu virker importfunktionen igen.

2-0 til wishlist.dk. (Det første point er for at lave en meget bedre ønskeseddelservice.)

Hvad bliver Ønskeskyens næste træk?

På jagt efter danske domænenavne

For lang tid siden fik jeg fat i domænet wishlist.dk til min ønskeseddelservice ved at klage over, at domænet så ud til at være registreret alene med henblik på at sælge det videre. Det må man nemlig ikke gøre med .dk-domæner.

Noget tid efter blev jeg kontaktet af en, der hedder Jakob, der havde læst mit indlæg og havde brugt samme metode til at få fat i et domænenavn, han havde drømt om.

Et nyt dataprojekt

Den oplevelse gav mig den idé, at jeg kunne tjene det godes sag, hvis jeg på en eller anden måde kunne lave en liste over domæner registreret med henblik på videresalg og offentliggøre listen. Jeg tænkte, jeg kunne:

  • Hente en liste over danske domænenavne
  • Lave en robot, der henter noget data om hvert domænenavn og måske tager et skærmbillede af siden
  • Kategorisere et par tusind domæner efter hvad de i mine øjne bliver brugt til
  • Bruge noget smart maskinlærings-AI-hokus-pokus til at kategorisere de resterende domænenavne

Men, men, men:

Ingen ville hjælpe med en domænenavnsliste

Jeg troede, at første trin i min plan om at finde en liste over .dk-domænenavne ville være det nemmeste. Jeg vidste, at jeg kan slå oplysninger op om domæner hos DK Hostmaster, der administrerer .dk-domæner (domænerne er ejet af staten). Og jeg havde også en anelse om, at der i Domæneloven står noget i retning af:

WHOIS-databasen
§ 18. Administrator skal oprette og vedligeholde en database indeholdende oplysninger om registranternes navn, adresse og telefonnummer.
Stk. 2. Administrator skal sikre, at oplysningerne nævnt i stk. 1 er retvisende, opdaterede og offentligt tilgængelige.

Jeg skrev til DK Hostmaster og fik nej. Jeg skrev til Dansk Internet Forum og fik intet svar. Jeg spurgte Klagenævnet for Domænenavne om jeg kunne klage over DK Hostmaster og fik nej. Så prøvede jeg at få aktindsigt i oplysningerne:

Det gik heller ikke så godt.

De oplysninger, jeg troede var offentlige, bliver åbenbart holdt tæt ind til kroppen. For at beskytte mig (og dig) mod spam!(?)

Efter afslaget klagede jeg til Erhvervsstyrelsen (nej!), forsøgte med aktindsigt hos Det Kongelige Bibliotek, som jeg fandt ud af var i besiddelse af listen (nej!), og skrev også til et par legitime domæneregistrationssælgere, som får listen af DK Hostmaster, om de var indstillet på at dele (nej!).

Jeg havde været ihærdig, men spildt så meget af min egen og andres tid, at jeg besluttede mig for at ændre kurs.

På datahøst

Det gik op for mig, at jeg ikke behøvede at kende alle .dk-domæner til mit hobbyprojekt. Nogle hundrede tusinde eller en million ville sikkert være rigeligt (der er registreret ca. 1,4 millioner i skrivende stund).

  • Jeg fandt en lang liste over danske ord og lavede en lille robot til at slå ordene op som domæner hos DK Hostmaster.
  • Jeg fandt en lignende liste med de mest brugte engelske ord og slog ordene op.
  • Jeg søgte på ord fra ordlisterne hos en kendt søgemaskine og fik søgeresultater tilbage med danske domænenavne.
  • Jeg fik API-adgang til CVR-registeret (som sjovt nok er rigtigt offentligt) og hentede domænenavne for alle danske virksomheder.

Disse metoder gav mig de første ca. 350.000 .dk-domæner og gav mig lejlighed til at skrive en masse små Python-scripts til at automatisere det meste.

Guldminen

Så fandt jeg guldminen. Apple, Google, Facebook, Cloudflare og andre hæderkronede virksomheder har et lidt andet syn på sikkerhed end DK Hostmaster, der jo gerne vil hemmeligholde danske domænenavne for at forhindre spam.

For at bekæmpe snyd med certifikater til sikker kommunikation på nettet (hængelåsen i browseren, du ved), logger de udstedelsen af certikater og har indtil videre logget lige under 8,5 milliard certifikater. I stedet for hemmeligholdelse: transparens.

Aha! Så når en ejer af et .dk-domæne får udstedt et certifikat til sin fine hjemmeside, bliver udstedelsen logget.

Jeg satte et smart program, der hedder Axeman, til at begynde at downloade logs, filtrerede for .dk-domænenavne og begyndte at tilføje dem til min database.

Det går langsomt, men det giver resultater.

https://wallnot.dk/dotdk/ har jeg nu samlet 639.485 .dk-domænenavne til fri download og videredistribution. Og der er mange, mange flere på vej.

Tag den!

…Men hvad med projektet?

Jeg har faktisk fået kategoriseret nogle domænenavne efter hvordan jeg vurderer, de bliver brugt. Og taget en masse skærmbilleder, som jeg håber jeg kan bruge noget maskinlæring på. Men listen over .dk-domæner, jeg mener er registreret med henblik på videresalg, har lange udsigter.

Til gengæld håber jeg at listen over .dk-domæner i sig selv kan bruges af andre til et eller andet. Vi får se.

Til kamp mod phishing på lnk.dk: del 2

Mit første naive forsøg på at forhindre kriminelles brug af lnk.dk til at pege på diverse phishing-sider lykkedes, mildt sagt, ikke.

Nu har jeg taget skrappere midler i brug.

I min models.py i min Django-applikation, tilføjer jeg et felt til at gemme ip-adresse på den, der har oprettet et link, og et felt til at markere, om et link er usikkert:

class Link(models.Model):
    # Short link is only lower case
    def save(self, *args, **kwargs):
        self.shortlink = self.shortlink.lower()
        return super(Link, self).save(*args, **kwargs)

    destination = models.URLField('Destinationslink', max_length=65535, validators=[validate_destination])
    shortlink = models.SlugField('Kort link', max_length=100, unique=True, allow_unicode=False, validators=[validate_shortlink])
    LINK_TYPE_CHOICES = (
        ('automatic', 'Automatisk'),
        ('manual', 'Manuelt'),
    )   
    submitter_ip = models.GenericIPAddressField(null=True)
    unsafe_link = models.BooleanField(default=False)
    type = models.CharField('Type', max_length=10, choices=LINK_TYPE_CHOICES)
    date = models.DateTimeField(default=timezone.now, editable=False)

Derudover tilføjer jeg tabeller til at kunne blokere for ip-adresser og domæner, der ikke skal kunne oprette brugbare links:

class Ban(models.Model):
    banned_ip = models.GenericIPAddressField(unique=True)

class BanDomain(models.Model):
    banned_domain = models.CharField(max_length=255, unique=True)

Med det på plads tilpasser jeg min logik til oprettelse af links i views.py sådan, at:

  • kun brugere med ip-adresser kan oprette links,
  • links bliver tjekket med Google Safe Browsing (efter råd på Twitter)
  • links til domæner og fra ip-adresser, der er blokeret, bliver automatisk markeret som usikre

Her er funktionen til at tjekke links op mod Google Safe Browsing:

# Google safe browsing API check function
def is_url_google_safe_browsing_safe(url):
    params = {
        'key': ''
    }
    json  = {
        "client": {
            "clientId":      "lnk.dk",
            "clientVersion": "1.0"
        },
        "threatInfo": {
            "threatTypes":      ["MALWARE", "SOCIAL_ENGINEERING", "UNWANTED_SOFTWARE"],
            "platformTypes":    ["ANY_PLATFORM"],
            "threatEntryTypes": ["URL"],
            "threatEntries": [
                {"url": url}
          ]
        }
     }
    api_url = 'https://safebrowsing.googleapis.com/v4/threatMatches:find'
    try:
        response_json = requests.post(api_url, params=params, json=json).json()
        if response_json:
            return True
        else:
            return False
    # If something unexpected is returned from Google, link creation is allowed
    except:
        return False

Og her er min nye logik til at tjekke modtagne links. Læg mærke til, at brugere uden ip-adresse automatisk bliver rickrolled

if form.is_valid():
	client_ip, is_routable = get_client_ip(request)
	# Hiding your IP seems illegit, so user is rickrolled
	if client_ip is None:
		return HttpResponseRedirect('https://www.youtube.com/watch?v=dQw4w9WgXcQ')
	else:
		destination = form.cleaned_data['destination'] # Submitted destination
		shortlink = form.cleaned_data['shortlink'] # Submitted slug
		
		# Google Safe Browsing check
		unsafe_url = is_url_google_safe_browsing_safe(destination)
		
		# Ban domain check
		domain_info = extract(destination)
		domain = domain_info.domain + '.' + domain_info.suffix                    
		domain_ban = BanDomain.objects.filter(banned_domain=domain)
		if len(domain_ban) > 0:
			banned_domain = True
		else:
			banned_domain = False
		
		# Ban ip check
		ip_ban = Ban.objects.filter(banned_ip=client_ip)
		if len(ip_ban) > 0:
			banned_ip = True
		else:
			banned_ip = False

Til sidst har jeg forsøgt at narre phisherne ved at links til usikre sider virker, for den, der selv har oprettet linket. Alle andre bliver rickrollet, hvis de klikker på et usikkert link:

# Short link redirect to destination URL
def redirect(request, shortlink):
    # Query the database for short link, if there is a hit, redirect to destination URL
    # In case of uppercase characters in user input shortlink, the link is made lowercase
    # Also, check if ip is banned
    try:
        client_ip, is_routable = get_client_ip(request)
        link = Link.objects.get(shortlink=shortlink.lower())
        # Legit users are rickrolled when an unsafe link is visited from an IP that is not banned
        if link.unsafe_link == True and not client_ip == link.submitter_ip:
            return HttpResponseRedirect('https://www.youtube.com/watch?v=dQw4w9WgXcQ')
        # For safe links AND for unsafe links visited from banned IPs, user is redirected to destination
        else:
            # If there's a referer and it's the same as the destination link, show a 404 to avoid an endless loop
            if 'HTTP_REFERER' in request.META and link.destination == request.META['HTTP_REFERER']:
                return render(request, 'links/404.html', status=404)
            return HttpResponseRedirect(link.destination)
    # In case of an error, show 404 page
    except:
        return render(request, 'links/404.html', status=404)

Udvikling i portefølje og på konti hos Nordnet

Opdatering 18/02/2023: Nordnet ændrer tit på deres ting. På https://github.com/helmstedt/nordnet-utilities forsøger jeg at følge med, så hent gerne din kode der, hvis koden neden for ikke virker længere.

I Nordnets porteføljerapport og kontooversigter, kan man se nogle flotte grafer over udviklingen i ens portefølje og/eller konti.

Et eksempel på porteføljeudvikling hos Nordnet. I dette tilfælde en nedadgående graf.

Jeg blev spurgt, om jeg ikke ville hjælpe med, hvordan man kan hive den slags ud af Nordnet til eget brug. Det er lidt nemmere til et hurtigt overblik, end hvis man skal hive alle sine transaktioner og kurser ud i Excel og tilrettelægge data der.

Her er nogle eksempler på mulighederne. Du kan også finde eksemplerne på GitHub:

# This program provides two examples of logging into a Nordnet account
# and extracting account performance as json data. One is based on standard
# intervals. The other is based on a user-defined interval.
# Storing and processing of returned data is left to you.
import requests
from nordnet_configuration import accounts
from nordnet_login import nordnet_login

session = requests.Session()
session = nordnet_login(session)
accounts_list = [value for value in accounts.values()]

### Nordnet standard intervals (one month, three months, six months, ytd, 1 year, 3 years and 5 years)
accounts_string = ','.join(accounts_list)
url = 'https://www.nordnet.dk/api/2/accounts/' + accounts_string + '/returns/performance'
period_options = ['m1','m3','m6','ty','y1','y3','y5']

standard_graph_data = {}
for period in period_options:
    params = {
        'period': period,
        'start_at_zero': False
    }
    graph = session.get(url, params=params)
    standard_graph_data[period] = graph.json()
# Store and process graph_data as needed

### User defined date intervals
start_date = '2019-01-30'   # Edit as needed
end_date = '2019-05-14'    # Edit as needed
user_defined_graph_data = {}
for account in accounts_list:
    url = 'https://www.nordnet.dk/api/2/accounts/' + account + '/returns/performance'
    params = {
        'from': start_date,
        'to': end_date
    }
    user_defined_graph = session.get(url, params=params)
    user_defined_graph_data[account] = user_defined_graph.json()
# Store and process user_defined_graph_data as needed

Alle danskeres CPR-numre til fri download

Her er et Python-script, der genererer en liste over alle kombinationer af datoer i formatet ddmmyy med alle kombinationer af tallene fra 0 (0000) til 9999:

from datetime import datetime, date, timedelta

# Credits to https://stackoverflow.com/a/62248100, https://creativecommons.org/licenses/by-sa/4.0/
start = '2000-01-01'    # First date is in year 2000, not 1900, since 1900 was not a leap year.
end = '2100-01-01'      # Last date in range will be 2099-12-31.
start_date = date.fromisoformat(start)
end_date = date.fromisoformat(end)
date_range = [start_date + timedelta(days=i) for i in range((end_date - start_date).days)]

with open("cpr.txt", "a") as cprfile:
    for i in range(0,10000):
        print(i)
        for date in date_range:
            i_formated = f"{i:04d}"
            date_formated = datetime.strftime(date, '%d%m%y')
            cpr = date_formated + '-' + i_formated + '\n'
            cprfile.write(cpr)

Hele listen fylder 4 GB, men kan heldigvis komprimeres. Med 7zip fik jeg den ned på under 40 MB.

Download den komprimerede liste her.

Opdateret program til at logge på Nordnet med Powershell

Opdatering 18/02/2023: Nordnet ændrer tit på deres ting. På https://github.com/helmstedt/nordnet-utilities forsøger jeg at følge med, så hent gerne din kode der, hvis koden neden for ikke virker længere.

Jeg fik at vide, at mit eksempelprogram til at logge på Nordnet med Powershell ikke længere virkede. Nu har jeg lavet en ny udgave, der virker. Her er den:

[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$url = 'https://www.nordnet.dk/logind'
$r1 = iwr $url -SessionVariable cookies

$body = @{'username'=''; 'password'=''}
$headers = @{'Accept' = '*/*'; 'client-id' = 'NEXT'; 'sub-client-id' = 'NEXT'}
  
$url = 'https://www.nordnet.dk/api/2/authentication/basic/login'
$r2 = iwr $url -method 'POST' -Body $body -Headers $headers -WebSession $cookies

$url = 'https://www.nordnet.dk/mediaapi/transaction/csv/filtered?locale=da-DK&account_id=1&from=2019-08-01&to=2019-10-01'
$r3 = iwr $url -WebSession $cookies
 
$content = $r3.Content
$encoding = [System.Text.Encoding]::unicode
$bytes = $encoding.GetBytes($content)
 
$decoded_content = [System.Text.Encoding]::utf32.GetString($bytes)
$decoded_content = $decoded_content.Substring(1,$decoded_content.length-1)
Write-Host $decoded_content

Til kamp mod phishing på lnk.dk

Der er desværre nogle kriminelle, der har opdaget min kortlink-service lnk.dk og bruger siden til at lave korte links, der peger på forskellige phishing-formularer. De fleste på fransk, enkelte på dansk.

Jeg vil helst kun have, at min side bruges til lovlige formål, og derfor har jeg i første omgang lavet et kontrolspørgsmål i formularen til at oprette links. Jeg håber, at det kun er ærlige mennesker, der kan svare på spørgsmålet, og at det er relativt nemt for dem:

Nyt kontrolspørgsmål om en kendt dansk cykelrytter på lnk.dk

For at implementere det nye felt, redigerede jeg min Django-applikations forms.py med feltet og krav til validering:

from django.forms import ModelForm
from django import forms
from .models import Link
from django.core.exceptions import ValidationError

class LinkForm(ModelForm):
    everyoneknows = forms.CharField(label='Hvad er fornavnet på cykelrytteren, der vandt Tour de France for mænd i 2022?', error_messages={'required': 'Indtast cykelrytterens fornavn'})

    def clean_everyoneknows(self):
        answer = self.cleaned_data['everyoneknows'].lower()
        if answer != 'jonas':
            raise ValidationError("Det fornavn, du har indtastet, er forkert.")
        return answer
    
    def __init__(self, *args, **kwargs):
        super(LinkForm, self).__init__(*args, **kwargs)
        self.fields['destination'].widget.attrs['placeholder'] = 'https://eksempel.dk/meget/lang/url'
        self.fields['shortlink'].widget.attrs['placeholder'] = 'eksempel'
        self.fields['shortlink'].label_suffix = ""  # Remove colon after label
        self.fields['shortlink'].required = False   # Not required in form

    def clean_shortlink(self):
        shortlink = self.cleaned_data['shortlink']
        return shortlink.lower()
    
    class Meta:
        model = Link
        fields = ['destination', 'shortlink']
        labels = {
            'shortlink': ('Evt. selvvalgt kort link:'),
        }
        error_messages = {
            'destination': {
                'max_length': ('Din destinationsurl er for lang til denne kortlinkservice.'),
                'invalid': ('Din destinationsurl er ikke en gyldig adresse. Husk http://, https:// eller ftp:// foran dit link, hvis du har glemt det.'),
            },
            'shortlink': {
                'unique': ('Det selvvalgte link, du har valgt, er allerede i brug. Find på et andet.'),
                'max_length': ('Dit selvvalgte link må maksimalt være 100 tegn langt.'),
                'invalid': ('Du kan kun bruge bogstaver (dog ikke æ, ø, å - kun ASCII-tegnsættet), tal, bindestreg og understreg i din selvvalgte adresse.'),
            }
        }

Det bliver spændende at se, om ændringen har nogen effekt.