2024-12-12 02:22:54 -05:00
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 `
2024-12-19 02:27:53 -05:00
` r00t2.io/clientinfo/version `
2024-12-12 02:22:54 -05:00
"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 ) )
2024-12-19 02:27:53 -05:00
resp . Header ( ) . Set ( "ClientInfo-Version" , version . Ver . Short ( ) )
2024-12-12 02:22:54 -05:00
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
}
2024-12-19 02:27:53 -05:00
if parsedUA . Name != nil && htmlOverride [ * parsedUA . Name ] {
parsedUA . IsDesktop = true
}
2024-12-12 02:22:54 -05:00
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 )
2024-12-12 03:47:30 -05:00
resp . Header ( ) [ "Accept" ] = okAcceptMime
2024-12-12 02:22:54 -05:00
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" ] )
2024-12-12 03:47:30 -05:00
resp . Header ( ) [ "Accept" ] = okAcceptMime
2024-12-12 02:22:54 -05:00
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" ] )
2024-12-12 03:47:30 -05:00
resp . Header ( ) [ "Accept" ] = okAcceptMime
2024-12-12 02:22:54 -05:00
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 ) )
2024-12-19 02:27:53 -05:00
resp . Header ( ) . Set ( "ClientInfo-Version" , version . Ver . Short ( ) )
2024-12-12 02:22:54 -05:00
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 ) )
2024-12-19 02:27:53 -05:00
resp . Header ( ) . Set ( "ClientInfo-Version" , version . Ver . Short ( ) )
2024-12-12 02:22:54 -05:00
resp . Header ( ) . Set ( "Content-Type" , "text/html; charset=utf-8" )
2024-12-19 02:27:53 -05:00
2024-12-12 02:22:54 -05:00
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
2024-12-19 02:27:53 -05:00
resp . Header ( ) . Set ( "Content-Type" , "application/json" )
2024-12-12 02:22:54 -05:00
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 {
2024-12-12 03:47:30 -05:00
if b , err = mediaNoIndent [ * page . RawFmt ] ( page . Info ) ; err != nil {
2024-12-12 02:22:54 -05:00
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 )
}
}
2024-12-19 02:27:53 -05:00
if b != nil {
page . Raw = new ( string )
* page . Raw = string ( b )
}
2024-12-12 02:22:54 -05:00
}
2024-12-19 02:27:53 -05:00
resp . Header ( ) . Set ( "Content-Type" , "text/html; charset=utf-8" )
2024-12-12 02:22:54 -05:00
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
}
}
2024-12-19 02:27:53 -05:00
resp . Header ( ) . Set ( "Content-Type" , "application/xml; charset=utf-8" )
2024-12-12 02:22:54 -05:00
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
}
2024-12-19 02:27:53 -05:00
resp . Header ( ) . Set ( "Content-Type" , "application/yaml" )
2024-12-12 02:22:54 -05:00
if _ , err = resp . Write ( b ) ; err != nil {
s . log . Err ( "server.Server.renderJSON: Failed to send JSON: %v" , err )
return
}
return
}