From c592b6d195aedcd6ec86e8c60d3ba91d524e293b Mon Sep 17 00:00:00 2001 From: Xe Iaso Date: Mon, 19 Jun 2023 12:56:31 -0400 Subject: second reshuffling Signed-off-by: Xe Iaso --- misc/i18n/LICENSE.md | 21 ++ misc/i18n/README.md | 79 ++++++ misc/i18n/doc.go | 2 + misc/i18n/legal.go | 29 +++ misc/i18n/lingo.go | 132 ++++++++++ misc/i18n/lingo_test.go | 87 +++++++ misc/i18n/locale.go | 101 ++++++++ misc/i18n/locale_test.go | 39 +++ misc/i18n/translations/de_DE.json | 32 +++ misc/i18n/translations/en_US.json | 32 +++ misc/i18n/translations/sr_RS.json | 32 +++ misc/localca/LICENSE | 21 ++ misc/localca/doc.go | 10 + misc/localca/legal.go | 27 ++ misc/localca/localca.go | 121 +++++++++ misc/localca/localca_test.go | 83 ++++++ misc/localca/minica.go | 234 +++++++++++++++++ misc/localca/utils.go | 83 ++++++ misc/namegen/elfs/elfs.go | 511 +++++++++++++++++++++++++++++++++++++ misc/namegen/elfs/elfs_test.go | 11 + misc/namegen/namegen.go | 36 +++ misc/namegen/namegen_test.go | 11 + misc/namegen/tarot/doc.go | 3 + misc/namegen/tarot/namegen.go | 40 +++ misc/namegen/tarot/namegen_test.go | 10 + 25 files changed, 1787 insertions(+) create mode 100644 misc/i18n/LICENSE.md create mode 100644 misc/i18n/README.md create mode 100644 misc/i18n/doc.go create mode 100644 misc/i18n/legal.go create mode 100644 misc/i18n/lingo.go create mode 100644 misc/i18n/lingo_test.go create mode 100644 misc/i18n/locale.go create mode 100644 misc/i18n/locale_test.go create mode 100644 misc/i18n/translations/de_DE.json create mode 100644 misc/i18n/translations/en_US.json create mode 100644 misc/i18n/translations/sr_RS.json create mode 100644 misc/localca/LICENSE create mode 100644 misc/localca/doc.go create mode 100644 misc/localca/legal.go create mode 100644 misc/localca/localca.go create mode 100644 misc/localca/localca_test.go create mode 100644 misc/localca/minica.go create mode 100644 misc/localca/utils.go create mode 100644 misc/namegen/elfs/elfs.go create mode 100644 misc/namegen/elfs/elfs_test.go create mode 100644 misc/namegen/namegen.go create mode 100644 misc/namegen/namegen_test.go create mode 100644 misc/namegen/tarot/doc.go create mode 100644 misc/namegen/tarot/namegen.go create mode 100644 misc/namegen/tarot/namegen_test.go (limited to 'misc') diff --git a/misc/i18n/LICENSE.md b/misc/i18n/LICENSE.md new file mode 100644 index 0000000..c72833f --- /dev/null +++ b/misc/i18n/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Dusan Lilic + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/misc/i18n/README.md b/misc/i18n/README.md new file mode 100644 index 0000000..c260ea2 --- /dev/null +++ b/misc/i18n/README.md @@ -0,0 +1,79 @@ +lingo +===== + +Very basic Golang library for i18n. There are others that do the job, but this is my take on the problem. + +Features: +--------- +1. Storing messages in JSON files. +2. Support for nested declarations. +2. Detecting language based on Request headers. +3. Very simple to use. + +Usage: +------ + 1. Import Lingo into your project + + ```go + import "github.com/kortem/lingo" + ``` + 1. Create a dir to store translations, and write them in JSON files named [locale].json. For example: + + ``` + en_US.json + sr_RS.json + de.json + ... + ``` + You can write nested JSON too. + ```json + { + "main.title" : "CutleryPlus", + "main.subtitle" : "Knives that put cut in cutlery.", + "menu" : { + "home" : "Home", + "products": { + "self": "Products", + "forks" : "Forks", + "knives" : "Knives", + "spoons" : "Spoons" + }, + } + } + ``` + 2. Initialize a Lingo like this: + + ```go + l := lingo.New("default_locale", "path/to/translations/dir") + ``` + + 3. Get bundle for specific locale via either `string`: + + ```go + t1 := l.TranslationsForLocale("en_US") + t2 := l.TranslationsForLocale("de_DE") + ``` + This way Lingo will return the bundle for specific locale, or default if given is not found. + Alternatively (or primarily), you can get it with `*http.Request`: + + ```go + t := l.TranslationsForRequest(req) + ``` + This way Lingo finds best suited locale via `Accept-Language` header, or if there is no match, returns default. + `Accept-Language` header is set by the browser, so basically it will serve the language the user has set to his browser. + 4. Once you get T instance just fire away! + + ```go + r1 := t1.Value("main.subtitle") + // "Knives that put cut in cutlery." + r1 := t2.Value("main.subtitle") + // "Messer, die legte in Besteck geschnitten." + r3 := t1.Value("menu.products.self") + // "Products" + r5 := t1.Value("error.404", req.URL.Path) + // "Page index.html not found!" + ``` + +Contributions: +----- +I regard this little library as feature-complete, but if you have an idea on how to improve it, feel free to create issues. Also, pull requests are welcome. Enjoy! diff --git a/misc/i18n/doc.go b/misc/i18n/doc.go new file mode 100644 index 0000000..2aa7275 --- /dev/null +++ b/misc/i18n/doc.go @@ -0,0 +1,2 @@ +// Package i18n handles internationalization and the like for Go programs. +package i18n diff --git a/misc/i18n/legal.go b/misc/i18n/legal.go new file mode 100644 index 0000000..7f68dff --- /dev/null +++ b/misc/i18n/legal.go @@ -0,0 +1,29 @@ +package i18n + +import "go4.org/legal" + +func init() { + legal.RegisterLicense(`i18n library of this software copyright: + +The MIT License (MIT) + +Copyright (c) 2014 Dusan Lilic + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.`) +} diff --git a/misc/i18n/lingo.go b/misc/i18n/lingo.go new file mode 100644 index 0000000..4824c01 --- /dev/null +++ b/misc/i18n/lingo.go @@ -0,0 +1,132 @@ +package i18n + +import ( + "encoding/json" + "io/ioutil" + "log" + "net/http" + "strconv" + "strings" +) + +// L represents Lingo bundle, containing map of all Ts by locale, +// as well as default locale and list of supported locales +type L struct { + bundle map[string]T + deflt string + supported []Locale +} + +func (l *L) exists(locale string) bool { + _, exists := l.bundle[locale] + return exists +} + +// TranslationsForRequest will get the best matched T for given +// Request. If no T is found, returns default T +func (l *L) TranslationsForRequest(r *http.Request) T { + locales := GetLocales(r) + for _, locale := range locales { + t, exists := l.bundle[locales[0].Name()] + if exists { + return t + } + for _, sup := range l.supported { + if locale.Lang == sup.Lang { + return l.bundle[sup.Name()] + } + } + } + return l.bundle[l.deflt] +} + +// TranslationsForLocale will get the T for specific locale. +// If no locale is found, returns default T +func (l *L) TranslationsForLocale(locale string) T { + t, exists := l.bundle[locale] + if exists { + return t + } + return l.bundle[l.deflt] +} + +// T represents translations map for specific locale +type T struct { + transl map[string]interface{} +} + +// Value traverses the translations map and finds translation for +// given key. If no translation is found, returns value of given key. +func (t T) Value(key string, args ...string) string { + if t.exists(key) { + res, ok := t.transl[key].(string) + if ok { + return t.parseArgs(res, args) + } + } + ksplt := strings.Split(key, ".") + for i := range ksplt { + k1 := strings.Join(ksplt[0:i], ".") + k2 := strings.Join(ksplt[i:len(ksplt)], ".") + if t.exists(k1) { + newt := &T{ + transl: t.transl[k1].(map[string]interface{}), + } + return newt.Value(k2, args...) + } + } + return key +} + +// parseArgs replaces the argument placeholders with given arguments +func (t T) parseArgs(value string, args []string) string { + res := value + for i := 0; i < len(args); i++ { + tok := "{" + strconv.Itoa(i) + "}" + res = strings.Replace(res, tok, args[i], -1) + } + return res +} + +// exists checks if value exists for given key +func (t T) exists(key string) bool { + _, ok := t.transl[key] + return ok +} + +// New creates the Lingo bundle. +// Params: +// Default locale, to be used when requested locale +// is not found. +// Path, absolute or relative path to a folder where +// translation .json files are kept +func New(deflt, path string) *L { + files, _ := ioutil.ReadDir(path) + l := &L{ + bundle: make(map[string]T), + deflt: deflt, + supported: make([]Locale, 0), + } + for _, f := range files { + fileName := f.Name() + dat, err := ioutil.ReadFile(path + "/" + fileName) + if err != nil { + log.Printf("Cannot read file %s, file corrupt.", fileName) + log.Printf("Error: %s", err) + continue + } + t := T{ + transl: make(map[string]interface{}), + } + err = json.Unmarshal(dat, &t.transl) + if err != nil { + log.Printf("Cannot read file %s, invalid JSON.", fileName) + log.Printf("Error: %s", err) + continue + } + locale := strings.Split(fileName, ".")[0] + l.supported = append(l.supported, ParseLocale(locale)) + l.bundle[locale] = t + } + return l +} diff --git a/misc/i18n/lingo_test.go b/misc/i18n/lingo_test.go new file mode 100644 index 0000000..ca1f559 --- /dev/null +++ b/misc/i18n/lingo_test.go @@ -0,0 +1,87 @@ +package i18n + +import ( + "net/http" + "net/http/httptest" + "net/url" + "testing" +) + +func TestLingo(t *testing.T) { + l := New("de_DE", "translations") + t1 := l.TranslationsForLocale("en_US") + r1 := t1.Value("main.subtitle") + r1Exp := "Knives that put cut in cutlery." + if r1 != r1Exp { + t.Errorf("Expected \""+r1Exp+"\", got %s", r1) + t.Fail() + } + r2 := t1.Value("home.title") + r2Exp := "Welcome to CutleryPlus!" + if r2 != r2Exp { + t.Errorf("Expected \""+r2Exp+"\", got %s", r2) + t.Fail() + } + r3 := t1.Value("menu.products.self") + r3Exp := "Products" + if r3 != r3Exp { + t.Errorf("Expected \""+r3Exp+"\", got %s", r3) + t.Fail() + } + r4 := t1.Value("menu.non.existant") + r4Exp := "non.existant" + if r4 != r4Exp { + t.Errorf("Expected \""+r4Exp+"\", got %s", r4) + t.Fail() + } + r5 := t1.Value("error.404", "idnex.html") + r5Exp := "Page idnex.html not found!" + if r5 != r5Exp { + t.Errorf("Expected \""+r5Exp+"\", got \"%s\"", r5) + t.Fail() + } +} + +func TestLingoHttp(t *testing.T) { + l := New("en_US", "translations") + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + expected := r.Header.Get("Expected-Results") + t1 := l.TranslationsForRequest(r) + r1 := t1.Value("error.500") + if r1 != expected { + t.Errorf("Expected \""+expected+"\", got %s", r1) + t.Fail() + } + })) + defer srv.Close() + url, _ := url.Parse(srv.URL) + + req1 := &http.Request{ + Method: "GET", + Header: map[string][]string{ + "Accept-Language": {"sr, en-gb;q=0.8, en;q=0.7"}, + "Expected-Results": {"Greska sa nase strane, pokusajte ponovo."}, + }, + URL: url, + } + req2 := &http.Request{ + Method: "GET", + Header: map[string][]string{ + "Accept-Language": {"en-US, en-gb;q=0.8, en;q=0.7"}, + "Expected-Results": {"Something is wrong on our side, please try again."}, + }, + URL: url, + } + req3 := &http.Request{ + Method: "GET", + Header: map[string][]string{ + "Accept-Language": {"de-at, en-gb;q=0.8, en;q=0.7"}, + "Expected-Results": {"Stimmt etwas nicht auf unserer Seite ist, versuchen Sie es erneut."}, + }, + URL: url, + } + + http.DefaultClient.Do(req1) + http.DefaultClient.Do(req2) + http.DefaultClient.Do(req3) +} diff --git a/misc/i18n/locale.go b/misc/i18n/locale.go new file mode 100644 index 0000000..6b4a793 --- /dev/null +++ b/misc/i18n/locale.go @@ -0,0 +1,101 @@ +package i18n + +import ( + "errors" + "net/http" + "strconv" + "strings" +) + +// Locale is locale value from the Accept-Language header in request +type Locale struct { + Lang, Country string + Qual float64 +} + +// Name returns the locale value in 'lang' or 'lang_country' format +// eg: de_DE, en_US, gb +func (l *Locale) Name() string { + if len(l.Country) > 0 { + return l.Lang + "_" + l.Country + } + return l.Lang +} + +// ParseLocale creates a Locale from a locale string +func ParseLocale(locale string) Locale { + locsplt := strings.Split(locale, "_") + resp := Locale{} + resp.Lang = locsplt[0] + if len(locsplt) > 1 { + resp.Country = locsplt[1] + } + return resp +} + +const ( + acceptLanguage = "Accept-Language" +) + +func supportedLocales(alstr string) []Locale { + locales := make([]Locale, 0) + alstr = strings.Replace(alstr, " ", "", -1) + if alstr == "" { + return locales + } + al := strings.Split(alstr, ",") + for _, lstr := range al { + locales = append(locales, Locale{ + Lang: parseLang(lstr), + Country: parseCountry(lstr), + Qual: parseQual(lstr), + }) + } + return locales +} + +// GetLocales returns supported locales for the given requet +func GetLocales(r *http.Request) []Locale { + return supportedLocales(r.Header.Get(acceptLanguage)) +} + +// GetPreferredLocale return preferred locale for the given reuqest +// returns error if there is no preferred locale +func GetPreferredLocale(r *http.Request) (*Locale, error) { + locales := GetLocales(r) + if len(locales) == 0 { + return &Locale{}, errors.New("No locale found") + } + return &locales[0], nil +} + +func parseLang(val string) string { + locale := strings.Split(val, ";")[0] + lang := strings.Split(locale, "-")[0] + return lang +} + +func parseCountry(val string) string { + locale := strings.Split(val, ";")[0] + spl := strings.Split(locale, "-") + if len(spl) > 1 { + return spl[1] + } + return "" +} + +func parseQual(val string) float64 { + spl := strings.Split(val, ";") + if len(spl) > 1 { + qualSpl := strings.Split(spl[1], "=") + if len(qualSpl) > 1 { + qual, err := strconv.ParseFloat(qualSpl[1], 64) + if err != nil { + return 1 + } + return qual + } + } + return 1 +} + diff --git a/misc/i18n/locale_test.go b/misc/i18n/locale_test.go new file mode 100644 index 0000000..6887064 --- /dev/null +++ b/misc/i18n/locale_test.go @@ -0,0 +1,39 @@ +package i18n + +import ( + "testing" +) + +func TestLocale(t *testing.T) { + l0 := supportedLocales("ja-JP;q") + if len(l0) != 1 { + t.Errorf("Expected number of locales \"1\", got %d", len(l0)) + t.Fail() + } + l1 := supportedLocales("en,de-AT; q=0.8,de;q=0.6,bg; q=0.4,en-US;q=0.2,sr;q=0.2") + if len(l1) != 6 { + t.Errorf("Expected number of locales \"6\", got %d", len(l1)) + t.Fail() + } + l2 := supportedLocales("en") + if len(l2) != 1 { + t.Errorf("Expected number of locales \"1\", got %d", len(l2)) + t.Fail() + } + l3 := supportedLocales("") + if len(l3) != 0 { + t.Errorf("Expected number of locales \"0\", got %d", len(l3)) + t.Fail() + } + l4 := ParseLocale("en_US") + if l4.Lang != "en" || l4.Country != "US" { + t.Errorf("Expected \"en\" and \"US\", got %s and %s", l4.Lang, l4.Country) + t.Fail() + } + l5 := ParseLocale("en") + if l5.Lang != "en" || l5.Country != "" { + t.Errorf("Expected \"en\" and \"\", got %s and %s", l5.Lang, l5.Country) + t.Fail() + } + +} diff --git a/misc/i18n/translations/de_DE.json b/misc/i18n/translations/de_DE.json new file mode 100644 index 0000000..c12186d --- /dev/null +++ b/misc/i18n/translations/de_DE.json @@ -0,0 +1,32 @@ +{ + "main.title" : "CutleryPlus", + "main.subtitle" : "Messer, die legte in Besteck geschnitten.", + "menu" : { + "home" : "Home", + "products": { + "self": "Produkte", + "forks" : "Gabeln", + "knives" : "Messer", + "spoons" : "Löffel" + }, + "gallery" : "Galerie", + "about" : "Über uns", + "contact" : "Kontakt" + }, + "home" : { + "title": "Willkommen in CutleryPlus!", + "text" : { + "p1": "Lorem ipsum...", + "p2": "Ein weiterer ipsum lorem." + } + }, + "error" : { + "404" : "Seite {0} wurde nicht gefunden.", + "500" : "Stimmt etwas nicht auf unserer Seite ist, versuchen Sie es erneut.", + "contact" : { + "name" : "Sie müssen Ihren Namen eingeben.", + "email" : "Sie müssen Ihre E-Mail ein.", + "text" : "Sie können eine leere Nachricht nicht zu senden." + } + } +} \ No newline at end of file diff --git a/misc/i18n/translations/en_US.json b/misc/i18n/translations/en_US.json new file mode 100644 index 0000000..6fa0b8f --- /dev/null +++ b/misc/i18n/translations/en_US.json @@ -0,0 +1,32 @@ +{ + "main.title" : "CutleryPlus", + "main.subtitle" : "Knives that put cut in cutlery.", + "menu" : { + "home" : "Home", + "products": { + "self": "Products", + "forks" : "Forks", + "knives" : "Knives", + "spoons" : "Spoons" + }, + "gallery" : "Gallery", + "about" : "About us", + "contact" : "Contact" + }, + "home" : { + "title": "Welcome to CutleryPlus!", + "text" : { + "p1": "Lorem ipsum...", + "p2": "Another ipsum lorem." + } + }, + "error" : { + "404" : "Page {0} not found!", + "500" : "Something is wrong on our side, please try again.", + "contact" : { + "name" : "You must enter your name.", + "email" : "You must enter your email.", + "text" : "You cannot send an empty message." + } + } +} \ No newline at end of file diff --git a/misc/i18n/translations/sr_RS.json b/misc/i18n/translations/sr_RS.json new file mode 100644 index 0000000..fe2bea5 --- /dev/null +++ b/misc/i18n/translations/sr_RS.json @@ -0,0 +1,32 @@ +{ + "main.title" : "CutleryPlus", + "main.subtitle" : "Escajg za svakoga", + "menu" : { + "home" : "Pocetna", + "products": { + "self": "Proizvodi", + "forks" : "Viljuske", + "knives" : "Nozevi", + "spoons" : "Kasike" + }, + "gallery" : "Galerija", + "about" : "O nama", + "contact" : "Kontakt" + }, + "home" : { + "title": "Dobrodosli u CutleryPlus!", + "text" : { + "p1": "Lorem ipsum...", + "p2": "Jos jedan ipsum lorem." + } + }, + "error" : { + "404" : "Stranica {0} ne postoji.", + "500" : "Greska sa nase strane, pokusajte ponovo.", + "contact" : { + "name" : "Ime ne sme biti prazno.", + "email" : "Email ne sme biti prazan.", + "text" : "Ne mozete poslati praznu poruku." + } + } +} \ No newline at end of file diff --git a/misc/localca/LICENSE b/misc/localca/LICENSE new file mode 100644 index 0000000..6aa3d70 --- /dev/null +++ b/misc/localca/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016 Jacob Hoffman-Andrews + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/misc/localca/doc.go b/misc/localca/doc.go new file mode 100644 index 0000000..fb4e829 --- /dev/null +++ b/misc/localca/doc.go @@ -0,0 +1,10 @@ +// Package localca uses an autocert.Cache to store and generate TLS certificates +// for domains on demand. +// +// This is kind of powerful, and as such it is limited to only generate +// certificates as subdomains of a given domain. +// +// The design and implementation of this is kinda stolen from minica[1]. +// +// [1]: https://github.com/jsha/minica +package localca diff --git a/misc/localca/legal.go b/misc/localca/legal.go new file mode 100644 index 0000000..9d35318 --- /dev/null +++ b/misc/localca/legal.go @@ -0,0 +1,27 @@ +package localca + +import "go4.org/legal" + +func init() { + legal.RegisterLicense(`MIT License + +Copyright (c) 2016 Jacob Hoffman-Andrews + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.`) +} diff --git a/misc/localca/localca.go b/misc/localca/localca.go new file mode 100644 index 0000000..360b53f --- /dev/null +++ b/misc/localca/localca.go @@ -0,0 +1,121 @@ +package localca + +import ( + "context" + "crypto/tls" + "encoding/pem" + "errors" + "strings" + "time" + + "golang.org/x/crypto/acme/autocert" +) + +var ( + ErrBadData = errors.New("localca: certificate data is bad") + ErrDomainDoesntHaveSuffix = errors.New("localca: domain doesn't have the given suffix") +) + +// Manager automatically provisions and caches TLS certificates in a given +// autocert Cache. If it cannot fetch a certificate on demand, the certificate +// is dynamically generated with a lifetime of 100 years, which should be good +// enough. +type Manager struct { + Cache autocert.Cache + DomainSuffix string + + *issuer +} + +// New creates a new Manager with the given key filename, certificate filename, +// allowed domain suffix and autocert cache. All given certificates will be +// created if they don't already exist. +func New(keyFile, certFile, suffix string, cache autocert.Cache) (Manager, error) { + iss, err := getIssuer(keyFile, certFile, true) + + if err != nil { + return Manager{}, err + } + + result := Manager{ + DomainSuffix: suffix, + Cache: cache, + issuer: iss, + } + + return result, nil +} + +func (m Manager) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { + name := hello.ServerName + if !strings.Contains(strings.Trim(name, "."), ".") { + return nil, errors.New("localca: server name component count invalid") + } + + if !strings.HasSuffix(name, m.DomainSuffix) { + return nil, ErrDomainDoesntHaveSuffix + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancel() + + data, err := m.Cache.Get(ctx, name) + if err != nil && err != autocert.ErrCacheMiss { + return nil, err + } + + if err == autocert.ErrCacheMiss { + data, _, err = m.issuer.sign([]string{name}, nil) + if err != nil { + return nil, err + } + err = m.Cache.Put(ctx, name, data) + if err != nil { + return nil, err + } + } + + cert, err := loadCertificate(name, data) + if err != nil { + return nil, err + } + + return cert, nil +} + +func loadCertificate(name string, data []byte) (*tls.Certificate, error) { + priv, pub := pem.Decode(data) + if priv == nil || !strings.Contains(priv.Type, "PRIVATE") { + return nil, ErrBadData + } + privKey, err := parsePrivateKey(priv.Bytes) + if err != nil { + return nil, err + } + + // public + var pubDER [][]byte + for len(pub) > 0 { + var b *pem.Block + b, pub = pem.Decode(pub) + if b == nil { + break + } + pubDER = append(pubDER, b.Bytes) + } + if len(pub) > 0 { + return nil, ErrBadData + } + + // verify and create TLS cert + leaf, err := validCert(name, pubDER, privKey, time.Now()) + if err != nil { + return nil, err + } + tlscert := &tls.Certificate{ + Certificate: pubDER, + PrivateKey: privKey, + Leaf: leaf, + } + return tlscert, nil +} diff --git a/misc/localca/localca_test.go b/misc/localca/localca_test.go new file mode 100644 index 0000000..0c85fba --- /dev/null +++ b/misc/localca/localca_test.go @@ -0,0 +1,83 @@ +package localca + +import ( + "context" + "crypto/tls" + "io" + "io/ioutil" + "os" + "path" + "testing" + "time" + + "golang.org/x/crypto/acme/autocert" +) + +func TestLocalCA(t *testing.T) { + dir, err := ioutil.TempDir("", "localca-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + cache := autocert.DirCache(dir) + + keyFile := path.Join(dir, "key.pem") + certFile := path.Join(dir, "cert.pem") + const suffix = "club" + + m, err := New(keyFile, certFile, suffix, cache) + if err != nil { + t.Fatal(err) + } + + t.Run("local", func(t *testing.T) { + _, err = m.GetCertificate(&tls.ClientHelloInfo{ + ServerName: "foo.local.cetacean.club", + }) + if err != nil { + t.Fatal(err) + } + }) + + t.Run("network", func(t *testing.T) { + t.Skip("no") + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + tc := &tls.Config{ + GetCertificate: m.GetCertificate, + } + + go func() { + lis, err := tls.Listen("tcp", ":9293", tc) + if err != nil { + t.Fatal(err) + } + defer lis.Close() + + for { + select { + case <-ctx.Done(): + return + default: + } + + cli, err := lis.Accept() + if err != nil { + t.Fatal(err) + } + defer cli.Close() + + go io.Copy(cli, cli) + } + }() + + time.Sleep(130 * time.Millisecond) + cli, err := tls.Dial("tcp", "localhost:9293", &tls.Config{InsecureSkipVerify: true}) + if err != nil { + t.Fatal(err) + } + defer cli.Close() + + cli.Write([]byte("butts")) + }) +} diff --git a/misc/localca/minica.go b/misc/localca/minica.go new file mode 100644 index 0000000..d47330b --- /dev/null +++ b/misc/localca/minica.go @@ -0,0 +1,234 @@ +package localca + +import ( + "bytes" + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/hex" + "encoding/pem" + "fmt" + "io/ioutil" + "math" + "math/big" + "net" + "os" + "strings" + "time" +) + +type issuer struct { + key crypto.Signer + cert *x509.Certificate +} + +func getIssuer(keyFile, certFile string, autoCreate bool) (*issuer, error) { + keyContents, keyErr := ioutil.ReadFile(keyFile) + certContents, certErr := ioutil.ReadFile(certFile) + if os.IsNotExist(keyErr) && os.IsNotExist(certErr) { + err := makeIssuer(keyFile, certFile) + if err != nil { + return nil, err + } + return getIssuer(keyFile, certFile, false) + } else if keyErr != nil { + return nil, fmt.Errorf("%s (but %s exists)", keyErr, certFile) + } else if certErr != nil { + return nil, fmt.Errorf("%s (but %s exists)", certErr, keyFile) + } + key, err := readPrivateKey(keyContents) + if err != nil { + return nil, fmt.Errorf("reading private key from %s: %s", keyFile, err) + } + + cert, err := readCert(certContents) + if err != nil { + return nil, fmt.Errorf("reading CA certificate from %s: %s", certFile, err) + } + + equal, err := publicKeysEqual(key.Public(), cert.PublicKey) + if err != nil { + return nil, fmt.Errorf("comparing public keys: %s", err) + } else if !equal { + return nil, fmt.Errorf("public key in CA certificate %s doesn't match private key in %s", + certFile, keyFile) + } + return &issuer{key, cert}, nil +} + +func readPrivateKey(keyContents []byte) (crypto.Signer, error) { + block, _ := pem.Decode(keyContents) + if block == nil { + return nil, fmt.Errorf("no PEM found") + } else if block.Type != "RSA PRIVATE KEY" && block.Type != "ECDSA PRIVATE KEY" { + return nil, fmt.Errorf("incorrect PEM type %s", block.Type) + } + return x509.ParsePKCS1PrivateKey(block.Bytes) +} + +func readCert(certContents []byte) (*x509.Certificate, error) { + block, _ := pem.Decode(certContents) + if block == nil { + return nil, fmt.Errorf("no PEM found") + } else if block.Type != "CERTIFICATE" { + return nil, fmt.Errorf("incorrect PEM type %s", block.Type) + } + return x509.ParseCertificate(block.Bytes) +} + +func makeIssuer(keyFile, certFile string) error { + keyData, key, err := makeKey() + if err != nil { + return err + } + ioutil.WriteFile(keyFile, keyData, 0600) + certData, _, err := makeRootCert(key, certFile) + if err != nil { + return err + } + ioutil.WriteFile(certFile, certData, 0600) + return nil +} + +func makeKey() ([]byte, *rsa.PrivateKey, error) { + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, nil, err + } + der := x509.MarshalPKCS1PrivateKey(key) + if err != nil { + return nil, nil, err + } + buf := bytes.NewBuffer([]byte{}) + err = pem.Encode(buf, &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: der, + }) + if err != nil { + return nil, nil, err + } + return buf.Bytes(), key, nil +} + +func makeRootCert(key crypto.Signer, filename string) ([]byte, *x509.Certificate, error) { + serial, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64)) + if err != nil { + return nil, nil, err + } + template := &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "localca root ca " + hex.EncodeToString(serial.Bytes()[:3]), + }, + SerialNumber: serial, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(100, 0, 0), + + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, + BasicConstraintsValid: true, + IsCA: true, + MaxPathLenZero: true, + } + + der, err := x509.CreateCertificate(rand.Reader, template, template, key.Public(), key) + if err != nil { + return nil, nil, err + } + buf := bytes.NewBuffer([]byte{}) + err = pem.Encode(buf, &pem.Block{ + Type: "CERTIFICATE", + Bytes: der, + }) + if err != nil { + return nil, nil, err + } + result, err := x509.ParseCertificate(der) + return buf.Bytes(), result, err +} + +func parseIPs(ipAddresses []string) ([]net.IP, error) { + var parsed []net.IP + for _, s := range ipAddresses { + p := net.ParseIP(s) + if p == nil { + return nil, fmt.Errorf("invalid IP address %s", s) + } + parsed = append(parsed, p) + } + return parsed, nil +} + +func publicKeysEqual(a, b interface{}) (bool, error) { + aBytes, err := x509.MarshalPKIXPublicKey(a) + if err != nil { + return false, err + } + bBytes, err := x509.MarshalPKIXPublicKey(b) + if err != nil { + return false, err + } + return bytes.Compare(aBytes, bBytes) == 0, nil +} + +func (iss *issuer) sign(domains []string, ipAddresses []string) ([]byte, *x509.Certificate, error) { + var cn string + if len(domains) > 0 { + cn = domains[0] + } else if len(ipAddresses) > 0 { + cn = ipAddresses[0] + } else { + return nil, nil, fmt.Errorf("must specify at least one domain name or IP address") + } + keyData, key, err := makeKey() + if err != nil { + return nil, nil, err + } + buf := bytes.NewBuffer([]byte{}) + buf.Write(keyData) + + parsedIPs, err := parseIPs(ipAddresses) + if err != nil { + return nil, nil, err + } + serial, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64)) + if err != nil { + return nil, nil, err + } + template := &x509.Certificate{ + DNSNames: domains, + IPAddresses: parsedIPs, + Subject: pkix.Name{ + CommonName: cn, + }, + SerialNumber: serial, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(90, 0, 0), + + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, + BasicConstraintsValid: true, + IsCA: false, + } + der, err := x509.CreateCertificate(rand.Reader, template, iss.cert, key.Public(), iss.key) + if err != nil { + return nil, nil, err + } + err = pem.Encode(buf, &pem.Block{ + Type: "CERTIFICATE", + Bytes: der, + }) + if err != nil { + return nil, nil, err + } + result, err := x509.ParseCertificate(der) + return buf.Bytes(), result, err +} + +func split(s string) (results []string) { + if len(s) > 0 { + return strings.Split(s, ",") + } + return nil +} diff --git a/misc/localca/utils.go b/misc/localca/utils.go new file mode 100644 index 0000000..efc06f3 --- /dev/null +++ b/misc/localca/utils.go @@ -0,0 +1,83 @@ +package localca + +import ( + "crypto" + "crypto/ecdsa" + "crypto/rsa" + "crypto/x509" + "errors" + "time" +) + +// validCert parses a cert chain provided as der argument and verifies the leaf and der[0] +// correspond to the private key, the domain and key type match, and expiration dates +// are valid. It doesn't do any revocation checking. +// +// The returned value is the verified leaf cert. +func validCert(name string, der [][]byte, key crypto.Signer, now time.Time) (leaf *x509.Certificate, err error) { + // parse public part(s) + var n int + for _, b := range der { + n += len(b) + } + pub := make([]byte, n) + n = 0 + for _, b := range der { + n += copy(pub[n:], b) + } + x509Cert, err := x509.ParseCertificates(pub) + if err != nil || len(x509Cert) == 0 { + return nil, errors.New("localca: no public key found") + } + // verify the leaf is not expired and matches the domain name + leaf = x509Cert[0] + if now.Before(leaf.NotBefore) { + return nil, errors.New("localca: certificate is not valid yet") + } + if now.After(leaf.NotAfter) { + return nil, errors.New("localca: expired certificate") + } + if err := leaf.VerifyHostname(name); err != nil { + return nil, err + } + // ensure the leaf corresponds to the private key and matches the certKey type + switch pub := leaf.PublicKey.(type) { + case *rsa.PublicKey: + prv, ok := key.(*rsa.PrivateKey) + if !ok { + return nil, errors.New("localca: private key type does not match public key type") + } + if pub.N.Cmp(prv.N) != 0 { + return nil, errors.New("localca: private key does not match public key") + } + default: + return nil, errors.New("localca: unknown public key algorithm") + } + return leaf, nil +} + +// Attempt to parse the given private key DER block. OpenSSL 0.9.8 generates +// PKCS#1 private keys by default, while OpenSSL 1.0.0 generates PKCS#8 keys. +// OpenSSL ecparam generates SEC1 EC private keys for ECDSA. We try all three. +// +// Inspired by parsePrivateKey in crypto/tls/tls.go. +func parsePrivateKey(der []byte) (crypto.Signer, error) { + if key, err := x509.ParsePKCS1PrivateKey(der); err == nil { + return key, nil + } + if key, err := x509.ParsePKCS8PrivateKey(der); err == nil { + switch key := key.(type) { + case *rsa.PrivateKey: + return key, nil + case *ecdsa.PrivateKey: + return key, nil + default: + return nil, errors.New("localca: unknown private key type in PKCS#8 wrapping") + } + } + if key, err := x509.ParseECPrivateKey(der); err == nil { + return key, nil + } + + return nil, errors.New("localca: failed to parse private key") +} diff --git a/misc/namegen/elfs/elfs.go b/misc/namegen/elfs/elfs.go new file mode 100644 index 0000000..5eb559a --- /dev/null +++ b/misc/namegen/elfs/elfs.go @@ -0,0 +1,511 @@ +/* +Package elfs is this project's heroku style name generator. +*/ +package elfs + +import ( + "fmt" + "math/rand" + "strings" + "time" +) + +func init() { + rand.Seed(time.Now().UnixNano()) +} + +// Names is the name of every Pokemon from Pokemon Vietnamese Crystal. +var Names = []string{ + "SEED", + "GRASS", + "FLOWE", + "SHAD", + "CABR", + "SNAKE", + "GOLD", + "COW", + "GUIKI", + "PEDAL", + "DELAN", + "B-FLY", + "BIDE", + "KEYU", + "FORK", + "LAP", + "PIGE", + "PIJIA", + "CAML", + "LAT", + "BIRD", + "BABOO", + "VIV", + "ABOKE", + "PIKAQ", + "RYE", + "SAN", + "BREAD", + "LIDEL", + "LIDE", + "PIP", + "PIKEX", + "ROK", + "JUGEN", + "PUD", + "BUDE", + "ZHIB", + "GELU", + "GRAS", + "FLOW", + "LAFUL", + "ATH", + "BALA", + "CORN", + "MOLUF", + "DESP", + "DAKED", + "MIMI", + "BOLUX", + "KODA", + "GELUD", + "MONK", + "SUMOY", + "GEDI", + "WENDI", + "NILEM", + "NILE", + "NILEC", + "KEZI", + "YONGL", + "HUDE", + "WANLI", + "GELI", + "GUAIL", + "MADAQ", + "WUCI", + "WUCI", + "MUJEF", + "JELLY", + "SICIB", + "GELU", + "NELUO", + "BOLI", + "JIALE", + "YED", + "YEDE", + "CLO", + "SCARE", + "AOCO", + "DEDE", + "DEDEI", + "BAWU", + "JIUG", + "BADEB", + "BADEB", + "HOLE", + "BALUX", + "GES", + "FANT", + "QUAR", + "YIHE", + "SWAB", + "SLIPP", + "CLU", + "DEPOS", + "BILIY", + "YUANO", + "SOME", + "NO", + "YELA", + "EMPT", + "ZECUN", + "XIAHE", + "BOLEL", + "DEJI", + "MACID", + "XIHON", + "XITO", + "LUCK", + "MENJI", + "GELU", + "DECI", + "XIDE", + "DASAJ", + "DONGN", + "RICUL", + "MINXI", + "BALIY", + "ZENDA", + "LUZEL", + "HELE5", + "0FENB", + "KAIL", + "JIAND", + "CARP", + "JINDE", + "LAPU", + "MUDE", + "YIFU", + "LINLI", + "SANDI", + "HUSI", + "JINC", + "OUMU", + "OUMUX", + "CAP", + "KUIZA", + "PUD", + "TIAO", + "FRMAN", + "CLAU", + "SPARK", + "DRAGO", + "BOLIU", + "GUAIL", + "MIYOU", + "MIY", + "QIAOK", + "BEIL", + "MUKEI", + "RIDED", + "MADAM", + "BAGEP", + "CROC", + "ALIGE", + "OUDAL", + "OUD", + "DADA", + "HEHE", + "YEDEA", + "NUXI", + "NUXIN", + "ROUY", + "ALIAD", + "STICK", + "QIANG", + "LAAND", + "PIQI", + "PI", + "PUPI", + "DEKE", + "DEKEJ", + "NADI", + "NADIO", + "MALI", + "PEA", + "ELECT", + "FLOWE", + "MAL", + "MALI", + "HUSHU", + "NILEE", + "YUZI", + "POPOZ", + "DUZI", + "HEBA", + "XIAN", + "SHAN", + "YEYEA", + "WUY", + "LUO", + "KEFE", + "HULA", + "CROW", + "YADEH", + "MOW", + "ANNAN", + "SUONI", + "KYLI", + "HULU", + "HUDEL", + "YEHE", + "GULAE", + "YEHE", + "BLU", + "GELAN", + "BOAT", + "NIP", + "POIT", + "HELAK", + "XINL", + "BEAR", + "LINB", + "MAGEH", + "MAGEJ", + "WULI", + "YIDE", + "RIVE", + "FISH", + "AOGU", + "DELIE", + "MANTE", + "KONMU", + "DELU", + "HELU", + "HUAN", + "HUMA", + "DONGF", + "JINCA", + "HEDE", + "DEFU", + "LIBY", + "JIAPA", + "MEJI", + "HELE", + "BUHU", + "MILK", + "HABI", + "THUN", + "GARD", + "DON", + "YANGQ", + "SANAQ", + "BANQ", + "LUJ", + "PHIX", + "SIEI", + "EGG", +} + +// Moves is every single move from Pokemon Vietnamese Crystal. +var Moves = []string{ + "ABLE", + "ABNORMA", + "AGAIN", + "AIREXPL", + "ANG", + "ANGER", + "ASAIL", + "ATTACK", + "AURORA", + "AWL", + "BAN", + "BAND", + "BARE", + "BEAT", + "BEATED", + "BELLY", + "BIND", + "BITE", + "BLOC", + "BLOOD", + "BODY", + "BOOK", + "BREATH", + "BUMP", + "CAST", + "CHAM", + "CLAMP", + "CLAP", + "CLAW", + "CLEAR", + "CLI", + "CLIP", + "CLOUD", + "CONTRO", + "CONVY", + "COOLHIT", + "CRASH", + "CRY", + "CUT", + "DESCRI", + "D-FIGHT", + "DIG", + "DITCH", + "DIV", + "DOZ", + "DRE", + "DUL", + "DU-PIN", + "DYE", + "EARTH", + "EDU", + "EG-BOMB", + "EGG", + "ELEGY", + "ELE-HIT", + "EMBODY", + "EMPLI", + "ENGL", + "ERUPT", + "EVENS", + "EXPLOR", + "EYES", + "FALL", + "FAST", + "F-CAR", + "F-DANCE", + "FEARS", + "F-FIGHT", + "FIGHT", + "FIR", + "FIRE", + "FIREHIT", + "FLAME", + "FLAP", + "FLASH", + "FLEW", + "FORCE", + "FRA", + "FREEZE", + "FROG", + "G-BIRD", + "GENKISS", + "GIFT", + "G-KISS", + "G-MOUSE", + "GRADE", + "GROW", + "HAMMER", + "HARD", + "HAT", + "HATE", + "H-BOMB", + "HELL-R", + "HEMP", + "HINT", + "HIT", + "HU", + "HUNT", + "HYPNOSI", + "INHA", + "IRO", + "IRONBAR", + "IR-WING", + "J-GUN", + "KEE", + "KICK", + "KNIF", + "KNIFE", + "KNOCK", + "LEVEL", + "LIGH", + "LIGHHIT", + "LIGHT", + "LIVE", + "L-WALL", + "MAD", + "MAJUS", + "MEL", + "MELO", + "MESS", + "MILK", + "MIMI", + "MISS", + "MIXING", + "MOVE", + "MUD", + "NI-BED", + "NOISY", + "NOONLI", + "NULL", + "N-WAVE", + "PAT", + "PEACE", + "PIN", + "PLAN", + "PLANE", + "POIS", + "POL", + "POWDE", + "POWE", + "POWER", + "PRIZE", + "PROTECT", + "PROUD", + "RAGE", + "RECOR", + "REFLAC", + "REFREC", + "REGR", + "RELIV", + "RENEW", + "R-FIGHT", + "RING", + "RKICK", + "ROCK", + "ROUND", + "RUS", + "RUSH", + "SAND", + "SAW", + "SCISSOR", + "SCRA", + "SCRIPT", + "SEEN", + "SERVER", + "SHADOW", + "SHELL", + "SHINE", + "SHO", + "SIGHT", + "SIN", + "SMALL", + "SMELT", + "SMOK", + "SNAKE", + "SNO", + "SNOW", + "SOU", + "SO-WAVE", + "SPAR", + "SPEC", + "SPID", + "S-PIN", + "SPRA", + "STAM", + "STARE", + "STEA", + "STONE", + "STORM", + "STRU", + "STRUG", + "STUDEN", + "SUBS", + "SUCID", + "SUN-LIG", + "SUNRIS", + "SUPLY", + "S-WAVE", + "TAILS", + "TANGL", + "TASTE", + "TELLI", + "THANK", + "TONKICK", + "TOOTH", + "TORL", + "TRAIN", + "TRIKICK", + "TUNGE", + "VOLT", + "WA-GUN", + "WATCH", + "WAVE", + "W-BOMB", + "WFALL", + "WFING", + "WHIP", + "WHIRL", + "WIND", + "WOLF", + "WOOD", + "WOR", + "YUJA", +} + +func randomMove() string { + return Moves[rand.Intn(len(Moves))] +} + +func randomName() string { + return Names[rand.Intn(len(Names))] +} + +// Next generates a new domain name based on the moves and Pokemon names +// from Pokemon Vietnamese Crystal. +func Next() string { + move1 := randomMove() + move2 := randomMove() + poke := randomName() + return strings.ToLower(fmt.Sprintf("%s-%s-%s", move1, move2, poke)) +} diff --git a/misc/namegen/elfs/elfs_test.go b/misc/namegen/elfs/elfs_test.go new file mode 100644 index 0000000..1609111 --- /dev/null +++ b/misc/namegen/elfs/elfs_test.go @@ -0,0 +1,11 @@ +package elfs + +import "testing" + +func TestNext(t *testing.T) { + n := Next() + t.Log(n) + if len(n) == 0 { + t.Fatalf("MakeName had a zero output") + } +} diff --git a/misc/namegen/namegen.go b/misc/namegen/namegen.go new file mode 100644 index 0000000..c23eb83 --- /dev/null +++ b/misc/namegen/namegen.go @@ -0,0 +1,36 @@ +// Package namegen generates a random name with one of several strategies. +package namegen + +import ( + "math/rand" + "time" + + "cirello.io/goherokuname" + "within.website/x/misc/namegen/elfs" + "within.website/x/misc/namegen/tarot" +) + +// Generator is a name generation function. +type Generator func() string + +// AddGenerator adds a generator to the list +func AddGenerator(g Generator) { + strats = append(strats, g) +} + +func init() { + rand.Seed(time.Now().UnixNano()) + + AddGenerator(elfs.Next) + AddGenerator(tarot.Next) + AddGenerator(goherokuname.HaikunateHex) +} + +var strats []Generator + +// Next gives you the next name in the series. +func Next() string { + gen := rand.Intn(len(strats)) + + return strats[gen]() +} diff --git a/misc/namegen/namegen_test.go b/misc/namegen/namegen_test.go new file mode 100644 index 0000000..36c66f1 --- /dev/null +++ b/misc/namegen/namegen_test.go @@ -0,0 +1,11 @@ +package namegen + +import "testing" + +func TestNext(t *testing.T) { + name := Next() + t.Log(name) + if name == "" { + t.Fatal("expected a name") + } +} diff --git a/misc/namegen/tarot/doc.go b/misc/namegen/tarot/doc.go new file mode 100644 index 0000000..9fb79cc --- /dev/null +++ b/misc/namegen/tarot/doc.go @@ -0,0 +1,3 @@ +// Package tarot is an automatic name generator. It generates names that could +// pass for tarot cards if you squint hard enough. +package tarot diff --git a/misc/namegen/tarot/namegen.go b/misc/namegen/tarot/namegen.go new file mode 100644 index 0000000..ec18d11 --- /dev/null +++ b/misc/namegen/tarot/namegen.go @@ -0,0 +1,40 @@ +package tarot + +import ( + "fmt" + "math/rand" + "time" +) + +func init() { + rand.Seed(time.Now().Unix()) +} + +// The ranks and suits of this name generator. +var ( + Ranks = []string{ + "one", "two", "three", "four", "five", + "six", "seven", "eight", "nine", "ten", + "ace", "king", "page", "princess", "queen", "jack", + "king", "magus", "prince", "knight", "challenger", + "daughter", "son", "priestess", "shaman", + } + + Suits = []string{ + "clubs", "hearts", "spades", "diamonds", // common playing cards + "swords", "cups", "pentacles", "wands", // tarot + "disks", // thoth tarot + "coins", // karma + "earth", "wind", "water", "air", // classical elements + "aether", "spirits", "nirvana", // new age sounding things + "chakras", "dilutions", "goings", + } +) + +// Next creates a name. +func Next() string { + rank := Ranks[rand.Int()%len(Ranks)] + suit := Suits[rand.Int()%len(Suits)] + + return fmt.Sprintf("%s-of-%s-%d", rank, suit, rand.Int63()%100000) +} diff --git a/misc/namegen/tarot/namegen_test.go b/misc/namegen/tarot/namegen_test.go new file mode 100644 index 0000000..e4f2640 --- /dev/null +++ b/misc/namegen/tarot/namegen_test.go @@ -0,0 +1,10 @@ +package tarot + +import ( + "log" + "testing" +) + +func TestNext(t *testing.T) { + log.Println(Next()) +} -- cgit v1.2.3