// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved. // Revel Framework source code and usage is governed by a MIT style // license that can be found in the LICENSE file. // Package harness for a Revel Framework. // // It has a following responsibilities: // 1. Parse the user program, generating a main.go file that registers // controller classes and starts the user's server. // 2. Build and run the user program. Show compile errors. // 3. Monitor the user source and re-build / restart the program when necessary. // // Source files are generated in the app/tmp directory. package harness import ( "crypto/tls" "fmt" "go/build" "io" "net" "net/http" "net/http/httputil" "net/url" "os" "os/signal" "path/filepath" "strings" "sync/atomic" "github.com/revel/revel" "sync" ) var ( doNotWatch = []string{"tmp", "views", "routes"} lastRequestHadError int32 ) // Harness reverse proxies requests to the application server. // It builds / runs / rebuilds / restarts the server when code is changed. type Harness struct { app *App serverHost string port int proxy *httputil.ReverseProxy watcher *revel.Watcher mutex *sync.Mutex } func renderError(iw http.ResponseWriter, ir *http.Request, err error) { context := revel.NewGoContext(nil) context.Request.SetRequest(ir) context.Response.SetResponse(iw) c := revel.NewController(context) c.RenderError(err).Apply(c.Request, c.Response) } // ServeHTTP handles all requests. // It checks for changes to app, rebuilds if necessary, and forwards the request. func (h *Harness) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Don't rebuild the app for favicon requests. if lastRequestHadError > 0 && r.URL.Path == "/favicon.ico" { return } // Flush any change events and rebuild app if necessary. // Render an error page if the rebuild / restart failed. err := h.watcher.Notify() if err != nil { // In a thread safe manner update the flag so that a request for // /favicon.ico does not trigger a rebuild atomic.CompareAndSwapInt32(&lastRequestHadError, 0, 1) renderError(w, r, err) return } // In a thread safe manner update the flag so that a request for // /favicon.ico is allowed atomic.CompareAndSwapInt32(&lastRequestHadError, 1, 0) // Reverse proxy the request. // (Need special code for websockets, courtesy of bradfitz) if strings.EqualFold(r.Header.Get("Upgrade"), "websocket") { proxyWebsocket(w, r, h.serverHost) } else { h.proxy.ServeHTTP(w, r) } } // NewHarness method returns a reverse proxy that forwards requests // to the given port. func NewHarness() *Harness { // Get a template loader to render errors. // Prefer the app's views/errors directory, and fall back to the stock error pages. revel.MainTemplateLoader = revel.NewTemplateLoader( []string{filepath.Join(revel.RevelPath, "templates")}) if err := revel.MainTemplateLoader.Refresh(); err != nil { revel.RevelLog.Error("Template loader error", "error", err) } addr := revel.HTTPAddr port := revel.Config.IntDefault("harness.port", 0) scheme := "http" if revel.HTTPSsl { scheme = "https" } // If the server is running on the wildcard address, use "localhost" if addr == "" { addr = "localhost" } if port == 0 { port = getFreePort() } serverURL, _ := url.ParseRequestURI(fmt.Sprintf(scheme+"://%s:%d", addr, port)) serverHarness := &Harness{ port: port, serverHost: serverURL.String()[len(scheme+"://"):], proxy: httputil.NewSingleHostReverseProxy(serverURL), mutex: &sync.Mutex{}, } if revel.HTTPSsl { serverHarness.proxy.Transport = &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, } } return serverHarness } // Refresh method rebuilds the Revel application and run it on the given port. func (h *Harness) Refresh() (err *revel.Error) { // Allow only one thread to rebuild the process h.mutex.Lock() defer h.mutex.Unlock() if h.app != nil { h.app.Kill() } revel.RevelLog.Debug("Rebuild Called") h.app, err = Build() if err != nil { return } h.app.Port = h.port if err2 := h.app.Cmd().Start(); err2 != nil { return &revel.Error{ Title: "App failed to start up", Description: err2.Error(), } } return } // WatchDir method returns false to file matches with doNotWatch // otheriwse true func (h *Harness) WatchDir(info os.FileInfo) bool { return !revel.ContainsString(doNotWatch, info.Name()) } // WatchFile method returns true given filename HasSuffix of ".go" // otheriwse false - implements revel.DiscerningListener func (h *Harness) WatchFile(filename string) bool { return strings.HasSuffix(filename, ".go") } // Run the harness, which listens for requests and proxies them to the app // server, which it runs and rebuilds as necessary. func (h *Harness) Run() { var paths []string if revel.Config.BoolDefault("watch.gopath", false) { gopaths := filepath.SplitList(build.Default.GOPATH) paths = append(paths, gopaths...) } paths = append(paths, revel.CodePaths...) h.watcher = revel.NewWatcher() h.watcher.Listen(h, paths...) h.watcher.Notify() go func() { addr := fmt.Sprintf("%s:%d", revel.HTTPAddr, revel.HTTPPort) revel.RevelLog.Infof("Listening on %s", addr) var err error if revel.HTTPSsl { err = http.ListenAndServeTLS( addr, revel.HTTPSslCert, revel.HTTPSslKey, h) } else { err = http.ListenAndServe(addr, h) } if err != nil { revel.RevelLog.Error("Failed to start reverse proxy:", "error", err) } }() // Kill the app on signal. ch := make(chan os.Signal) signal.Notify(ch, os.Interrupt, os.Kill) <-ch if h.app != nil { h.app.Kill() } os.Exit(1) } // Find an unused port func getFreePort() (port int) { conn, err := net.Listen("tcp", ":0") if err != nil { revel.RevelLog.Fatal("Unable to fetch a freee port address", "error", err) } port = conn.Addr().(*net.TCPAddr).Port err = conn.Close() if err != nil { revel.RevelLog.Fatal("Unable to close port", "error", err) } return port } // proxyWebsocket copies data between websocket client and server until one side // closes the connection. (ReverseProxy doesn't work with websocket requests.) func proxyWebsocket(w http.ResponseWriter, r *http.Request, host string) { var ( d net.Conn err error ) if revel.HTTPSsl { // since this proxy isn't used in production, // it's OK to set InsecureSkipVerify to true // no need to add another configuration option. d, err = tls.Dial("tcp", host, &tls.Config{InsecureSkipVerify: true}) } else { d, err = net.Dial("tcp", host) } if err != nil { http.Error(w, "Error contacting backend server.", 500) revel.RevelLog.Error("Error dialing websocket backend ", "host", host, "error", err) return } hj, ok := w.(http.Hijacker) if !ok { http.Error(w, "Not a hijacker?", 500) return } nc, _, err := hj.Hijack() if err != nil { revel.RevelLog.Error("Hijack error", "error", err) return } defer func() { if err = nc.Close(); err != nil { revel.RevelLog.Error("Connection close error", "error", err) } if err = d.Close(); err != nil { revel.RevelLog.Error("Dial close error", "error", err) } }() err = r.Write(d) if err != nil { revel.RevelLog.Error("Error copying request to target", "error", err) return } errc := make(chan error, 2) cp := func(dst io.Writer, src io.Reader) { _, err := io.Copy(dst, src) errc <- err } go cp(d, nc) go cp(nc, d) <-errc }