diff options
Diffstat (limited to 'website')
-rw-r--r-- | website/BUILD | 3 | ||||
-rw-r--r-- | website/_layouts/docs.html | 33 | ||||
-rw-r--r-- | website/blog/README.md | 62 | ||||
-rw-r--r-- | website/blog/index.html | 5 | ||||
-rw-r--r-- | website/cmd/server/BUILD | 3 | ||||
-rw-r--r-- | website/cmd/server/main.go | 222 | ||||
-rw-r--r-- | website/defs.bzl | 10 |
7 files changed, 298 insertions, 40 deletions
diff --git a/website/BUILD b/website/BUILD index 676c2b701..d5315abce 100644 --- a/website/BUILD +++ b/website/BUILD @@ -38,6 +38,7 @@ genrule( ":syscallmd", "//website/blog:posts", "//website/cmd/server", + "@google_root_pem//file", ], outs = ["files.tgz"], cmd = "set -x; " + @@ -61,6 +62,8 @@ genrule( "ruby /checks.rb " + "/output && " + "cp $(location //website/cmd/server) $$T/output/server && " + + "mkdir -p $$T/output/etc/ssl && " + + "cp $(location @google_root_pem//file) $$T/output/etc/ssl/cert.pem && " + "tar -zcf $@ -C $$T/output . && " + "rm -rf $$T", tags = [ diff --git a/website/_layouts/docs.html b/website/_layouts/docs.html index 0422f9fb0..d45a781a4 100644 --- a/website/_layouts/docs.html +++ b/website/_layouts/docs.html @@ -16,21 +16,24 @@ categories: <ul class="sidebar-nav"> {% assign subcats = site.pages | where: 'layout', 'docs' | where: 'category', category | group_by: 'subcategory' | sort: 'name', 'first' %} {% for subcategory in subcats %} - {% assign sorted_pages = subcategory.items | sort: 'weight', 'last' %} - {% if subcategory.name != "" %} - {% assign ac = "aria-controls" %} - {% assign cid = category | remove: " " | downcase %} - {% assign sid = subcategory.name | remove: " " | downcase %} - <li> - <a class="sidebar-nav-heading" data-toggle="collapse" href="#{{ cid }}-{{ sid }}" aria-expanded="false" {{ ac }}="{{ cid }}-{{ sid }}">{{ subcategory.name }}<span class="caret"></span></a> - <ul class="collapse sidebar-nav sidebar-submenu" id="{{ cid }}-{{ sid }}"> - {% endif %} - {% for p in sorted_pages %} - <li><a href="{{ p.url }}">{{ p.title }}</a></li> - {% endfor %} - {% if subcategory.name != "" %} - </li> - </ul> + {% assign sorted_pages = subcategory.items | where: 'include_in_menu', true | sort: 'weight', 'last' %} + {% comment %}If all pages in the subcategory are excluded don't show it.{% endcomment %} + {% if sorted_pages.size > 0 %} + {% if subcategory.name != "" %} + {% assign ac = "aria-controls" %} + {% assign cid = category | remove: " " | downcase %} + {% assign sid = subcategory.name | remove: " " | downcase %} + <li> + <a class="sidebar-nav-heading" data-toggle="collapse" href="#{{ cid }}-{{ sid }}" aria-expanded="false" {{ ac }}="{{ cid }}-{{ sid }}">{{ subcategory.name }}<span class="caret"></span></a> + <ul class="collapse sidebar-nav sidebar-submenu" id="{{ cid }}-{{ sid }}"> + {% endif %} + {% for p in sorted_pages %} + <li><a href="{{ p.url }}">{{ p.title }}</a></li> + {% endfor %} + {% if subcategory.name != "" %} + </li> + </ul> + {% endif %} {% endif %} {% endfor %} </ul> diff --git a/website/blog/README.md b/website/blog/README.md new file mode 100644 index 000000000..e1d685288 --- /dev/null +++ b/website/blog/README.md @@ -0,0 +1,62 @@ +# gVisor blog + +The gVisor blog is owned and run by the gVisor team. + +## Contact + +Reach out to us on [gitter](https://gitter.im/gvisor/community) or the +[mailing list](https://groups.google.com/forum/#!forum/gvisor-users) if you +would like to write a blog post. + +## Submit a Post + +Anyone can write a blog post and submit it for review. Purely commercial content +or vendor pitches are not allowed. Please refer to the +[blog guidelines](#blog-guidelines) for more guidance about content is that +allowed. + +To submit a blog post, follow the steps below. + +1. [Sign the Contributor License Agreements](https://gvisor.dev/contributing/) + if you have not yet done so. +1. Familiarize yourself with the Markdown format for the + [existing blog posts](https://github.com/google/gvisor/tree/master/website/blog). +1. Write your blog post in a text editor of your choice. +1. (Optional) If you need help with markdown, check out + [StakEdit](https://stackedit.io/app#) or read + [Jekyll's formatting reference](https://jekyllrb.com/docs/posts/#creating-posts) + for more information. +1. Click **Add file** > **Create new file**. +1. Paste your content into the editor and save it. Name the file in the + following way: *[BLOG] Your proposed title* , but don’t put the date in the + file name. The blog reviewers will work with you on the final file name, and + the date on which the blog will be published. +1. When you save the file, GitHub will walk you through the pull request (PR) + process. +1. Send us a message on [gitter](https://gitter.im/gvisor/community) with a + link to your recently created PR. +1. A reviewer will be assigned to the pull request. They check your submission, + and work with you on feedback and final details. When the pull request is + approved, the blog will be scheduled for publication. + +### Blog Guidelines {#blog-guidelines} + +#### Suitable content: + +- **Original content only** +- gVisor features or project updates +- Tutorials and demos +- Use cases +- Content that is specific to a vendor or platform about gVisor installation + and use + +#### Unsuitable Content: + +- Blogs with no content relevant to gVisor +- Vendor pitches + +## Review Process + +Each blog post should be approved by at least one person on the team. Once all +of the review comments have been addressed and approved, a member of the team +will schedule publication of the blog post. diff --git a/website/blog/index.html b/website/blog/index.html index 5c67c95fc..272917fc4 100644 --- a/website/blog/index.html +++ b/website/blog/index.html @@ -20,3 +20,8 @@ pagination: {% if paginator.total_pages > 1 %} {% include paginator.html %} {% endif %} + +<hr> + +If you would like to contribute to the gVisor blog check out the +<a href="https://github.com/google/gvisor/tree/master/website/blog">instructions</a>. diff --git a/website/cmd/server/BUILD b/website/cmd/server/BUILD index 6b5a08f0d..e4cf91e07 100644 --- a/website/cmd/server/BUILD +++ b/website/cmd/server/BUILD @@ -7,4 +7,7 @@ go_binary( srcs = ["main.go"], pure = True, visibility = ["//website:__pkg__"], + deps = [ + "@com_github_google_pprof//driver:go_default_library", + ], ) diff --git a/website/cmd/server/main.go b/website/cmd/server/main.go index ac09550a9..9f0092ed6 100644 --- a/website/cmd/server/main.go +++ b/website/cmd/server/main.go @@ -20,9 +20,13 @@ import ( "fmt" "log" "net/http" + "net/url" "os" + "path" "regexp" "strings" + + "github.com/google/pprof/driver" ) var redirects = map[string]string{ @@ -58,19 +62,37 @@ var redirects = map[string]string{ // Deprecated, but links continue to work. "/cl": "https://gvisor-review.googlesource.com", + + // Access package documentation. + "/gvisor": "https://pkg.go.dev/gvisor.dev/gvisor", + + // Code search root. + "/cs": "https://cs.opensource.google/gvisor/gvisor", } -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", +type prefixInfo struct { + baseURL string + checkValidID bool + queryEscape bool +} + +var prefixHelpers = map[string]prefixInfo{ + "change": {baseURL: "https://github.com/google/gvisor/commit/%s", checkValidID: true}, + "issue": {baseURL: "https://github.com/google/gvisor/issues/%s", checkValidID: true}, + "issues": {baseURL: "https://github.com/google/gvisor/issues/%s", checkValidID: true}, + "pr": {baseURL: "https://github.com/google/gvisor/pull/%s", checkValidID: true}, // Redirects to compatibility docs. - "c/linux/amd64": "/docs/user_guide/compatibility/linux/amd64/#%s", + "c/linux/amd64": {baseURL: "/docs/user_guide/compatibility/linux/amd64/#%s", checkValidID: true}, // Deprecated, but links continue to work. - "cl": "https://gvisor-review.googlesource.com/c/gvisor/+/%s", + "cl": {baseURL: "https://gvisor-review.googlesource.com/c/gvisor/+/%s", checkValidID: true}, + + // Redirect to source documentation. + "gvisor": {baseURL: "https://pkg.go.dev/gvisor.dev/gvisor/%s"}, + + // Redirect to code search, with the path as the query. + "cs": {baseURL: "https://cs.opensource.google/search?q=%s&ss=gvisor", queryEscape: true}, } var ( @@ -144,7 +166,7 @@ func hostRedirectHandler(h http.Handler) http.Handler { } // prefixRedirectHandler returns a handler that redirects to the given formated url. -func prefixRedirectHandler(prefix, baseURL string) http.Handler { +func prefixRedirectHandler(prefix string, info prefixInfo) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if p := r.URL.Path; p == prefix { // Redirect /prefix/ to /prefix. @@ -152,11 +174,14 @@ func prefixRedirectHandler(prefix, baseURL string) http.Handler { return } id := r.URL.Path[len(prefix):] - if !validID.MatchString(id) { + if info.checkValidID && !validID.MatchString(id) { http.Error(w, "Not found", http.StatusNotFound) return } - target := fmt.Sprintf(baseURL, id) + if info.queryEscape { + id = url.QueryEscape(id) + } + target := fmt.Sprintf(info.baseURL, id) redirectWithQuery(w, r, target) }) } @@ -168,30 +193,178 @@ func redirectHandler(target string) http.Handler { }) } -// redirectRedirects registers redirect http handlers. +// registerRedirects registers redirect http handlers. func registerRedirects(mux *http.ServeMux) { - if mux == nil { - mux = http.DefaultServeMux - } - - for prefix, baseURL := range prefixHelpers { + for prefix, info := range prefixHelpers { p := "/" + prefix + "/" - mux.Handle(p, hostRedirectHandler(wrappedHandler(prefixRedirectHandler(p, baseURL)))) + mux.Handle(p, hostRedirectHandler(wrappedHandler(prefixRedirectHandler(p, info)))) } - for path, redirect := range redirects { mux.Handle(path, hostRedirectHandler(wrappedHandler(redirectHandler(redirect)))) } } -// registerStatic registers static file handlers +// registerStatic registers static file handlers. func registerStatic(mux *http.ServeMux, staticDir string) { - if mux == nil { - mux = http.DefaultServeMux - } 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/" + +// allowedBuckets enforces constraints on the pprof target. +// +// If the continuous integration system is changed in the future to use +// additional buckets, they may be whitelisted here. See registerProfile. +var allowedBuckets = map[string]bool{ + "gvisor-buildkite": true, +} + +// 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. + // + // The base target typically shows the dot-based graph, + // which will not work in the image (due to the lack of + // a dot binary to execute). Therefore, we redirect to + // the flamegraph handler. Everything should otherwise + // work the exact same way, except the "Graph" link. + handlerPath = target + handler = redirectHandler(path.Join(handlerPath, "flamegraph")) + } + 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, and further limit the buckets that +// may be used. This contains the vast majority of concerns, +// since objects must at least be uploaded by our CI system. +// +// However, we additionally consider the possibility that users +// 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. +// +// Note that all of the above scenarios would require uploading +// malicious profiles to controller buckets, and a clear audit +// trail would exist in those cases. +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):], "/") + if len(parts) == 0 { + http.Error(w, "Invalid URL: no bucket provided.", http.StatusNotFound) + return + } + if !allowedBuckets[parts[0]] { + http.Error(w, fmt.Sprintf("Invalid URL: not an allowed bucket (%s).", parts[0]), http.StatusNotFound) + return + } + 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 @@ -211,8 +384,9 @@ var ( func main() { flag.Parse() - registerRedirects(nil) - registerStatic(nil, *staticDir) + registerRedirects(http.DefaultServeMux) + registerStatic(http.DefaultServeMux, *staticDir) + registerProfile(http.DefaultServeMux) log.Printf("Listening on %s...", *addr) log.Fatal(http.ListenAndServe(*addr, nil)) diff --git a/website/defs.bzl b/website/defs.bzl index f52946c15..703040882 100644 --- a/website/defs.bzl +++ b/website/defs.bzl @@ -7,6 +7,7 @@ load("//tools:defs.bzl", "short_path") # dynamically. This is done the via BUILD system so that the plain # documentation files can be viewable without non-compliant markdown headers. DocInfo = provider( + "Encapsulates information for a documentation page.", fields = [ "layout", "description", @@ -16,6 +17,7 @@ DocInfo = provider( "weight", "editpath", "authors", + "include_in_menu", ], ) @@ -33,6 +35,7 @@ def _doc_impl(ctx): weight = ctx.attr.weight, editpath = short_path(ctx.files.src[0].short_path), authors = ctx.attr.authors, + include_in_menu = ctx.attr.include_in_menu, ), ] @@ -74,6 +77,10 @@ doc = rule( default = "50", ), "authors": attr.string_list(), + "include_in_menu": attr.bool( + doc = "Include document in the navigation menu.", + default = True, + ), }, ) @@ -111,7 +118,8 @@ subcategory: {subcategory} weight: {weight} editpath: {editpath} authors: {authors} -layout: {layout}""" +layout: {layout} +include_in_menu: {include_in_menu}""" for f in dep.files.to_list(): # Is this a markdown file? If not, then we ensure that it ends up |