package api import ( "bytes" "context" "crypto/tls" "errors" "fmt" "io" "io/fs" "net/http" "os" "path" "regexp" "runtime/debug" "strconv" "strings" "time" gqlHandler "github.com/99designs/gqlgen/graphql/handler" gqlExtension "github.com/99designs/gqlgen/graphql/handler/extension" gqlLru "github.com/99designs/gqlgen/graphql/handler/lru" gqlTransport "github.com/99designs/gqlgen/graphql/handler/transport" gqlPlayground "github.com/99designs/gqlgen/graphql/playground" "github.com/go-chi/chi" "github.com/go-chi/chi/middleware" "github.com/gorilla/websocket" "github.com/vearutop/statigz" "github.com/go-chi/cors" "github.com/go-chi/httplog" "github.com/stashapp/stash/internal/api/loaders" "github.com/stashapp/stash/internal/manager" "github.com/stashapp/stash/internal/manager/config" "github.com/stashapp/stash/pkg/fsutil" "github.com/stashapp/stash/pkg/logger" "github.com/stashapp/stash/pkg/plugin" "github.com/stashapp/stash/pkg/utils" "github.com/stashapp/stash/ui" ) const ( loginEndpoint = "/login" logoutEndpoint = "/logout" gqlEndpoint = "/graphql" playgroundEndpoint = "/playground" ) var version string var buildstamp string var githash string var uiBox = ui.UIBox var loginUIBox = ui.LoginUIBox func Start() error { initialiseImages() r := chi.NewRouter() r.Use(middleware.Heartbeat("/healthz")) r.Use(cors.AllowAll().Handler) r.Use(authenticateHandler()) visitedPluginHandler := manager.GetInstance().SessionStore.VisitedPluginHandler() r.Use(visitedPluginHandler) r.Use(middleware.Recoverer) c := config.GetInstance() if c.GetLogAccess() { httpLogger := httplog.NewLogger("Stash", httplog.Options{ Concise: true, }) r.Use(httplog.RequestLogger(httpLogger)) } r.Use(SecurityHeadersMiddleware) r.Use(middleware.DefaultCompress) r.Use(middleware.StripSlashes) r.Use(BaseURLMiddleware) recoverFunc := func(ctx context.Context, err interface{}) error { logger.Error(err) debug.PrintStack() message := fmt.Sprintf("Internal system error. Error <%v>", err) return errors.New(message) } txnManager := manager.GetInstance().Repository dataloaders := loaders.Middleware{ DatabaseProvider: txnManager, Repository: txnManager, } r.Use(dataloaders.Middleware) pluginCache := manager.GetInstance().PluginCache sceneService := manager.GetInstance().SceneService imageService := manager.GetInstance().ImageService galleryService := manager.GetInstance().GalleryService resolver := &Resolver{ txnManager: txnManager, repository: txnManager, sceneService: sceneService, imageService: imageService, galleryService: galleryService, hookExecutor: pluginCache, } gqlSrv := gqlHandler.New(NewExecutableSchema(Config{Resolvers: resolver})) gqlSrv.SetRecoverFunc(recoverFunc) gqlSrv.AddTransport(gqlTransport.Websocket{ Upgrader: websocket.Upgrader{ CheckOrigin: func(r *http.Request) bool { return true }, }, KeepAlivePingInterval: 10 * time.Second, }) gqlSrv.AddTransport(gqlTransport.Options{}) gqlSrv.AddTransport(gqlTransport.GET{}) gqlSrv.AddTransport(gqlTransport.POST{}) gqlSrv.AddTransport(gqlTransport.MultipartForm{ MaxUploadSize: c.GetMaxUploadSize(), }) gqlSrv.SetQueryCache(gqlLru.New(1000)) gqlSrv.Use(gqlExtension.Introspection{}) gqlSrv.SetErrorPresenter(gqlErrorHandler) gqlHandlerFunc := func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Cache-Control", "no-store") gqlSrv.ServeHTTP(w, r) } // register GQL handler with plugin cache // chain the visited plugin handler // also requires the dataloader middleware gqlHandler := visitedPluginHandler(dataloaders.Middleware(http.HandlerFunc(gqlHandlerFunc))) manager.GetInstance().PluginCache.RegisterGQLHandler(gqlHandler) r.HandleFunc(gqlEndpoint, gqlHandlerFunc) r.HandleFunc(playgroundEndpoint, func(w http.ResponseWriter, r *http.Request) { setPageSecurityHeaders(w, r) endpoint := getProxyPrefix(r) + gqlEndpoint gqlPlayground.Handler("GraphQL playground", endpoint)(w, r) }) r.Mount("/performer", performerRoutes{ txnManager: txnManager, performerFinder: txnManager.Performer, }.Routes()) r.Mount("/scene", sceneRoutes{ txnManager: txnManager, sceneFinder: txnManager.Scene, fileFinder: txnManager.File, captionFinder: txnManager.File, sceneMarkerFinder: txnManager.SceneMarker, tagFinder: txnManager.Tag, }.Routes()) r.Mount("/image", imageRoutes{ txnManager: txnManager, imageFinder: txnManager.Image, fileFinder: txnManager.File, }.Routes()) r.Mount("/studio", studioRoutes{ txnManager: txnManager, studioFinder: txnManager.Studio, }.Routes()) r.Mount("/movie", movieRoutes{ txnManager: txnManager, movieFinder: txnManager.Movie, }.Routes()) r.Mount("/tag", tagRoutes{ txnManager: txnManager, tagFinder: txnManager.Tag, }.Routes()) r.Mount("/downloads", downloadsRoutes{}.Routes()) r.HandleFunc("/css", cssHandler(c, pluginCache)) r.HandleFunc("/javascript", javascriptHandler(c, pluginCache)) r.HandleFunc("/customlocales", customLocalesHandler(c)) staticLoginUI := statigz.FileServer(loginUIBox.(fs.ReadDirFS)) r.Get(loginEndpoint, handleLogin(loginUIBox)) r.Post(loginEndpoint, handleLoginPost(loginUIBox)) r.Get(logoutEndpoint, handleLogout()) r.HandleFunc(loginEndpoint+"/*", func(w http.ResponseWriter, r *http.Request) { r.URL.Path = strings.TrimPrefix(r.URL.Path, loginEndpoint) w.Header().Set("Cache-Control", "no-cache") staticLoginUI.ServeHTTP(w, r) }) // Serve static folders customServedFolders := c.GetCustomServedFolders() if customServedFolders != nil { r.Mount("/custom", customRoutes{ servedFolders: customServedFolders, }.Routes()) } customUILocation := c.GetCustomUILocation() staticUI := statigz.FileServer(uiBox.(fs.ReadDirFS)) // Serve the web app r.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) { ext := path.Ext(r.URL.Path) if customUILocation != "" { if r.URL.Path == "index.html" || ext == "" { r.URL.Path = "/" } http.FileServer(http.Dir(customUILocation)).ServeHTTP(w, r) return } if ext == ".html" || ext == "" { themeColor := c.GetThemeColor() data, err := fs.ReadFile(uiBox, "index.html") if err != nil { panic(err) } indexHtml := string(data) prefix := getProxyPrefix(r) indexHtml = strings.ReplaceAll(indexHtml, "%COLOR%", themeColor) indexHtml = strings.Replace(indexHtml, `