Files
passwordsuggester/main.go
groot ede98d9a15
All checks were successful
release-tag / release-image (push) Successful in 2m21s
Anpassung der Wortliste und des Wördings im HTML
2025-10-06 09:31:39 +00:00

621 lines
20 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// main.go
package main
import (
"crypto/rand"
"encoding/json"
"fmt"
"html"
"log"
"math/big"
"net/http"
"os"
"strconv"
"strings"
"unicode"
)
type Config struct {
Addr string
MinLength int
MinLower int
MinUpper int
MinDigits int
MinSpecial int
Specials string
LeetMap map[rune]string
Suggestions int
RuleSets []string // e.g. ["random","passphrase","hybrid"]
LeetProb float64
}
type genRequest struct {
Sentence string `json:"sentence"`
}
type complexity struct {
Lower int `json:"lower"`
Upper int `json:"upper"`
Digits int `json:"digits"`
Special int `json:"special"`
Length int `json:"length"`
}
type genResponse struct {
Password string `json:"password"`
Complexity complexity `json:"complexity"`
Ok bool `json:"ok"`
Message string `json:"message,omitempty"`
}
func main() {
cfg := loadConfig()
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprint(w, indexHTML(cfg))
})
http.HandleFunc("/api/generate", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
var req genRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSON(w, genResponse{Ok: false, Message: "Ungültige Eingabe"})
return
}
pw, cplx := generateFromSentence(strings.TrimSpace(req.Sentence), cfg)
ok := meets(cfg, cplx)
writeJSON(w, genResponse{Ok: ok, Password: pw, Complexity: cplx})
})
http.HandleFunc("/api/suggest", func(w http.ResponseWriter, r *http.Request) {
// Gibt drei+ Vorschläge aus mindestens 3 RuleSets zurück
out := struct {
Suggestions []string `json:"suggestions"`
}{Suggestions: suggestions(cfg)}
writeJSON(w, out)
})
log.Printf("Listening on %s ...", cfg.Addr)
if err := http.ListenAndServe(cfg.Addr, nil); err != nil {
log.Fatal(err)
}
}
/* -------------------- Config & ENV -------------------- */
func floatEnv(k string, def float64) float64 {
if v := os.Getenv(k); v != "" {
v = strings.ReplaceAll(v, ",", ".") // deutsche Kommas erlauben
if f, err := strconv.ParseFloat(v, 64); err == nil && f >= 0 && f <= 1 {
return f
}
}
return def
}
func randChance(p float64) bool {
if p <= 0 { return false }
if p >= 1 { return true }
// 1e6 Auflösung reicht; kryptografisch sicher über mustCryptoInt
return mustCryptoInt(1_000_000) < int(p*1_000_000)
}
func loadConfig() Config {
minLen := intEnv("MIN_LENGTH", 12)
minLower := intEnv("MIN_LOWER", 2)
minUpper := intEnv("MIN_UPPER", 2)
minDigits := intEnv("MIN_DIGITS", 2)
minSpecial := intEnv("MIN_SPECIAL", 2)
addr := strEnv("SERVER_ADDR", ":8080")
specials := strEnv("SPECIALS", "!@#$%&*()-=+:?")
leet := parseLeetMap(strEnv("LEET_MAP", "a:4,e:3,o:0,t:7"))
leetProb := floatEnv("LEET_PROB", 0.35)
sug := intEnv("SUGGESTION_COUNT", 5)
rules := strEnv("RULESETS", "random,passphrase,hybrid")
rs := []string{}
for _, p := range strings.Split(rules, ",") {
p = strings.TrimSpace(p)
if p != "" {
rs = append(rs, p)
}
}
if len(rs) == 0 {
rs = []string{"random", "passphrase", "hybrid"}
}
return Config{
Addr: addr,
MinLength: minLen,
MinLower: minLower,
MinUpper: minUpper,
MinDigits: minDigits,
MinSpecial: minSpecial,
Specials: specials,
LeetMap: leet,
Suggestions: sug,
RuleSets: rs,
LeetProb: leetProb,
}
}
func strEnv(k, def string) string {
if v := os.Getenv(k); v != "" {
return v
}
return def
}
func intEnv(k string, def int) int {
if v := os.Getenv(k); v != "" {
if n, err := strconv.Atoi(v); err == nil {
return n
}
}
return def
}
func parseLeetMap(s string) map[rune]string {
// Format: "a:4,e:3,o:0,t:7"
out := map[rune]string{}
for _, pair := range strings.Split(s, ",") {
p := strings.Split(strings.TrimSpace(pair), ":")
if len(p) == 2 && len(p[0]) == 1 {
k := []rune(strings.ToLower(p[0]))[0]
out[k] = p[1]
}
}
return out
}
/* -------------------- Core Generation -------------------- */
func generateFromSentence(sentence string, cfg Config) (string, complexity) {
if sentence == "" {
// Leerer Input -> generiere einen Vorschlag nach "hybrid"
pw := genHybrid(cfg)
return ensureAll(pw, cfg)
}
// 1) Normalisieren & Leet
words := strings.Fields(sentence)
if len(words) == 0 {
pw := genHybrid(cfg)
return ensureAll(pw, cfg)
}
// ersetze Leerzeichen durch zufällige Sonderzeichen
var b strings.Builder
for i, w := range words {
if i > 0 {
b.WriteString(randFrom(cfg.Specials))
}
b.WriteString(applyLeet(w, cfg.LeetMap, cfg.LeetProb))
}
pw := b.String()
// 2) Zufällige Groß-/Kleinschreibung einstreuen
pw = sprinkleCase(pw)
// 3) Komplexität & Mindestlänge sicherstellen
return ensureAll(pw, cfg)
}
func applyLeet(s string, leet map[rune]string, p float64) string {
var b strings.Builder
for _, r := range s {
lr := unicode.ToLower(r)
if rep, ok := leet[lr]; ok && randChance(p) {
b.WriteString(rep) // Ersetze nur mit Chance p
} else {
b.WriteRune(r)
}
}
return b.String()
}
func sprinkleCase(s string) string {
rs := []rune(s)
for i, r := range rs {
if unicode.IsLetter(r) {
// ~50% Chance flip case
if coin() {
if unicode.IsUpper(r) {
rs[i] = unicode.ToLower(r)
} else {
rs[i] = unicode.ToUpper(r)
}
}
}
}
return string(rs)
}
func ensureAll(pw string, cfg Config) (string, complexity) {
// Schrittweise Komplexität erzwingen, dann Mindestlänge
pw, c := ensureComplexity(pw, cfg)
if runeLen(pw) < cfg.MinLength {
need := cfg.MinLength - runeLen(pw)
allSet := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + cfg.Specials
for i := 0; i < need; i++ {
pw = insertAt(pw, randPos(pw), randFrom(allSet))
}
c = countComplexity(pw, cfg)
}
return pw, c
}
func ensureComplexity(s string, cfg Config) (string, complexity) {
pw := s
c := countComplexity(pw, cfg)
// Ziffern
for c.Digits < cfg.MinDigits {
pw = insertAt(pw, randPos(pw), randFrom("0123456789"))
c = countComplexity(pw, cfg)
}
// Sonderzeichen
for c.Special < cfg.MinSpecial {
pw = insertAt(pw, randPos(pw), randFrom(cfg.Specials))
c = countComplexity(pw, cfg)
}
// Großbuchstaben
for c.Upper < cfg.MinUpper {
if i := findIndexLetter(pw, false); i >= 0 { // irgendein Buchstabe
pw = setCaseAt(pw, i, true)
} else {
pw = insertAt(pw, randPos(pw), randFrom("ABCDEFGHIJKLMNOPQRSTUVWXYZ"))
}
c = countComplexity(pw, cfg)
}
// Kleinbuchstaben
for c.Lower < cfg.MinLower {
if i := findIndexLetter(pw, false); i >= 0 {
pw = setCaseAt(pw, i, false)
} else {
pw = insertAt(pw, randPos(pw), randFrom("abcdefghijklmnopqrstuvwxyz"))
}
c = countComplexity(pw, cfg)
}
return pw, c
}
func countComplexity(s string, cfg Config) complexity {
var c complexity
for _, r := range s {
switch {
case unicode.IsLower(r):
c.Lower++
case unicode.IsUpper(r):
c.Upper++
case unicode.IsDigit(r):
c.Digits++
default:
if strings.ContainsRune(cfg.Specials, r) {
c.Special++
}
}
}
c.Length = runeLen(s)
return c
}
func meets(cfg Config, c complexity) bool {
return c.Length >= cfg.MinLength &&
c.Lower >= cfg.MinLower &&
c.Upper >= cfg.MinUpper &&
c.Digits >= cfg.MinDigits &&
c.Special >= cfg.MinSpecial
}
/* -------------------- Suggestions (3+ RuleSets) -------------------- */
func suggestions(cfg Config) []string {
rs := []string{}
// Mindestens eins je RuleSet
for _, set := range cfg.RuleSets {
switch set {
case "random":
rs = append(rs, genRandom(cfg))
case "passphrase":
rs = append(rs, genPassphrase(cfg))
case "hybrid":
rs = append(rs, genHybrid(cfg))
}
}
// Falls mehr gewünscht, fülle gemischt auf
for len(rs) < cfg.Suggestions {
switch randPick([]string{"random", "passphrase", "hybrid"}) {
case "random":
rs = append(rs, genRandom(cfg))
case "passphrase":
rs = append(rs, genPassphrase(cfg))
default:
rs = append(rs, genHybrid(cfg))
}
}
return rs
}
func genRandom(cfg Config) string {
all := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + cfg.Specials
// Länge: mindestens MinLength bis MinLength+6
L := cfg.MinLength + mustCryptoInt(5) + 2 // +2..+6
var b strings.Builder
for i := 0; i < L; i++ {
b.WriteString(randFrom(all))
}
pw, _ := ensureAll(b.String(), cfg)
return pw
}
func genPassphrase(cfg Config) string {
// 34 Wörter, zufällige Trenner, zufällige Großschreibung & Leet-sprinkles
n := 3 + mustCryptoInt(2) // 3..4
parts := make([]string, 0, n)
for i := 0; i < n; i++ {
w := wordList[mustCryptoInt(len(wordList))]
if coin() {
w = strings.ToUpper(w[:1]) + w[1:]
}
// gelegentlich Leet auf einzelne Buchstaben
if coin() && len(w) > 1 {
idx := mustCryptoInt(len(w))
r := []rune(w)
lr := unicode.ToLower(r[idx])
if rep, ok := cfg.LeetMap[lr]; ok {
r[idx] = []rune(rep)[0]
w = string(r)
}
}
parts = append(parts, w)
}
sep := randFrom(cfg.Specials)
pw := strings.Join(parts, sep)
// Zahlen zufällig einsprenkeln
if coin() {
pw = insertAt(pw, randPos(pw), strconv.Itoa(10+mustCryptoInt(89)))
}
pw, _ = ensureAll(pw, cfg)
return pw
}
func genHybrid(cfg Config) string {
// 2 Wörter + 2 Sonderzeichen + 35 Ziffern gemischt
w1 := wordList[mustCryptoInt(len(wordList))]
w2 := wordList[mustCryptoInt(len(wordList))]
if coin() {
w1 = strings.ToUpper(w1[:1]) + w1[1:]
}
if coin() {
w2 = strings.ToUpper(w2[:1]) + w2[1:]
}
base := applyLeet(w1, cfg.LeetMap, cfg.LeetProb) + randFrom(cfg.Specials) + applyLeet(w2, cfg.LeetMap, cfg.LeetProb)
// streue zweite Sonderzeichen
base = insertAt(base, randPos(base), randFrom(cfg.Specials))
// 35 Ziffern an zufälligen Stellen
numN := 3 + mustCryptoInt(3)
for i := 0; i < numN; i++ {
base = insertAt(base, randPos(base), randFrom("0123456789"))
}
pw, _ := ensureAll(base, cfg)
return pw
}
/* -------------------- Utilities -------------------- */
func writeJSON(w http.ResponseWriter, v any) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
_ = json.NewEncoder(w).Encode(v)
}
func runeLen(s string) int { return len([]rune(s)) }
func randFrom(pool string) string {
i := mustCryptoInt(len(pool))
return string(pool[i])
}
func randPos(s string) int {
L := runeLen(s)
if L == 0 {
return 0
}
return mustCryptoInt(L + 1) // erlauben "ans Ende"
}
func insertAt(s string, pos int, piece string) string {
rs := []rune(s)
if pos <= 0 {
return piece + s
}
if pos >= len(rs) {
return s + piece
}
var b strings.Builder
b.WriteString(string(rs[:pos]))
b.WriteString(piece)
b.WriteString(string(rs[pos:]))
return b.String()
}
func findIndexLetter(s string, onlyLower bool) int {
rs := []rune(s)
// Zufällige Startposition, um Muster zu vermeiden
start := mustCryptoInt(len(rs) + 1)
for i := 0; i < len(rs); i++ {
idx := (start + i) % len(rs)
r := rs[idx]
if unicode.IsLetter(r) {
if onlyLower {
if unicode.IsLower(r) {
return idx
}
} else {
return idx
}
}
}
return -1
}
func setCaseAt(s string, idx int, upper bool) string {
rs := []rune(s)
if idx < 0 || idx >= len(rs) {
return s
}
if !unicode.IsLetter(rs[idx]) {
// wenn keine Letter -> füge stattdessen einen passenden Buchstaben ein
if upper {
return insertAt(s, idx, randFrom("ABCDEFGHIJKLMNOPQRSTUVWXYZ"))
}
return insertAt(s, idx, randFrom("abcdefghijklmnopqrstuvwxyz"))
}
if upper {
rs[idx] = unicode.ToUpper(rs[idx])
} else {
rs[idx] = unicode.ToLower(rs[idx])
}
return string(rs)
}
func coin() bool { return mustCryptoInt(2) == 0 }
func mustCryptoInt(max int) int {
if max <= 1 {
return 0
}
nBig, err := rand.Int(rand.Reader, big.NewInt(int64(max)))
if err != nil {
panic(err)
}
return int(nBig.Int64())
}
func randPick(options []string) string {
return options[mustCryptoInt(len(options))]
}
/* -------------------- Minimal UI (alles lokal) -------------------- */
func indexHTML(cfg Config) string {
var b strings.Builder
b.WriteString("<!doctype html>\n")
b.WriteString("<html lang='de'>\n<head>\n<meta charset='utf-8'>\n<meta name='viewport' content='width=device-width,initial-scale=1'>\n")
b.WriteString("<title>Password Forge (Go)</title>\n<style>\n")
b.WriteString(":root{--bg:#0b0f14;--card:#111827;--ink:#e5e7eb;--muted:#9ca3af;--accent:#60a5fa;--ok:#34d399;--bad:#f87171}\n")
b.WriteString("*{box-sizing:border-box}\n")
b.WriteString("body{margin:0;background:var(--bg);color:var(--ink);font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif}\n")
b.WriteString(".container{max-width:880px;margin:32px auto;padding:0 16px}\n")
b.WriteString(".card{background:var(--card);border-radius:16px;box-shadow:0 10px 30px rgba(0,0,0,.35);padding:20px}\n")
b.WriteString("h1{margin:0 0 8px;font-size:24px}\n")
b.WriteString("p.sub{margin:0 0 18px;color:var(--muted)}\n")
b.WriteString("label{display:block;font-size:14px;color:var(--muted);margin-bottom:6px}\n")
b.WriteString("textarea{width:100%;min-height:90px;border:1px solid #1f2937;background:#0f1622;color:var(--ink);border-radius:12px;padding:12px;resize:vertical}\n")
b.WriteString("button{cursor:pointer;border:0;border-radius:12px;padding:10px 14px;background:var(--accent);color:#0b0f14;font-weight:600}\n")
b.WriteString(".row{display:flex;gap:12px;align-items:flex-end;flex-wrap:wrap}\n")
b.WriteString(".output{font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', monospace;background:#0f1622;color:#e2e8f0;border-radius:12px;padding:12px;word-break:break-all}\n")
b.WriteString(".grid{display:grid;grid-template-columns:1fr;gap:12px}\n")
b.WriteString("@media(min-width:800px){.grid{grid-template-columns:2fr 1fr}}\n")
b.WriteString(".badge{display:inline-flex;align-items:center;gap:6px;border-radius:999px;background:#0f1622;padding:6px 10px;font-size:12px;color:var(--muted)}\n")
b.WriteString(".badge.ok{color:var(--ok)}\n")
b.WriteString(".badge.bad{color:var(--bad)}\n")
b.WriteString("small.kv{color:var(--muted);display:block}\n")
b.WriteString(".suggestion{padding:10px;border:1px dashed #283243;border-radius:10px;background:#0f1622;word-break:break-all}\n")
b.WriteString("kbd{background:#111827;border:1px solid #223046;border-radius:6px;padding:3px 6px}\n")
b.WriteString("footer{opacity:.7;margin-top:20px;font-size:12px}\n")
b.WriteString("</style>\n</head>\n<body>\n")
b.WriteString("<div class='container'>\n <div class='card'>\n <h1>🔐 Password Forge</h1>\n")
b.WriteString(" <p class='sub'>Aus einem Satz ein starkes Kennwort erzeugen - alternativ geben Sie einzelne Wörter ein.</p>\n")
b.WriteString(" <div class='grid'>\n <div>\n <label for='sentence'>Eingabesatz</label>\n")
b.WriteString(" <textarea id='sentence' placeholder='Z.B. “Morgen ist schönes Wetter”'></textarea>\n")
b.WriteString(" <div class='row' style='margin-top:10px'>\n")
b.WriteString(" <button id='btnGen'>Kennwort erzeugen</button>\n")
b.WriteString(" <span id='status' class='badge'>Mindestlänge: ")
b.WriteString(strconv.Itoa(cfg.MinLength))
b.WriteString(" • lower≥")
b.WriteString(strconv.Itoa(cfg.MinLower))
b.WriteString(" upper≥")
b.WriteString(strconv.Itoa(cfg.MinUpper))
b.WriteString(" digits≥")
b.WriteString(strconv.Itoa(cfg.MinDigits))
b.WriteString(" special≥")
b.WriteString(strconv.Itoa(cfg.MinSpecial))
b.WriteString("</span>\n")
b.WriteString(" </div>\n <div style='margin-top:12px'>\n <label>Ergebnis</label>\n")
b.WriteString(" <div id='out' class='output'></div>\n <small id='cplx' class='kv'></small>\n")
b.WriteString(" </div>\n </div>\n <div>\n")
b.WriteString(" <div style='display:flex;justify-content:space-between;align-items:center'>\n")
b.WriteString(" <label>Zufällige Vorschläge</label>\n")
b.WriteString(" <button id='btnRefresh' style='background:#38bdf8'>Neu</button>\n")
b.WriteString(" </div>\n")
b.WriteString(" <div id='sugs' style='display:grid;gap:8px'></div>\n")
b.WriteString(" </div>\n </div>\n <footer>\n Running on <kbd>")
b.WriteString(html.EscapeString(cfg.Addr))
b.WriteString("</kbd> • ENV: <kbd>MIN_LENGTH=")
b.WriteString(strconv.Itoa(cfg.MinLength))
b.WriteString("</kbd> <kbd>MIN_LOWER=")
b.WriteString(strconv.Itoa(cfg.MinLower))
b.WriteString("</kbd> <kbd>MIN_UPPER=")
b.WriteString(strconv.Itoa(cfg.MinUpper))
b.WriteString("</kbd> <kbd>MIN_DIGITS=")
b.WriteString(strconv.Itoa(cfg.MinDigits))
b.WriteString("</kbd> <kbd>MIN_SPECIAL=")
b.WriteString(strconv.Itoa(cfg.MinSpecial))
b.WriteString("</kbd>\n")
b.WriteString(" </footer>\n </div>\n</div>\n<script>\n")
b.WriteString("const $ = function(sel){ return document.querySelector(sel); };\n")
b.WriteString("function renderComplexity(c, ok){\n")
b.WriteString(" const badge = ok ? 'badge ok' : 'badge bad';\n")
b.WriteString(" return '<span class=\"' + badge + '\">L:' + c.length + '</span> '\n")
b.WriteString(" + '<span class=\"' + badge + '\">a:' + c.lower + '</span> '\n")
b.WriteString(" + '<span class=\"' + badge + '\">A:' + c.upper + '</span> '\n")
b.WriteString(" + '<span class=\"' + badge + '\">0:' + c.digits + '</span> '\n")
b.WriteString(" + '<span class=\"' + badge + '\">#:' + c.special + '</span>'; }\n")
b.WriteString("async function postJSON(url, data){\n")
b.WriteString(" const res = await fetch(url,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)});\n")
b.WriteString(" return await res.json(); }\n")
b.WriteString("async function getJSON(url){ const res = await fetch(url); return await res.json(); }\n")
b.WriteString("async function generate(){\n")
b.WriteString(" const sentence = $('#sentence').value;\n")
b.WriteString(" const res = await postJSON('/api/generate', { sentence });\n")
b.WriteString(" if(res && res.password){ $('#out').textContent = res.password; $('#cplx').innerHTML = renderComplexity(res.complexity, res.ok); }\n")
b.WriteString(" else { $('#out').textContent = res.message || 'Fehler'; $('#cplx').textContent = ''; } }\n")
b.WriteString("async function loadSugs(){\n")
b.WriteString(" const data = await getJSON('/api/suggest');\n")
b.WriteString(" const suggestions = (data && data.suggestions) ? data.suggestions : [];\n")
b.WriteString(" const wrap = $('#sugs'); wrap.innerHTML = '';\n")
b.WriteString(" suggestions.forEach(function(s){ const d = document.createElement('div'); d.className='suggestion'; d.textContent = s; wrap.appendChild(d); }); }\n")
b.WriteString("$('#btnGen').addEventListener('click', generate);\n")
b.WriteString("$('#btnRefresh').addEventListener('click', loadSugs);\n")
b.WriteString("loadSugs();\n")
b.WriteString("</script>\n</body>\n</html>\n")
return b.String()
}
/* -------------------- Small built-in word list -------------------- */
var wordList = []string{
"amt","behörde","rathaus","kreisamt","meldeamt","bauamt","forstamt","jagdamt","kasse","haushalt",
"doppik","kameral","konto","buchung","beleg","rechnung","revision","vergabe","gesetz","satzung",
"erlass","bescheid","antrag","auflage","ermessen","frist","recht","klage","einwand","widerruf",
"freigabe","lizenz","akte","akten","postfach","stempel","vorlage","signatur","eakte","dms",
"vorgang","ablage","archiv","ozg","eid","ifg","dsgvo","bsi","egov","portal",
"leitweg","peppol","vpn","backup","rollen","rechte","audit","token","tls","rat",
"stadtrat","kreistag","landtag","senat","wahl","wahlamt","mandat","sitzung","gebühr","kosten",
"ordnung","bußgeld","einsatz","streife","polizei","zoll","rettung","lage","sirene","nina",
"thw","verkehr","öpnv","fahrplan","tarif","ticket","fahrzeug","zeugnis","bauplan","planung",
"abnahme","denkmal","forst","wasser","abfall","müll","klärwerk","luftplan","lärm","energie",
"steuer","einkauf","umsatz","mahnung","sperre","vermerk","mittel","zuschuss","projekt","bericht",
"kennzahl","ziel","qualität","schulung","barriere","feedback","hotline","service","presse","bekannt",
"zustell","zwang","treuhand","amtsweg","hauspost","intranet","aufsicht","referat","dezernat","bereich",
"stelle","termin","formular","zahlung","automat","opendata","abfrage","meldung","gewerbe","parken",
"ausweis","urkunde","pass","schein","bafög","wohngeld","schule","kita","kiga","jugend",
"hilfe","pflege","rente","familie","arbeit","agentur","asyl","visum","titel","status",
}
/* -------------------- END -------------------- */