102 lines
3.2 KiB
Go
102 lines
3.2 KiB
Go
package openplatform
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type StandaloneConfig struct {
|
|
StaticDir string
|
|
SessionTTL time.Duration
|
|
RequestTimeout time.Duration
|
|
Release string
|
|
TDengine *sql.DB
|
|
TDengineDatabase string
|
|
}
|
|
|
|
func NewStandaloneServer(db *sql.DB, cfg StandaloneConfig) http.Handler {
|
|
repository := NewMySQLRepository(db)
|
|
if cfg.TDengine != nil {
|
|
repository.WithTDengine(cfg.TDengine, cfg.TDengineDatabase)
|
|
}
|
|
service := NewService(repository)
|
|
portal := NewPortalService(db, cfg.SessionTTL)
|
|
external := NewExternalHandler(service, portal)
|
|
|
|
api := http.NewServeMux()
|
|
api.HandleFunc("GET /healthz", func(w http.ResponseWriter, _ *http.Request) {
|
|
writeStandaloneJSON(w, http.StatusOK, map[string]any{
|
|
"status": "ok",
|
|
"service": "open-platform-api",
|
|
"release": strings.TrimSpace(cfg.Release),
|
|
})
|
|
})
|
|
api.Handle("/", external)
|
|
|
|
handler := standaloneStatic(cfg.StaticDir, api)
|
|
handler = WithDocs(handler)
|
|
handler = standaloneSecurityHeaders(handler)
|
|
timeout := cfg.RequestTimeout
|
|
if timeout <= 0 {
|
|
timeout = 10 * time.Second
|
|
}
|
|
return http.TimeoutHandler(handler, timeout, `{"error":{"code":"REQUEST_TIMEOUT","message":"请求处理超时"}}`)
|
|
}
|
|
|
|
func standaloneStatic(dir string, fallback http.Handler) http.Handler {
|
|
dir = strings.TrimSpace(dir)
|
|
if dir == "" {
|
|
return fallback
|
|
}
|
|
files := http.FileServer(http.Dir(dir))
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if isStandaloneAPIRoute(r.URL.Path) {
|
|
fallback.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
clean := filepath.Clean("/" + r.URL.Path)
|
|
path := filepath.Join(dir, clean)
|
|
if info, err := os.Stat(path); err == nil && !info.IsDir() {
|
|
if clean == "/index.html" {
|
|
w.Header().Set("Cache-Control", "no-cache")
|
|
} else if strings.HasPrefix(clean, "/assets/") {
|
|
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
|
|
}
|
|
files.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
w.Header().Set("Cache-Control", "no-cache")
|
|
http.ServeFile(w, r, filepath.Join(dir, "index.html"))
|
|
})
|
|
}
|
|
|
|
func isStandaloneAPIRoute(path string) bool {
|
|
return path == "/healthz" ||
|
|
strings.HasPrefix(path, "/api/") ||
|
|
strings.HasPrefix(path, "/portal-api/") ||
|
|
strings.HasPrefix(path, "/open-api/")
|
|
}
|
|
|
|
func standaloneSecurityHeaders(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("X-Content-Type-Options", "nosniff")
|
|
w.Header().Set("X-Frame-Options", "DENY")
|
|
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
|
|
w.Header().Set("Permissions-Policy", "camera=(), microphone=(), geolocation=()")
|
|
w.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'self'; style-src 'self' https://fonts.googleapis.com; font-src https://fonts.gstatic.com; img-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'")
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
func writeStandaloneJSON(w http.ResponseWriter, status int, body any) {
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
w.Header().Set("Cache-Control", "no-store")
|
|
w.WriteHeader(status)
|
|
_ = json.NewEncoder(w).Encode(body)
|
|
}
|