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 }