Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

bskyweb: gzip HTTP responses + some other minor improvements (#826)

* bskyweb: gzip HTTP responses + JSON logging + minor refactoring

* reduce timeout and max header size

* add a security.txt

authored by

Jake Gold and committed by
GitHub
49840f3a 8fde55b5

+145 -50
+1 -1
bskyweb/cmd/bskyweb/.gitignore
··· 1 - bskyweb 1 + /bskyweb
+5 -3
bskyweb/cmd/bskyweb/mailmodo.go
··· 14 14 httpClient *http.Client 15 15 APIKey string 16 16 BaseURL string 17 + ListName string 17 18 } 18 19 19 - func NewMailmodo(apiKey string) *Mailmodo { 20 + func NewMailmodo(apiKey, listName string) *Mailmodo { 20 21 return &Mailmodo{ 21 22 APIKey: apiKey, 22 23 BaseURL: "https://api.mailmodo.com/api/v1", 23 24 httpClient: &http.Client{}, 25 + ListName: listName, 24 26 } 25 27 } 26 28 ··· 56 58 return nil 57 59 } 58 60 59 - func (m *Mailmodo) AddToList(ctx context.Context, listName, email string) error { 61 + func (m *Mailmodo) AddToList(ctx context.Context, email string) error { 60 62 return m.request(ctx, "POST", "addToList", map[string]any{ 61 - "listName": listName, 63 + "listName": m.ListName, 62 64 "email": email, 63 65 "data": map[string]any{ 64 66 "email_hashed": fmt.Sprintf("%x", sha256.Sum256([]byte(email))),
+136 -46
bskyweb/cmd/bskyweb/server.go
··· 3 3 import ( 4 4 "context" 5 5 "encoding/json" 6 + "errors" 6 7 "fmt" 7 8 "io/fs" 8 9 "io/ioutil" 9 10 "net/http" 10 11 "os" 12 + "os/signal" 11 13 "strings" 14 + "syscall" 15 + "time" 12 16 13 17 comatproto "github.com/bluesky-social/indigo/api/atproto" 14 18 appbsky "github.com/bluesky-social/indigo/api/bsky" ··· 17 21 "github.com/bluesky-social/social-app/bskyweb" 18 22 19 23 "github.com/flosch/pongo2/v6" 24 + "github.com/klauspost/compress/gzhttp" 25 + "github.com/klauspost/compress/gzip" 20 26 "github.com/labstack/echo/v4" 21 27 "github.com/labstack/echo/v4/middleware" 22 28 "github.com/urfave/cli/v2" 23 29 ) 24 30 25 31 type Server struct { 26 - xrpcc *xrpc.Client 32 + echo *echo.Echo 33 + httpd *http.Server 34 + mailmodo *Mailmodo 35 + xrpcc *xrpc.Client 27 36 } 28 37 29 38 func serve(cctx *cli.Context) error { ··· 35 44 mailmodoAPIKey := cctx.String("mailmodo-api-key") 36 45 mailmodoListName := cctx.String("mailmodo-list-name") 37 46 47 + // Echo 48 + e := echo.New() 49 + 38 50 // Mailmodo client. 39 - mailmodo := NewMailmodo(mailmodoAPIKey) 51 + mailmodo := NewMailmodo(mailmodoAPIKey, mailmodoListName) 40 52 41 53 // create a new session 42 54 // TODO: does this work with no auth at all? ··· 60 72 xrpcc.Auth.Did = auth.Did 61 73 xrpcc.Auth.Handle = auth.Handle 62 74 63 - server := Server{xrpcc} 75 + // httpd 76 + var ( 77 + httpTimeout = 2 * time.Minute 78 + httpMaxHeaderBytes = 2 * (1024 * 1024) 79 + gzipMinSizeBytes = 1024 * 2 80 + gzipCompressionLevel = gzip.BestSpeed 81 + gzipExceptMIMETypes = []string{"image/png"} 82 + ) 64 83 65 - staticHandler := http.FileServer(func() http.FileSystem { 66 - if debug { 67 - log.Debugf("serving static file from the local file system") 68 - return http.FS(os.DirFS("static")) 69 - } 70 - fsys, err := fs.Sub(bskyweb.StaticFS, "static") 71 - if err != nil { 72 - log.Fatal(err) 73 - } 74 - return http.FS(fsys) 75 - }()) 84 + // Wrap the server handler in a gzip handler to compress larger responses. 85 + gzipHandler, err := gzhttp.NewWrapper( 86 + gzhttp.MinSize(gzipMinSizeBytes), 87 + gzhttp.CompressionLevel(gzipCompressionLevel), 88 + gzhttp.ExceptContentTypes(gzipExceptMIMETypes), 89 + ) 90 + if err != nil { 91 + return err 92 + } 93 + 94 + // 95 + // server 96 + // 97 + server := &Server{ 98 + echo: e, 99 + mailmodo: mailmodo, 100 + xrpcc: xrpcc, 101 + } 102 + 103 + // Create the HTTP server. 104 + server.httpd = &http.Server{ 105 + Handler: gzipHandler(server), 106 + Addr: httpAddress, 107 + WriteTimeout: httpTimeout, 108 + ReadTimeout: httpTimeout, 109 + MaxHeaderBytes: httpMaxHeaderBytes, 110 + } 76 111 77 - e := echo.New() 78 112 e.HideBanner = true 79 113 // SECURITY: Do not modify without due consideration. 80 114 e.Use(middleware.SecureWithConfig(middleware.SecureConfig{ ··· 90 124 Skipper: func(c echo.Context) bool { 91 125 return strings.HasPrefix(c.Request().URL.Path, "/static") 92 126 }, 93 - Format: "method=${method} path=${uri} status=${status} latency=${latency_human}\n", 94 127 })) 95 128 e.Renderer = NewRenderer("templates/", &bskyweb.TemplateFS, debug) 96 - e.HTTPErrorHandler = customHTTPErrorHandler 129 + e.HTTPErrorHandler = server.errorHandler 97 130 98 131 // redirect trailing slash to non-trailing slash. 99 132 // all of our current endpoints have no trailing slash. ··· 106 139 // 107 140 108 141 // static files 142 + staticHandler := http.FileServer(func() http.FileSystem { 143 + if debug { 144 + log.Debugf("serving static file from the local file system") 145 + return http.FS(os.DirFS("static")) 146 + } 147 + fsys, err := fs.Sub(bskyweb.StaticFS, "static") 148 + if err != nil { 149 + log.Fatal(err) 150 + } 151 + return http.FS(fsys) 152 + }()) 109 153 e.GET("/robots.txt", echo.WrapHandler(staticHandler)) 110 154 e.GET("/static/*", echo.WrapHandler(http.StripPrefix("/static/", staticHandler))) 111 155 e.GET("/.well-known/*", echo.WrapHandler(staticHandler)) 156 + e.GET("/security.txt", func(c echo.Context) error { 157 + return c.Redirect(http.StatusMovedPermanently, "/.well-known/security.txt") 158 + }) 112 159 113 160 // home 114 161 e.GET("/", server.WebHome) ··· 147 194 e.GET("/profile/:handle/post/:rkey/reposted-by", server.WebGeneric) 148 195 149 196 // Mailmodo 150 - e.POST("/api/waitlist", func(c echo.Context) error { 151 - type jsonError struct { 152 - Error string `json:"error"` 153 - } 197 + e.POST("/api/waitlist", server.apiWaitlist) 154 198 155 - // Read the API request. 156 - type apiRequest struct { 157 - Email string `json:"email"` 199 + // Start the server. 200 + log.Infof("starting server address=%s", httpAddress) 201 + go func() { 202 + if err := server.httpd.ListenAndServe(); err != nil { 203 + if !errors.Is(err, http.ErrServerClosed) { 204 + log.Errorf("HTTP server shutting down unexpectedly: %s", err) 205 + } 158 206 } 207 + }() 159 208 160 - bodyReader := http.MaxBytesReader(c.Response(), c.Request().Body, 16*1024) 161 - payload, err := ioutil.ReadAll(bodyReader) 162 - if err != nil { 163 - return err 164 - } 165 - var req apiRequest 166 - if err := json.Unmarshal(payload, &req); err != nil { 167 - return c.JSON(http.StatusBadRequest, jsonError{Error: "Invalid API request"}) 168 - } 209 + // Wait for a signal to exit. 210 + log.Info("registering OS exit signal handler") 211 + quit := make(chan struct{}) 212 + exitSignals := make(chan os.Signal, 1) 213 + signal.Notify(exitSignals, syscall.SIGINT, syscall.SIGTERM) 214 + go func() { 215 + sig := <-exitSignals 216 + log.Infof("received OS exit signal: %s", sig) 169 217 170 - if req.Email == "" { 171 - return c.JSON(http.StatusBadRequest, jsonError{Error: "Please enter a valid email address."}) 218 + // Shut down the HTTP server. 219 + if err := server.Shutdown(); err != nil { 220 + log.Errorf("HTTP server shutdown error: %s", err) 172 221 } 173 222 174 - if err := mailmodo.AddToList(c.Request().Context(), mailmodoListName, req.Email); err != nil { 175 - log.Errorf("adding email to waitlist failed: %s", err) 176 - return c.JSON(http.StatusBadRequest, jsonError{ 177 - Error: "Storing email in waitlist failed. Please enter a valid email address.", 178 - }) 179 - } 180 - return c.JSON(http.StatusOK, map[string]bool{"success": true}) 181 - }) 223 + // Trigger the return that causes an exit. 224 + close(quit) 225 + }() 226 + <-quit 227 + log.Infof("graceful shutdown complete") 228 + return nil 229 + } 230 + 231 + func (srv *Server) ServeHTTP(rw http.ResponseWriter, req *http.Request) { 232 + srv.echo.ServeHTTP(rw, req) 233 + } 234 + 235 + func (srv *Server) Shutdown() error { 236 + log.Info("shutting down") 237 + 238 + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 239 + defer cancel() 182 240 183 - log.Infof("starting server address=%s", httpAddress) 184 - return e.Start(httpAddress) 241 + return srv.httpd.Shutdown(ctx) 185 242 } 186 243 187 - func customHTTPErrorHandler(err error, c echo.Context) { 244 + func (srv *Server) errorHandler(err error, c echo.Context) { 188 245 code := http.StatusInternalServerError 189 246 if he, ok := err.(*echo.HTTPError); ok { 190 247 code = he.Code ··· 260 317 261 318 return c.Render(http.StatusOK, "profile.html", data) 262 319 } 320 + 321 + func (srv *Server) apiWaitlist(c echo.Context) error { 322 + type jsonError struct { 323 + Error string `json:"error"` 324 + } 325 + 326 + // Read the API request. 327 + type apiRequest struct { 328 + Email string `json:"email"` 329 + } 330 + 331 + bodyReader := http.MaxBytesReader(c.Response(), c.Request().Body, 16*1024) 332 + payload, err := ioutil.ReadAll(bodyReader) 333 + if err != nil { 334 + return err 335 + } 336 + var req apiRequest 337 + if err := json.Unmarshal(payload, &req); err != nil { 338 + return c.JSON(http.StatusBadRequest, jsonError{Error: "Invalid API request"}) 339 + } 340 + 341 + if req.Email == "" { 342 + return c.JSON(http.StatusBadRequest, jsonError{Error: "Please enter a valid email address."}) 343 + } 344 + 345 + if err := srv.mailmodo.AddToList(c.Request().Context(), req.Email); err != nil { 346 + log.Errorf("adding email to waitlist failed: %s", err) 347 + return c.JSON(http.StatusBadRequest, jsonError{ 348 + Error: "Storing email in waitlist failed. Please enter a valid email address.", 349 + }) 350 + } 351 + return c.JSON(http.StatusOK, map[string]bool{"success": true}) 352 + }
+1
bskyweb/go.mod
··· 7 7 github.com/flosch/pongo2/v6 v6.0.0 8 8 github.com/ipfs/go-log v1.0.5 9 9 github.com/joho/godotenv v1.5.1 10 + github.com/klauspost/compress v1.16.5 10 11 github.com/labstack/echo/v4 v4.10.2 11 12 github.com/urfave/cli/v2 v2.25.3 12 13 )
+2
bskyweb/go.sum
··· 105 105 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 106 106 github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 107 107 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 108 + github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI= 109 + github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= 108 110 github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 109 111 github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 110 112 github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=