├── .gitignore ├── README.md ├── .goreleaser.yml ├── router_test.go └── router.go /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | *.swp 3 | *.config 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Routing for fun 2 | 3 | Worse than HA Proxy. Route traffic based on request URI's Host to locally running webservers. 4 | 5 | 6 | ## Run 7 | 8 | `sudo go run router.go -port 8080 -config whatever.config` 9 | 10 | 11 | ## Config file 12 | 13 | New-line separated in the format `%s: %d` 14 | 15 | ``` 16 | whatever.org: 9000 17 | nevermind.business: 9001 18 | anyway.net: 9002 19 | ``` 20 | 21 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # This is an example goreleaser.yaml file with some sane defaults. 2 | # Make sure to check the documentation at http://goreleaser.com 3 | before: 4 | hooks: 5 | - go generate ./... 6 | builds: 7 | - 8 | id: "router" 9 | main: ./router.go 10 | binary: router 11 | env: 12 | - CGO_ENABLED=0 13 | # - GO11MODULE=on 14 | ldflags: 15 | - -extldflags -s -X main.version={{.Version}} 16 | checksum: 17 | name_template: 'checksums.txt' 18 | snapshot: 19 | name_template: "{{ .Tag }}-next" 20 | changelog: 21 | sort: asc 22 | filters: 23 | exclude: 24 | - '^docs:' 25 | - '^test:' 26 | -------------------------------------------------------------------------------- /router_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestPortMap(t *testing.T) { 8 | cases := []struct { 9 | Want string 10 | Expect int 11 | }{ 12 | {"abc.net", 9090}, 13 | {"blah.org", 9999}, 14 | {"okokok.org", 9002}, 15 | } 16 | 17 | LoadConfig("./whatever.config") 18 | 19 | for _, c := range cases { 20 | if p := PortMap(c.Want); p != c.Expect { 21 | t.Errorf("PortMap(%s) equals %d, but it should equal %d", c.Want, p, c.Expect) 22 | } 23 | } 24 | } 25 | 26 | // 27 | func TestConcurrency(t *testing.T) { 28 | 29 | LoadConfig("./whatever.config") 30 | 31 | possible_urls := []string{"abc.net", "okokok.org"} 32 | 33 | for i := 0; i < 1000; i++ { 34 | go PortMap(possible_urls[i%len(possible_urls)]) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /router.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "flag" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | "os" 10 | "strconv" 11 | "strings" 12 | "sync" 13 | ) 14 | 15 | var version string = "?" 16 | 17 | var ( 18 | MapLock sync.Mutex 19 | Map map[string]int = map[string]int{} 20 | ) 21 | 22 | // PortMap returns the map 23 | func PortMap(url string) (port int) { 24 | 25 | // Maps aren't concurrency safe in go, but I don't they get concurrent read errors, 26 | // If they're n ot updated 27 | // MapLock.Lock() 28 | // defer MapLock.Unlock() 29 | 30 | var ok bool 31 | if port, ok = Map[url]; !ok { 32 | port = 9999 33 | } 34 | return 35 | } 36 | 37 | // ProxyRequest proxies a get request 38 | func ProxyRequest(r *http.Request) (resp *http.Response, err error) { 39 | port := PortMap(r.Host) 40 | 41 | // Clones the request, but shaves the host to something local:PORT 42 | context := r.Context() 43 | r2 := r.Clone(context) 44 | r2.URL.Scheme = "http" 45 | r2.URL.Host = fmt.Sprintf("127.0.0.1:%v", port) 46 | fmt.Printf("Proxy %v%v to \"%v\"\n", r.Host, r.URL, port) 47 | 48 | return http.DefaultTransport.RoundTrip(r2) 49 | } 50 | 51 | // This just parses lines here 52 | func LoadConfig(file_name string) { 53 | fi, err := os.Open(file_name) 54 | 55 | if err != nil { 56 | fmt.Println("Error:", err) 57 | os.Exit(2) 58 | return 59 | } 60 | 61 | scanner := bufio.NewScanner(fi) 62 | 63 | for scanner.Scan() { 64 | line := scanner.Text() 65 | 66 | if line == "" { 67 | fmt.Println("Skipping empty line") 68 | continue 69 | } 70 | 71 | pieces := strings.Split(line, ":") 72 | 73 | if len(pieces) != 2 { 74 | fmt.Println("Skipping invalid line:", line) 75 | continue 76 | } 77 | 78 | pieces[0] = strings.TrimSpace(pieces[0]) 79 | pieces[1] = strings.TrimSpace(pieces[1]) 80 | 81 | if pieces[0] == "" { 82 | fmt.Println("Skipping empty hostname") 83 | continue 84 | } 85 | 86 | if n, e := strconv.Atoi(pieces[1]); e == nil { 87 | Map[pieces[0]] = n 88 | } 89 | } 90 | } 91 | 92 | func main() { 93 | 94 | port := flag.Int("port", 8080, "listen on port") 95 | config := flag.String("config", "", "") 96 | flag.Parse() 97 | 98 | // This mutates the map! 99 | LoadConfig(*config) 100 | 101 | // Single proxy handler 102 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 103 | if resp, err := ProxyRequest(r); err != nil { 104 | fmt.Println(err) 105 | } else if body, err := ioutil.ReadAll(resp.Body); err != nil { 106 | fmt.Println(err) 107 | } else { 108 | if content_type := resp.Header.Get("Content-Type"); content_type != "" { 109 | w.Header().Add("Content-Type", content_type) 110 | } 111 | if n, e := w.Write(body); e != nil { 112 | fmt.Println(e) 113 | } else { 114 | fmt.Printf("Received %d bytes, Sent %d bytes\n", len(body), n) 115 | } 116 | } 117 | }) 118 | 119 | fmt.Printf("Router v%v\n", version) 120 | fmt.Printf("Port ....... %v\n", *port) 121 | for host, port := range Map { 122 | fmt.Printf("%v -> %v\n", host, port) 123 | } 124 | fmt.Println(http.ListenAndServe(fmt.Sprintf(":%v", *port), nil)) 125 | } 126 | --------------------------------------------------------------------------------