Feature(Go): Embed and serve web UI assets
This commit is contained in:
119
server/go/web/server.go
Normal file
119
server/go/web/server.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/yuanyuanxiang/SimpleRemoter/server/go/logger"
|
||||
)
|
||||
|
||||
// Server serves the web remote desktop UI: the embedded index.html, xterm.js
|
||||
// static assets, and the PWA manifest. WebSocket signaling, device list and
|
||||
// screen streaming will be wired up in later phases.
|
||||
type Server struct {
|
||||
port int
|
||||
log *logger.Logger
|
||||
srv *http.Server
|
||||
}
|
||||
|
||||
// New creates an HTTP server bound to the given port. port=0 disables the server.
|
||||
func New(port int, log *logger.Logger) *Server {
|
||||
return &Server{port: port, log: log}
|
||||
}
|
||||
|
||||
// Start launches the server in a goroutine and returns immediately.
|
||||
// If port is 0, returns nil without starting anything.
|
||||
func (s *Server) Start() error {
|
||||
if s.port == 0 {
|
||||
s.log.Info("HTTP server disabled (-http-port=0)")
|
||||
return nil
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/", s.handleIndex)
|
||||
mux.HandleFunc("/health", s.handleHealth)
|
||||
mux.HandleFunc("/manifest.json", s.handleManifest)
|
||||
mux.HandleFunc("/static/xterm.js", staticHandler(xtermJS, "application/javascript; charset=utf-8"))
|
||||
mux.HandleFunc("/static/xterm.css", staticHandler(xtermCSS, "text/css; charset=utf-8"))
|
||||
mux.HandleFunc("/static/xterm-fit.js", staticHandler(xtermFitJS, "application/javascript; charset=utf-8"))
|
||||
|
||||
s.srv = &http.Server{
|
||||
Addr: ":" + strconv.Itoa(s.port),
|
||||
Handler: mux,
|
||||
ReadHeaderTimeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
// Bind synchronously so port-in-use / permission errors propagate to the
|
||||
// caller instead of being lost inside the goroutine after a misleading
|
||||
// "started" log line.
|
||||
ln, err := net.Listen("tcp", s.srv.Addr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("listen on :%d: %w", s.port, err)
|
||||
}
|
||||
s.log.Info("HTTP server started on :%d", s.port)
|
||||
go func() {
|
||||
if err := s.srv.Serve(ln); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
s.log.Error("HTTP server stopped: %v", err)
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop gracefully shuts the server down.
|
||||
func (s *Server) Stop() {
|
||||
if s.srv == nil {
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
_ = s.srv.Shutdown(ctx)
|
||||
}
|
||||
|
||||
func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/" && r.URL.Path != "/index.html" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
_, _ = w.Write(IndexHTML)
|
||||
}
|
||||
|
||||
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"status":"ok"}`))
|
||||
}
|
||||
|
||||
// PWA manifest. Referenced by <link rel="manifest"> in index.html.
|
||||
// Static JSON, no template needed.
|
||||
const manifestJSON = `{
|
||||
"name": "SimpleRemoter",
|
||||
"short_name": "Remoter",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"orientation": "any",
|
||||
"background_color": "#1a1a2e",
|
||||
"theme_color": "#1a1a2e"
|
||||
}`
|
||||
|
||||
func (s *Server) handleManifest(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/manifest+json")
|
||||
w.Header().Set("Cache-Control", "public, max-age=86400")
|
||||
_, _ = w.Write([]byte(manifestJSON))
|
||||
}
|
||||
|
||||
// staticHandler returns an http.HandlerFunc that serves a fixed byte slice
|
||||
// with the given content-type. Used for embedded third-party assets (xterm.js etc.)
|
||||
// that change infrequently — 1-day browser cache is fine.
|
||||
func staticHandler(body []byte, contentType string) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
w.Header().Set("Cache-Control", "public, max-age=86400")
|
||||
_, _ = w.Write(body)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user