861 lines
27 KiB
Go
861 lines
27 KiB
Go
package server
|
|
|
|
import (
|
|
`crypto/tls`
|
|
`encoding/json`
|
|
`encoding/xml`
|
|
"errors"
|
|
"fmt"
|
|
`mime/multipart`
|
|
"net"
|
|
"net/http"
|
|
`net/http/fcgi`
|
|
"net/netip"
|
|
"net/url"
|
|
"os"
|
|
"os/signal"
|
|
"strings"
|
|
"sync"
|
|
`syscall`
|
|
|
|
sysd "github.com/coreos/go-systemd/daemon"
|
|
"github.com/davecgh/go-spew/spew"
|
|
`github.com/goccy/go-yaml`
|
|
"r00t2.io/goutils/multierr"
|
|
)
|
|
|
|
// Close cleanly closes any remnants of a Server. Stop should be used instead to cleanly shut down; this is a little more aggressive.
|
|
func (s *Server) Close() (err error) {
|
|
|
|
s.log.Debug("server.Server.Close: Closing sockets.")
|
|
|
|
if err = s.cleanup(false); err != nil {
|
|
s.log.Err("server.Server.Close: Received error closing sockets: %v", err)
|
|
}
|
|
|
|
s.log.Debug("server.Server.Close: Sockets closed.")
|
|
|
|
return
|
|
}
|
|
|
|
/*
|
|
Run starts and runs the server. This process blocks and will shutdown on a systemd notify signal or kill signal.
|
|
Non-HTML requests will be of type R00tInfo serialized to the requested MIME type.
|
|
*/
|
|
func (s *Server) Run() (err error) {
|
|
|
|
var wg sync.WaitGroup
|
|
var errChan chan error
|
|
var mErr *multierr.MultiError = multierr.NewMultiError(nil)
|
|
var numJobs int = 2 // sigs, listener
|
|
|
|
s.log.Debug("server.Server.Run: Starting server.")
|
|
|
|
signal.Notify(s.reloadChan, reloadSigs...)
|
|
signal.Notify(s.stopChan, stopSigs...)
|
|
s.doneChan = make(chan bool, 1)
|
|
|
|
errChan = make(chan error, numJobs)
|
|
wg.Add(numJobs)
|
|
|
|
// sigs
|
|
go func() {
|
|
var sigErr error
|
|
var sig os.Signal
|
|
var smErr *multierr.MultiError = multierr.NewMultiError(nil)
|
|
|
|
defer wg.Done()
|
|
|
|
sigtrap:
|
|
for !s.isStopping {
|
|
if s.isStopping {
|
|
break sigtrap
|
|
}
|
|
sig = <-s.reloadChan
|
|
s.log.Debug("server.Server.Run: Recived signal %v (%#v): %v", sig, sig, sig.String())
|
|
switch sig {
|
|
case syscall.SIGHUP:
|
|
s.log.Debug("server.Server.Run: Recived reload signal.")
|
|
if s.isStopping {
|
|
s.log.Debug("server.Server.Run: Server is stopping; abandoning reload.")
|
|
if sigErr = s.Stop(); sigErr != nil {
|
|
s.log.Err("server.Server.Run: Received error while stopping the server: %v", sigErr)
|
|
sigErr = nil
|
|
}
|
|
} else {
|
|
if sigErr = s.Reload(); sigErr != nil {
|
|
s.log.Err("server.Server.Run: Received error while reloading the server: %v", sigErr)
|
|
smErr.AddError(sigErr)
|
|
sigErr = nil
|
|
}
|
|
break sigtrap
|
|
}
|
|
default:
|
|
// Stop signal.
|
|
s.log.Debug("server.Server.Run: Recived stop signal.")
|
|
if sigErr = s.Stop(); sigErr != nil {
|
|
s.log.Err("server.Server.Run: Received error while stopping the server: %v", sigErr)
|
|
smErr.AddError(sigErr)
|
|
sigErr = nil
|
|
}
|
|
}
|
|
}
|
|
|
|
if !smErr.IsEmpty() {
|
|
errChan <- smErr
|
|
return
|
|
}
|
|
}()
|
|
|
|
// listener
|
|
go func() {
|
|
var lErr error
|
|
|
|
defer wg.Done()
|
|
|
|
if isSystemd {
|
|
var supported bool
|
|
|
|
// https://www.freedesktop.org/software/systemd/man/sd_notify.html
|
|
if supported, lErr = sysd.SdNotify(false, sysd.SdNotifyReady); lErr != nil {
|
|
s.log.Err(
|
|
"server.Server.Run: Error encountered when notifying systemd of changestate to READY (supported: %v): %v",
|
|
supported, lErr,
|
|
)
|
|
err = nil
|
|
}
|
|
}
|
|
|
|
switch s.listenUri.Scheme {
|
|
case "unix", "tcp":
|
|
if lErr = fcgi.Serve(s.sock, s.mux); lErr != nil {
|
|
if errors.Is(lErr, net.ErrClosed) {
|
|
lErr = nil
|
|
} else {
|
|
errChan <- lErr
|
|
}
|
|
return
|
|
}
|
|
case "http":
|
|
if lErr = http.Serve(s.sock, s.mux); lErr != nil {
|
|
if errors.Is(lErr, http.ErrServerClosed) || errors.Is(lErr, net.ErrClosed) {
|
|
lErr = nil
|
|
} else {
|
|
errChan <- lErr
|
|
}
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
go func() {
|
|
wg.Wait()
|
|
close(errChan)
|
|
s.doneChan <- true
|
|
}()
|
|
|
|
<-s.doneChan
|
|
|
|
for i := 0; i < numJobs; i++ {
|
|
if err = <-errChan; err != nil {
|
|
mErr.AddError(err)
|
|
err = nil
|
|
}
|
|
}
|
|
|
|
if !mErr.IsEmpty() {
|
|
err = mErr
|
|
return
|
|
}
|
|
|
|
s.log.Debug("server.Server.Run: Server shut down.")
|
|
|
|
return
|
|
}
|
|
|
|
// Stop stops the server.
|
|
func (s *Server) Stop() (err error) {
|
|
|
|
s.log.Debug("server.Server.Stop: Stopping server.")
|
|
|
|
s.isStopping = true
|
|
|
|
if isSystemd {
|
|
// https://www.freedesktop.org/software/systemd/man/sd_notify.html
|
|
if _, err = sysd.SdNotify(false, sysd.SdNotifyStopping); err != nil {
|
|
s.log.Err("server.Server.stop: Received error notifying systemd of stop: %v", err)
|
|
err = nil
|
|
}
|
|
}
|
|
|
|
if err = s.Close(); err != nil {
|
|
s.log.Err("server.Server.stop: Received error closing server connections: %v", err)
|
|
err = nil
|
|
}
|
|
|
|
s.log.Debug("server.Server.Stop: Server stopped.")
|
|
|
|
return
|
|
}
|
|
|
|
// cleanup cleans up remaining sockets, closes channels, etc.
|
|
func (s *Server) cleanup(init bool) (err error) {
|
|
|
|
var mErr *multierr.MultiError = multierr.NewMultiError(nil)
|
|
|
|
s.log.Debug("server.Server.cleanup: Cleaning up sockets, etc.")
|
|
|
|
if s.sock != nil && !init {
|
|
if err = s.sock.Close(); err != nil {
|
|
s.log.Err("server.Server.cleanup: Received error closing socket: %v", err)
|
|
mErr.AddError(err)
|
|
err = nil
|
|
}
|
|
}
|
|
if s.listenUri.Scheme == "unix" {
|
|
if err = os.Remove(s.listenUri.Path); err != nil {
|
|
if !errors.Is(err, os.ErrNotExist) {
|
|
s.log.Err("server.Server.cleanup: Failed to remove UDS '%s': %v", s.listenUri.Path, err)
|
|
mErr.AddError(err)
|
|
}
|
|
err = nil
|
|
}
|
|
}
|
|
|
|
if !mErr.IsEmpty() {
|
|
err = mErr
|
|
return
|
|
}
|
|
|
|
s.log.Debug("server.Server.cleanup: Completed cleanup.")
|
|
|
|
return
|
|
}
|
|
|
|
func (s *Server) Reload() (err error) {
|
|
|
|
s.log.Debug("server.Server.Reload: Reload called, but nothing was done; this is a placeholder as there are no reload-associated operations assigned.")
|
|
if isSystemd {
|
|
// https://www.freedesktop.org/software/systemd/man/sd_notify.html
|
|
if _, err = sysd.SdNotify(false, sysd.SdNotifyReloading); err != nil {
|
|
s.log.Err("server.Server.Reload: Received error notifying systemd of reload: %v", err)
|
|
return
|
|
}
|
|
}
|
|
|
|
// TODO?
|
|
|
|
if isSystemd {
|
|
var supported bool
|
|
|
|
// https://www.freedesktop.org/software/systemd/man/sd_notify.html
|
|
if supported, err = sysd.SdNotify(false, sysd.SdNotifyReady); err != nil {
|
|
s.log.Err(
|
|
"server.Server.Reload: Error encountered when notifying systemd of changestate to READY (supported: %v): %v",
|
|
supported, err,
|
|
)
|
|
err = nil
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (s *Server) explicit404(resp http.ResponseWriter, req *http.Request) {
|
|
resp.WriteHeader(http.StatusNotFound)
|
|
}
|
|
|
|
func (s *Server) handleDefault(resp http.ResponseWriter, req *http.Request) {
|
|
|
|
var vals url.Values
|
|
var uaVals []string
|
|
var doInclude bool
|
|
var doIndent bool
|
|
var err error
|
|
var ok bool
|
|
var b []byte
|
|
var remAddrPort string
|
|
var okMedia []string
|
|
var nAP netip.AddrPort
|
|
var parsedFmts []*parsedMIME
|
|
var renderPage *Page = new(Page)
|
|
var format string = mediaJSON
|
|
var indent string = " "
|
|
var client *R00tInfo = new(R00tInfo)
|
|
|
|
renderPage.RawIndent = " "
|
|
renderPage.PageType = "index"
|
|
|
|
s.log.Debug("server.Server.handleDefault: Handling request:\n%s", spew.Sdump(req))
|
|
|
|
/*
|
|
if req.URL != nil &&
|
|
req.URL.Path != "" &&
|
|
req.URL.Path != "/" &&
|
|
req.URL.Path != "/index" &&
|
|
req.URL.Path != "/index.html" {
|
|
resp.WriteHeader(http.StatusNotFound)
|
|
}
|
|
*/
|
|
|
|
client.Req = req
|
|
remAddrPort = req.RemoteAddr
|
|
if s.isHttp && req.Header.Get(httpRealHdr) != "" {
|
|
remAddrPort = req.Header.Get(httpRealHdr)
|
|
req.Header.Del(httpRealHdr)
|
|
}
|
|
if remAddrPort != "" {
|
|
if nAP, err = netip.ParseAddrPort(remAddrPort); err != nil {
|
|
s.log.Err("server.Server.handleDefault: Failed to parse remote address '%s': %v", req.RemoteAddr, err)
|
|
// Don't return an error in case we're doing weird things like direct socket clients.
|
|
err = nil
|
|
/*
|
|
http.Error(resp, "ERROR: Failed to parse client address", http.StatusInternalServerError)
|
|
return
|
|
*/
|
|
}
|
|
client.IP = net.ParseIP(nAP.Addr().String())
|
|
client.Port = nAP.Port()
|
|
}
|
|
client.Headers = XmlHeaders(req.Header)
|
|
|
|
uaVals = req.Header.Values("User-Agent")
|
|
if uaVals != nil && len(uaVals) > 0 {
|
|
client.Client = make([]*R00tClient, len(uaVals))
|
|
for idx, ua := range uaVals {
|
|
if client.Client[idx], err = NewClient(ua); err != nil {
|
|
s.log.Err("server.Server.handleDefault: Failed to create client for '%s': %v", ua, err)
|
|
http.Error(resp, fmt.Sprintf("ERROR: Failed to parse 'User-Agent' '%s'", ua), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
if client.Client != nil && len(client.Client) > 0 {
|
|
// Check the passed UAs for a browser. We then change the "default" format if so.
|
|
for _, ua := range client.Client {
|
|
if ua.IsMobile || ua.IsDesktop {
|
|
format = mediaHTML
|
|
break
|
|
}
|
|
}
|
|
}
|
|
renderPage.Info = client
|
|
|
|
vals = req.URL.Query()
|
|
|
|
// Determine the format/MIME type of the response.
|
|
if vals.Has("mime") {
|
|
format = req.URL.Query().Get("mime")
|
|
} else {
|
|
if parsedFmts, err = parseAccept(strings.Join(req.Header.Values("Accept"), ",")); err != nil {
|
|
s.log.Err("server.Server.handleDefault: Failed to parse Accept header: %v", err)
|
|
http.Error(
|
|
resp,
|
|
"ERROR: Invalid 'Accept' header value; see RFC 9110 § 12.5.1 and https://www.iana.org/assignments/media-types/media-types.xhtml",
|
|
http.StatusBadRequest,
|
|
)
|
|
return
|
|
}
|
|
if format, err = decideParseAccept(parsedFmts, mediaJSON); err != nil {
|
|
if errors.Is(err, ErrUnsupportedMIME) {
|
|
s.log.Err("server.Server.handleDefault: No supported MIME type found for '%s'.", req.RemoteAddr)
|
|
for mt := range mediaNoIndent {
|
|
okMedia = append(okMedia, mt)
|
|
}
|
|
req.Header.Set("Accept", strings.Join(okMedia, ", "))
|
|
http.Error(resp, "ERROR: No supported MIME type specified; see 'Accept' header in response for valid types.", http.StatusNotAcceptable)
|
|
return
|
|
} else {
|
|
s.log.Err("server.Server.handleDefault: Received unknown error choosing an Accept header for '%s': %v", req.RemoteAddr, err)
|
|
http.Error(resp, "ERROR: Unknown error occurred when negotiationg MIME type.", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
s.log.Debug("server.Server.handleDefault: Using format '%s' for '%s'", format, req.RemoteAddr)
|
|
// If it's HTML and they want an include, that needs to be validated too.
|
|
if format == mediaHTML && vals.Has("include") {
|
|
doInclude = true
|
|
if parsedFmts, err = parseAccept(strings.Join(vals["include"], ", ")); err != nil {
|
|
s.log.Err("server.Server.handleDefault: Failed to parse include parameter: %v", err)
|
|
http.Error(
|
|
resp,
|
|
"ERROR: Invalid 'include' parameter value; see RFC 9110 § 12.5.1 and https://www.iana.org/assignments/media-types/media-types.xhtml",
|
|
http.StatusBadRequest,
|
|
)
|
|
return
|
|
}
|
|
if renderPage.RawFmt, err = decideParseAccept(parsedFmts, format); err != nil {
|
|
if errors.Is(err, ErrUnsupportedMIME) {
|
|
s.log.Err("server.Server.handleDefault: No supported MIME type found for '%#v' 'include'.", vals["include"], req.RemoteAddr)
|
|
for mt := range mediaNoIndent {
|
|
okMedia = append(okMedia, mt)
|
|
}
|
|
req.Header.Set("Accept", strings.Join(okMedia, ", "))
|
|
http.Error(resp, "ERROR: No supported MIME type specified for 'include'; see 'Accept' header in response for valid types.", http.StatusNotAcceptable)
|
|
return
|
|
} else {
|
|
s.log.Err("server.Server.handleDefault: Received unknown error choosing an include format for '%s': %v", req.RemoteAddr, err)
|
|
http.Error(resp, "ERROR: Unknown error occurred when negotiationg MIME type.", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
// The indentation is set below.
|
|
}
|
|
|
|
// Determine indentation (if the format even supports it).
|
|
if format == mediaHTML {
|
|
if doInclude {
|
|
if _, ok = mediaIndent[renderPage.RawFmt]; ok {
|
|
doIndent = vals.Has("indent")
|
|
if doIndent {
|
|
if req.URL.Query().Get("indent") != "" {
|
|
renderPage.RawIndent = req.URL.Query().Get("indent")
|
|
renderPage.DoRawIndent = true
|
|
}
|
|
}
|
|
} else if _, ok = mediaNoIndent[renderPage.RawFmt]; !ok {
|
|
// It's not a supported MIME.
|
|
s.log.Err("server.Server.handleDefault: Requested MIME type '%s' for '%s' unsupported.", renderPage.RawFmt, req.RemoteAddr)
|
|
for mt := range mediaNoIndent {
|
|
okMedia = append(okMedia, mt)
|
|
}
|
|
req.Header.Set("Accept", strings.Join(okMedia, ", "))
|
|
http.Error(
|
|
resp,
|
|
fmt.Sprintf("ERROR: MIME type '%s' unsupported for 'include'; see Accept header in response for valid types.", renderPage.RawFmt),
|
|
http.StatusNotAcceptable,
|
|
)
|
|
return
|
|
} else {
|
|
// This seems backwards, but "non-indented" formats actually need indenting enabled so their whitespace renders properly.
|
|
renderPage.DoRawIndent = true
|
|
}
|
|
}
|
|
} else {
|
|
if _, ok = mediaIndent[format]; ok {
|
|
doIndent = vals.Has("indent")
|
|
if doIndent {
|
|
if req.URL.Query().Get("indent") != "" {
|
|
indent = req.URL.Query().Get("indent")
|
|
}
|
|
}
|
|
} else if _, ok = mediaNoIndent[format]; !ok {
|
|
// It's not a supported MIME.
|
|
s.log.Err("server.Server.handleDefault: Requested MIME type '%s' for '%s' unsupported.", format, req.RemoteAddr)
|
|
for mt := range mediaNoIndent {
|
|
okMedia = append(okMedia, mt)
|
|
}
|
|
req.Header.Set("Accept", strings.Join(okMedia, ", "))
|
|
http.Error(resp, fmt.Sprintf("ERROR: MIME type '%s' unsupported; see Accept header in response for valid types.", format), http.StatusNotAcceptable)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Now render the response.
|
|
if format == mediaHTML {
|
|
// This gets special treatment since it's templated.
|
|
resp.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
if doInclude {
|
|
renderPage.Raw = new(string)
|
|
if doIndent {
|
|
if b, err = mediaIndent[renderPage.RawFmt](client, "", renderPage.RawIndent); err != nil {
|
|
s.log.Err("server.Server.handleDefault: Failed to render indented raw '%s' for '%s': %v", renderPage.RawFmt, req.RemoteAddr, err)
|
|
http.Error(resp, fmt.Sprintf("ERROR: Failed to render 'include' '%s'", renderPage.RawFmt), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
} else {
|
|
if b, err = mediaNoIndent[renderPage.RawFmt](client); err != nil {
|
|
s.log.Err("server.Server.handleDefault: Failed to render raw '%s' for '%s': %v", renderPage.RawFmt, req.RemoteAddr, err)
|
|
http.Error(resp, fmt.Sprintf("ERROR: Failed to render '%s'", renderPage.RawFmt), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
*renderPage.Raw = string(b)
|
|
}
|
|
if err = tpl.ExecuteTemplate(resp, "index", renderPage); err != nil {
|
|
s.log.Err("server.Server.handleDefault: Failed to execute template for '%s': %v", req.RemoteAddr, err)
|
|
http.Error(resp, "ERROR: Failed to render HTML", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
} else {
|
|
resp.Header().Set("Content-Type", format)
|
|
if doIndent {
|
|
// This was already filtered to valid specified MIME above.
|
|
if b, err = mediaIndent[format](client, "", indent); err != nil {
|
|
s.log.Err("server.Server.handleDefault: Failed to render indented '%s' for '%s': %v", format, req.RemoteAddr, err)
|
|
http.Error(resp, fmt.Sprintf("ERROR: Failed to render '%s'", format), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
} else {
|
|
if b, err = mediaNoIndent[format](client); err != nil {
|
|
s.log.Err("server.Server.handleDefault: Failed to render '%s' for '%s': %v", format, req.RemoteAddr, err)
|
|
http.Error(resp, fmt.Sprintf("ERROR: Failed to render '%s'", format), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
if _, err = resp.Write(b); err != nil {
|
|
s.log.Err("server.Server.handleDefault: Failed to serve indented '%s' to '%s': %v", format, req.RemoteAddr, err)
|
|
return
|
|
}
|
|
}
|
|
|
|
s.log.Debug("server.Server.handleDefault: Handled request:\n%s", spew.Sdump(req))
|
|
|
|
return
|
|
}
|
|
|
|
func (s *Server) handleDefaultNew(resp http.ResponseWriter, req *http.Request) {
|
|
|
|
var err error
|
|
var page *Page
|
|
var uas []string
|
|
var reqdMimes []string
|
|
var parsedUA *R00tClient
|
|
var nAP netip.AddrPort
|
|
var remAddrPort string
|
|
var parsedFmts []*parsedMIME
|
|
var renderer outerRenderer
|
|
var includeFmt string
|
|
var params url.Values = make(url.Values)
|
|
var outerFmt string = mediaJSON
|
|
|
|
s.log.Debug("server.Server.handleDefault: Handling request:\n%s", spew.Sdump(req))
|
|
|
|
page = &Page{
|
|
Info: &R00tInfo{
|
|
Client: nil,
|
|
IP: nil,
|
|
Port: 0,
|
|
Headers: XmlHeaders(req.Header),
|
|
Req: req,
|
|
},
|
|
PageType: "index",
|
|
Raw: nil,
|
|
RawFmt: nil,
|
|
Indent: "",
|
|
DoIndent: false,
|
|
}
|
|
|
|
// First the client info.
|
|
remAddrPort = req.RemoteAddr
|
|
if s.isHttp && req.Header.Get(httpRealHdr) != "" {
|
|
// TODO: WHitelist explicit reverse proxy addr(s)?
|
|
remAddrPort = req.Header.Get(httpRealHdr)
|
|
req.Header.Del(httpRealHdr)
|
|
}
|
|
if remAddrPort != "" {
|
|
if nAP, err = netip.ParseAddrPort(remAddrPort); err != nil {
|
|
s.log.Warning("server.Server.handleDefault: Failed to parse remote address '%s': %v", remAddrPort, err)
|
|
// Don't return an error in case we're doing weird things like direct socket clients.
|
|
/*
|
|
http.Error(resp, "ERROR: Failed to parse client address", http.StatusInternalServerError)
|
|
return
|
|
*/
|
|
err = nil
|
|
}
|
|
page.Info.IP = net.ParseIP(nAP.Addr().String())
|
|
page.Info.Port = nAP.Port()
|
|
}
|
|
if req.URL != nil {
|
|
params = req.URL.Query()
|
|
}
|
|
uas = req.Header.Values("User-Agent")
|
|
if uas != nil && len(uas) > 0 {
|
|
page.Info.Client = make([]*R00tClient, 0, len(uas))
|
|
for _, ua := range uas {
|
|
if parsedUA, err = NewClient(ua); err != nil {
|
|
s.log.Err("server.Server.handleDefault: Failed to create client for '%s': %v", ua, err)
|
|
http.Error(resp, fmt.Sprintf("ERROR: Failed to parse 'User-Agent' '%s'", ua), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
page.Info.Client = append(page.Info.Client, parsedUA)
|
|
}
|
|
}
|
|
if page.Info.Client != nil && len(page.Info.Client) > 0 {
|
|
// Check the passed UAs for a browser. We then change the "default" format if so.
|
|
for _, ua := range page.Info.Client {
|
|
if ua.IsMobile || ua.IsDesktop {
|
|
outerFmt = mediaHTML
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
At this point, the outer format *default*, client IP, client port, and client version (UA) is set.
|
|
From here, we handle explicit content requests/overrides.
|
|
*/
|
|
// `Accept` request header...
|
|
reqdMimes = req.Header.Values("Accept")
|
|
if reqdMimes != nil && len(reqdMimes) > 0 {
|
|
if parsedFmts, err = parseAccept(strings.Join(reqdMimes, ",")); err != nil {
|
|
s.log.Err("server.Server.handleDefault: Failed to parse Accept header '%#v' for '%s': %v", reqdMimes, remAddrPort, err)
|
|
resp.Header()["Accept"] = okAcceptMime
|
|
http.Error(
|
|
resp,
|
|
"ERROR: Invalid 'Accept' header value; see RFC 9110 § 12.5.1, https://www.iana.org/assignments/media-types/media-types.xhtml, and this response's 'Accept' header.",
|
|
http.StatusBadRequest,
|
|
)
|
|
return
|
|
}
|
|
if outerFmt, err = decideParseAccept(parsedFmts, outerFmt); err != nil {
|
|
if errors.Is(err, ErrUnsupportedMIME) {
|
|
s.log.Err("server.Server.handleDefault: No supported MIME type found for '%s' via '%#v'.", remAddrPort, reqdMimes)
|
|
req.Header["Accept"] = okAcceptMime
|
|
http.Error(resp, "ERROR: No supported MIME type specified via request 'Accept'; see 'Accept' header in response for valid types.", http.StatusNotAcceptable)
|
|
return
|
|
} else {
|
|
s.log.Err("server.Server.handleDefault: Received unknown error choosing from Accept header for '%s': %v", remAddrPort, err)
|
|
http.Error(resp, "ERROR: Unknown error occurred when negotiating MIME type.", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
// `mime` URL query parameter.
|
|
if params.Has("mime") {
|
|
if parsedFmts, err = parseAccept(strings.Join(params["mime"], ",")); err != nil {
|
|
s.log.Err("server.Server.handleDefault: Failed to parse 'mime' URL parameter '%#v' for '%s': %v", params["mime"], remAddrPort, err)
|
|
resp.Header()["Accept"] = okAcceptMime
|
|
http.Error(
|
|
resp,
|
|
"ERROR: Invalid 'mime' URL parameter value; see RFC 9110 § 12.5.1, https://www.iana.org/assignments/media-types/media-types.xhtml, and this response's 'Accept' header.",
|
|
http.StatusBadRequest,
|
|
)
|
|
return
|
|
}
|
|
if outerFmt, err = decideParseAccept(parsedFmts, outerFmt); err != nil {
|
|
if errors.Is(err, ErrUnsupportedMIME) {
|
|
s.log.Err("server.Server.handleDefault: No supported MIME type found for '%s' via '%#v'.", remAddrPort, params["mime"])
|
|
req.Header["Accept"] = okAcceptMime
|
|
http.Error(resp, "ERROR: No supported MIME type specified via URL parameter 'mime'; see 'Accept' header in response for valid types.", http.StatusNotAcceptable)
|
|
return
|
|
} else {
|
|
s.log.Err("server.Server.handleDefault: Received unknown error choosing from 'mime' URL parameter for '%s': %v", remAddrPort, err)
|
|
http.Error(resp, "ERROR: Unknown error occurred when negotiating MIME type.", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
// 'include' URL query parameter (only for text/html).
|
|
if outerFmt == mediaHTML && params.Has("include") {
|
|
if parsedFmts, err = parseAccept(strings.Join(params["include"], ",")); err != nil {
|
|
s.log.Err("server.Server.handleDefault: Failed to parse 'include' URL parameter '%#v' for '%s': %v", params["include"], remAddrPort, err)
|
|
resp.Header()["Accept"] = okAcceptMime
|
|
http.Error(
|
|
resp,
|
|
"ERROR: Invalid 'include' URL parameter value; see RFC 9110 § 12.5.1, https://www.iana.org/assignments/media-types/media-types.xhtml, and this response's 'Accept' header.",
|
|
http.StatusBadRequest,
|
|
)
|
|
return
|
|
}
|
|
if includeFmt, err = decideParseAccept(parsedFmts, includeFmt); err != nil {
|
|
if errors.Is(err, ErrUnsupportedMIME) {
|
|
s.log.Err("server.Server.handleDefault: No supported MIME type found for '%s' via '%#v'.", remAddrPort, params["include"])
|
|
req.Header["Accept"] = okAcceptMime
|
|
http.Error(resp, "ERROR: No supported MIME type specified via URL parameter 'include'; see 'Accept' header in response for valid types.", http.StatusNotAcceptable)
|
|
return
|
|
} else {
|
|
s.log.Err("server.Server.handleDefault: Received unknown error choosing from 'include' URL parameter for '%s': %v", remAddrPort, err)
|
|
http.Error(resp, "ERROR: Unknown error occurred when negotiating MIME type.", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
if includeFmt != "" {
|
|
page.RawFmt = new(string)
|
|
*page.RawFmt = includeFmt
|
|
}
|
|
}
|
|
// 'indent' URL query parameter.
|
|
if params.Has("indent") {
|
|
page.DoIndent = true
|
|
if params.Get("indent") != "" {
|
|
page.Indent = params.Get("indent")
|
|
} else {
|
|
page.Indent = dfltIndent
|
|
}
|
|
}
|
|
|
|
switch outerFmt {
|
|
case mediaJSON:
|
|
renderer = s.renderJSON
|
|
case mediaHTML:
|
|
renderer = s.renderHTML
|
|
case mediaXML:
|
|
renderer = s.renderXML
|
|
case mediaYAML:
|
|
renderer = s.renderYML
|
|
default:
|
|
s.log.Err("server.Server.handleDefault: Unknown output format '%s'", outerFmt)
|
|
http.Error(resp, "ERROR: Unable to determine default renderer.", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
if err = renderer(page, resp); err != nil {
|
|
s.log.Err("server.Server.handleDefault: Failed to render request from '%s' as '%s': %v", remAddrPort, outerFmt, err)
|
|
// The renderer handles the error-handling with the client.
|
|
return
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (s *Server) handleAbout(resp http.ResponseWriter, req *http.Request) {
|
|
|
|
var err error
|
|
var renderPage *Page = &Page{
|
|
Info: &R00tInfo{
|
|
Req: req,
|
|
},
|
|
PageType: "about",
|
|
}
|
|
|
|
s.log.Debug("server.Server.handleAbout: Handling request:\n%s", spew.Sdump(req))
|
|
|
|
resp.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
|
|
if err = tpl.ExecuteTemplate(resp, "about", renderPage); err != nil {
|
|
s.log.Err("server.Server.handleAbout: Failed to execute template for '%s': %v", req.RemoteAddr, err)
|
|
http.Error(resp, "ERROR: Failed to render HTML", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
s.log.Debug("server.Server.handleAbout: Handled request:\n%s", spew.Sdump(req))
|
|
|
|
return
|
|
}
|
|
|
|
func (s *Server) handleUsage(resp http.ResponseWriter, req *http.Request) {
|
|
|
|
var err error
|
|
var renderPage *Page = &Page{
|
|
Info: &R00tInfo{
|
|
Req: req,
|
|
},
|
|
PageType: "usage",
|
|
}
|
|
|
|
s.log.Debug("server.Server.handleUsage: Handling request:\n%s", spew.Sdump(req))
|
|
|
|
resp.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
if err = tpl.ExecuteTemplate(resp, "usage", renderPage); err != nil {
|
|
s.log.Err("server.Server.handleAbout: Failed to execute template for '%s': %v", req.RemoteAddr, err)
|
|
http.Error(resp, "ERROR: Failed to render HTML", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
s.log.Debug("server.Server.handleUsage: Handled request:\n%s", spew.Sdump(req))
|
|
|
|
return
|
|
}
|
|
|
|
func (s *Server) renderJSON(page *Page, resp http.ResponseWriter) (err error) {
|
|
|
|
var b []byte
|
|
|
|
if page.DoIndent {
|
|
if b, err = json.MarshalIndent(page.Info, "", page.Indent); err != nil {
|
|
s.log.Err("server.Server.renderJSON: Failed to render to indented JSON: %v", err)
|
|
http.Error(resp, "ERROR: Failed to render JSON", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
} else {
|
|
if b, err = json.Marshal(page.Info); err != nil {
|
|
s.log.Err("server.Server.renderJSON: Failed to render to JSON: %v", err)
|
|
http.Error(resp, "ERROR: Failed to render JSON", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
if _, err = resp.Write(b); err != nil {
|
|
s.log.Err("server.Server.renderJSON: Failed to send JSON: %v", err)
|
|
return
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (s *Server) renderHTML(page *Page, resp http.ResponseWriter) (err error) {
|
|
|
|
var b []byte
|
|
|
|
if page.RawFmt != nil {
|
|
switch *page.RawFmt {
|
|
case mediaHTML:
|
|
_ = "" // Explicit no-op; we're *serving* HTML.
|
|
// Indentable
|
|
case mediaJSON, mediaXML:
|
|
if page.DoIndent {
|
|
if b, err = mediaIndent[*page.RawFmt](page.Info, "", page.Indent); err != nil {
|
|
s.log.Err("server.Server.renderHTML: Failed to render to indented include '%s': %v", *page.RawFmt, err)
|
|
http.Error(resp, "ERROR: Failed to render include format", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
} else {
|
|
if b, err = mediaNoIndent[*page.RawFmt](page.Indent); err != nil {
|
|
s.log.Err("server.Server.renderHTML: Failed to render to include '%s': %v", *page.RawFmt, err)
|
|
http.Error(resp, "ERROR: Failed to render include format", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
// Non-indentable
|
|
case mediaYAML:
|
|
if b, err = mediaNoIndent[*page.RawFmt](page.Info); err != nil {
|
|
s.log.Err("server.Server.renderHTML: Failed to render to '%s': %v", *page.RawFmt, err)
|
|
}
|
|
}
|
|
page.Raw = new(string)
|
|
*page.Raw = string(b)
|
|
}
|
|
|
|
if err = tpl.ExecuteTemplate(resp, "index", page); err != nil {
|
|
s.log.Err("server.Server.renderHTML: Failed to render template: %v", err)
|
|
http.Error(resp, "ERROR: Failed to render HTML", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (s *Server) renderXML(page *Page, resp http.ResponseWriter) (err error) {
|
|
|
|
var b []byte
|
|
|
|
if page.DoIndent {
|
|
if b, err = xml.MarshalIndent(page.Info, "", page.Indent); err != nil {
|
|
s.log.Err("server.Server.renderXML: Failed to render to indented XML: %v", err)
|
|
http.Error(resp, "ERROR: Failed to render XML", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
} else {
|
|
if b, err = xml.Marshal(page.Info); err != nil {
|
|
s.log.Err("server.Server.renderXML: Failed to render to XML: %v", err)
|
|
http.Error(resp, "ERROR: Failed to render XML", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
if _, err = resp.Write(b); err != nil {
|
|
s.log.Err("server.Server.renderXML: Failed to send XML: %v", err)
|
|
return
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (s *Server) renderYML(page *Page, resp http.ResponseWriter) (err error) {
|
|
|
|
var b []byte
|
|
|
|
if b, err = yaml.Marshal(page.Info); err != nil {
|
|
s.log.Err("server.Server.renderJSON: Failed to render to JSON: %v", err)
|
|
http.Error(resp, "ERROR: Failed to render JSON", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
if _, err = resp.Write(b); err != nil {
|
|
s.log.Err("server.Server.renderJSON: Failed to send JSON: %v", err)
|
|
return
|
|
}
|
|
|
|
return
|
|
}
|