| // scrapexchange is the web admin and API server for the scrap exchange service. |
| // |
| // See http://go/scrap-exchange for more details. |
| package main |
| |
| import ( |
| "context" |
| "flag" |
| "html/template" |
| "net/http" |
| "path/filepath" |
| "runtime" |
| |
| "cloud.google.com/go/storage" |
| "github.com/go-chi/chi/v5" |
| "github.com/unrolled/secure" |
| "go.skia.org/infra/go/alogin" |
| "go.skia.org/infra/go/alogin/proxylogin" |
| "go.skia.org/infra/go/auth" |
| "go.skia.org/infra/go/baseapp" |
| "go.skia.org/infra/go/common" |
| "go.skia.org/infra/go/gcs/gcsclient" |
| "go.skia.org/infra/go/httputils" |
| "go.skia.org/infra/go/roles" |
| "go.skia.org/infra/go/skerr" |
| "go.skia.org/infra/go/sklog" |
| "go.skia.org/infra/scrap/go/api" |
| "go.skia.org/infra/scrap/go/scrap" |
| "golang.org/x/oauth2/google" |
| "google.golang.org/api/option" |
| ) |
| |
| type flags struct { |
| internalPort string |
| bucket string |
| local bool |
| port string |
| promPort string |
| resourcesDir string |
| } |
| |
| func (f *flags) Register(fs *flag.FlagSet) { |
| fs.StringVar(&f.internalPort, "internal_port", ":9000", "HTTP internal service address (e.g., ':9000') for unauthenticated in-cluster requests.") |
| fs.StringVar(&f.bucket, "bucket", "", "The Google Cloud Storage bucket that scraps are stored in.") |
| fs.BoolVar(&f.local, "local", false, "Running locally if true. As opposed to in production.") |
| fs.StringVar(&f.port, "port", ":8000", "HTTP service address (e.g., ':8000')") |
| fs.StringVar(&f.promPort, "prom_port", ":20000", "Metrics service address (e.g., ':10110')") |
| fs.StringVar(&f.resourcesDir, "resources_dir", "", "The directory to find templates, JS, and CSS files. If blank the current directory will be used.") |
| } |
| |
| // server is the state of the server. |
| type server struct { |
| flags *flags |
| apiEndpoints *api.Api |
| templates *template.Template |
| login alogin.Login |
| } |
| |
| func new() (*server, error) { |
| // Register and parse flags. |
| flags := &flags{} |
| flagSet := flag.NewFlagSet("scrapexchange", flag.ExitOnError) |
| flags.Register(flagSet) |
| |
| common.InitWithMust( |
| "scrapexchange", |
| common.PrometheusOpt(&flags.promPort), |
| common.FlagSetOpt(flagSet), |
| ) |
| |
| if flags.bucket == "" { |
| return nil, skerr.Fmt("--bucket is a required flag.") |
| } |
| |
| // Fix up flag values. |
| if flags.resourcesDir == "" { |
| _, filename, _, _ := runtime.Caller(1) |
| flags.resourcesDir = filepath.Join(filepath.Dir(filename), "../../dist") |
| } |
| |
| ctx := context.Background() |
| ts, err := google.DefaultTokenSource(ctx, storage.ScopeFullControl, auth.ScopeUserinfoEmail) |
| if err != nil { |
| return nil, skerr.Wrap(err) |
| } |
| client := httputils.DefaultClientConfig().WithTokenSource(ts).WithoutRetries().Client() |
| storageClient, err := storage.NewClient(ctx, option.WithHTTPClient(client)) |
| if err != nil { |
| return nil, skerr.Wrap(err) |
| } |
| gcsClient := gcsclient.New(storageClient, flags.bucket) |
| scrapExchange, err := scrap.New(gcsClient) |
| if err != nil { |
| return nil, skerr.Wrapf(err, "Failed to create ScrapExchange.") |
| } |
| |
| apiEndpoints := api.New(scrapExchange) |
| |
| srv := &server{ |
| flags: flags, |
| apiEndpoints: apiEndpoints, |
| login: proxylogin.NewWithDefaults(), |
| } |
| srv.loadTemplates() |
| srv.startInternalServer() |
| |
| return srv, nil |
| } |
| |
| func (srv *server) loadTemplates() { |
| srv.templates = template.Must(template.New("").Delims("{%", "%}").ParseFiles( |
| filepath.Join(srv.flags.resourcesDir, "index.html"), |
| )) |
| } |
| |
| func (srv *server) mainHandler(w http.ResponseWriter, r *http.Request) { |
| w.Header().Set("Content-Type", "text/html") |
| if srv.flags.local { |
| srv.loadTemplates() |
| } |
| if err := srv.templates.ExecuteTemplate(w, "index.html", map[string]string{ |
| // Look in //scrap/pages/BUILD.bazel for where the nonce templates are injected. |
| "Nonce": secure.CSPNonce(r.Context()), |
| }); err != nil { |
| sklog.Errorf("Failed to expand template: %s", err) |
| } |
| } |
| |
| // gzip applies gzip to HTTP responses. |
| func gzip(h http.Handler) http.Handler { |
| return httputils.GzipRequestResponse(h) |
| } |
| |
| func (srv *server) AddHandlers(r chi.Router) { |
| r.Get("/", srv.mainHandler) |
| r.Get("/dist/*", http.StripPrefix("/dist/", gzip(http.HandlerFunc(httputils.MakeResourceHandler(srv.flags.resourcesDir)))).ServeHTTP) |
| srv.apiEndpoints.AddHandlers(r, api.DoNotAddProtectedEndpoints) |
| } |
| |
| func (srv *server) startInternalServer() { |
| // Internal endpoints that are only accessible from within the cluster. |
| internal := chi.NewRouter() |
| srv.apiEndpoints.AddHandlers(internal, api.AddProtectedEndpoints) |
| go func() { |
| sklog.Fatal(http.ListenAndServe(srv.flags.internalPort, internal)) |
| }() |
| } |
| |
| func main() { |
| s, err := new() |
| if err != nil { |
| sklog.Fatal(err) |
| } |
| |
| // Add HTTP handlers. |
| r := chi.NewRouter() |
| s.AddHandlers(r) |
| |
| // Do not wrap http.Handler with security or authentication middleware if we |
| // are running locally. |
| var h http.Handler = r |
| if !s.flags.local { |
| h = baseapp.SecurityMiddleware([]string{"scrap.skia.org"}, s.flags.local, nil)(h) |
| h = alogin.ForceRoleMiddleware(s.login, roles.Viewer)(h) |
| h = httputils.HealthzAndHTTPS(h) |
| } |
| |
| sklog.Infof("Ready to serve at: %q", s.flags.port) |
| server := &http.Server{ |
| Addr: s.flags.port, |
| Handler: h, |
| MaxHeaderBytes: 1 << 20, |
| } |
| sklog.Fatal(server.ListenAndServe()) |
| } |