go_clientinfo/server/funcs_server.go
brent saner 6b75e17f48
BUGFIX: Content-Type, Nil Raw Body Edge Case, Links
FIXES:
* There was an edge case where a raw body for HTML would potentially
  result in a nil byte slice exception. This has been fixed.
  (I don't even know if it was possible, I just made sure it wasn't.)
* The links browser is now explicitly returned as HTML and properly
  detected as a "browser".
* Hyperlinks for links, w3m added to Usage page.
* Content-Type is now always set correctly; there were cases where it
  was improperly returning e.g. text/plain for JSON.
2024-12-19 02:27:53 -05:00

639 lines
18 KiB
Go

package server
import (
`encoding/json`
`encoding/xml`
"errors"
"fmt"
"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/clientinfo/version`
"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 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))
resp.Header().Set("ClientInfo-Version", version.Ver.Short())
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
}
if parsedUA.Name != nil && htmlOverride[*parsedUA.Name] {
parsedUA.IsDesktop = true
}
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)
resp.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"])
resp.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"])
resp.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("ClientInfo-Version", version.Ver.Short())
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("ClientInfo-Version", version.Ver.Short())
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
resp.Header().Set("Content-Type", "application/json")
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.Info); 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)
}
}
if b != nil {
page.Raw = new(string)
*page.Raw = string(b)
}
}
resp.Header().Set("Content-Type", "text/html; charset=utf-8")
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
}
}
resp.Header().Set("Content-Type", "application/xml; charset=utf-8")
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
}
resp.Header().Set("Content-Type", "application/yaml")
if _, err = resp.Write(b); err != nil {
s.log.Err("server.Server.renderJSON: Failed to send JSON: %v", err)
return
}
return
}