this repo has no description
13
fork

Configure Feed

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

at netclient 201 lines 5.0 kB view raw
1package main 2 3import ( 4 "context" 5 "embed" 6 "errors" 7 "fmt" 8 "io/fs" 9 "log/slog" 10 "net/http" 11 "os" 12 "os/signal" 13 "syscall" 14 "time" 15 16 "github.com/bluesky-social/indigo/atproto/identity" 17 18 "github.com/flosch/pongo2/v6" 19 "github.com/labstack/echo/v4" 20 "github.com/labstack/echo/v4/middleware" 21 slogecho "github.com/samber/slog-echo" 22 "github.com/urfave/cli/v2" 23 "gorm.io/driver/sqlite" 24 "gorm.io/gorm" 25) 26 27//go:embed static/* 28var StaticFS embed.FS 29 30type WebServer struct { 31 echo *echo.Echo 32 httpd *http.Server 33 db *gorm.DB 34 dir identity.Directory 35 jetstreamHost string 36} 37 38func NewWebServer(cctx *cli.Context) (*WebServer, error) { 39 debug := cctx.Bool("debug") 40 httpAddress := cctx.String("bind") 41 jetstreamHost := cctx.String("jetstream-host") 42 db, err := gorm.Open(sqlite.Open(cctx.String("sqlite-path"))) 43 if err != nil { 44 return nil, fmt.Errorf("failed to open db: %w", err) 45 } 46 47 e := echo.New() 48 49 // httpd 50 var ( 51 httpTimeout = 1 * time.Minute 52 httpMaxHeaderBytes = 1 * (1024 * 1024) 53 ) 54 55 srv := &WebServer{ 56 echo: e, 57 db: db, 58 dir: identity.DefaultDirectory(), 59 jetstreamHost: jetstreamHost, 60 } 61 srv.httpd = &http.Server{ 62 Handler: srv, 63 Addr: httpAddress, 64 WriteTimeout: httpTimeout, 65 ReadTimeout: httpTimeout, 66 MaxHeaderBytes: httpMaxHeaderBytes, 67 } 68 69 e.HideBanner = true 70 e.Use(slogecho.New(slog.Default())) 71 e.Use(middleware.Recover()) 72 e.Use(middleware.BodyLimit("64M")) 73 e.HTTPErrorHandler = srv.errorHandler 74 e.Renderer = NewRenderer("templates/", &TemplateFS, debug) 75 e.Use(middleware.SecureWithConfig(middleware.SecureConfig{ 76 ContentTypeNosniff: "nosniff", 77 XFrameOptions: "SAMEORIGIN", 78 HSTSMaxAge: 31536000, // 365 days 79 // TODO: 80 // ContentSecurityPolicy 81 // XSSProtection 82 })) 83 84 // redirect trailing slash to non-trailing slash. 85 // all of our current endpoints have no trailing slash. 86 e.Use(middleware.RemoveTrailingSlashWithConfig(middleware.TrailingSlashConfig{ 87 RedirectCode: http.StatusFound, 88 })) 89 90 staticHandler := http.FileServer(func() http.FileSystem { 91 if debug { 92 return http.FS(os.DirFS("static")) 93 } 94 fsys, err := fs.Sub(StaticFS, "static") 95 if err != nil { 96 slog.Error("static template error", "err", err) 97 os.Exit(-1) 98 } 99 return http.FS(fsys) 100 }()) 101 102 e.GET("/static/*", echo.WrapHandler(http.StripPrefix("/static/", staticHandler))) 103 e.GET("/_health", srv.HandleHealthCheck) 104 105 // basic static routes 106 e.GET("/robots.txt", echo.WrapHandler(staticHandler)) 107 e.GET("/favicon.ico", echo.WrapHandler(staticHandler)) 108 109 // actual content 110 e.GET("/", srv.WebHome) 111 e.GET("/query", srv.WebQuery) 112 e.GET("/recent", srv.WebRecent) 113 e.GET("/domain/:domain", srv.WebDomain) 114 e.GET("/lexicon/:nsid", srv.WebLexicon) 115 e.GET("/lexicon/:nsid/history", srv.WebLexiconHistory) 116 // TODO: e.GET("/lexicon/:nsid/def/:name", srv.WebLexiconDef) 117 118 e.GET("/demo/record", srv.WebDemoRecord) 119 e.GET("/demo/query", srv.WebDemoQuery) 120 121 return srv, nil 122} 123 124// Starts the server in a goroutine, and returns 125func (srv *WebServer) RunWeb() { 126 // Start the server 127 slog.Info("starting server", "bind", srv.httpd.Addr) 128 go func() { 129 if err := srv.httpd.ListenAndServe(); err != nil { 130 if !errors.Is(err, http.ErrServerClosed) { 131 slog.Error("HTTP server shutting down unexpectedly", "err", err) 132 } 133 } 134 }() 135} 136 137// Runs in this thread 138func (srv *WebServer) RunSignalHandler() error { 139 // Wait for a signal to exit. 140 slog.Info("registering OS exit signal handler") 141 quit := make(chan struct{}) 142 exitSignals := make(chan os.Signal, 1) 143 signal.Notify(exitSignals, syscall.SIGINT, syscall.SIGTERM) 144 go func() { 145 sig := <-exitSignals 146 slog.Info("received OS exit signal", "signal", sig) 147 148 // Shut down the HTTP server 149 if err := srv.Shutdown(); err != nil { 150 slog.Error("HTTP server shutdown error", "err", err) 151 } 152 153 // Trigger the return that causes an exit. 154 close(quit) 155 }() 156 <-quit 157 slog.Info("graceful shutdown complete") 158 return nil 159} 160 161type GenericStatus struct { 162 Daemon string `json:"daemon"` 163 Status string `json:"status"` 164 Message string `json:"msg,omitempty"` 165} 166 167func (srv *WebServer) errorHandler(err error, c echo.Context) { 168 code := http.StatusInternalServerError 169 var errorMessage string 170 if he, ok := err.(*echo.HTTPError); ok { 171 code = he.Code 172 errorMessage = fmt.Sprintf("%s", he.Message) 173 } 174 if code >= 500 { 175 slog.Warn("lexidex-http-internal-error", "err", err) 176 } 177 data := pongo2.Context{ 178 "statusCode": code, 179 "errorMessage": errorMessage, 180 } 181 if !c.Response().Committed { 182 c.Render(code, "error.html", data) 183 } 184} 185 186func (srv *WebServer) ServeHTTP(rw http.ResponseWriter, req *http.Request) { 187 srv.echo.ServeHTTP(rw, req) 188} 189 190func (srv *WebServer) Shutdown() error { 191 slog.Info("shutting down") 192 193 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 194 defer cancel() 195 196 return srv.httpd.Shutdown(ctx) 197} 198 199func (s *WebServer) HandleHealthCheck(c echo.Context) error { 200 return c.JSON(200, GenericStatus{Status: "ok", Daemon: "lexidex"}) 201}