gvisor.dev/gvisor@v0.0.0-20240520182842-f9d4d51c7e0f/website/cmd/server/main.go (about) 1 // Copyright 2019 The gVisor Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // https://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Server is the main gvisor.dev binary. 16 package main 17 18 import ( 19 "flag" 20 "fmt" 21 "log" 22 "net/http" 23 "net/url" 24 "os" 25 "path" 26 "regexp" 27 "strings" 28 29 "github.com/google/pprof/driver" 30 ) 31 32 var redirects = map[string]string{ 33 // GitHub redirects. 34 "/change": "https://github.com/google/gvisor", 35 "/issue": "https://github.com/google/gvisor/issues", 36 "/issues": "https://github.com/google/gvisor/issues", 37 "/issue/new": "https://github.com/google/gvisor/issues/new/choose", 38 "/pr": "https://github.com/google/gvisor/pulls", 39 40 // For links. 41 "/faq": "/docs/user_guide/faq/", 42 43 // From 2020-05-12 to 2020-06-30, the FAQ URL was uppercase. Redirect that 44 // back to maintain any links. 45 "/docs/user_guide/FAQ/": "/docs/user_guide/faq/", 46 47 // Redirects to compatibility docs. 48 "/c": "/docs/user_guide/compatibility/", 49 "/c/linux/amd64": "/docs/user_guide/compatibility/linux/amd64/", 50 51 // Redirect for old URLs. 52 "/docs/user_guide/compatibility/amd64/": "/docs/user_guide/compatibility/linux/amd64/", 53 "/docs/user_guide/compatibility/amd64": "/docs/user_guide/compatibility/linux/amd64/", 54 "/docs/user_guide/kubernetes/": "/docs/user_guide/quick_start/kubernetes/", 55 "/docs/user_guide/kubernetes": "/docs/user_guide/quick_start/kubernetes/", 56 "/docs/user_guide/oci/": "/docs/user_guide/quick_start/oci/", 57 "/docs/user_guide/oci": "/docs/user_guide/quick_start/oci/", 58 "/docs/user_guide/docker/": "/docs/user_guide/quick_start/docker/", 59 "/docs/user_guide/docker": "/docs/user_guide/quick_start/docker/", 60 "/blog/2020/09/22/platform-portability": "/blog/2020/10/22/platform-portability/", 61 "/blog/2020/09/22/platform-portability/": "/blog/2020/10/22/platform-portability/", 62 63 // Deprecated, but links continue to work. 64 "/cl": "https://gvisor-review.googlesource.com", 65 66 // Access package documentation. 67 "/gvisor": "https://pkg.go.dev/gvisor.dev/gvisor", 68 69 // Code search root. 70 "/cs": "https://cs.opensource.google/gvisor/gvisor", 71 } 72 73 type prefixInfo struct { 74 baseURL string 75 checkValidID bool 76 queryEscape bool 77 } 78 79 var prefixHelpers = map[string]prefixInfo{ 80 "change": {baseURL: "https://github.com/google/gvisor/commit/%s", checkValidID: true}, 81 "issue": {baseURL: "https://github.com/google/gvisor/issues/%s", checkValidID: true}, 82 "issues": {baseURL: "https://github.com/google/gvisor/issues/%s", checkValidID: true}, 83 "pr": {baseURL: "https://github.com/google/gvisor/pull/%s", checkValidID: true}, 84 85 // Redirects to compatibility docs. 86 "c/linux/amd64": {baseURL: "/docs/user_guide/compatibility/linux/amd64/#%s", checkValidID: true}, 87 88 // Deprecated, but links continue to work. 89 "cl": {baseURL: "https://gvisor-review.googlesource.com/c/gvisor/+/%s", checkValidID: true}, 90 91 // Redirect to source documentation. 92 "gvisor": {baseURL: "https://pkg.go.dev/gvisor.dev/gvisor/%s"}, 93 94 // Redirect to code search, with the path as the query. 95 "cs": {baseURL: "https://cs.opensource.google/search?q=%s&ss=gvisor", queryEscape: true}, 96 } 97 98 var ( 99 validID = regexp.MustCompile(`^[A-Za-z0-9-]*/?$`) 100 goGetHTML5 = `<!doctype html><html><head><meta charset=utf-8> 101 <meta name="go-import" content="gvisor.dev/gvisor git https://github.com/google/gvisor"> 102 <meta name="go-import" content="gvisor.dev/website git https://github.com/google/gvisor-website"> 103 <title>Go-get</title></head><body></html>` 104 ) 105 106 // cronHandler wraps an http.Handler to check that the request is from the App 107 // Engine Cron service. 108 // See: https://cloud.google.com/appengine/docs/standard/go112/scheduling-jobs-with-cron-yaml#validating_cron_requests 109 func cronHandler(h http.Handler) http.Handler { 110 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 111 if r.Header.Get("X-Appengine-Cron") != "true" { 112 http.NotFound(w, r) 113 return 114 } 115 // Fallthrough. 116 h.ServeHTTP(w, r) 117 }) 118 } 119 120 // wrappedHandler wraps an http.Handler. 121 // 122 // If the query parameters include go-get=1, then we redirect to a single 123 // static page that allows us to serve arbitrary Go packages. 124 func wrappedHandler(h http.Handler) http.Handler { 125 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 126 gg, ok := r.URL.Query()["go-get"] 127 if ok && len(gg) == 1 && gg[0] == "1" { 128 // Serve a trivial html page. 129 w.Write([]byte(goGetHTML5)) 130 return 131 } 132 // Fallthrough. 133 h.ServeHTTP(w, r) 134 }) 135 } 136 137 // redirectWithQuery redirects to the given target url preserving query parameters. 138 func redirectWithQuery(w http.ResponseWriter, r *http.Request, target string) { 139 url := target 140 if qs := r.URL.RawQuery; qs != "" { 141 url += "?" + qs 142 } 143 http.Redirect(w, r, url, http.StatusFound) 144 } 145 146 // domainMatch returns whether the domain is accepted and if we should redirect 147 // to a custom domain. 148 func domainMatch(host string) (redirect bool, ok bool) { 149 if *customHost == "*" { 150 redirect = false 151 ok = true 152 return 153 } 154 155 // Custom Host handling. 156 if *customHost != "" { 157 if host == "www."+*customHost { 158 redirect = true 159 ok = true 160 return 161 } 162 163 if host == *customHost { 164 ok = true 165 return 166 } 167 } 168 169 // Cloud Run App domain handling. 170 if strings.HasPrefix(host, *projectID+"-") && strings.HasSuffix(host, ".run.app") { 171 redirect = *customHost != "" 172 ok = true 173 return 174 } 175 176 return 177 } 178 179 // hostRedirectHandler redirects the www. domain to the naked domain. 180 func hostRedirectHandler(h http.Handler) http.Handler { 181 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 182 redirect, ok := domainMatch(r.Host) 183 if !ok { 184 // Do not serve on invalid domains. 185 http.Error(w, "Bad Request", http.StatusBadRequest) 186 return 187 } 188 189 if redirect { 190 // Redirect to the naked domain. 191 r.URL.Scheme = "https" // Assume https. 192 r.URL.Host = *customHost 193 http.Redirect(w, r, r.URL.String(), http.StatusMovedPermanently) 194 return 195 } 196 197 h.ServeHTTP(w, r) 198 }) 199 } 200 201 // prefixRedirectHandler returns a handler that redirects to the given formated url. 202 func prefixRedirectHandler(prefix string, info prefixInfo) http.Handler { 203 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 204 if p := r.URL.Path; p == prefix { 205 // Redirect /prefix/ to /prefix. 206 http.Redirect(w, r, p[:len(p)-1], http.StatusFound) 207 return 208 } 209 id := r.URL.Path[len(prefix):] 210 if info.checkValidID && !validID.MatchString(id) { 211 http.Error(w, "Not found", http.StatusNotFound) 212 return 213 } 214 if info.queryEscape { 215 id = url.QueryEscape(id) 216 } 217 target := fmt.Sprintf(info.baseURL, id) 218 redirectWithQuery(w, r, target) 219 }) 220 } 221 222 // redirectHandler returns a handler that redirects to the given url. 223 func redirectHandler(target string) http.Handler { 224 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 225 redirectWithQuery(w, r, target) 226 }) 227 } 228 229 // registerRedirects registers redirect http handlers. 230 func registerRedirects(mux *http.ServeMux) { 231 for prefix, info := range prefixHelpers { 232 p := "/" + prefix + "/" 233 mux.Handle(p, hostRedirectHandler(wrappedHandler(prefixRedirectHandler(p, info)))) 234 } 235 for path, redirect := range redirects { 236 mux.Handle(path, hostRedirectHandler(wrappedHandler(redirectHandler(redirect)))) 237 } 238 } 239 240 // registerStatic registers static file handlers. 241 func registerStatic(mux *http.ServeMux, staticDir string) { 242 mux.Handle("/", hostRedirectHandler(wrappedHandler(http.FileServer(http.Dir(staticDir))))) 243 } 244 245 // profileMeta implements synthetic flags for pprof. 246 type profileMeta struct { 247 // Mux is the mux to register on. 248 Mux *http.ServeMux 249 250 // SourceURL is the source of the profile. 251 SourceURL string 252 } 253 254 func (*profileMeta) ExtraUsage() string { return "" } 255 func (*profileMeta) AddExtraUsage(string) {} 256 func (*profileMeta) Bool(_ string, def bool, _ string) *bool { return &def } 257 func (*profileMeta) Int(_ string, def int, _ string) *int { return &def } 258 func (*profileMeta) Float64(_ string, def float64, _ string) *float64 { return &def } 259 func (*profileMeta) StringList(_ string, def string, _ string) *[]*string { return new([]*string) } 260 func (*profileMeta) String(option string, def string, _ string) *string { 261 switch option { 262 case "http": 263 // Only http is specified. Other options may be accessible via 264 // the web interface, so we just need to spoof a valid option 265 // here. The server is actually bound by HTTPServer, below. 266 value := "localhost:80" 267 return &value 268 case "symbolize": 269 // Don't attempt symbolization. Most profiles should come with 270 // mappings built-in to the profile itself. 271 value := "none" 272 return &value 273 default: 274 return &def // Default. 275 } 276 } 277 278 // Parse implements plugin.FlagSet.Parse. 279 func (p *profileMeta) Parse(usage func()) []string { 280 // Just return the SourceURL. This is interpreted as the profile to 281 // download. We validate that the URL corresponds to a Google Cloud 282 // Storage URL below. 283 return []string{p.SourceURL} 284 } 285 286 // pprofFixedPrefix is used to limit the exposure to SSRF. 287 // 288 // See registerProfile below. 289 const pprofFixedPrefix = "https://storage.googleapis.com/" 290 291 // allowedBuckets enforces constraints on the pprof target. 292 // 293 // If the continuous integration system is changed in the future to use 294 // additional buckets, they may be allowed here. See registerProfile. 295 var allowedBuckets = map[string]bool{ 296 "gvisor-buildkite": true, 297 } 298 299 // Target returns the URL target. 300 func (p *profileMeta) Target() string { 301 return fmt.Sprintf("/profile/%s/", p.SourceURL[len(pprofFixedPrefix):]) 302 } 303 304 // HTTPServer is a function passed to driver.PProf. 305 func (p *profileMeta) HTTPServer(args *driver.HTTPServerArgs) error { 306 target := p.Target() 307 for subpath, handler := range args.Handlers { 308 handlerPath := path.Join(target, subpath) 309 if len(handlerPath) < len(target) { 310 // Don't clean the target, match only as the literal 311 // directory path in order to keep relative links 312 // working in the profile. E.g. /profile/foo/ is the 313 // base URL for the profile at https://.../foo. 314 // 315 // The base target typically shows the dot-based graph, 316 // which will not work in the image (due to the lack of 317 // a dot binary to execute). Therefore, we redirect to 318 // the flamegraph handler. Everything should otherwise 319 // work the exact same way, except the "Graph" link. 320 handlerPath = target 321 handler = redirectHandler(path.Join(handlerPath, "flamegraph")) 322 } 323 p.Mux.Handle(handlerPath, handler) 324 } 325 return nil 326 } 327 328 // registerProfile registers the profile handler. 329 // 330 // Note that this has a security surface worth considering. 331 // 332 // We are passed effectively a URL, which we fetch and parse, 333 // then display the profile output. We limit the possibility of 334 // SSRF by interpreting the URL strictly as a part to an object 335 // in Google Cloud Storage, and further limit the buckets that 336 // may be used. This contains the vast majority of concerns, 337 // since objects must at least be uploaded by our CI system. 338 // 339 // However, we additionally consider the possibility that users 340 // craft malicious profile objects (somehow) and pass those URLs 341 // here as well. It seems feasible that we could parse a profile 342 // that causes a crash (DOS), but this would be automatically 343 // handled without a blip. It seems unlikely that we could parse a 344 // profile that gives full code execution, but even so there is 345 // nothing in this image except this code and CA certs. At worst, 346 // code execution would enable someone to serve up content under the 347 // web domain. This would be ephemeral with the specific instance, 348 // and persisting such an attack would require constantly crashing 349 // instances in whatever way gives remote code execution. Even if 350 // this were possible, it's unlikely that exploiting such a crash 351 // could be done so constantly and consistently. 352 // 353 // The user can also fill the "disk" of this container instance, 354 // causing an OOM and a crash. This has similar semantics to the 355 // DOS scenario above, and would just be handled by Cloud Run. 356 // 357 // Note that all of the above scenarios would require uploading 358 // malicious profiles to controller buckets, and a clear audit 359 // trail would exist in those cases. 360 func registerProfile(mux *http.ServeMux) { 361 const urlPrefix = "/profile/" 362 mux.Handle(urlPrefix, hostRedirectHandler(wrappedHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 363 // Extract the URL; this is everything except the final /. 364 parts := strings.Split(r.URL.Path[len(urlPrefix):], "/") 365 if len(parts) == 0 { 366 http.Error(w, "Invalid URL: no bucket provided.", http.StatusNotFound) 367 return 368 } 369 if !allowedBuckets[parts[0]] { 370 http.Error(w, fmt.Sprintf("Invalid URL: not an allowed bucket (%s).", parts[0]), http.StatusNotFound) 371 return 372 } 373 url := pprofFixedPrefix + strings.Join(parts[:len(parts)-1], "/") 374 if url == pprofFixedPrefix { 375 http.Error(w, "Invalid URL: no path provided.", http.StatusNotFound) 376 return 377 } 378 379 // Set up the meta handler. This will modify the original mux 380 // accordingly, and we ultimately return a redirect that 381 // includes all the original arguments. This means that if we 382 // ever hit a server that does not have this profile loaded, it 383 // will load and redirect again. 384 meta := &profileMeta{ 385 Mux: mux, 386 SourceURL: url, 387 } 388 if err := driver.PProf(&driver.Options{ 389 Flagset: meta, 390 HTTPServer: meta.HTTPServer, 391 }); err != nil { 392 http.Error(w, fmt.Sprintf("Invalid profile: %v", err), http.StatusNotImplemented) 393 return 394 } 395 396 // Serve the path directly. 397 mux.ServeHTTP(w, r) 398 })))) 399 } 400 401 func envFlagString(name, def string) string { 402 if val, ok := os.LookupEnv(name); ok { 403 return val 404 } 405 return def 406 } 407 408 var ( 409 addr = flag.String("http", envFlagString("HTTP", ":"+envFlagString("PORT", "8080")), "HTTP service address") 410 staticDir = flag.String("static-dir", envFlagString("STATIC_DIR", "_site"), "static files directory") 411 412 // NOTE: GOOGLE_CLOUD_PROJECT environment variable does not seem to be used 413 // anymore by Google Cloud but we can continue to look for it and fall back 414 // to the default project. 415 projectID = flag.String("project-id", envFlagString("GOOGLE_PROJECT_ID", "gvisordev"), "The Google Cloud project ID.") 416 customHost = flag.String("custom-domain", envFlagString("CUSTOM_DOMAIN", "gvisor.dev"), "The application's custom domain.") 417 ) 418 419 func main() { 420 flag.Parse() 421 422 if *customHost == "*" { 423 fmt.Println("WARNING: custom domain of '*' matches anything and is potentially insecure.") 424 } 425 426 if *projectID == "" { 427 fmt.Fprintf(os.Stderr, "Project ID not set.\n") 428 os.Exit(1) 429 } 430 431 registerRedirects(http.DefaultServeMux) 432 registerStatic(http.DefaultServeMux, *staticDir) 433 registerProfile(http.DefaultServeMux) 434 435 log.Printf("Listening on %s...", *addr) 436 log.Fatal(http.ListenAndServe(*addr, nil)) 437 }