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 }