Bluesky app fork with some witchin' additions 馃挮
at post-text-option 271 lines 7.5 kB view raw
1package main 2 3import ( 4 "context" 5 "errors" 6 "fmt" 7 "html/template" 8 "io/fs" 9 "net/http" 10 "os" 11 "os/signal" 12 "strings" 13 "syscall" 14 "time" 15 16 _ "net/http/pprof" 17 18 "github.com/bluesky-social/indigo/atproto/identity" 19 "github.com/bluesky-social/indigo/util/cliutil" 20 "github.com/bluesky-social/indigo/xrpc" 21 "github.com/bluesky-social/social-app/bskyweb" 22 "github.com/prometheus/client_golang/prometheus/promhttp" 23 24 "github.com/labstack/echo-contrib/echoprometheus" 25 "github.com/labstack/echo/v4" 26 "github.com/labstack/echo/v4/middleware" 27 28 "github.com/klauspost/compress/gzhttp" 29 "github.com/klauspost/compress/gzip" 30 "github.com/urfave/cli/v2" 31) 32 33type Server struct { 34 echo *echo.Echo 35 httpd *http.Server 36 metricsHttpd *http.Server 37 xrpcc *xrpc.Client 38 dir identity.Directory 39} 40 41func serve(cctx *cli.Context) error { 42 debug := cctx.Bool("debug") 43 httpAddress := cctx.String("http-address") 44 appviewHost := cctx.String("appview-host") 45 metricsAddress := cctx.String("metrics-address") 46 47 // Echo 48 e := echo.New() 49 50 // create a new session (no auth) 51 xrpcc := &xrpc.Client{ 52 Client: cliutil.NewHttpClient(), 53 Host: appviewHost, 54 } 55 56 // httpd 57 var ( 58 httpTimeout = 2 * time.Minute 59 httpMaxHeaderBytes = 2 * (1024 * 1024) 60 gzipMinSizeBytes = 1024 * 2 61 gzipCompressionLevel = gzip.BestSpeed 62 gzipExceptMIMETypes = []string{"image/png"} 63 ) 64 65 // Wrap the server handler in a gzip handler to compress larger responses. 66 gzipHandler, err := gzhttp.NewWrapper( 67 gzhttp.MinSize(gzipMinSizeBytes), 68 gzhttp.CompressionLevel(gzipCompressionLevel), 69 gzhttp.ExceptContentTypes(gzipExceptMIMETypes), 70 ) 71 if err != nil { 72 return err 73 } 74 75 metricsMux := http.DefaultServeMux 76 metricsMux.Handle("/metrics", promhttp.Handler()) 77 78 metricsHttpd := &http.Server{ 79 Addr: metricsAddress, 80 Handler: metricsMux, 81 } 82 83 go func() { 84 if err := metricsHttpd.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { 85 log.Error("failed to start metrics server", "error", err) 86 } 87 }() 88 89 // 90 // server 91 // 92 server := &Server{ 93 echo: e, 94 xrpcc: xrpcc, 95 dir: identity.DefaultDirectory(), 96 metricsHttpd: metricsHttpd, 97 } 98 99 // Create the HTTP server. 100 server.httpd = &http.Server{ 101 Handler: gzipHandler(server), 102 Addr: httpAddress, 103 WriteTimeout: httpTimeout, 104 ReadTimeout: httpTimeout, 105 MaxHeaderBytes: httpMaxHeaderBytes, 106 } 107 108 e.HideBanner = true 109 110 tmpl := &Template{ 111 templates: template.Must(template.ParseFS(bskyweb.EmbedrTemplateFS, "embedr-templates/*.html")), 112 } 113 e.Renderer = tmpl 114 e.HTTPErrorHandler = server.errorHandler 115 116 e.IPExtractor = echo.ExtractIPFromXFFHeader() 117 118 // SECURITY: Do not modify without due consideration. 119 e.Use(middleware.SecureWithConfig(middleware.SecureConfig{ 120 ContentTypeNosniff: "nosniff", 121 // diable XFrameOptions; we're embedding here! 122 HSTSMaxAge: 31536000, // 365 days 123 // TODO: 124 // ContentSecurityPolicy 125 // XSSProtection 126 })) 127 e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ 128 // Don't log requests for static content. 129 Skipper: func(c echo.Context) bool { 130 return strings.HasPrefix(c.Request().URL.Path, "/static") 131 }, 132 })) 133 e.Use(middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{ 134 Skipper: middleware.DefaultSkipper, 135 Store: middleware.NewRateLimiterMemoryStoreWithConfig( 136 middleware.RateLimiterMemoryStoreConfig{ 137 Rate: 20, // requests per second 138 Burst: 150, // allow bursts 139 ExpiresIn: 3 * time.Minute, // garbage collect entries older than 3 minutes 140 }, 141 ), 142 IdentifierExtractor: func(ctx echo.Context) (string, error) { 143 id := ctx.RealIP() 144 return id, nil 145 }, 146 DenyHandler: func(c echo.Context, identifier string, err error) error { 147 return c.String(http.StatusTooManyRequests, "Your request has been rate limited. Please try again later. Contact support@bsky.app if you believe this was a mistake.\n") 148 }, 149 })) 150 151 // redirect trailing slash to non-trailing slash. 152 // all of our current endpoints have no trailing slash. 153 e.Use(middleware.RemoveTrailingSlashWithConfig(middleware.TrailingSlashConfig{ 154 RedirectCode: http.StatusFound, 155 })) 156 157 echoprom := echoprometheus.NewMiddlewareWithConfig( 158 echoprometheus.MiddlewareConfig{ 159 DoNotUseRequestPathFor404: true, 160 }, 161 ) 162 163 e.Use(echoprom) 164 165 // 166 // configure routes 167 // 168 // static files 169 staticHandler := http.FileServer(func() http.FileSystem { 170 if debug { 171 log.Debugf("serving static file from the local file system") 172 return http.FS(os.DirFS("embedr-static")) 173 } 174 fsys, err := fs.Sub(bskyweb.EmbedrStaticFS, "embedr-static") 175 if err != nil { 176 log.Fatal(err) 177 } 178 return http.FS(fsys) 179 }()) 180 181 e.GET("/robots.txt", echo.WrapHandler(staticHandler)) 182 e.GET("/ips-v4", echo.WrapHandler(staticHandler)) 183 e.GET("/ips-v6", echo.WrapHandler(staticHandler)) 184 e.GET("/.well-known/*", echo.WrapHandler(staticHandler)) 185 e.GET("/security.txt", func(c echo.Context) error { 186 return c.Redirect(http.StatusMovedPermanently, "/.well-known/security.txt") 187 }) 188 e.GET("/static/*", echo.WrapHandler(http.StripPrefix("/static/", staticHandler)), func(next echo.HandlerFunc) echo.HandlerFunc { 189 return func(c echo.Context) error { 190 path := c.Request().URL.Path 191 maxAge := 1 * (60 * 60) // default is 1 hour 192 193 // Cache javascript and images files for 1 week, which works because 194 // they're always versioned (e.g. /static/js/main.64c14927.js) 195 if strings.HasPrefix(path, "/static/js/") || strings.HasPrefix(path, "/static/images/") { 196 maxAge = 7 * (60 * 60 * 24) // 1 week 197 } 198 199 c.Response().Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d", maxAge)) 200 return next(c) 201 } 202 }) 203 204 // actual routes 205 e.GET("/", server.WebHome) 206 e.GET("/iframe-resize.js", echo.WrapHandler(staticHandler)) 207 e.GET("/embed.js", echo.WrapHandler(staticHandler)) 208 e.GET("/oembed", server.WebOEmbed) 209 e.GET("/embed/:did/app.bsky.feed.post/:rkey", server.WebPostEmbed) 210 211 // Start the server. 212 log.Infof("starting server address=%s", httpAddress) 213 go func() { 214 if err := server.httpd.ListenAndServe(); err != nil { 215 if !errors.Is(err, http.ErrServerClosed) { 216 log.Errorf("HTTP server shutting down unexpectedly: %s", err) 217 } 218 } 219 }() 220 221 // Wait for a signal to exit. 222 log.Info("registering OS exit signal handler") 223 quit := make(chan struct{}) 224 exitSignals := make(chan os.Signal, 1) 225 signal.Notify(exitSignals, syscall.SIGINT, syscall.SIGTERM) 226 go func() { 227 sig := <-exitSignals 228 log.Infof("received OS exit signal: %s", sig) 229 230 // Shut down the HTTP server. 231 if err := server.Shutdown(); err != nil { 232 log.Errorf("HTTP server shutdown error: %s", err) 233 } 234 235 // Trigger the return that causes an exit. 236 close(quit) 237 }() 238 <-quit 239 log.Infof("graceful shutdown complete") 240 return nil 241} 242 243func (srv *Server) ServeHTTP(rw http.ResponseWriter, req *http.Request) { 244 srv.echo.ServeHTTP(rw, req) 245} 246 247func (srv *Server) Shutdown() error { 248 log.Info("shutting down") 249 250 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 251 defer cancel() 252 253 // Shutdown metrics server too 254 if srv.metricsHttpd != nil { 255 srv.metricsHttpd.Shutdown(ctx) 256 } 257 258 return srv.httpd.Shutdown(ctx) 259} 260 261func (srv *Server) errorHandler(err error, c echo.Context) { 262 code := http.StatusInternalServerError 263 if he, ok := err.(*echo.HTTPError); ok { 264 code = he.Code 265 } 266 c.Logger().Error(err) 267 data := map[string]interface{}{ 268 "statusCode": code, 269 } 270 c.Render(code, "error.html", data) 271}