blob: 3eb3d34c9963cf61d0e7c5de5fde79b01ecdc7f6 [file] [log] [blame]
package baseapp
import (
"encoding/json"
"flag"
"fmt"
"net/http"
"path/filepath"
"runtime"
"time"
"github.com/gorilla/mux"
"github.com/unrolled/secure"
"go.skia.org/infra/go/common"
"go.skia.org/infra/go/httputils"
"go.skia.org/infra/go/sklog"
)
var (
Local = flag.Bool("local", false, "Running locally if true. As opposed to in production.")
Port = flag.String("port", ":8000", "HTTP service address (e.g., ':8000')")
PromPort = flag.String("prom_port", ":20000", "Metrics service address (e.g., ':10110')")
ResourcesDir = flag.String("resources_dir", "", "The directory to find templates, JS, and CSS files. If blank the current directory will be used.")
)
const (
SERVER_READ_TIMEOUT = 5 * time.Minute
SERVER_WRITE_TIMEOUT = 5 * time.Minute
)
// Applications that want to use Serve() must conform the App interface.
type App interface {
// AddHandlers is called by Serve and the receiver must add all handlers
// to the passed in mux.Router.
AddHandlers(*mux.Router)
// AddMiddleware returns a list of mux.Middleware's to add to the router.
// This is a good place to add auth middleware.
AddMiddleware() []mux.MiddlewareFunc
}
// Constructor is a function that builds an App instance.
//
// Used as a parameter to Serve.
type Constructor func() (App, error)
// cspReporter takes csp failure reports and turns them into structured log
// entries.
func cspReporter(w http.ResponseWriter, r *http.Request) {
var body interface{}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
sklog.Errorf("Failed to decode csp report: %s", err)
return
}
c := struct {
Type string `json:"type"`
Body interface{} `json:"body"`
}{
Type: "csp",
Body: body,
}
b, err := json.Marshal(c)
if err != nil {
sklog.Errorf("Failed to marshal csp log entry: %s", err)
return
}
fmt.Println(string(b))
return
}
func securityMiddleware(allowedHosts []string) mux.MiddlewareFunc {
// Configure Content Security Policy (CSP).
addScriptSrc := ""
if *Local {
// webpack uses eval() in development mode, so allow unsafe-eval when local.
addScriptSrc = "'unsafe-eval'"
}
// This non-local CSP string passes the tests at https://csp-evaluator.withgoogle.com/.
//
// See also: https://csp.withgoogle.com/docs/strict-csp.html
cspString := fmt.Sprintf("base-uri 'none'; img-src 'self' ; object-src 'none' ; style-src 'self' 'unsafe-inline' ; script-src 'strict-dynamic' $NONCE 'unsafe-inline' %s https: http: ; report-uri /cspreport ;", addScriptSrc)
// Apply CSP and other security minded headers.
secureMiddleware := secure.New(secure.Options{
AllowedHosts: allowedHosts,
HostsProxyHeaders: []string{"X-Forwarded-Host"},
SSLRedirect: true,
SSLProxyHeaders: map[string]string{"X-Forwarded-Proto": "https"},
STSSeconds: 60 * 60 * 24 * 365,
STSIncludeSubdomains: true,
ContentSecurityPolicy: cspString,
IsDevelopment: *Local,
})
return secureMiddleware.Handler
}
// Serve builds and runs the App in a secure manner in out kubernetes cluster.
//
// The constructor builds an App instance. Note that we don't pass in an App
// instance directly, because we want the constructor called after the
// common.Init*() functions are called, i.e. after flags are parsed.
//
// The allowedHosts are the list of domains that are allowed to make requests
// to this app. Make sure to include the domain name of the app itself. For
// example; []string{"am.skia.org"}.
//
// See https://csp.withgoogle.com/docs/strict-csp.html for more information on
// Strict CSP in general.
//
// For this to work every script and style tag must have a nonce attribute
// whose value matches the one sent in the Content-Security-Policy: header. You
// can have webpack inject an attribute with a template for the nonce by adding
// the HtmlWebPackInjectAttributesPlugin to your plugins, i.e.
//
// config.plugins.push(
// new HtmlWebpackInjectAttributesPlugin({
// nonce: "{%.nonce%}",
// }),
// );
//
// And then include that nonce when expanding any pages:
//
// if err := srv.templates.ExecuteTemplate(w, "index.html", map[string]string{
// "nonce": secure.CSPNonce(r.Context()),
// }); err != nil {
// sklog.Errorf("Failed to expand template: %s", err)
// }
//
// Since our audience is small and only uses modern browsers we shouldn't need
// any further XSS protection. For example, even if a user is logged into
// another Google site that is compromised, while they can request the main
// index page and get both the csrf token and value, they couldn't POST it back
// to the site we are serving since that site wouldn't be listed in
// allowedHosts.
//
// CSP failures will be logged as structured log events.
//
// Static resources, e.g. webpack output, will be served at '/static/' and will
// serve the contents of the '/dist' directory.
func Serve(constructor Constructor, allowedHosts []string) {
// Do common init.
common.InitWithMust(
"generic-k8s-app", // The app name is only used by ../go/sklog/cloud_logging, and we don't use that on k8s.
common.PrometheusOpt(PromPort),
common.MetricsLoggingOpt(),
)
// Fix up flag values.
if *ResourcesDir == "" {
_, filename, _, _ := runtime.Caller(1)
*ResourcesDir = filepath.Join(filepath.Dir(filename), "../../dist")
}
// Build App instance.
app, err := constructor()
if err != nil {
sklog.Fatal(err)
}
// Add all routing.
r := mux.NewRouter()
r.HandleFunc("/cspreport", cspReporter).Methods("POST")
r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.HandlerFunc(httputils.MakeResourceHandler(*ResourcesDir))))
app.AddHandlers(r)
// Layer on all the middleware.
middleware := []mux.MiddlewareFunc{}
if !*Local {
middleware = append(middleware, httputils.HealthzAndHTTPS)
}
middleware = append(middleware, app.AddMiddleware()...)
middleware = append(middleware,
httputils.LoggingGzipRequestResponse,
securityMiddleware(allowedHosts),
)
r.Use(middleware...)
// Start serving.
sklog.Infoln("Ready to serve.")
server := &http.Server{
Addr: *Port,
Handler: r,
ReadTimeout: SERVER_READ_TIMEOUT,
WriteTimeout: SERVER_WRITE_TIMEOUT,
MaxHeaderBytes: 1 << 20,
}
sklog.Fatal(server.ListenAndServe())
}