From aa3a6bf5992ad7246fdf4a9ae068bbf0ab1aecaa Mon Sep 17 00:00:00 2001 From: Ammar Bandukwala Date: Wed, 3 Feb 2016 22:36:11 -0600 Subject: [PATCH] hello version control --- LICENSE | 21 ++++++++++ README.md | 37 +++++++++++++++++ config.go | 103 ++++++++++++++++++++++++++++++++++++++++++++++ hostsplitter.go | 70 +++++++++++++++++++++++++++++++ signal_handler.go | 19 +++++++++ site.go | 23 +++++++++++ 6 files changed, 273 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 config.go create mode 100644 hostsplitter.go create mode 100644 signal_handler.go create mode 100644 site.go diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..aab608c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Ammar Bandukwala + +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/README.md b/README.md new file mode 100644 index 0000000..b54542b --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +# HostSplitter +HostSplitter is an HTTP reverse proxy and load balancer that distributes requests to an arbitrary amount of sites based on the Host header. + + +## Motivation +I commonly run into an issue developing small golang websites: I want to use the same IP address for many sites that aren't large enough to justify their own server. + +## Site files +HostSplitter will look for site files by default in "/etc/hostsplitter/". HostSplitter will only read files with the .json extension. + +A each site file should look like +```json +{ + "hostnames": [ + "ammar.io", + "www.ammar.io" + ], + "backends": [ + "127.0.0.1:9000" + ], + "secret": "puppies1234" +} +``` + +The "secret" field is passed along with every request to that site in the ``Hostsplitter-Secret`` header. This is intended to be checked before trusting the passed along IP. + +## Real IP +The original requester's IP is located in the ``X-Forwarded-For`` header. + +## Reloading +HostSplitter provides 0 downtime reload functionality via SIGUSR1. E.g +```bash +pkill -10 hostsplitter +``` + +## Roadmap +- SSL \ No newline at end of file diff --git a/config.go b/config.go new file mode 100644 index 0000000..3621793 --- /dev/null +++ b/config.go @@ -0,0 +1,103 @@ +package main + +import ( + "encoding/json" + "io/ioutil" + "log" + "os" + "path" + "reflect" +) + +var ( + config map[string]interface{} +) + +func LoadConfig() { + stagedRoutedHostnames := make(map[string]int) + stagedSites := []Site{} + + var err error + var files []os.FileInfo + + if err = os.MkdirAll(*sitesLoc, 0600); err != nil { + log.Print(err) + } + + if files, err = ioutil.ReadDir(*sitesLoc); err != nil { + log.Print(err) + return + } + + var configFiles []os.FileInfo + + //Filter out non json files + for _, f := range files { + if f.IsDir() { + continue + } + if path.Ext(f.Name()) != ".json" { + continue + } + configFiles = append(configFiles, f) + } + + //Process each site + for _, f := range configFiles { + var dat []byte + var err error + + siteConf := make(map[string]interface{}) + + siteIndex := len(stagedSites) + stagedSites = append(stagedSites, Site{}) + + if dat, err = ioutil.ReadFile(*sitesLoc + "/" + f.Name()); err != nil { + log.Print(err) + } + + if err = json.Unmarshal(dat, &siteConf); err != nil { + log.Print(err) + } + + switch siteConf["hostnames"].(type) { + case []interface{}: + for _, hostname := range siteConf["hostnames"].([]interface{}) { + switch hostname.(type) { + case string: + log.Print("Adding hostname -> ", hostname) + stagedRoutedHostnames[hostname.(string)] = siteIndex + default: + log.Print("Expected string but got ", reflect.TypeOf(hostname), " while parsing hostname in ", f.Name()) + } + } + default: + log.Print("Expected array but got ", reflect.TypeOf(siteConf["hostnames"]), " while parsing hosts in ", f.Name()) + } + + switch siteConf["backends"].(type) { + case []interface{}: + for _, backend := range siteConf["backends"].([]interface{}) { + switch backend.(type) { + case string: + log.Print("Adding backend -> ", backend) + stagedSites[siteIndex].Backends = append(stagedSites[siteIndex].Backends, backend.(string)) + default: + log.Print("Expected string but got ", reflect.TypeOf(backend), " while parsing backend in ", f.Name()) + } + } + default: + log.Print("Expected array but got ", reflect.TypeOf(siteConf["backends"]), " while parsing backends in ", f.Name()) + } + + switch siteConf["secret"].(type) { + case string: + stagedSites[siteIndex].Secret = siteConf["secret"].(string) + default: + log.Print("Expected string but got ", reflect.TypeOf(siteConf["secret"]), " while parsing secret in ", f.Name()) + } + + routedHostnames = stagedRoutedHostnames + Sites = stagedSites + } +} diff --git a/hostsplitter.go b/hostsplitter.go new file mode 100644 index 0000000..2582569 --- /dev/null +++ b/hostsplitter.go @@ -0,0 +1,70 @@ +package main + +import ( + "fmt" + "github.com/VividCortex/godaemon" + "gopkg.in/alecthomas/kingpin.v2" + "log" + "net/http" + "net/http/httputil" + "os" +) + +var ( + routedHostnames map[string]int + Sites []Site + HTTPClient http.Client +) + +var ( + logFileLoc = kingpin.Flag("log", "Location of the log file").Default("stdout").String() + daemonize = kingpin.Flag("daemon", "If daemonized, the program will run in the background.").Default("true").Bool() + sitesLoc = kingpin.Flag("sites_dir", "Location of site files").Short('h').Default("/etc/hostsplitter/").String() + bindAddr = kingpin.Flag("bind", "Bind address").Short('b').Default(":80").String() +) + +func main() { + HTTPClient = http.Client{} + + kingpin.Parse() + + log.Print("Starting hostsplitter") + + if *logFileLoc != "stdout" { + logFile, err := os.OpenFile(*logFileLoc, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0640) + defer logFile.Close() + if err != nil { + log.Fatalf("error opening file: %v", err) + } + log.Print("Using ", *logFileLoc, " for logging") + log.SetOutput(logFile) + } + + if *daemonize { + log.Print("Daemonizing... Bye Bye") + godaemon.MakeDaemon(&godaemon.DaemonAttr{}) + } + + LoadConfig() + + go SignalHandler() + + log.Fatal(http.ListenAndServe(*bindAddr, &httputil.ReverseProxy{ + Director: func(r *http.Request) { + HTTPLogger(r) + if i, ok := routedHostnames[string(r.Host)]; ok { + r.Header.Set("Hostsplitter-Secret", Sites[i].Secret) + r.Header.Set("Host", r.Host) + r.URL.Scheme = "http" + r.URL.Host = Sites[i].GetBackend() + r.RequestURI = "" + } else { + log.Print("%q is not routed", r.Host) + } + }, + })) +} + +func HTTPLogger(r *http.Request) { + log.Print(fmt.Sprintf("httplog> %v %v (%v) (conlen %v)", r.Host, r.Method, r.RequestURI, r.RemoteAddr)) +} diff --git a/signal_handler.go b/signal_handler.go new file mode 100644 index 0000000..bd57e02 --- /dev/null +++ b/signal_handler.go @@ -0,0 +1,19 @@ +package main + +import ( + "log" + "os" + "os/signal" + "syscall" +) + +func SignalHandler() { + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, syscall.Signal(0xa)) + for { + if <-sigs == syscall.Signal(0xa) { + log.Print("Recieved 0xa, reloading config") + LoadConfig() + } + } +} diff --git a/site.go b/site.go new file mode 100644 index 0000000..e358a41 --- /dev/null +++ b/site.go @@ -0,0 +1,23 @@ +package main + +type Site struct { + backendIndex int + Backends []string + Secret string +} + +func (this *Site) GetBackend() string { + if len(this.Backends) == 0 { + return "" + } + + index := this.backendIndex + + if this.backendIndex == len(this.Backends)-1 { + this.backendIndex = 0 + } else { + this.backendIndex = this.backendIndex + 1 + } + + return this.Backends[index] +}