commit 63cd062dddd3321986e6e188e9fe72ab3a3dad66 Author: Christine Dodrill Date: Mon Jun 1 00:12:10 2020 -0400 initial commit diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..be81fed --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +eval "$(lorri direnv)" \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..691910f --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +relayd diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3d1119d --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2020 Christine Dodrill + +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/go.mod b/go.mod new file mode 100644 index 0000000..6a78861 --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module tulpa.dev/cadey/relayd + +go 1.14 + +require ( + github.com/facebookgo/flagenv v0.0.0-20160425205200-fcd59fca7456 + golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a393585 --- /dev/null +++ b/go.sum @@ -0,0 +1,11 @@ +github.com/facebookgo/flagenv v0.0.0-20160425205200-fcd59fca7456 h1:CkmB2l68uhvRlwOTPrwnuitSxi/S3Cg4L5QYOcL9MBc= +github.com/facebookgo/flagenv v0.0.0-20160425205200-fcd59fca7456/go.mod h1:zFhibDvPDWmtk4dAQ05sRobtyoffEHygEt3wSNuAzz8= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37 h1:cg5LA/zNPRzIXIWSCxQW10Rvpy94aQh3LT/ShoCpkHw= +golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/main.go b/main.go new file mode 100644 index 0000000..667a637 --- /dev/null +++ b/main.go @@ -0,0 +1,126 @@ +// Command relayd is a simple TLS terminator using let's encrypt. +package main + +import ( + "context" + "crypto/tls" + "encoding/json" + "errors" + "flag" + "fmt" + "log" + "net/http" + "net/http/httputil" + "net/url" + "os" + "strings" + "time" + + "github.com/facebookgo/flagenv" + "golang.org/x/crypto/acme/autocert" +) + +func fwdhttps(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case "POST", "PUT", "PATCH": + http.Error(w, "HTTPS access required", 400) + return + default: + http.RedirectHandler(fmt.Sprintf("https://%s%s", r.Host, r.RequestURI), http.StatusPermanentRedirect).ServeHTTP(w, r) + } +} + +var ( + insecurePort = flag.String("insecure-bind", ":80", "host/port to bind on for insecure (HTTP) traffic") + securePort = flag.String("secure-bind", ":443", "host/port to bind on for secure (HTTPS) traffic") + config = flag.String("cfg", "./relayd.json", "config file to read backend data from") + dataDir = flag.String("data-dir", "./.relayd", "data dir for certificates") +) + +type Site struct { + Domain string `json:"domain"` + URL *url.URL `json:"-"` + Target string `json:"target"` + PathPrefix string `json:"path_prefix"` +} + +type Config []Site + +func main() { + flagenv.Parse() + flag.Parse() + + go http.ListenAndServe(*insecurePort, http.HandlerFunc(fwdhttps)) + + var cfg Config + fin, err := os.Open(*config) + if err != nil { + log.Fatalf("can't open config file: %v", err) + } + defer fin.Close() + + err = json.NewDecoder(fin).Decode(&cfg) + if err != nil { + log.Fatalf("can't parse config: %v", err) + } + + m := autocert.Manager{ + Prompt: autocert.AcceptTOS, + HostPolicy: cfg.checkCert, + Cache: autocert.DirCache(*dataDir), + } + + for _, cfg := range cfg { + u, err := url.Parse(cfg.Target + cfg.PathPrefix) + if err != nil { + log.Fatalf("%v somehow isn't a url: %v", cfg.Target, err) + } + cfg.URL = u + } + + go func() { + err := http.ListenAndServe(*insecurePort, m.HTTPHandler(http.HandlerFunc(http.NotFound))) + if err != nil { + log.Fatal(err) + } + }() + + s := &http.Server{ + IdleTimeout: 5 * time.Minute, + Addr: *securePort, + TLSConfig: &tls.Config{GetCertificate: m.GetCertificate}, + Handler: cfg, + } + s.ListenAndServeTLS("", "") +} + +func (c Config) ServeHTTP(w http.ResponseWriter, r *http.Request) { + var s Site + for _, cfg := range c { + if r.Host == cfg.Domain && strings.HasPrefix(r.URL.Path, cfg.PathPrefix) { + s = cfg + } + } + + if s == *new(Site) { + http.Error(w, "unknown domain "+r.Host, http.StatusNotFound) + return + } + + if s.PathPrefix != "" { + r.URL.Path = strings.Split(r.URL.Path, s.PathPrefix)[1] + } + + rp := httputil.NewSingleHostReverseProxy(s.URL) + rp.ServeHTTP(w, r) +} + +func (c Config) checkCert(ctx context.Context, host string) error { + for _, cfg := range c { + if host == cfg.Domain { + return nil + } + } + + return errors.New("not allowed") +} diff --git a/relayd.json b/relayd.json new file mode 100644 index 0000000..ff13018 --- /dev/null +++ b/relayd.json @@ -0,0 +1,11 @@ +[ + { + "domain": "start.localhost.cetacean.club", + "target": "https://start.akua" + }, + { + "domain": "sekrit.localhost.cetacean.club", + "target": "http://127.0.0.1:42069", + "path_prefix": "/butts" + } +] diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..1503eb1 --- /dev/null +++ b/shell.nix @@ -0,0 +1,10 @@ +let + pkgs = import {}; +in +pkgs.mkShell { + buildInputs = with pkgs; [ + go + goimports + nur.repos.xe.gopls + ]; +}