aboutsummaryrefslogtreecommitdiff
path: root/misc
diff options
context:
space:
mode:
Diffstat (limited to 'misc')
-rw-r--r--misc/i18n/LICENSE.md21
-rw-r--r--misc/i18n/README.md79
-rw-r--r--misc/i18n/doc.go2
-rw-r--r--misc/i18n/legal.go29
-rw-r--r--misc/i18n/lingo.go132
-rw-r--r--misc/i18n/lingo_test.go87
-rw-r--r--misc/i18n/locale.go101
-rw-r--r--misc/i18n/locale_test.go39
-rw-r--r--misc/i18n/translations/de_DE.json32
-rw-r--r--misc/i18n/translations/en_US.json32
-rw-r--r--misc/i18n/translations/sr_RS.json32
-rw-r--r--misc/localca/LICENSE21
-rw-r--r--misc/localca/doc.go10
-rw-r--r--misc/localca/legal.go27
-rw-r--r--misc/localca/localca.go121
-rw-r--r--misc/localca/localca_test.go83
-rw-r--r--misc/localca/minica.go234
-rw-r--r--misc/localca/utils.go83
-rw-r--r--misc/namegen/elfs/elfs.go511
-rw-r--r--misc/namegen/elfs/elfs_test.go11
-rw-r--r--misc/namegen/namegen.go36
-rw-r--r--misc/namegen/namegen_test.go11
-rw-r--r--misc/namegen/tarot/doc.go3
-rw-r--r--misc/namegen/tarot/namegen.go40
-rw-r--r--misc/namegen/tarot/namegen_test.go10
25 files changed, 1787 insertions, 0 deletions
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) {