// Copyright 2019 The gVisor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Server is the main gvisor.dev binary.
package main
import (
"flag"
"fmt"
"log"
"net/http"
"os"
"path"
"regexp"
"strings"
"github.com/google/pprof/driver"
)
var redirects = map[string]string{
// GitHub redirects.
"/change": "https://github.com/google/gvisor",
"/issue": "https://github.com/google/gvisor/issues",
"/issues": "https://github.com/google/gvisor/issues",
"/issue/new": "https://github.com/google/gvisor/issues/new",
"/pr": "https://github.com/google/gvisor/pulls",
// For links.
"/faq": "/docs/user_guide/faq/",
// From 2020-05-12 to 2020-06-30, the FAQ URL was uppercase. Redirect that
// back to maintain any links.
"/docs/user_guide/FAQ/": "/docs/user_guide/faq/",
// Redirects to compatibility docs.
"/c": "/docs/user_guide/compatibility/",
"/c/linux/amd64": "/docs/user_guide/compatibility/linux/amd64/",
// Redirect for old URLs.
"/docs/user_guide/compatibility/amd64/": "/docs/user_guide/compatibility/linux/amd64/",
"/docs/user_guide/compatibility/amd64": "/docs/user_guide/compatibility/linux/amd64/",
"/docs/user_guide/kubernetes/": "/docs/user_guide/quick_start/kubernetes/",
"/docs/user_guide/kubernetes": "/docs/user_guide/quick_start/kubernetes/",
"/docs/user_guide/oci/": "/docs/user_guide/quick_start/oci/",
"/docs/user_guide/oci": "/docs/user_guide/quick_start/oci/",
"/docs/user_guide/docker/": "/docs/user_guide/quick_start/docker/",
"/docs/user_guide/docker": "/docs/user_guide/quick_start/docker/",
"/blog/2020/09/22/platform-portability": "/blog/2020/10/22/platform-portability/",
"/blog/2020/09/22/platform-portability/": "/blog/2020/10/22/platform-portability/",
// Deprecated, but links continue to work.
"/cl": "https://gvisor-review.googlesource.com",
}
var prefixHelpers = map[string]string{
"change": "https://github.com/google/gvisor/commit/%s",
"issue": "https://github.com/google/gvisor/issues/%s",
"issues": "https://github.com/google/gvisor/issues/%s",
"pr": "https://github.com/google/gvisor/pull/%s",
// Redirects to compatibility docs.
"c/linux/amd64": "/docs/user_guide/compatibility/linux/amd64/#%s",
// Deprecated, but links continue to work.
"cl": "https://gvisor-review.googlesource.com/c/gvisor/+/%s",
}
var (
validID = regexp.MustCompile(`^[A-Za-z0-9-]*/?$`)
goGetHTML5 = `
Go-get`
)
// cronHandler wraps an http.Handler to check that the request is from the App
// Engine Cron service.
// See: https://cloud.google.com/appengine/docs/standard/go112/scheduling-jobs-with-cron-yaml#validating_cron_requests
func cronHandler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("X-Appengine-Cron") != "true" {
http.NotFound(w, r)
return
}
// Fallthrough.
h.ServeHTTP(w, r)
})
}
// wrappedHandler wraps an http.Handler.
//
// If the query parameters include go-get=1, then we redirect to a single
// static page that allows us to serve arbitrary Go packages.
func wrappedHandler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gg, ok := r.URL.Query()["go-get"]
if ok && len(gg) == 1 && gg[0] == "1" {
// Serve a trivial html page.
w.Write([]byte(goGetHTML5))
return
}
// Fallthrough.
h.ServeHTTP(w, r)
})
}
// redirectWithQuery redirects to the given target url preserving query parameters.
func redirectWithQuery(w http.ResponseWriter, r *http.Request, target string) {
url := target
if qs := r.URL.RawQuery; qs != "" {
url += "?" + qs
}
http.Redirect(w, r, url, http.StatusFound)
}
// hostRedirectHandler redirects the www. domain to the naked domain.
func hostRedirectHandler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.Host, "www.") {
// Redirect to the naked domain.
r.URL.Scheme = "https" // Assume https.
r.URL.Host = r.Host[4:] // Remove the 'www.'
http.Redirect(w, r, r.URL.String(), http.StatusMovedPermanently)
return
}
if *projectID != "" && r.Host == *projectID+".appspot.com" && *customHost != "" {
// Redirect to the custom domain.
r.URL.Scheme = "https" // Assume https.
r.URL.Host = *customHost
http.Redirect(w, r, r.URL.String(), http.StatusMovedPermanently)
return
}
h.ServeHTTP(w, r)
})
}
// prefixRedirectHandler returns a handler that redirects to the given formated url.
func prefixRedirectHandler(prefix, baseURL string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if p := r.URL.Path; p == prefix {
// Redirect /prefix/ to /prefix.
http.Redirect(w, r, p[:len(p)-1], http.StatusFound)
return
}
id := r.URL.Path[len(prefix):]
if !validID.MatchString(id) {
http.Error(w, "Not found", http.StatusNotFound)
return
}
target := fmt.Sprintf(baseURL, id)
redirectWithQuery(w, r, target)
})
}
// redirectHandler returns a handler that redirects to the given url.
func redirectHandler(target string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
redirectWithQuery(w, r, target)
})
}
// registerRedirects registers redirect http handlers.
func registerRedirects(mux *http.ServeMux) {
for prefix, baseURL := range prefixHelpers {
p := "/" + prefix + "/"
mux.Handle(p, hostRedirectHandler(wrappedHandler(prefixRedirectHandler(p, baseURL))))
}
for path, redirect := range redirects {
mux.Handle(path, hostRedirectHandler(wrappedHandler(redirectHandler(redirect))))
}
registerModuleDocRedirects(http.DefaultServeMux)
}
// registerModuleDocs registers redirect http handlers to redirect module paths
// directly to their docs on pkg.go.dev.
func registerModuleDocRedirects(mux *http.ServeMux) {
const prefix = "/gvisor/"
mux.Handle(prefix, hostRedirectHandler(wrappedHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
pkg := r.URL.Path[len(prefix):]
redirectWithQuery(w, r, fmt.Sprintf("https://pkg.go.dev/gvisor.dev/gvisor/%s", pkg))
}))))
}
// registerStatic registers static file handlers.
func registerStatic(mux *http.ServeMux, staticDir string) {
mux.Handle("/", hostRedirectHandler(wrappedHandler(http.FileServer(http.Dir(staticDir)))))
}
// profileMeta implements synthetic flags for pprof.
type profileMeta struct {
// Mux is the mux to register on.
Mux *http.ServeMux
// SourceURL is the source of the profile.
SourceURL string
}
func (*profileMeta) ExtraUsage() string { return "" }
func (*profileMeta) AddExtraUsage(string) {}
func (*profileMeta) Bool(_ string, def bool, _ string) *bool { return &def }
func (*profileMeta) Int(_ string, def int, _ string) *int { return &def }
func (*profileMeta) Float64(_ string, def float64, _ string) *float64 { return &def }
func (*profileMeta) StringList(_ string, def string, _ string) *[]*string { return new([]*string) }
func (*profileMeta) String(option string, def string, _ string) *string {
switch option {
case "http":
// Only http is specified. Other options may be accessible via
// the web interface, so we just need to spoof a valid option
// here. The server is actually bound by HTTPServer, below.
value := "localhost:80"
return &value
case "symbolize":
// Don't attempt symbolization. Most profiles should come with
// mappings built-in to the profile itself.
value := "none"
return &value
default:
return &def // Default.
}
}
// Parse implements plugin.FlagSet.Parse.
func (p *profileMeta) Parse(usage func()) []string {
// Just return the SourceURL. This is interpreted as the profile to
// download. We validate that the URL corresponds to a Google Cloud
// Storage URL below.
return []string{p.SourceURL}
}
// pprofFixedPrefix is used to limit the exposure to SSRF.
//
// See registerProfile below.
const pprofFixedPrefix = "https://storage.googleapis.com/"
// Target returns the URL target.
func (p *profileMeta) Target() string {
return fmt.Sprintf("/profile/%s/", p.SourceURL[len(pprofFixedPrefix):])
}
// HTTPServer is a function passed to driver.PProf.
func (p *profileMeta) HTTPServer(args *driver.HTTPServerArgs) error {
target := p.Target()
for subpath, handler := range args.Handlers {
handlerPath := path.Join(target, subpath)
if len(handlerPath) < len(target) {
// Don't clean the target, match only as the literal
// directory path in order to keep relative links
// working in the profile. E.g. /profile/foo/ is the
// base URL for the profile at https://.../foo.
handlerPath = target
}
p.Mux.Handle(handlerPath, handler)
}
return nil
}
// registerProfile registers the profile handler.
//
// Note that this has a security surface worth considering.
//
// We are passed effectively a URL, which we fetch and parse,
// then display the profile output. We limit the possibility of
// SSRF by interpreting the URL strictly as a part to an object
// in Google Cloud Storage, but we allow the user to specify any
// bucket (since this may change with the CI system).
//
// We additionally must consider the possibility that users may
// craft malicious profile objects (somehow) and pass those URLs
// here as well. It seems feasible that we could parse a profile
// that causes a crash (DOS), but this would be automatically
// handled without a blip. It seems unlikely that we could parse a
// profile that gives full code execution, but even so there is
// nothing in this image except this code and CA certs. At worst,
// code execution would enable someone to serve up content under the
// web domain. This would be ephemeral with the specific instance,
// and persisting such an attack would require constantly crashing
// instances in whatever way gives remote code execution. Even if
// this were possible, it's unlikely that exploiting such a crash
// could be done so constantly and consistently.
//
// The user can also fill the "disk" of this container instance,
// causing an OOM and a crash. This has similar semantics to the
// DOS scenario above, and would just be handled by Cloud Run.
//
// Finally, a malicious user could cause us to repeatedly fetch
// extremely large objects. However, since we fetch objects via
// the unauthenticated URL, such accesses would always be charged
// to the object owner. Downloading large objects can lead to the
// filling of the "disk" scenario above, but this is similarly a
// minor issue and immediately mitigated.
func registerProfile(mux *http.ServeMux) {
const urlPrefix = "/profile/"
mux.Handle(urlPrefix, hostRedirectHandler(wrappedHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Extract the URL; this is everything except the final /.
parts := strings.Split(r.URL.Path[len(urlPrefix):], "/")
url := pprofFixedPrefix + strings.Join(parts[:len(parts)-1], "/")
if url == pprofFixedPrefix {
http.Error(w, "Invalid URL: no path provided.", http.StatusNotFound)
return
}
// Set up the meta handler. This will modify the original mux
// accordingly, and we ultimately return a redirect that
// includes all the original arguments. This means that if we
// ever hit a server that does not have this profile loaded, it
// will load and redirect again.
meta := &profileMeta{
Mux: mux,
SourceURL: url,
}
if err := driver.PProf(&driver.Options{
Flagset: meta,
HTTPServer: meta.HTTPServer,
}); err != nil {
http.Error(w, fmt.Sprintf("Invalid profile: %v", err), http.StatusNotImplemented)
return
}
// Serve the path directly.
mux.ServeHTTP(w, r)
}))))
}
func envFlagString(name, def string) string {
if val := os.Getenv(name); val != "" {
return val
}
return def
}
var (
addr = flag.String("http", envFlagString("HTTP", ":"+envFlagString("PORT", "8080")), "HTTP service address")
staticDir = flag.String("static-dir", envFlagString("STATIC_DIR", "_site"), "static files directory")
// Uses the standard GOOGLE_CLOUD_PROJECT environment variable set by App Engine.
projectID = flag.String("project-id", envFlagString("GOOGLE_CLOUD_PROJECT", ""), "The App Engine project ID.")
customHost = flag.String("custom-domain", envFlagString("CUSTOM_DOMAIN", "gvisor.dev"), "The application's custom domain.")
)
func main() {
flag.Parse()
registerRedirects(http.DefaultServeMux)
registerStatic(http.DefaultServeMux, *staticDir)
registerProfile(http.DefaultServeMux)
log.Printf("Listening on %s...", *addr)
log.Fatal(http.ListenAndServe(*addr, nil))
}