Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 26 additions & 9 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -90,21 +90,38 @@ build-api-protos:

build-protos: build-osv-protos build-api-protos

run-website:
cd gcp/website/frontend3 && pnpm install && pnpm run build
cd gcp/website/blog && hugo --buildFuture -d ../dist/static/blog
build-website-frontend:
cd website/frontend3 && pnpm install && pnpm run build
cd website/blog && hugo --buildFuture -d ../dist/static/blog

run-website: build-website-frontend
cd gcp/website && $(install-cmd) && GOOGLE_CLOUD_PROJECT=oss-vdb OSV_VULNERABILITIES_BUCKET=osv-vulnerabilities $(run-cmd) python main.py

run-website-staging:
cd gcp/website/frontend3 && pnpm install && pnpm run build
cd gcp/website/blog && hugo --buildFuture -d ../dist/static/blog
run-website-staging: build-website-frontend
cd gcp/website && $(install-cmd) && GOOGLE_CLOUD_PROJECT=oss-vdb-test OSV_VULNERABILITIES_BUCKET=osv-test-vulnerabilities $(run-cmd) python main.py

run-website-emulator:
cd gcp/website/frontend3 && pnpm install && pnpm run build
cd gcp/website/blog && hugo --buildFuture -d ../dist/static/blog
run-website-emulator: build-website-frontend
cd gcp/website && $(install-cmd) && DATASTORE_EMULATOR_PORT=5002 $(run-cmd) python frontend_emulator.py

run-go-website: build-website-frontend
cd go && GOOGLE_CLOUD_PROJECT=oss-vdb OSV_VULNERABILITIES_BUCKET=osv-vulnerabilities go run ./cmd/website -static-dir ../website/dist -docs-dir ../docs

run-go-website-staging: build-website-frontend
cd go && GOOGLE_CLOUD_PROJECT=oss-vdb-test OSV_VULNERABILITIES_BUCKET=osv-test-vulnerabilities go run ./cmd/website -static-dir ../website/dist -docs-dir ../docs

run-go-website-emulator: build-website-frontend
cd go && DATASTORE_EMULATOR_HOST=localhost:5002 go run ./cmd/website -static-dir ../website/dist -docs-dir ../docs

