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