github.com/brandur/modulir@v0.0.0-20240305213423-94ee82929cbd/http.go (about)

     1  package modulir
     2  
     3  //go:generate go run scripts/embed_js/main.go
     4  
     5  import (
     6  	"errors"
     7  	"fmt"
     8  	"net/http"
     9  	"path"
    10  	"sync"
    11  	"text/template"
    12  	"time"
    13  
    14  	"github.com/gorilla/websocket"
    15  	"golang.org/x/xerrors"
    16  )
    17  
    18  //////////////////////////////////////////////////////////////////////////////
    19  //
    20  //
    21  //
    22  // Public
    23  //
    24  //
    25  //
    26  //////////////////////////////////////////////////////////////////////////////
    27  
    28  // Starts serving the built site over HTTP on the configured port. A server
    29  // instance is returned so that it can be shut down gracefully.
    30  func startServingTargetDirHTTP(c *Context, buildComplete *sync.Cond) *http.Server {
    31  	c.Log.Infof("Serving '%s' to: http://localhost:%v/", path.Clean(c.TargetDir), c.Port)
    32  
    33  	mux := http.NewServeMux()
    34  	mux.Handle("/", http.FileServer(http.Dir(c.TargetDir)))
    35  
    36  	if c.Websocket {
    37  		mux.HandleFunc("/websocket.js", getWebsocketJSHandler(c))
    38  		mux.HandleFunc("/websocket", getWebsocketHandler(c, buildComplete))
    39  	}
    40  
    41  	server := &http.Server{
    42  		Addr:              fmt.Sprintf(":%v", c.Port),
    43  		Handler:           mux,
    44  		ReadHeaderTimeout: 5 * time.Second, // protect against Slowloris attack
    45  	}
    46  
    47  	go func() {
    48  		err := server.ListenAndServe()
    49  
    50  		// ListenAndServe always returns a non-nil error (but if started
    51  		// successfully, it'll block for a long time).
    52  		if !errors.Is(err, http.ErrServerClosed) {
    53  			exitWithError(xerrors.Errorf("error starting HTTP server: %w", err))
    54  		}
    55  	}()
    56  
    57  	return server
    58  }
    59  
    60  //////////////////////////////////////////////////////////////////////////////
    61  //
    62  //
    63  //
    64  // Private
    65  //
    66  //
    67  //
    68  //////////////////////////////////////////////////////////////////////////////
    69  
    70  // A type representing the extremely basic messages that we'll be serializing
    71  // and sending back over a websocket.
    72  type websocketEvent struct {
    73  	Type string `json:"type"`
    74  }
    75  
    76  const (
    77  	// Maximum message size allowed from peer.
    78  	websocketMaxMessageSize = 512
    79  
    80  	// The frequency at which to send pings back to clients connected over a
    81  	// websocket. Must be less than websocketPongWait.
    82  	websocketPingPeriod = (websocketPongWait * 9) / 10
    83  
    84  	// Time allowed to read the next pong message from the peer.
    85  	websocketPongWait = 10 * time.Second
    86  
    87  	// Time allowed to write a message to the peer.
    88  	websocketWriteWait = 10 * time.Second
    89  )
    90  
    91  // A template that will render the websocket JavaScript code that connecting
    92  // clients will load and run. The `websocketJS` source of this template comes
    93  // from `js.go` which is generated from sources found in the `./js` directory
    94  // with `go generate`.
    95  var websocketJSTemplate = template.Must(template.New("websocket.js").Parse(websocketJS))
    96  
    97  // Part of the Gorilla websocket infrastructure that upgrades HTTP connections
    98  // to websocket connections when we see an incoming websocket request.
    99  var websocketUpgrader = websocket.Upgrader{
   100  	CheckOrigin: func(r *http.Request) bool {
   101  		// Thought about doing localhost only, but it may cause trouble for
   102  		// something eventually. If end user can connect to the web page,
   103  		// assume they're also safe for websockets.
   104  		return true
   105  	},
   106  	ReadBufferSize:  1024,
   107  	WriteBufferSize: 1024,
   108  }
   109  
   110  func getWebsocketHandler(c *Context, buildComplete *sync.Cond) func(w http.ResponseWriter, r *http.Request) {
   111  	return func(w http.ResponseWriter, r *http.Request) {
   112  		conn, err := websocketUpgrader.Upgrade(w, r, nil)
   113  		if err != nil {
   114  			c.Log.Errorf("Error upgrading websocket connection: %v", err)
   115  			return
   116  		}
   117  
   118  		connClosed := make(chan struct{}, 1)
   119  
   120  		go websocketReadPump(c, conn, connClosed)
   121  		go websocketWritePump(c, conn, connClosed, buildComplete)
   122  		c.Log.Infof(logPrefix(c, conn) + "Opened")
   123  	}
   124  }
   125  
   126  func getWebsocketJSHandler(c *Context) func(w http.ResponseWriter, r *http.Request) {
   127  	return func(w http.ResponseWriter, r *http.Request) {
   128  		w.Header().Set("Content-Type", "text/javascript")
   129  		err := websocketJSTemplate.Execute(w, map[string]interface{}{
   130  			"Port": c.Port,
   131  		})
   132  		if err != nil {
   133  			c.Log.Errorf("Error executing template/writing websocket.js: %v", err)
   134  			return
   135  		}
   136  	}
   137  }
   138  
   139  // Produces a log prefix like `<WebSocket [::1]:53555>` which is colored if
   140  // appropriate.
   141  func logPrefix(c *Context, conn *websocket.Conn) string {
   142  	return fmt.Sprintf(c.colorizer.Bold("<WebSocket %v> ").String(),
   143  		conn.RemoteAddr())
   144  }
   145  
   146  func websocketReadPump(c *Context, conn *websocket.Conn, connClosed chan struct{}) {
   147  	defer func() {
   148  		conn.Close()
   149  		connClosed <- struct{}{}
   150  	}()
   151  
   152  	conn.SetReadLimit(websocketMaxMessageSize)
   153  
   154  	if err := conn.SetReadDeadline(time.Now().Add(websocketPongWait)); err != nil {
   155  		c.Log.Errorf(logPrefix(c, conn)+"Couldn't set WebSocket read deadline: %v",
   156  			err)
   157  		return
   158  	}
   159  
   160  	conn.SetPongHandler(func(string) error {
   161  		c.Log.Debugf(logPrefix(c, conn) + "Received pong")
   162  		if err := conn.SetReadDeadline(time.Now().Add(websocketPongWait)); err != nil {
   163  			c.Log.Errorf(logPrefix(c, conn)+"Couldn't set WebSocket read deadline: %v",
   164  				err)
   165  		}
   166  		return nil
   167  	})
   168  
   169  	for {
   170  		_, _, err := conn.ReadMessage()
   171  		if err != nil {
   172  			if websocket.IsUnexpectedCloseError(err) {
   173  				c.Log.Infof(logPrefix(c, conn)+"Closed: %v", err)
   174  			} else {
   175  				c.Log.Errorf(logPrefix(c, conn)+"Error reading message: %v",
   176  					err)
   177  			}
   178  			break
   179  		}
   180  
   181  		// We don't expect clients to send anything right now, so just ignore
   182  		// incoming messages.
   183  	}
   184  
   185  	c.Log.Debugf(logPrefix(c, conn) + "Read pump ending")
   186  }
   187  
   188  func websocketWritePump(c *Context, conn *websocket.Conn,
   189  	connClosed chan struct{}, buildComplete *sync.Cond,
   190  ) {
   191  	ticker := time.NewTicker(websocketPingPeriod)
   192  	defer func() {
   193  		ticker.Stop()
   194  		conn.Close()
   195  	}()
   196  
   197  	var done bool
   198  	var writeErr error
   199  	sendComplete := make(chan struct{}, 1)
   200  
   201  	// This is a hack because of course there's no way to select on a
   202  	// conditional variable. Instead, we have a separate Goroutine wait on the
   203  	// conditional variable and signal the main select below through a channel.
   204  	buildCompleteChan := make(chan struct{}, 1)
   205  	go func() {
   206  		for {
   207  			buildComplete.L.Lock()
   208  			buildComplete.Wait()
   209  			buildComplete.L.Unlock()
   210  
   211  			buildCompleteChan <- struct{}{}
   212  
   213  			// Break out of the Goroutine when we can to prevent a Goroutine
   214  			// leak.
   215  			//
   216  			// Unfortunately this isn't perfect. If we were sending a
   217  			// build_complete, the Goroutine will die right away because the
   218  			// wait below will fall through after the message was fully
   219  			// received, and the client-side JavaScript will being the page
   220  			// reload and close the websocket before that occurs. That's good.
   221  			//
   222  			// What isn't so good is that for other exit conditions like a
   223  			// closed connection or a failed ping, the Goroutine will still be
   224  			// waiting on the conditional variable's Wait above, and not exit
   225  			// right away. The good news is that the next build event that
   226  			// triggers will cause it to fall through and end the Goroutine. So
   227  			// it will eventually be cleaned up, but that clean up may be
   228  			// delayed.
   229  			<-sendComplete
   230  			if done {
   231  				break
   232  			}
   233  		}
   234  
   235  		c.Log.Debugf(logPrefix(c, conn) + "Build complete feeder ending")
   236  	}()
   237  
   238  	for {
   239  		select {
   240  		case <-buildCompleteChan:
   241  			if err := conn.SetWriteDeadline(time.Now().Add(websocketWriteWait)); err != nil {
   242  				c.Log.Errorf(logPrefix(c, conn)+"Couldn't set WebSocket read deadline: %v",
   243  					err)
   244  			}
   245  			writeErr = conn.WriteJSON(websocketEvent{Type: "build_complete"})
   246  
   247  			// Send shouldn't strictly need to be non-blocking, but we do one
   248  			// anyway just to hedge against future or unexpected problems so as
   249  			// not to accidentally stall out this loop.
   250  			select {
   251  			case sendComplete <- struct{}{}:
   252  			default:
   253  			}
   254  
   255  		case <-connClosed:
   256  			done = true
   257  
   258  		case <-ticker.C:
   259  			c.Log.Debugf(logPrefix(c, conn) + "Sending ping")
   260  			if err := conn.SetWriteDeadline(time.Now().Add(websocketWriteWait)); err != nil {
   261  				c.Log.Errorf(logPrefix(c, conn)+"Couldn't set WebSocket read deadline: %v",
   262  					err)
   263  			}
   264  			writeErr = conn.WriteMessage(websocket.PingMessage, nil)
   265  		}
   266  
   267  		if writeErr != nil {
   268  			c.Log.Errorf(logPrefix(c, conn)+"Error writing: %v",
   269  				writeErr)
   270  			done = true
   271  		}
   272  
   273  		if done {
   274  			break
   275  		}
   276  	}
   277  
   278  	c.Log.Debugf(logPrefix(c, conn) + "Write pump ending")
   279  }