Fix SB_URL_PREFIX handling, added SB_LOG_PUSH to ask clients to push
client logs to the server
This commit is contained in:
+77
-15
@@ -4,9 +4,11 @@ import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log"
|
||||
"math"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
@@ -27,15 +29,14 @@ func addAuthEndpoints(r chi.Router, config *ServerConfig) {
|
||||
// Logout
|
||||
r.Get("/.logout", func(w http.ResponseWriter, r *http.Request) {
|
||||
host := extractHost(r)
|
||||
spaceConfig := spaceConfigFromContext(r.Context())
|
||||
cookieOptions := CookieOptions{
|
||||
Path: fmt.Sprintf("%s/", spaceConfig.HostURLPrefix),
|
||||
Path: fmt.Sprintf("%s/", config.HostURLPrefix),
|
||||
}
|
||||
|
||||
deleteCookie(w, authCookieName(host), cookieOptions)
|
||||
deleteCookie(w, "refreshLogin", cookieOptions)
|
||||
|
||||
http.Redirect(w, r, applyURLPrefix("/.auth", spaceConfig.HostURLPrefix), http.StatusFound)
|
||||
http.Redirect(w, r, applyURLPrefix("/.auth", config.HostURLPrefix), http.StatusFound)
|
||||
})
|
||||
|
||||
// Auth page
|
||||
@@ -58,7 +59,7 @@ func addAuthEndpoints(r chi.Router, config *ServerConfig) {
|
||||
HostPrefix string
|
||||
SpaceName string
|
||||
}{
|
||||
HostPrefix: spaceConfig.HostURLPrefix,
|
||||
HostPrefix: config.HostURLPrefix,
|
||||
SpaceName: spaceConfig.SpaceName,
|
||||
}
|
||||
|
||||
@@ -88,13 +89,13 @@ func addAuthEndpoints(r chi.Router, config *ServerConfig) {
|
||||
from := r.FormValue("from")
|
||||
|
||||
if username == "" || password == "" {
|
||||
http.Redirect(w, r, applyURLPrefix("/.auth?error=0", spaceConfig.HostURLPrefix), http.StatusFound)
|
||||
http.Redirect(w, r, applyURLPrefix("/.auth?error=0", config.HostURLPrefix), http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
if spaceConfig.LockoutTimer.IsLocked() {
|
||||
log.Println("Authentication locked out, redirecting to auth page.")
|
||||
http.Redirect(w, r, applyURLPrefix("/.auth?error=2", spaceConfig.HostURLPrefix), http.StatusFound)
|
||||
http.Redirect(w, r, applyURLPrefix("/.auth?error=2", config.HostURLPrefix), http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -122,7 +123,7 @@ func addAuthEndpoints(r chi.Router, config *ServerConfig) {
|
||||
inAWeek := time.Now().Add(time.Duration(authenticationExpirySeconds) * time.Second)
|
||||
|
||||
cookieOptions := CookieOptions{
|
||||
Path: fmt.Sprintf("%s/", spaceConfig.HostURLPrefix),
|
||||
Path: fmt.Sprintf("%s/", config.HostURLPrefix),
|
||||
Expires: inAWeek,
|
||||
}
|
||||
|
||||
@@ -137,12 +138,12 @@ func addAuthEndpoints(r chi.Router, config *ServerConfig) {
|
||||
redirectPath = from
|
||||
}
|
||||
|
||||
http.Redirect(w, r, applyURLPrefix(redirectPath, spaceConfig.HostURLPrefix), http.StatusFound)
|
||||
http.Redirect(w, r, applyURLPrefix(redirectPath, config.HostURLPrefix), http.StatusFound)
|
||||
} else {
|
||||
log.Println("Authentication failed, redirecting to auth page.")
|
||||
spaceConfig.LockoutTimer.AddCount()
|
||||
|
||||
http.Redirect(w, r, applyURLPrefix("/.auth?error=1", spaceConfig.HostURLPrefix), http.StatusFound)
|
||||
http.Redirect(w, r, applyURLPrefix("/.auth?error=1", config.HostURLPrefix), http.StatusFound)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -178,7 +179,7 @@ func authMiddleware(config *ServerConfig) func(http.Handler) http.Handler {
|
||||
return
|
||||
}
|
||||
|
||||
path := removeURLPrefix(r.URL.Path, spaceConfig.HostURLPrefix)
|
||||
path := removeURLPrefix(r.URL.Path, config.HostURLPrefix)
|
||||
host := extractHost(r)
|
||||
|
||||
if isExcludedPath(path) {
|
||||
@@ -211,7 +212,7 @@ func authMiddleware(config *ServerConfig) func(http.Handler) http.Handler {
|
||||
|
||||
if authCookie == "" {
|
||||
log.Printf("Unauthorized access to %s, redirecting to auth page", path)
|
||||
redirectToAuth(w, "/.auth", path, spaceConfig.HostURLPrefix)
|
||||
redirectToAuth(w, "/.auth", path, config.HostURLPrefix)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -219,14 +220,14 @@ func authMiddleware(config *ServerConfig) func(http.Handler) http.Handler {
|
||||
claims, err := spaceConfig.JwtIssuer.VerifyAndDecodeJWT(authCookie)
|
||||
if err != nil {
|
||||
log.Printf("Error verifying JWT on %s, redirecting to auth page: %v\n", path, err)
|
||||
redirectToAuth(w, "/.auth", path, spaceConfig.HostURLPrefix)
|
||||
redirectToAuth(w, "/.auth", path, config.HostURLPrefix)
|
||||
return
|
||||
}
|
||||
|
||||
username, ok := claims["username"].(string)
|
||||
if !ok || username != spaceConfig.Auth.User {
|
||||
log.Printf("Username mismatch in JWT on %s", path)
|
||||
redirectToAuth(w, "/.auth", path, spaceConfig.HostURLPrefix)
|
||||
redirectToAuth(w, "/.auth", path, config.HostURLPrefix)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -239,13 +240,12 @@ func authMiddleware(config *ServerConfig) func(http.Handler) http.Handler {
|
||||
// refreshLogin refreshes the login cookie if needed
|
||||
func refreshLogin(w http.ResponseWriter, r *http.Request, config *ServerConfig, host string) {
|
||||
if getCookie(r, "refreshLogin") != "" {
|
||||
spaceConfig := spaceConfigFromContext(r.Context())
|
||||
inAWeek := time.Now().Add(time.Duration(authenticationExpirySeconds) * time.Second)
|
||||
jwt := getCookie(r, authCookieName(host))
|
||||
|
||||
if jwt != "" {
|
||||
cookieOptions := CookieOptions{
|
||||
Path: fmt.Sprintf("%s/", spaceConfig.HostURLPrefix),
|
||||
Path: fmt.Sprintf("%s/", config.HostURLPrefix),
|
||||
Expires: inAWeek,
|
||||
}
|
||||
|
||||
@@ -254,3 +254,65 @@ func refreshLogin(w http.ResponseWriter, r *http.Request, config *ServerConfig,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// LockoutTimer implements a simple rate limiter to prevent brute force attacks
|
||||
type LockoutTimer struct {
|
||||
mutex sync.Mutex
|
||||
bucketTime int64
|
||||
bucketCount int
|
||||
bucketSize int64 // duration in milliseconds
|
||||
limit int
|
||||
disabled bool
|
||||
}
|
||||
|
||||
// NewLockoutTimer creates a new lockout timer
|
||||
// countPeriodMs: time window in milliseconds
|
||||
// limit: maximum attempts allowed in the time window
|
||||
func NewLockoutTimer(countPeriodMs int, limit int) *LockoutTimer {
|
||||
disabled := math.IsNaN(float64(countPeriodMs)) || math.IsNaN(float64(limit)) ||
|
||||
countPeriodMs < 1 || limit < 1
|
||||
|
||||
return &LockoutTimer{
|
||||
bucketSize: int64(countPeriodMs),
|
||||
limit: limit,
|
||||
disabled: disabled,
|
||||
}
|
||||
}
|
||||
|
||||
// updateBucketTime updates the current bucket time and resets count if needed
|
||||
func (lt *LockoutTimer) updateBucketTime() {
|
||||
currentBucketTime := time.Now().UnixMilli() / lt.bucketSize
|
||||
if lt.bucketTime == currentBucketTime {
|
||||
return
|
||||
}
|
||||
// the bucket is too old - empty it
|
||||
lt.bucketTime = currentBucketTime
|
||||
lt.bucketCount = 0
|
||||
}
|
||||
|
||||
// IsLocked checks if the timer is currently locked due to too many attempts
|
||||
func (lt *LockoutTimer) IsLocked() bool {
|
||||
if lt.disabled {
|
||||
return false
|
||||
}
|
||||
|
||||
lt.mutex.Lock()
|
||||
defer lt.mutex.Unlock()
|
||||
|
||||
lt.updateBucketTime()
|
||||
return lt.bucketCount >= lt.limit
|
||||
}
|
||||
|
||||
// AddCount increments the attempt counter
|
||||
// IsLocked() should be called first to keep bucketTime current
|
||||
func (lt *LockoutTimer) AddCount() {
|
||||
if lt.disabled {
|
||||
return
|
||||
}
|
||||
|
||||
lt.mutex.Lock()
|
||||
defer lt.mutex.Unlock()
|
||||
|
||||
// updateBucketTime should have been called by IsLocked first
|
||||
lt.bucketCount++
|
||||
}
|
||||
|
||||
@@ -53,6 +53,11 @@ func buildConfig(bundledFiles fs.FS, args []string) *server.ServerConfig {
|
||||
rootSpaceConfig.SpaceFolderPath = os.Getenv("SB_FOLDER")
|
||||
}
|
||||
|
||||
if os.Getenv("SB_LOG_PUSH") != "" {
|
||||
rootSpaceConfig.LogPush = true
|
||||
log.Println("Client log push enabled")
|
||||
}
|
||||
|
||||
if rootSpaceConfig.SpaceFolderPath == "" {
|
||||
log.Fatal("No folder specified. Please pass a folder as an argument or set SB_FOLDER environment variable.")
|
||||
}
|
||||
@@ -126,7 +131,7 @@ func buildConfig(bundledFiles fs.FS, args []string) *server.ServerConfig {
|
||||
|
||||
if hostUrlPrefix != "" {
|
||||
log.Printf("Host URL Prefix: %s", hostUrlPrefix)
|
||||
rootSpaceConfig.HostURLPrefix = hostUrlPrefix
|
||||
serverConfig.HostURLPrefix = hostUrlPrefix
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,7 +146,6 @@ func buildConfig(bundledFiles fs.FS, args []string) *server.ServerConfig {
|
||||
serverConfig.ClientBundle = server.NewReadOnlyFallthroughSpacePrimitives(bundledFiles, "dist_client_bundle", bundlePathDate, nil)
|
||||
rootSpaceConfig.SpacePrimitives = server.NewReadOnlyFallthroughSpacePrimitives(bundledFiles, "dist_base_fs_bundle", bundlePathDate, spacePrimitives)
|
||||
|
||||
log.Printf("Starting SilverBullet binding to %s:%d", serverConfig.BindHost, serverConfig.Port)
|
||||
if serverConfig.BindHost == "127.0.0.1" {
|
||||
log.Println("SilverBullet will only be available locally, to allow outside connections, pass -L0.0.0.0 as a flag, and put a TLS terminator on top.")
|
||||
}
|
||||
@@ -173,7 +177,7 @@ func ServerCommand(bundledFiles fs.FS) *cobra.Command {
|
||||
Args: cobra.MinimumNArgs(0),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
serverConfig := buildConfig(bundledFiles, args)
|
||||
if port != 0 {
|
||||
if port != 3000 {
|
||||
serverConfig.Port = port
|
||||
}
|
||||
if hostname != "" {
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"math"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// LockoutTimer implements a simple rate limiter to prevent brute force attacks
|
||||
type LockoutTimer struct {
|
||||
mutex sync.Mutex
|
||||
bucketTime int64
|
||||
bucketCount int
|
||||
bucketSize int64 // duration in milliseconds
|
||||
limit int
|
||||
disabled bool
|
||||
}
|
||||
|
||||
// NewLockoutTimer creates a new lockout timer
|
||||
// countPeriodMs: time window in milliseconds
|
||||
// limit: maximum attempts allowed in the time window
|
||||
func NewLockoutTimer(countPeriodMs int, limit int) *LockoutTimer {
|
||||
disabled := math.IsNaN(float64(countPeriodMs)) || math.IsNaN(float64(limit)) ||
|
||||
countPeriodMs < 1 || limit < 1
|
||||
|
||||
return &LockoutTimer{
|
||||
bucketSize: int64(countPeriodMs),
|
||||
limit: limit,
|
||||
disabled: disabled,
|
||||
}
|
||||
}
|
||||
|
||||
// updateBucketTime updates the current bucket time and resets count if needed
|
||||
func (lt *LockoutTimer) updateBucketTime() {
|
||||
currentBucketTime := time.Now().UnixMilli() / lt.bucketSize
|
||||
if lt.bucketTime == currentBucketTime {
|
||||
return
|
||||
}
|
||||
// the bucket is too old - empty it
|
||||
lt.bucketTime = currentBucketTime
|
||||
lt.bucketCount = 0
|
||||
}
|
||||
|
||||
// IsLocked checks if the timer is currently locked due to too many attempts
|
||||
func (lt *LockoutTimer) IsLocked() bool {
|
||||
if lt.disabled {
|
||||
return false
|
||||
}
|
||||
|
||||
lt.mutex.Lock()
|
||||
defer lt.mutex.Unlock()
|
||||
|
||||
lt.updateBucketTime()
|
||||
return lt.bucketCount >= lt.limit
|
||||
}
|
||||
|
||||
// AddCount increments the attempt counter
|
||||
// IsLocked() should be called first to keep bucketTime current
|
||||
func (lt *LockoutTimer) AddCount() {
|
||||
if lt.disabled {
|
||||
return
|
||||
}
|
||||
|
||||
lt.mutex.Lock()
|
||||
defer lt.mutex.Unlock()
|
||||
|
||||
// updateBucketTime should have been called by IsLocked first
|
||||
lt.bucketCount++
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/render"
|
||||
)
|
||||
|
||||
// ManifestIcon represents an icon in the PWA manifest
|
||||
type ManifestIcon struct {
|
||||
Src string `json:"src"`
|
||||
Type string `json:"type"`
|
||||
Sizes string `json:"sizes"`
|
||||
}
|
||||
|
||||
// Manifest represents the structure of a PWA manifest.json file
|
||||
type Manifest struct {
|
||||
ShortName string `json:"short_name"`
|
||||
Name string `json:"name"`
|
||||
Icons []ManifestIcon `json:"icons"`
|
||||
CaptureLinks string `json:"capture_links"`
|
||||
StartURL string `json:"start_url"`
|
||||
Display string `json:"display"`
|
||||
DisplayOverride []string `json:"display_override"`
|
||||
Scope string `json:"scope"`
|
||||
ThemeColor string `json:"theme_color"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
// Generate PWA manifest.json with dynamic values
|
||||
func manifestHandler(config *ServerConfig) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
spaceConfig := spaceConfigFromContext(r.Context())
|
||||
manifest := Manifest{
|
||||
ShortName: spaceConfig.SpaceName,
|
||||
Name: spaceConfig.SpaceName,
|
||||
Icons: []ManifestIcon{
|
||||
{
|
||||
Src: "/.client/logo-dock.png",
|
||||
Type: "image/png",
|
||||
Sizes: "512x512",
|
||||
},
|
||||
},
|
||||
CaptureLinks: "new-client",
|
||||
StartURL: "/#boot",
|
||||
Display: "standalone",
|
||||
DisplayOverride: []string{"window-controls-overlay"},
|
||||
Scope: "/",
|
||||
ThemeColor: "#e1e1e1",
|
||||
Description: spaceConfig.SpaceDescription,
|
||||
}
|
||||
|
||||
if spaceConfig.HostURLPrefix != "" {
|
||||
manifest.Icons[0].Src = spaceConfig.HostURLPrefix + manifest.Icons[0].Src
|
||||
manifest.StartURL = spaceConfig.HostURLPrefix + manifest.StartURL
|
||||
manifest.Scope = spaceConfig.HostURLPrefix + manifest.Scope
|
||||
}
|
||||
|
||||
render.JSON(w, r, manifest)
|
||||
}
|
||||
}
|
||||
+74
-76
@@ -16,81 +16,79 @@ func init() {
|
||||
localhostRegex = regexp.MustCompile(`^(localhost|127\.0\.0\.1|\d+\.\d+\.\d+\.\d+)`)
|
||||
}
|
||||
|
||||
func proxyHandler(config *ServerConfig) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
spaceConfig := spaceConfigFromContext(r.Context())
|
||||
if spaceConfig.ReadOnlyMode {
|
||||
http.Error(w, "Read only mode, no proxy allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Extract the URI from the path
|
||||
path := chi.URLParam(r, "*")
|
||||
if path == "" {
|
||||
http.Error(w, "No URI provided", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Add query parameters if they exist
|
||||
if r.URL.RawQuery != "" {
|
||||
path += "?" + r.URL.RawQuery
|
||||
}
|
||||
|
||||
// Determine protocol based on hostname
|
||||
var targetURL string
|
||||
if localhostRegex.MatchString(path) {
|
||||
targetURL = "http://" + path
|
||||
} else {
|
||||
targetURL = "https://" + path
|
||||
}
|
||||
|
||||
log.Printf("Proxying to %s\n", targetURL)
|
||||
|
||||
// Create the proxy request
|
||||
req, err := http.NewRequest(r.Method, targetURL, r.Body)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Copy safe headers
|
||||
safeHeaders := []string{"Authorization", "Accept", "Content-Type"}
|
||||
for _, header := range safeHeaders {
|
||||
if value := r.Header.Get(header); value != "" {
|
||||
req.Header.Set(header, value)
|
||||
}
|
||||
}
|
||||
|
||||
// Copy headers starting with X-Proxy-Header-, removing the prefix
|
||||
for key, values := range r.Header {
|
||||
if strings.HasPrefix(strings.ToLower(key), "x-proxy-header-") {
|
||||
headerName := key[len("x-proxy-header-"):]
|
||||
for _, value := range values {
|
||||
req.Header.Set(headerName, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Make the request
|
||||
client := http.DefaultClient
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
log.Printf("Error fetching proxied URL: %v\n", err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Copy response headers
|
||||
for key, values := range resp.Header {
|
||||
for _, value := range values {
|
||||
w.Header().Set(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
// Set status code and copy body
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
io.Copy(w, resp.Body)
|
||||
func proxyHandler(w http.ResponseWriter, r *http.Request) {
|
||||
spaceConfig := spaceConfigFromContext(r.Context())
|
||||
if spaceConfig.ReadOnlyMode {
|
||||
http.Error(w, "Read only mode, no proxy allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Extract the URI from the path
|
||||
path := chi.URLParam(r, "*")
|
||||
if path == "" {
|
||||
http.Error(w, "No URI provided", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Add query parameters if they exist
|
||||
if r.URL.RawQuery != "" {
|
||||
path += "?" + r.URL.RawQuery
|
||||
}
|
||||
|
||||
// Determine protocol based on hostname
|
||||
var targetURL string
|
||||
if localhostRegex.MatchString(path) {
|
||||
targetURL = "http://" + path
|
||||
} else {
|
||||
targetURL = "https://" + path
|
||||
}
|
||||
|
||||
log.Printf("Proxying to %s\n", targetURL)
|
||||
|
||||
// Create the proxy request
|
||||
req, err := http.NewRequest(r.Method, targetURL, r.Body)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Copy safe headers
|
||||
safeHeaders := []string{"Authorization", "Accept", "Content-Type"}
|
||||
for _, header := range safeHeaders {
|
||||
if value := r.Header.Get(header); value != "" {
|
||||
req.Header.Set(header, value)
|
||||
}
|
||||
}
|
||||
|
||||
// Copy headers starting with X-Proxy-Header-, removing the prefix
|
||||
for key, values := range r.Header {
|
||||
if strings.HasPrefix(strings.ToLower(key), "x-proxy-header-") {
|
||||
headerName := key[len("x-proxy-header-"):]
|
||||
for _, value := range values {
|
||||
req.Header.Set(headerName, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Make the request
|
||||
client := http.DefaultClient
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
log.Printf("Error fetching proxied URL: %v\n", err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Copy response headers
|
||||
for key, values := range resp.Header {
|
||||
for _, value := range values {
|
||||
w.Header().Set(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
// Set status code and copy body
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
io.Copy(w, resp.Body)
|
||||
}
|
||||
|
||||
+24
-7
@@ -17,6 +17,9 @@ type BootConfig struct {
|
||||
SpaceFolderPath string `json:"spaceFolderPath"`
|
||||
IndexPage string `json:"indexPage"`
|
||||
ReadOnly bool `json:"readOnly"`
|
||||
|
||||
// Whether or not the client should push logs to the server
|
||||
LogPush bool `json:"logPush"`
|
||||
}
|
||||
|
||||
func RunServer(config *ServerConfig) error {
|
||||
@@ -26,6 +29,8 @@ func RunServer(config *ServerConfig) error {
|
||||
r.Use(middleware.Logger)
|
||||
}
|
||||
|
||||
r.Use(middleware.RealIP)
|
||||
|
||||
// Expose space primitives and path to the request
|
||||
r.Use(spaceMiddleware(config))
|
||||
|
||||
@@ -35,22 +40,25 @@ func RunServer(config *ServerConfig) error {
|
||||
// Authentication endpoints (must come before auth middleware)
|
||||
addAuthEndpoints(r, config)
|
||||
|
||||
r.Get("/.ping", func(w http.ResponseWriter, r *http.Request) {
|
||||
routes := chi.NewRouter()
|
||||
|
||||
routes.Get("/.ping", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("OK"))
|
||||
})
|
||||
|
||||
// Mount filesystem routes under /.fs
|
||||
r.Mount("/.fs", buildFsRoutes())
|
||||
routes.Mount("/.fs", buildFsRoutes())
|
||||
|
||||
// Config endpoint
|
||||
r.Get("/.config", func(w http.ResponseWriter, r *http.Request) {
|
||||
routes.Get("/.config", func(w http.ResponseWriter, r *http.Request) {
|
||||
spaceConfig := spaceConfigFromContext(r.Context())
|
||||
clientConfig := &BootConfig{
|
||||
SpaceFolderPath: spaceConfig.SpaceFolderPath,
|
||||
IndexPage: spaceConfig.IndexPage,
|
||||
ReadOnly: spaceConfig.ReadOnlyMode,
|
||||
LogPush: spaceConfig.LogPush,
|
||||
}
|
||||
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
@@ -58,15 +66,18 @@ func RunServer(config *ServerConfig) error {
|
||||
})
|
||||
|
||||
// Shell endpoint
|
||||
r.Post("/.shell", handleShellEndpoint(config))
|
||||
routes.Post("/.shell", handleShellEndpoint)
|
||||
|
||||
// Log collection endpoint
|
||||
routes.Post("/.logs", handleLogsEndpoint)
|
||||
|
||||
// Proxy endpoint
|
||||
r.HandleFunc("/.proxy/*", proxyHandler(config))
|
||||
routes.HandleFunc("/.proxy/*", proxyHandler)
|
||||
|
||||
// Manifest endpoint
|
||||
r.HandleFunc("/.client/manifest.json", manifestHandler(config))
|
||||
routes.HandleFunc("/.client/manifest.json", manifestHandler)
|
||||
|
||||
r.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) {
|
||||
routes.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) {
|
||||
path := chi.URLParam(r, "*")
|
||||
|
||||
spaceConfig := spaceConfigFromContext(r.Context())
|
||||
@@ -90,6 +101,12 @@ func RunServer(config *ServerConfig) error {
|
||||
ServerSideRender(config, spaceConfig, path, w, r)
|
||||
})
|
||||
|
||||
if config.HostURLPrefix == "" {
|
||||
r.Mount("/", routes)
|
||||
} else {
|
||||
r.Mount(config.HostURLPrefix, routes)
|
||||
}
|
||||
|
||||
// Display the final server running message
|
||||
visibleHostname := config.BindHost
|
||||
if config.BindHost == "127.0.0.1" {
|
||||
|
||||
+21
-23
@@ -7,30 +7,28 @@ import (
|
||||
)
|
||||
|
||||
// handleShellEndpoint handles POST requests to /.shell for executing shell commands
|
||||
func handleShellEndpoint(config *ServerConfig) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
spaceConfig := spaceConfigFromContext(r.Context())
|
||||
// Parse the request body
|
||||
var shellRequest ShellRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&shellRequest); err != nil {
|
||||
http.Error(w, "Invalid JSON request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
func handleShellEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
spaceConfig := spaceConfigFromContext(r.Context())
|
||||
// Parse the request body
|
||||
var shellRequest ShellRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&shellRequest); err != nil {
|
||||
http.Error(w, "Invalid JSON request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Execute the shell command
|
||||
shellResponse, err := spaceConfig.ShellBackend.Handle(shellRequest)
|
||||
if err != nil {
|
||||
log.Printf("Shell error: %v\n", err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// Execute the shell command
|
||||
shellResponse, err := spaceConfig.ShellBackend.Handle(shellRequest)
|
||||
if err != nil {
|
||||
log.Printf("Shell error: %v\n", err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Return the response as JSON
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(shellResponse); err != nil {
|
||||
log.Printf("Error encoding shell response: %v\n", err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// Return the response as JSON
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(shellResponse); err != nil {
|
||||
log.Printf("Error encoding shell response: %v\n", err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
+8
-1
@@ -6,12 +6,15 @@ import (
|
||||
)
|
||||
|
||||
const spaceConfigKey = "spaceConfig"
|
||||
const serverConfigKey = "serverConfig"
|
||||
|
||||
func spaceMiddleware(config *ServerConfig) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
spaceConfig := config.SpaceConfigResolver(r)
|
||||
augmentedRequest := r.WithContext(context.WithValue(r.Context(), spaceConfigKey, spaceConfig))
|
||||
ctx := context.WithValue(r.Context(), spaceConfigKey, spaceConfig)
|
||||
ctx = context.WithValue(ctx, serverConfigKey, config)
|
||||
augmentedRequest := r.WithContext(ctx)
|
||||
next.ServeHTTP(w, augmentedRequest)
|
||||
})
|
||||
}
|
||||
@@ -20,3 +23,7 @@ func spaceMiddleware(config *ServerConfig) func(http.Handler) http.Handler {
|
||||
func spaceConfigFromContext(ctx context.Context) *SpaceConfig {
|
||||
return ctx.Value(spaceConfigKey).(*SpaceConfig)
|
||||
}
|
||||
|
||||
func serverConfigFromContext(ctx context.Context) *ServerConfig {
|
||||
return ctx.Value(serverConfigKey).(*ServerConfig)
|
||||
}
|
||||
|
||||
+1
-1
@@ -69,7 +69,7 @@ func ServerSideRender(config *ServerConfig, spaceConfig *SpaceConfig, pageName s
|
||||
Content string
|
||||
}{
|
||||
Title: "SilverBullet",
|
||||
HostPrefix: spaceConfig.HostURLPrefix,
|
||||
HostPrefix: config.HostURLPrefix,
|
||||
}
|
||||
|
||||
if spaceConfig.ReadOnlyMode && spaceConfig.Auth == nil {
|
||||
|
||||
+5
-3
@@ -11,14 +11,15 @@ type ServerConfig struct {
|
||||
BindHost string
|
||||
Port int
|
||||
EnableHTTPLogging bool
|
||||
// TODO: Ideally this is configurable per space, but kinda hard
|
||||
HostURLPrefix string
|
||||
|
||||
ClientBundle SpacePrimitives
|
||||
}
|
||||
|
||||
type SpaceConfig struct {
|
||||
Hostname string
|
||||
HostURLPrefix string
|
||||
Auth *AuthOptions
|
||||
Hostname string
|
||||
Auth *AuthOptions
|
||||
|
||||
SpacePrimitives SpacePrimitives
|
||||
|
||||
@@ -26,6 +27,7 @@ type SpaceConfig struct {
|
||||
IndexPage string
|
||||
GitIgnore string
|
||||
ReadOnlyMode bool
|
||||
LogPush bool
|
||||
|
||||
// Used for the PWA manifest and login page
|
||||
SpaceName string
|
||||
|
||||
Reference in New Issue
Block a user