stage-website-assets: build-website-frontend
mkdir -p go/cmd/website/dist go/cmd/website/docs
cp -r website/dist/* go/cmd/website/dist/
cp docs/osv_service_v1.swagger.json go/cmd/website/docs/

run-go-website-prod: stage-website-assets
cd go && GOOGLE_CLOUD_PROJECT=oss-vdb OSV_VULNERABILITIES_BUCKET=osv-vulnerabilities go run -tags embedstatic ./cmd/website



# Run with `make run-api-server ARGS=--no-backend` to launch esp without backend.
# Run the Go developer server orchestrator (launches both ESPv2 and the Go API server).
# Run with `make run-api-server ARGS=--no-backend` to launch esp without backend.
Expand Down
2 changes: 2 additions & 0 deletions go/cmd/website/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
dist/
docs/
106 changes: 106 additions & 0 deletions go/cmd/website/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Package main implements the entry point for the OSV website server in Go.
package main

import (
"context"
"errors"
"flag"
"fmt"
"log/slog"
"net/http"
"os"
"os/signal"
"strconv"
"syscall"
"time"

"github.com/google/osv.dev/go/internal/website"
"github.com/google/osv.dev/go/logger"
)

func main() {
if err := run(); err != nil {
os.Exit(1)
}
}

func run() error {
logger.InitGlobalLogger()
defer logger.Close()

ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()

defaultPort := 8000
if portStr := os.Getenv("PORT"); portStr != "" {
if p, err := strconv.Atoi(portStr); err == nil {
defaultPort = p
} else {
logger.ErrorContext(ctx, "Invalid PORT environment variable, using default", slog.Any("error", err))
}
}

port := flag.Int("port", defaultPort, "port for the website server")
staticDir := flag.String("static-dir", "dist", "directory containing static assets")
docsDir := flag.String("docs-dir", "docs", "directory containing API docs")
flag.Parse()

staticFiles, err := getStaticFS(*staticDir)
if err != nil {
logger.ErrorContext(ctx, "Failed to load static filesystem", slog.String("dir", *staticDir), slog.Any("error", err))

return fmt.Errorf("failed to load static filesystem %q: %w", *staticDir, err)
}

docsFiles, err := getDocsFS(*docsDir)
if err != nil {
logger.ErrorContext(ctx, "Failed to load docs filesystem", slog.String("dir", *docsDir), slog.Any("error", err))

return fmt.Errorf("failed to load docs filesystem %q: %w", *docsDir, err)
}

srv, err := website.NewServer(website.Config{
StaticFS: staticFiles,
DocsFS: docsFiles,
})
if err != nil {
logger.ErrorContext(ctx, "Failed to create website server", slog.Any("error", err))

return fmt.Errorf("failed to create website server: %w", err)
}

httpServer := &http.Server{
Addr: fmt.Sprintf(":%d", *port),
Handler: srv,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}

serverErrors := make(chan error, 1)
go func() {
url := fmt.Sprintf("http://localhost:%d", *port)
logger.InfoContext(ctx, "Starting OSV website server at "+url, slog.Int("port", *port), slog.String("url", url))
if err := httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
serverErrors <- err
}
}()

select {
case err := <-serverErrors:
logger.ErrorContext(ctx, "Server error", slog.Any("error", err))
return err
case <-ctx.Done():
shutdownCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 10*time.Second)
defer cancel()

logger.InfoContext(shutdownCtx, "Shutting down website server...")
if err := httpServer.Shutdown(shutdownCtx); err != nil {
logger.ErrorContext(shutdownCtx, "Error during server shutdown", slog.Any("error", err))

return err
}
}

return nil
}
16 changes: 16 additions & 0 deletions go/cmd/website/static_dev.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//go:build !embedstatic

package main

import (
"io/fs"
"os"
)

func getStaticFS(dir string) (fs.FS, error) {
return os.DirFS(dir), nil
}

func getDocsFS(dir string) (fs.FS, error) {
return os.DirFS(dir), nil
}
22 changes: 22 additions & 0 deletions go/cmd/website/static_embed.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//go:build embedstatic

package main

import (
"embed"
"io/fs"
)

//go:embed dist/*
var embeddedDist embed.FS

//go:embed docs/*
var embeddedDocs embed.FS

func getStaticFS(_ string) (fs.FS, error) {
return fs.Sub(embeddedDist, "dist")
}

func getDocsFS(_ string) (fs.FS, error) {
return fs.Sub(embeddedDocs, "docs")
}
34 changes: 34 additions & 0 deletions go/internal/website/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package website

import (
"fmt"
"net/http"
)

// handleLogin handles initiating Google OAuth authentication login flow.
// Query parameters:
// - redirect: optional post-login redirect destination
func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
redirectURI := r.URL.Query().Get("redirect")

// TODO: Redirect user to Google OAuth authorization endpoint
http.Error(w, fmt.Sprintf("OAuth login handler stub (redirect=%q)", redirectURI), http.StatusNotImplemented)
}

// handleAuthCallback handles processing OAuth callback tokens.
// Query parameters:
// - code: authorization code from OAuth provider
// - state: CSRF protection state token
func (s *Server) handleAuthCallback(w http.ResponseWriter, r *http.Request) {
code := r.URL.Query().Get("code")
state := r.URL.Query().Get("state")

// TODO: Exchange code for session token and store in cookie
http.Error(w, fmt.Sprintf("OAuth callback handler stub (code=%q, state=%q)", code, state), http.StatusNotImplemented)
}

// handleLogout handles logging out user and clearing authentication session cookies.
func (s *Server) handleLogout(w http.ResponseWriter, _ *http.Request) {
// TODO: Clear authentication cookie and redirect user
http.Error(w, "OAuth logout handler stub", http.StatusNotImplemented)
}
45 changes: 45 additions & 0 deletions go/internal/website/blog.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package website

import (
"fmt"
"net/http"
)

// handleBlogIndex handles serving the blog landing page /blog/.
func (s *Server) handleBlogIndex(w http.ResponseWriter, _ *http.Request) {
// TODO: Load blog index content from StaticFS (static/blog/index.html) and render blog template
http.Error(w, "Blog index handler stub", http.StatusNotImplemented)
}

// handleBlogRSS handles serving the blog RSS feed /blog/index.xml.
func (s *Server) handleBlogRSS(w http.ResponseWriter, _ *http.Request) {
// TODO: Serve static/blog/index.xml from StaticFS
http.Error(w, "Blog RSS handler stub", http.StatusNotImplemented)
}

// handleBlogPost handles serving individual blog posts /blog/posts/{blog_name}/.
func (s *Server) handleBlogPost(w http.ResponseWriter, r *http.Request) {
blogName := r.PathValue("blog_name")
if blogName == "" {
http.NotFound(w, r)

return
}

// TODO: Validate blog_name, load static/blog/posts/{blog_name}/index.html and render post template
http.Error(w, fmt.Sprintf("Blog post handler stub (blogName=%q)", blogName), http.StatusNotImplemented)
}

// handleBlogPostFile handles serving static assets inside blog post directories /blog/posts/{blog_name}/{file_name}.
func (s *Server) handleBlogPostFile(w http.ResponseWriter, r *http.Request) {
blogName := r.PathValue("blog_name")
fileName := r.PathValue("file_name")
if blogName == "" || fileName == "" {
http.NotFound(w, r)

return
}

// TODO: Serve static/blog/posts/{blog_name}/{file_name} from StaticFS
http.Error(w, fmt.Sprintf("Blog post asset handler stub (blogName=%q, fileName=%q)", blogName, fileName), http.StatusNotImplemented)
}
33 changes: 33 additions & 0 deletions go/internal/website/linter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package website

import (
"fmt"
"net/http"
)

// handleLinterPage handles serving the linter findings UI page.
func (s *Server) handleLinterPage(w http.ResponseWriter, _ *http.Request) {
// TODO: Serve linter UI page / template
http.Error(w, "Linter UI page stub", http.StatusNotImplemented)
}

// handleLinterSources handles listing sources that have linter findings from GCS.
func (s *Server) handleLinterSources(w http.ResponseWriter, _ *http.Request) {
// TODO: List prefixes in GCS bucket osv-test-public-import-logs under linter-result/
w.Header().Set("Content-Type", "application/json")
http.Error(w, `{"error": "Linter sources handler stub"}`, http.StatusNotImplemented)
}

// handleLinterFindings handles fetching linter findings JSON for a specific source from GCS.
func (s *Server) handleLinterFindings(w http.ResponseWriter, r *http.Request) {
source := r.PathValue("source")
if source == "" {
http.NotFound(w, r)

return
}

// TODO: Download linter-result/<source>/result.json from GCS bucket
w.Header().Set("Content-Type", "application/json")
http.Error(w, fmt.Sprintf(`{"error": "Linter findings handler stub", "source": %q}`, source), http.StatusNotImplemented)
}
20 changes: 20 additions & 0 deletions go/internal/website/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package website

import (
"fmt"
"net/http"
)

// handleList handles rendering the vulnerability listing page and backing paginated list queries.
// Query parameters:
// - q: search query string
// - ecosystem: ecosystem filter
// - page: page number
func (s *Server) handleList(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query().Get("q")
ecosystem := r.URL.Query().Get("ecosystem")
page := r.URL.Query().Get("page")

// TODO: Query ListedVulnerability entities from Datastore and render page / JSON
http.Error(w, fmt.Sprintf("Vulnerability list handler stub (q=%q, ecosystem=%q, page=%q)", q, ecosystem, page), http.StatusNotImplemented)
}
19 changes: 19 additions & 0 deletions go/internal/website/search.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package website

import (
"fmt"
"net/http"
)

// handleSearchSuggestions handles auto-complete search suggestions querying Datastore models.
// Query parameters:
// - q: search query string / prefix
// - ecosystem: optional ecosystem filter
func (s *Server) handleSearchSuggestions(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query().Get("q")
ecosystem := r.URL.Query().Get("ecosystem")

// TODO: Perform prefix / search matching on Datastore vulnerability indices
w.Header().Set("Content-Type", "application/json")
http.Error(w, fmt.Sprintf(`{"error": "Search suggestions handler stub", "q": %q, "ecosystem": %q}`, q, ecosystem), http.StatusNotImplemented)
}
Loading
Loading