github.com/orteth01/up@v0.2.0/http/relay/relay.go (about) 1 // Package relay provides a reverse proxy which 2 // relays requests to your "vanilla" HTTP server, 3 // and supports crash recovery. 4 package relay 5 6 import ( 7 "fmt" 8 "net" 9 "net/http" 10 "net/http/httputil" 11 "net/url" 12 "os" 13 "os/exec" 14 "sync" 15 "time" 16 17 "github.com/apex/log" 18 "github.com/facebookgo/freeport" 19 "github.com/jpillora/backoff" 20 "github.com/pkg/errors" 21 22 "github.com/apex/up" 23 "github.com/apex/up/internal/logs" 24 "github.com/apex/up/internal/logs/writer" 25 ) 26 27 // TODO: Wait() and handle error 28 // TODO: add timeout 29 // TODO: scope all plugin logs to their plugin name 30 // TODO: utilize BufferPool 31 // TODO: if the first Start() fails then bail 32 33 // log context. 34 var ctx = logs.Plugin("relay") 35 36 // DefaultTransport used by relay. 37 var DefaultTransport http.RoundTripper = &http.Transport{ 38 DialContext: (&net.Dialer{ 39 Timeout: 2 * time.Second, 40 KeepAlive: 2 * time.Second, 41 DualStack: true, 42 }).DialContext, 43 MaxIdleConns: 0, 44 MaxIdleConnsPerHost: 10, 45 IdleConnTimeout: 5 * time.Minute, 46 TLSHandshakeTimeout: 2 * time.Second, 47 ExpectContinueTimeout: 1 * time.Second, 48 } 49 50 // Proxy is a reverse proxy and sub-process monitor 51 // for ensuring your web server is running. 52 type Proxy struct { 53 config *up.Config 54 55 mu sync.Mutex 56 restarts int 57 port int 58 target *url.URL 59 *httputil.ReverseProxy 60 } 61 62 // New proxy. 63 func New(c *up.Config) (http.Handler, error) { 64 p := &Proxy{ 65 config: c, 66 } 67 68 if err := p.Start(); err != nil { 69 return nil, err 70 } 71 72 return p, nil 73 } 74 75 // Start the server. 76 func (p *Proxy) Start() error { 77 if err := p.start(); err != nil { 78 return err 79 } 80 81 p.ReverseProxy = httputil.NewSingleHostReverseProxy(p.target) 82 p.ReverseProxy.Transport = p 83 84 timeout := time.Duration(p.config.Proxy.Timeout) * time.Second 85 ctx.Infof("waiting for %s (timeout %s)", p.target.String()) 86 87 if err := waitForListen(p.target, timeout); err != nil { 88 return errors.Wrapf(err, "waiting for %s to be in listening state", p.target.String()) 89 } 90 91 return nil 92 } 93 94 // Restart the server. 95 func (p *Proxy) Restart() error { 96 ctx.Warn("restarting") 97 p.restarts++ 98 99 if err := p.Start(); err != nil { 100 return err 101 } 102 103 ctx.WithField("restarts", p.restarts).Warn("restarted") 104 return nil 105 } 106 107 // ServeHTTP implementation. 108 func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { 109 p.mu.Lock() 110 defer p.mu.Unlock() 111 p.ReverseProxy.ServeHTTP(w, r) 112 } 113 114 // RoundTrip implementation. 115 func (p *Proxy) RoundTrip(r *http.Request) (*http.Response, error) { 116 // TODO: give up after N attempts 117 118 b := p.config.Proxy.Backoff.Backoff() 119 120 retry: 121 // replace host as it will change on restart 122 r.URL.Host = p.target.Host 123 res, err := DefaultTransport.RoundTrip(r) 124 125 // everything is fine 126 if err == nil { 127 return res, nil 128 } 129 130 // temporary error, try again 131 if e, ok := err.(net.Error); ok && e.Temporary() { 132 ctx.WithError(err).Warn("temporary error") 133 time.Sleep(b.Duration()) 134 goto retry 135 } 136 137 // timeout error, try again 138 if e, ok := err.(net.Error); ok && e.Timeout() { 139 ctx.WithError(err).Warn("timed out") 140 time.Sleep(b.Duration()) 141 goto retry 142 } 143 144 // restart the server, try again 145 ctx.WithError(err).Error("network error") 146 if err := p.Restart(); err != nil { 147 return nil, errors.Wrap(err, "restarting") 148 } 149 150 goto retry 151 } 152 153 // environment returns the server env variables. 154 func (p *Proxy) environment() []string { 155 return []string{ 156 env("PORT", p.port), 157 env("UP_RESTARTS", p.restarts), 158 } 159 } 160 161 // start the server on a free port. 162 func (p *Proxy) start() error { 163 port, err := freeport.Get() 164 if err != nil { 165 return errors.Wrap(err, "getting free port") 166 } 167 p.port = port 168 ctx.Infof("found free port %d", port) 169 170 ctx.Infof("executing %q", p.config.Proxy.Command) 171 cmd := exec.Command("sh", "-c", p.config.Proxy.Command) 172 cmd.Stdout = writer.New(log.InfoLevel) 173 cmd.Stderr = writer.New(log.ErrorLevel) 174 env := append(p.environment(), "PATH=node_modules/.bin:"+os.Getenv("PATH")) 175 cmd.Env = append(os.Environ(), env...) 176 177 if err := cmd.Start(); err != nil { 178 return errors.Wrap(err, "running command") 179 } 180 181 target, err := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", port)) 182 if err != nil { 183 return errors.Wrap(err, "parsing url") 184 } 185 p.target = target 186 187 return nil 188 } 189 190 // env returns an environment variable. 191 func env(name string, val interface{}) string { 192 return fmt.Sprintf("%s=%v", name, val) 193 } 194 195 // waitForListen blocks until `u` is listening with timeout. 196 func waitForListen(u *url.URL, timeout time.Duration) error { 197 timedout := time.After(timeout) 198 199 b := backoff.Backoff{ 200 Min: 100 * time.Millisecond, 201 Max: time.Second, 202 Factor: 1.5, 203 } 204 205 for { 206 select { 207 case <-timedout: 208 return errors.Errorf("timed out after %s", timeout) 209 case <-time.After(b.Duration()): 210 if isListening(u) { 211 return nil 212 } 213 } 214 } 215 } 216 217 // isListening returns true if there's a server listening on `u`. 218 func isListening(u *url.URL) bool { 219 conn, err := net.Dial("tcp", u.Host) 220 if err != nil { 221 return false 222 } 223 224 conn.Close() 225 return true 226 }