From d17e5ab9cfb96352bcda90aafbf56178e227d46f Mon Sep 17 00:00:00 2001 From: Trevor Slocum Date: Tue, 24 Nov 2020 09:29:57 -0800 Subject: [PATCH] Parse configuration file --- CONFIGURATION.md | 33 +++++++++++++++++++++++++++++++ README.md | 4 ++++ config.go | 44 +++++++++++++++++++++++++++++++++++++++++ go.mod | 2 ++ go.sum | 4 ++++ main.go | 40 ++++++++++++++++++++++++++++++++++++- pkg/gmitohtml/daemon.go | 22 +++++++++++++-------- 7 files changed, 140 insertions(+), 9 deletions(-) create mode 100644 CONFIGURATION.md create mode 100644 config.go diff --git a/CONFIGURATION.md b/CONFIGURATION.md new file mode 100644 index 0000000..33ab118 --- /dev/null +++ b/CONFIGURATION.md @@ -0,0 +1,33 @@ +`gmitohtml` loads its configuration from `~/.config/gmitohtml/config.yaml` by +default. You may specify a different location via the `--config` argument. + +# Configuration options + +## Client certificates + +Client certificates may be specified via the `Certs` option. + +To generate a client certificate, run the following: + +```bash +openssl req -x509 -out localhost.crt -keyout localhost.key \ + -newkey rsa:2048 -nodes -sha256 \ + -subj '/CN=localhost' -extensions EXT -config <( \ + printf "[dn]\nCN=localhost\n[req]\ndistinguished_name = dn\n[EXT]\nsubjectAltName=DNS:localhost\nkeyUsage=digitalSignature\nextendedKeyUsage=serverAuth") +``` + +Files `localhost.crt` and `localhost.key` are generated. Rename these files to +match the domain where the certificate will be used. + +# Example config.yaml + +```yaml +certs: + astrobotany.mozz.us: + cert: /home/dioscuri/.config/gmitohtml/astrobotany.mozz.us.crt + key: /home/dioscuri/.config/gmitohtml/astrobotany.mozz.us.crt + gemini.rocks: + cert: /home/dioscuri/.config/gmitohtml/gemini.rocks.crt + key: /home/dioscuri/.config/gmitohtml/gemini.rocks.key + +``` diff --git a/README.md b/README.md index 00f0b77..1802bfc 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,10 @@ go get gitlab.com/tslocum/gmitohtml The resulting binary is available as `~/go/bin/gmitohtml`. +## Configure + +See [CONFIGURATION.md](https://gitlab.com/tslocum/gmitohtml/blob/master/CONFIGURATION.md) + ## Usage Run daemon at [http://localhost:1967](http://localhost:1967): diff --git a/config.go b/config.go new file mode 100644 index 0000000..0e19dcf --- /dev/null +++ b/config.go @@ -0,0 +1,44 @@ +package main + +import ( + "crypto/tls" + "errors" + "io/ioutil" + + "gopkg.in/yaml.v3" +) + +type certConfig struct { + Cert string + Key string + + cert tls.Certificate +} + +type appConfig struct { + Certs map[string]*certConfig +} + +var config = &appConfig{ + Certs: make(map[string]*certConfig), +} + +func readconfig(configPath string) error { + if configPath == "" { + return errors.New("file unspecified") + } + + configData, err := ioutil.ReadFile(configPath) + if err != nil { + return err + } + + var newConfig *appConfig + err = yaml.Unmarshal(configData, &newConfig) + if err != nil { + return err + } + config = newConfig + + return nil +} diff --git a/go.mod b/go.mod index 06d5296..7638ace 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module gitlab.com/tslocum/gmitohtml go 1.15 + +require gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 diff --git a/go.sum b/go.sum index e69de29..d1f72c7 100644 --- a/go.sum +++ b/go.sum @@ -0,0 +1,4 @@ +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index f6438e1..71e647e 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,8 @@ package main import ( + "path" + "gitlab.com/tslocum/gmitohtml/pkg/gmitohtml" "flag" @@ -33,11 +35,47 @@ func openBrowser(url string) { func main() { var view bool var daemon string + var configFile string flag.BoolVar(&view, "view", false, "open web browser") flag.StringVar(&daemon, "daemon", "", "start daemon on specified address") + flag.StringVar(&configFile, "config", "", "path to configuration file") // TODO option to include response header in page flag.Parse() + if configFile == "" { + homedir, err := os.UserHomeDir() + if err == nil && homedir != "" { + defaultConfig := path.Join(homedir, ".config", "gmitohtml", "config.yaml") + if _, err := os.Stat(defaultConfig); !os.IsNotExist(err) { + configFile = defaultConfig + } + } + } + + if configFile != "" { + err := readconfig(configFile) + if err != nil { + log.Fatalf("failed to read configuration file at %s: %v\nSee CONFIGURATION.md for information on configuring gmitohtml", configFile, err) + } + } + + for domain, cc := range config.Certs { + certData, err := ioutil.ReadFile(cc.Cert) + if err != nil { + log.Fatalf("failed to load client certificate for domain %s: %s", domain, err) + } + + keyData, err := ioutil.ReadFile(cc.Key) + if err != nil { + log.Fatalf("failed to load client certificate for domain %s: %s", domain, err) + } + + err = gmitohtml.SetClientCertificate(domain, certData, keyData) + if err != nil { + log.Fatalf("failed to load client certificate for domain %s", domain) + } + } + if daemon != "" { err := gmitohtml.StartDaemon(daemon) if err != nil { @@ -48,7 +86,7 @@ func main() { openBrowser("http://" + daemon) } - select {} //TODO + select {} } data, err := ioutil.ReadAll(os.Stdin) diff --git a/pkg/gmitohtml/daemon.go b/pkg/gmitohtml/daemon.go index ee04f36..7596348 100644 --- a/pkg/gmitohtml/daemon.go +++ b/pkg/gmitohtml/daemon.go @@ -3,6 +3,7 @@ package gmitohtml import ( "bytes" "crypto/tls" + "crypto/x509" "errors" "fmt" "io/ioutil" @@ -147,13 +148,6 @@ func handleRequest(writer http.ResponseWriter, request *http.Request) { return } - inputText := request.PostFormValue("input") - if inputText != "" { - request.URL.RawQuery = inputText - http.Redirect(writer, request, request.URL.String(), http.StatusSeeOther) - return - } - pathSplit := strings.Split(request.URL.Path, "/") if len(pathSplit) < 2 || pathSplit[1] != "gemini" { writer.Write([]byte("Error: invalid protocol, only Gemini is supported")) @@ -169,6 +163,13 @@ func handleRequest(writer http.ResponseWriter, request *http.Request) { u.RawQuery = request.URL.RawQuery } + inputText := request.PostFormValue("input") + if inputText != "" { + u.RawQuery = inputText + http.Redirect(writer, request, rewriteURL(u.String(), u), http.StatusSeeOther) + return + } + header, data, err := fetch(u.String()) if err != nil { fmt.Fprintf(writer, "Error: failed to fetch %s: %s", u, err) @@ -178,7 +179,7 @@ func handleRequest(writer http.ResponseWriter, request *http.Request) { if len(header) > 0 && header[0] == '3' { split := bytes.SplitN(header, []byte(" "), 2) if len(split) == 2 { - http.Redirect(writer, request, rewriteURL(string(split[1]), request.URL), http.StatusSeeOther) + http.Redirect(writer, request, rewriteURL(string(split[1]), u), http.StatusSeeOther) return } } @@ -234,6 +235,11 @@ func SetClientCertificate(domain string, certificate []byte, privateKey []byte) return ErrInvalidCertificate } + leafCert, err := x509.ParseCertificate(clientCert.Certificate[0]) + if err == nil { + clientCert.Leaf = leafCert + } + clientCerts[domain] = clientCert return nil }