Skip to content

Commit

Permalink
Add base URL support (#1104)
Browse files Browse the repository at this point in the history
* wip - apps functional + martin's changes to paths.js and index.html

* wip: Respect base url when running tour

* wip: Fix file upload; move all paths to ui.ts

* wip: Merge file server and store; point /f to /f/

* wip: Fix file upload and browser download (download() / unload() broken)

* wip: Fix browser-upload, py-download, py-unload

* wip: Make test.py usable via pytest

* wip: Point socket and proxy to _s/ _p/; add test for proxy

* wip: Fix public/private dir hosting; add tests

* wip: Fix cache routing

* wip: Expose cache on Site and AsyncSite; add caching test

* wip: Fix multipart image handling

* fix: point static assets without baseURL to /static, not //static

* Add -proxy, -public-dir to make pytest happy

* Revert changes to index.tsx

* Publish new version to NPM

* Bump h2o-wave package version
  • Loading branch information
lo5 authored Nov 5, 2021
1 parent 77653ac commit 41a43ba
Show file tree
Hide file tree
Showing 24 changed files with 439 additions and 448 deletions.
3 changes: 1 addition & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ setup-ts: ## Set up NPM package and symlinks
cd ts && npm ci && npm run build
cd ts && npm link
cd ui && npm link h2o-wave
cd u && npm link h2o-wave

.PHONY: build
build: build-ui build-server ## Build everything
Expand Down Expand Up @@ -86,7 +85,7 @@ build-docker:
.

run: ## Run server
go run cmd/wave/main.go -web-dir ./ui/build -debug -editable
go run cmd/wave/main.go -web-dir ./ui/build -debug -editable -proxy -public-dir /assets/@./assets

run-db: ## Run database server
go run cmd/wavedb/main.go
Expand Down
6 changes: 4 additions & 2 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,11 @@ type Client struct {
routes []string // watched routes
data chan []byte // send data
editable bool // allow editing? // TODO move to user; tie to role
baseURL string
}

func newClient(addr string, auth *Auth, session *Session, broker *Broker, conn *websocket.Conn, editable bool) *Client {
return &Client{uuid.New().String(), auth, addr, session, broker, conn, nil, make(chan []byte, 256), editable}
func newClient(addr string, auth *Auth, session *Session, broker *Broker, conn *websocket.Conn, editable bool, baseURL string) *Client {
return &Client{uuid.New().String(), auth, addr, session, broker, conn, nil, make(chan []byte, 256), editable, baseURL}
}

func (c *Client) refreshToken() error {
Expand Down Expand Up @@ -110,6 +111,7 @@ func (c *Client) listen() {
}

m := parseMsg(msg)
m.addr = resolveURL(m.addr, c.baseURL)
switch m.t {
case patchMsgT:
if c.editable { // allow only if editing is enabled
Expand Down
1 change: 1 addition & 0 deletions cmd/wave/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ func main() {

flag.BoolVar(&version, "version", false, "print version and exit")
stringVar(&conf.Listen, "listen", ":10101", "listen on this address")
stringVar(&conf.BaseURL, "base-url", "/", "the base URL (path prefix) to be used for resolving relative URLs (e.g. /foo/ or /foo/bar/, without the host)")
stringVar(&conf.WebDir, "web-dir", "./www", "directory to serve web assets from, hosted at /")
stringVar(&conf.DataDir, "data-dir", "./data", "directory to store site data")
stringsVar(&conf.PublicDirs, "public-dir", "additional directory to serve files from, in the format \"[url-path]@[filesystem-path]\", e.g. \"/public/files/@/some/local/path\" will host /some/local/path/foo.txt at /public/files/foo.txt; multiple directory mappings allowed")
Expand Down
1 change: 1 addition & 0 deletions conf.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ type ServerConf struct {
Version string
BuildDate string
Listen string
BaseURL string
WebDir string
DataDir string
PublicDirs Strings
Expand Down
90 changes: 88 additions & 2 deletions file_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,17 @@
package wave

import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"path"
"path/filepath"
"strings"

"github.com/google/uuid"
"github.com/h2oai/wave/pkg/keychain"
)

Expand All @@ -31,21 +35,28 @@ type FileServer struct {
keychain *keychain.Keychain
auth *Auth
handler http.Handler
baseURL string
}

func newFileServer(dir string, keychain *keychain.Keychain, auth *Auth) http.Handler {
func newFileServer(dir string, keychain *keychain.Keychain, auth *Auth, baseURL string) http.Handler {
return &FileServer{
dir,
keychain,
auth,
http.FileServer(http.Dir(dir)),
baseURL,
}
}

var (
errInvalidUnloadPath = errors.New("invalid file path")
)

// UploadResponse represents a response to a file upload operation.
type UploadResponse struct {
Files []string `json:"files"`
}

func (fs *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
Expand All @@ -64,9 +75,34 @@ func (fs *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}

echo(Log{"t": "file_download", "path": r.URL.Path})
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/_f") // public
r.URL.Path = strings.TrimPrefix(r.URL.Path, fs.baseURL) // public
fs.handler.ServeHTTP(w, r)

case http.MethodPost:
// Disallow if:
// - unauthorized api call
// - auth enabled and unauthorized
if !fs.keychain.Allow(r) && (fs.auth != nil && !fs.auth.allow(r)) { // API or UI
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}

files, err := fs.acceptFiles(r)
if err != nil {
echo(Log{"t": "file_upload", "error": err.Error()})
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}

res, err := json.Marshal(UploadResponse{Files: files})
if err != nil {
echo(Log{"t": "file_upload", "error": err.Error()})
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(res)

case http.MethodDelete:
// TODO garbage collection

Expand All @@ -86,6 +122,56 @@ func (fs *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
}

func (fs *FileServer) acceptFiles(r *http.Request) ([]string, error) {
if err := r.ParseMultipartForm(32 << 20); err != nil { // 32 MB
return nil, fmt.Errorf("failed parsing upload form from request: %v", err)
}

form := r.MultipartForm
files, ok := form.File["files"]
if !ok {
return nil, errors.New("want 'files' field in upload form, got none")
}

uploadPaths := make([]string, len(files))
for i, file := range files {

id, err := uuid.NewRandom()
if err != nil {
return nil, fmt.Errorf("failed generating file id: %v", err)
}

src, err := file.Open()
if err != nil {
return nil, fmt.Errorf("failed opening uploaded file: %v", err)
}
defer src.Close()

fileID := id.String()
uploadDir := filepath.Join(fs.dir, fileID)

if err := os.MkdirAll(uploadDir, 0700); err != nil {
return nil, fmt.Errorf("failed creating upload dir %s: %v", uploadDir, err)
}

basename := filepath.Base(file.Filename)
uploadPath := filepath.Join(uploadDir, basename)

dst, err := os.Create(uploadPath)
if err != nil {
return nil, fmt.Errorf("failed writing uploaded file %s: %v", uploadPath, err)
}
defer dst.Close()

if _, err = io.Copy(dst, src); err != nil {
return nil, fmt.Errorf("failed copying uploaded file %s: %v", uploadPath, err)
}

uploadPaths[i] = path.Join(fs.baseURL, fileID, basename)
}
return uploadPaths, nil
}

func (fs *FileServer) deleteFile(url string) error {
tokens := strings.Split(path.Clean(url), "/")
if len(tokens) != 4 { // /_f/uuid/file.ext
Expand Down
127 changes: 0 additions & 127 deletions file_store.go

This file was deleted.

3 changes: 2 additions & 1 deletion ide/src/ide.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ interface ProxyResult {
}

async function proxy(req: ProxyRequest): Promise<ProxyResult> {
const res = await fetch('/_p', { method: 'POST', body: JSON.stringify(req) })
// TODO: prefix baseURL
const res = await fetch('/_p/', { method: 'POST', body: JSON.stringify(req) })
return await res.json()
}

Expand Down
5 changes: 1 addition & 4 deletions proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,7 @@ func newProxy(auth *Auth, maxRequestSize, maxResponseSize int64) *Proxy {
func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPost:
// Disallow if:
// - unauthorized api call
// - auth not enabled or auth enabled and unauthorized
if p.auth == nil || (p.auth != nil && !p.auth.allow(r)) {
if p.auth != nil && !p.auth.allow(r) {
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
Expand Down
Loading

0 comments on commit 41a43ba

Please sign in to comment.