summaryrefslogtreecommitdiffhomepage
path: root/website
diff options
context:
space:
mode:
Diffstat (limited to 'website')
-rw-r--r--website/BUILD3
-rw-r--r--website/_layouts/docs.html33
-rw-r--r--website/blog/README.md62
-rw-r--r--website/blog/index.html5
-rw-r--r--website/cmd/server/BUILD3
-rw-r--r--website/cmd/server/main.go222
-rw-r--r--website/defs.bzl10
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