├── .gitignore ├── Makefile ├── README.md └── reverseproxy ├── Makefile ├── config.go ├── config_test.go ├── main.go ├── proxy.go └── reverseproxy.conf /.gitignore: -------------------------------------------------------------------------------- 1 | syntax:glob 2 | *.[568ao] 3 | *.ao 4 | *.so 5 | *.pyc 6 | *.swp 7 | *.swo 8 | ._* 9 | .nfs.* 10 | [568a].out 11 | *~ 12 | *.orig 13 | *.pb.go 14 | core 15 | _obj 16 | _test 17 | src/pkg/Make.deps 18 | _testmain.go 19 | reverseproxy/reverseproxy 20 | 21 | syntax:regexp 22 | ^pkg/ 23 | ^src/cmd/(.*)/6?\1$ 24 | ^.*/core.[0-9]*$ 25 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright 2009 The Go Authors. All rights reserved. 2 | # Use of this source code is governed by a BSD-style 3 | # license that can be found in the LICENSE file. 4 | # 5 | # After editing the DIRS= list or adding imports to any Go files 6 | # in any of those directories, run: 7 | # 8 | # ./deps.bash 9 | # 10 | # to rebuild the dependency information in Make.deps. 11 | 12 | nullstring := 13 | space := $(nullstring) # a space at the end 14 | ifndef GOBIN 15 | QUOTED_HOME=$(subst $(space),\ ,$(HOME)) 16 | GOBIN=$(QUOTED_HOME)/bin 17 | endif 18 | QUOTED_GOBIN=$(subst $(space),\ ,$(GOBIN)) 19 | 20 | all: install 21 | 22 | DIRS=\ 23 | reverseproxy\ 24 | 25 | TEST=\ 26 | $(filter-out $(NOTEST),$(DIRS)) 27 | 28 | BENCH=\ 29 | $(filter-out $(NOBENCH),$(TEST)) 30 | 31 | clean.dirs: $(addsuffix .clean, $(DIRS)) 32 | install.dirs: $(addsuffix .install, $(DIRS)) 33 | nuke.dirs: $(addsuffix .nuke, $(DIRS)) 34 | test.dirs: $(addsuffix .test, $(TEST)) 35 | bench.dirs: $(addsuffix .bench, $(BENCH)) 36 | 37 | %.clean: 38 | +cd $* && $(QUOTED_GOBIN)/gomake clean 39 | 40 | %.install: 41 | +cd $* && $(QUOTED_GOBIN)/gomake install 42 | 43 | %.nuke: 44 | +cd $* && $(QUOTED_GOBIN)/gomake nuke 45 | 46 | %.test: 47 | +cd $* && $(QUOTED_GOBIN)/gomake test 48 | 49 | %.bench: 50 | +cd $* && $(QUOTED_GOBIN)/gomake bench 51 | 52 | clean: clean.dirs 53 | 54 | install: install.dirs 55 | 56 | test: test.dirs 57 | 58 | bench: bench.dirs 59 | 60 | nuke: nuke.dirs 61 | 62 | deps: 63 | ./deps.bash 64 | 65 | -include Make.deps 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | GoReverseProxy is an HTTP reverse proxy (similar to a simpler version of NGINX) 4 | whose primary purpose currently is to multiplex incoming HTTP requests to 5 | different backend servers according to virtual host, and to safeguard 6 | against malicious HTTP requests (GoReverseProxy will likely break before allowing 7 | a backend server to break). 8 | 9 | ## Features 10 | 11 | * Pipelining 12 | * Keepalive connections 13 | * Multiplexing by virtual hosts, specified in a config file 14 | * File-descriptor limiting 15 | * Connection timeouts 16 | 17 | ## Maturity 18 | 19 | I am running GoReverseProxy in production (albeit a small production) in front 20 | of my blog [Population Algorithms](http://popalg.org) and it has been working fine. 21 | The blog requests are generally pretty heavy (since they pull in a lot of resource 22 | files and things). Keepalive and pipelining have been working correctly. 23 | 24 | Nevertheless, it is still early to say that GoReverseProxy is truly production-ready. 25 | 26 | ## Installation 27 | 28 | To install, simply run 29 | 30 | git clone git://github.com/petar/GoReverseProxy.git GoReverseProxy-git 31 | cd GoReverseProxy-git 32 | make 33 | make install 34 | 35 | There is an example config file in the subdirectory `reverseproxy` which is 36 | simple and self-explanatory in JSON format. 37 | 38 | ## About 39 | 40 | GoReverseProxy is maintained by [Petar Maymounkov](http://pdos.csail.mit.edu/~petar/). 41 | -------------------------------------------------------------------------------- /reverseproxy/Makefile: -------------------------------------------------------------------------------- 1 | include $(GOROOT)/src/Make.inc 2 | 3 | TARG=reverseproxy 4 | GOFILES=\ 5 | main.go\ 6 | proxy.go\ 7 | config.go\ 8 | 9 | include $(GOROOT)/src/Make.cmd 10 | -------------------------------------------------------------------------------- /reverseproxy/config.go: -------------------------------------------------------------------------------- 1 | 2 | package main 3 | 4 | import ( 5 | "bytes" 6 | "fmt" 7 | "io/ioutil" 8 | "json" 9 | "os" 10 | "strings" 11 | "sync" 12 | ) 13 | 14 | type Config struct { 15 | sync.Mutex 16 | 17 | Timeout int64 // Keep-alive timeout in nanoseconds 18 | FDLimit int // Maximum number of file descriptors 19 | hosts map[string][]string // virtual host name -> array of actual net addr of server 20 | } 21 | 22 | func (c *Config) String() string { 23 | c.Lock() 24 | defer c.Unlock() 25 | 26 | var w bytes.Buffer 27 | fmt.Fprintf(&w, "Timeout=%g ns, FDLimit=%d\n", float64(c.Timeout), c.FDLimit) 28 | for v, aa := range c.hosts { 29 | fmt.Fprintf(&w, "%s->\n ", v) 30 | for _, a := range aa { 31 | fmt.Fprintf(&w, "%s, ", a) 32 | } 33 | fmt.Fprintln(&w, "") 34 | } 35 | return string(w.Bytes()) 36 | } 37 | 38 | func ParseConfigFile(filename string) (*Config, os.Error) { 39 | b, err := ioutil.ReadFile(filename) 40 | if err != nil { 41 | return nil, err 42 | } 43 | m := make(map[string]interface{}) 44 | err = json.Unmarshal(b, &m) 45 | if err != nil { 46 | return nil, err 47 | } 48 | return ParseConfigMap(m) 49 | } 50 | 51 | func ParseConfigMap(m map[string]interface{}) (*Config, os.Error) { 52 | c := &Config{ 53 | Timeout: 5e9, 54 | FDLimit: 200, 55 | hosts: make(map[string][]string), 56 | } 57 | // Timeout 58 | tmo_, ok := m["Timeout"] 59 | if ok { 60 | if tmo, ok := tmo_.(float64); ok { 61 | c.Timeout = int64(tmo) 62 | } 63 | } 64 | // FDLimit 65 | fdl_, ok := m["FDLimit"] 66 | if ok { 67 | if fdl, ok := fdl_.(float64); ok { 68 | c.FDLimit = int(fdl) 69 | } 70 | } 71 | // Virtual hosts 72 | for _, w_ := range getSliceInterface(m["Virtual"]) { 73 | w := getMapStringInterface(w_) 74 | vhosts := getSliceInterface(w["VHosts"]) 75 | ahosts := getSliceInterface(w["AHosts"]) 76 | a := []string{} 77 | for _, ah_ := range ahosts { 78 | ah := strings.TrimSpace(getString(ah_)) 79 | if ah != "" { 80 | a = append(a, strings.ToLower(ah)) 81 | } 82 | } 83 | if len(a) == 0 { 84 | continue 85 | } 86 | for _, vh_ := range vhosts { 87 | vh := strings.TrimSpace(getString(vh_)) 88 | if vh != "" { 89 | c.hosts[strings.ToLower(vh)] = a 90 | } 91 | } 92 | } 93 | return c, nil 94 | } 95 | 96 | func getString(s_ interface{}) string { 97 | if s_ == nil { 98 | return "" 99 | } 100 | if s, ok := s_.(string); ok { 101 | return s 102 | } 103 | return "" 104 | } 105 | 106 | func getMapStringInterface(v_ interface{}) map[string]interface{} { 107 | if v_ == nil { 108 | return make(map[string]interface{}) 109 | } 110 | if v, ok := v_.(map[string]interface{}); ok { 111 | return v 112 | } 113 | return make(map[string]interface{}) 114 | } 115 | 116 | func getSliceInterface(v_ interface{}) []interface{} { 117 | if v_ == nil { 118 | return []interface{}{} 119 | } 120 | if v, ok := v_.([]interface{}); ok { 121 | return v 122 | } 123 | return []interface{}{} 124 | } 125 | 126 | func (c *Config) ActualHost(vhost string) string { 127 | c.Lock() 128 | defer c.Unlock() 129 | 130 | aa, ok := c.hosts[vhost] 131 | if !ok { 132 | return "" 133 | } 134 | return aa[0] 135 | } 136 | -------------------------------------------------------------------------------- /reverseproxy/config_test.go: -------------------------------------------------------------------------------- 1 | 2 | package main 3 | 4 | import ( 5 | "testing" 6 | ) 7 | 8 | func TestParseConfig(t *testing.T) { 9 | _, err := ParseConfigFile("frontline.conf") 10 | if err != nil { 11 | t.Errorf("parse config: %s", err) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /reverseproxy/main.go: -------------------------------------------------------------------------------- 1 | 2 | package main 3 | 4 | import ( 5 | "flag" 6 | "fmt" 7 | "log" 8 | "os" 9 | ) 10 | 11 | var ( 12 | flagBind = flag.String("bind", "0.0.0.0:80", "Address to bind to") 13 | flagConfig = flag.String("config", "reverseproxy.conf", "Config file") 14 | ) 15 | 16 | func main() { 17 | fmt.Fprintf(os.Stderr, "GoReverseProxy — 2011 — by Petar Maymounkov, petar@csail.mit.edu\n") 18 | flag.Parse() 19 | p, err := NewProxyEasy(*flagBind, *flagConfig) 20 | if err != nil { 21 | log.Printf("Problem starting: %s\n", err) 22 | os.Exit(1) 23 | } 24 | fmt.Print(p.ConfigString()) 25 | p.Start() 26 | } 27 | -------------------------------------------------------------------------------- /reverseproxy/proxy.go: -------------------------------------------------------------------------------- 1 | 2 | package main 3 | 4 | import ( 5 | "container/list" 6 | "log" 7 | "net" 8 | "os" 9 | "strings" 10 | "sync" 11 | "time" 12 | "github.com/petar/GoHTTP/http" 13 | "github.com/petar/GoHTTP/server" 14 | "github.com/petar/GoHTTP/util" 15 | ) 16 | 17 | type Proxy struct { 18 | sync.Mutex // protects listen and conns 19 | 20 | listen net.Listener 21 | fdl util.FDLimiter 22 | pairs map[*connPair]int 23 | ech chan os.Error 24 | 25 | config *Config 26 | } 27 | 28 | type connPair struct { 29 | s *server.StampedServerConn 30 | c *server.StampedClientConn 31 | } 32 | 33 | func (cp *connPair) GetStamp() int64 { return min64(cp.s.GetStamp(), cp.c.GetStamp()) } 34 | 35 | func min64(p,q int64) int64 { 36 | if p < q { 37 | return p 38 | } 39 | return q 40 | } 41 | 42 | func NewProxy(l net.Listener, config *Config) (*Proxy, os.Error) { 43 | p := &Proxy{ 44 | listen: l, 45 | config: config, 46 | pairs: make(map[*connPair]int), 47 | ech: make(chan os.Error), 48 | } 49 | p.fdl.Init(config.FDLimit) 50 | return p, nil 51 | } 52 | 53 | func NewProxyEasy(addr, configfile string) (*Proxy, os.Error) { 54 | l, err := net.Listen("tcp", addr) 55 | if err != nil { 56 | return nil, err 57 | } 58 | conf, err := ParseConfigFile(configfile) 59 | if err != nil { 60 | return nil, err 61 | } 62 | return NewProxy(l, conf) 63 | } 64 | 65 | func (p *Proxy) Start() os.Error { 66 | go p.acceptLoop() 67 | go p.expireLoop() 68 | return <-p.ech 69 | } 70 | 71 | func (p *Proxy) ConfigString() string { return p.config.String() } 72 | 73 | func (p *Proxy) expireLoop() { 74 | for i := 0; ; i++ { 75 | p.Lock() 76 | if p.listen == nil { 77 | p.Unlock() 78 | return 79 | } 80 | now := time.Nanoseconds() 81 | kills := list.New() 82 | for q, _ := range p.pairs { 83 | if now - q.GetStamp() >= p.config.Timeout { 84 | kills.PushBack(q) 85 | } 86 | } 87 | p.Unlock() 88 | elm := kills.Front() 89 | for elm != nil { 90 | q := elm.Value.(*connPair) 91 | p.bury(q) 92 | elm = elm.Next() 93 | } 94 | kills.Init() 95 | kills = nil 96 | time.Sleep(p.config.Timeout) 97 | } 98 | } 99 | 100 | func (p *Proxy) acceptLoop() { 101 | for { 102 | p.Lock() 103 | l := p.listen 104 | p.Unlock() 105 | if l == nil { 106 | return 107 | } 108 | p.fdl.Lock() 109 | c, err := l.Accept() 110 | if err != nil { 111 | log.Printf("Error accepting: %s\n", err) 112 | if c != nil { 113 | c.Close() 114 | } 115 | p.fdl.Unlock() 116 | p.ech <- err 117 | return 118 | } 119 | go p.connLoop(c) 120 | } 121 | } 122 | 123 | // prepConn() takes a net.Conn and attaches a file descriptor release in its Close method 124 | func (p *Proxy) prepConn(c net.Conn) (net.Conn, os.Error) { 125 | c.(*net.TCPConn).SetKeepAlive(true) 126 | err := c.SetReadTimeout(p.config.Timeout) 127 | if err != nil { 128 | log.Printf("Error TCP set read timeout: %s\n", err) 129 | c.Close() 130 | p.fdl.Unlock() 131 | return c, err 132 | } 133 | err = c.SetWriteTimeout(p.config.Timeout) 134 | if err != nil { 135 | log.Printf("Error TCP set write timeout: %s\n", err) 136 | c.Close() 137 | p.fdl.Unlock() 138 | return c, err 139 | } 140 | return util.NewRunOnCloseConn(c, func() { p.fdl.Unlock() }), nil 141 | } 142 | 143 | func (p *Proxy) connLoop(s_ net.Conn) { 144 | s_, err := p.prepConn(s_) 145 | if err != nil { 146 | return 147 | } 148 | st := server.NewStampedServerConn(s_, nil) 149 | 150 | // Read and parse first request 151 | req0, err := st.Read() 152 | if err != nil { 153 | log.Printf("Read first Request: %s\n", err) 154 | st.Close() 155 | return 156 | } 157 | req0.Host = strings.ToLower(strings.TrimSpace(req0.Host)) 158 | if req0.Host == "" { 159 | st.Write(req0, http.NewResponse400String(req0, "GoReverseProxy: missing host")) 160 | st.Close() 161 | return 162 | } 163 | 164 | // Connect to host 165 | host := p.config.ActualHost(req0.Host) 166 | if host == "" { 167 | st.Write(req0, http.NewResponse400String(req0, "GoReverseProxy: unknwon host")) 168 | st.Close() 169 | return 170 | } 171 | p.fdl.Lock() 172 | c_, err := net.Dial("tcp", host) 173 | if err != nil { 174 | log.Printf("Dial server: %s\n", err) 175 | if c_ != nil { 176 | c_.Close() 177 | } 178 | p.fdl.Unlock() 179 | st.Write(req0, http.NewResponse400String(req0, "GoReverseProxy: error dialing host")) 180 | st.Close() 181 | return 182 | } 183 | c_, err = p.prepConn(c_) 184 | if err != nil { 185 | st.Write(req0, http.NewResponse400String(req0, "GoReverseProxy: error on host conn")) 186 | st.Close() 187 | return 188 | } 189 | ct := server.NewStampedClientConn(c_, nil) 190 | q := p.register(st, ct) 191 | 192 | ch := make(chan *http.Request, 5) 193 | go p.backLoop(ch, q) 194 | p.frontLoop(ch, q, req0) 195 | } 196 | 197 | // Read request from browser, write request to server, notify backLoop and repeat 198 | func (p *Proxy) frontLoop(ch chan<- *http.Request, q *connPair, req0 *http.Request) { 199 | var req *http.Request = req0 200 | for { 201 | // Read request from browser 202 | if req == nil { 203 | var err os.Error 204 | req, err = q.s.Read() 205 | if err != nil { 206 | // NOTE(petar): 'tcp read ... resource temporarily unavailable' errors 207 | // received here, I think, correspond to when the remote side has closed 208 | // the connection. This is OK. 209 | goto __Close 210 | } 211 | // TODO: Verify same Host 212 | } 213 | shouldClose := req.Close 214 | 215 | err := q.c.Write(req) 216 | if err != nil { 217 | log.Printf("Write Request: %s\n", err) 218 | goto __Close 219 | } 220 | ch <- req 221 | 222 | if shouldClose { 223 | goto __Close 224 | } 225 | req = nil 226 | } 227 | __Close: 228 | close(ch) 229 | } 230 | 231 | // Read request from frontLoop, read response from server, send response to browser, repeat 232 | func (p *Proxy) backLoop(ch <-chan *http.Request, q *connPair) { 233 | for { 234 | req, _ := <-ch 235 | if req == nil { 236 | goto __Close 237 | } 238 | 239 | resp, err := q.c.Read(req) 240 | if err != nil { 241 | log.Printf("Read Response: %s\n", err) 242 | goto __Close 243 | } 244 | 245 | err = q.s.Write(req, resp) 246 | if err != nil { 247 | log.Printf("Write Response: %s\n", err) 248 | goto __Close 249 | } 250 | } 251 | __Close: 252 | p.bury(q) 253 | } 254 | 255 | func (p *Proxy) register(st *server.StampedServerConn, ct *server.StampedClientConn) *connPair { 256 | p.Lock() 257 | defer p.Unlock() 258 | 259 | q := &connPair{st,ct} 260 | p.pairs[q] = 1 261 | return q 262 | } 263 | 264 | func (p *Proxy) bury(q *connPair) { 265 | p.Lock() 266 | defer p.Unlock() 267 | 268 | p.pairs[q] = 0, false 269 | q.s.Close() 270 | q.c.Close() 271 | } 272 | 273 | -------------------------------------------------------------------------------- /reverseproxy/reverseproxy.conf: -------------------------------------------------------------------------------- 1 | { 2 | "FDLimit": 200, 3 | "Timeout": 5e+09, 4 | "Virtual": [ 5 | { 6 | "VHosts": ["www.popalg.org", "popalg.org"], 7 | "AHosts": [ 8 | { 9 | "Path": "/debug", 10 | "Host": "127.0.0.1:23234" 11 | }, 12 | { 13 | "Path": "/", 14 | "Host": "127.0.0.1:23233" 15 | } 16 | ] 17 | } 18 | ] 19 | } 20 | --------------------------------------------------------------------------------