220 lines
5.3 KiB
Go
220 lines
5.3 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"html/template"
|
|
"net"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/go-chi/cors"
|
|
"github.com/pkg/errors"
|
|
"gitlab.com/wpetit/goweb/api"
|
|
"gitlab.com/wpetit/goweb/logger"
|
|
|
|
_ "embed"
|
|
)
|
|
|
|
var (
|
|
//go:embed templates/idle.html.gotmpl
|
|
rawIdleTemplate []byte
|
|
idleTemplate *template.Template
|
|
)
|
|
|
|
func init() {
|
|
tmpl, err := template.New("").Parse(string(rawIdleTemplate))
|
|
if err != nil {
|
|
panic(errors.Wrap(err, "could not parse idle template"))
|
|
}
|
|
|
|
idleTemplate = tmpl
|
|
}
|
|
|
|
func (s *Server) startHTTPServer(ctx context.Context) error {
|
|
router := chi.NewRouter()
|
|
|
|
if s.webApps {
|
|
ips, err := getLANIPv4Addrs()
|
|
if err != nil {
|
|
return errors.WithStack(err)
|
|
}
|
|
|
|
allowedOrigins := make([]string, len(ips))
|
|
for idx, ip := range ips {
|
|
allowedOrigins[idx] = fmt.Sprintf("http://%s:%d", ip, s.port)
|
|
}
|
|
|
|
router.Use(cors.Handler(cors.Options{
|
|
AllowedOrigins: allowedOrigins,
|
|
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
|
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type"},
|
|
AllowCredentials: false,
|
|
}))
|
|
}
|
|
|
|
router.Get("/", s.handleHome)
|
|
router.Post("/api/v1/cast", s.handleCast)
|
|
router.Delete("/api/v1/cast", s.handleReset)
|
|
router.Get("/api/v1/status", s.handleStatus)
|
|
|
|
if s.webApps {
|
|
router.Get("/apps", s.handleDefaultApp)
|
|
router.Get("/api/v1/apps", s.handleApps)
|
|
router.Handle("/apps/*", http.FileServer(http.FS(appsFS)))
|
|
}
|
|
|
|
server := http.Server{
|
|
Addr: s.address,
|
|
Handler: router,
|
|
}
|
|
|
|
listener, err := net.Listen("tcp", s.address)
|
|
if err != nil {
|
|
return errors.WithStack(err)
|
|
}
|
|
|
|
host, rawPort, err := net.SplitHostPort(listener.Addr().String())
|
|
if err != nil {
|
|
return errors.WithStack(err)
|
|
}
|
|
|
|
port, err := strconv.ParseInt(rawPort, 10, 32)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "could not parse listening port '%v'", rawPort)
|
|
}
|
|
|
|
logger.Debug(ctx, "listening for tcp connections", logger.F("port", port), logger.F("host", host))
|
|
|
|
s.port = int(port)
|
|
|
|
go func() {
|
|
logger.Debug(ctx, "starting http server")
|
|
if err := server.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
|
logger.Error(ctx, "could not listen", logger.CapturedE(errors.WithStack(err)))
|
|
}
|
|
}()
|
|
|
|
go func() {
|
|
<-ctx.Done()
|
|
logger.Debug(ctx, "closing http server")
|
|
if err := server.Close(); err != nil {
|
|
logger.Error(ctx, "could not close http server", logger.CapturedE(errors.WithStack(err)))
|
|
}
|
|
}()
|
|
|
|
if err := s.resetBrowser(); err != nil {
|
|
return errors.WithStack(err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type CastRequest struct {
|
|
URL string `json:"url" validate:"required"`
|
|
}
|
|
|
|
func (s *Server) handleCast(w http.ResponseWriter, r *http.Request) {
|
|
req := &CastRequest{}
|
|
if ok := api.Bind(w, r, req); !ok {
|
|
return
|
|
}
|
|
|
|
if err := s.browser.Load(req.URL); err != nil {
|
|
logger.Error(r.Context(), "could not load url", logger.CapturedE(errors.WithStack(err)))
|
|
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
|
|
|
|
return
|
|
}
|
|
|
|
http.Redirect(w, r, "/api/v1/status", http.StatusSeeOther)
|
|
}
|
|
|
|
func (s *Server) handleReset(w http.ResponseWriter, r *http.Request) {
|
|
if err := s.resetBrowser(); err != nil {
|
|
logger.Error(r.Context(), "could not unload url", logger.CapturedE(errors.WithStack(err)))
|
|
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
|
|
|
|
return
|
|
}
|
|
|
|
http.Redirect(w, r, "/api/v1/status", http.StatusSeeOther)
|
|
}
|
|
|
|
func (s *Server) resetBrowser() error {
|
|
idleURL := fmt.Sprintf("http://localhost:%d", s.port)
|
|
if err := s.browser.Reset(idleURL); err != nil {
|
|
return errors.WithStack(err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type StatusResponse struct {
|
|
ID string `json:"id"`
|
|
URL string `json:"url"`
|
|
Status string `json:"status"`
|
|
Title string `json:"title"`
|
|
}
|
|
|
|
func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) {
|
|
url, err := s.browser.URL()
|
|
if err != nil {
|
|
logger.Error(r.Context(), "could not retrieve browser url", logger.CapturedE(errors.WithStack(err)))
|
|
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
|
|
|
|
return
|
|
}
|
|
|
|
status, err := s.browser.Status()
|
|
if err != nil {
|
|
logger.Error(r.Context(), "could not retrieve browser status", logger.CapturedE(errors.WithStack(err)))
|
|
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
|
|
|
|
return
|
|
}
|
|
|
|
title, err := s.browser.Title()
|
|
if err != nil {
|
|
logger.Error(r.Context(), "could not retrieve browser page title", logger.CapturedE(errors.WithStack(err)))
|
|
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
|
|
|
|
return
|
|
}
|
|
|
|
api.DataResponse(w, http.StatusOK, &StatusResponse{
|
|
ID: s.instanceID,
|
|
URL: url,
|
|
Status: status.String(),
|
|
Title: title,
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleHome(w http.ResponseWriter, r *http.Request) {
|
|
type templateData struct {
|
|
IPs []string
|
|
Port int
|
|
ID string
|
|
WebApps bool
|
|
}
|
|
|
|
ips, err := getLANIPv4Addrs()
|
|
if err != nil {
|
|
logger.Error(r.Context(), "could not retrieve lan ip addresses", logger.CapturedE(errors.WithStack(err)))
|
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
d := templateData{
|
|
ID: s.instanceID,
|
|
IPs: ips,
|
|
Port: s.port,
|
|
WebApps: s.webApps,
|
|
}
|
|
|
|
if err := idleTemplate.Execute(w, d); err != nil {
|
|
logger.Error(r.Context(), "could not render idle page", logger.CapturedE(errors.WithStack(err)))
|
|
}
|
|
}
|