blob: f43559e5136b733a9b141915f8f54172f8b6ae5b [file] [log] [blame]
// 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())
}