ADDED:
* The ability to show both IPv4 and IPv6 addresses (if the client has
  dual-stack and either the server does as well or a separate ClientInfo
  is running on the "other" net family).
This commit is contained in:
brent saner
2025-12-13 04:19:05 -05:00
parent eb5c44e1c3
commit 9f97fcaf81
12 changed files with 244 additions and 93 deletions

View File

@@ -24,7 +24,7 @@ const (
trueUaFieldStr string = "Yes"
falseUaFieldStr string = "No"
dfltIndent string = " "
httpRealHdr string = "X-ClientInfo-RealIP"
httpRealHdr string = "X-ClientInfo-RealIP" // TODO: advise https://nginx.org/en/docs/http/ngx_http_realip_module.html NGINX module? Allow config for user-specified header?
)
var (
@@ -35,6 +35,8 @@ var (
Funcs(
template.FuncMap{
"getTitle": getTitle,
"getIpver": getIpver,
"safeUrl": safeUrl,
},
).ParseFS(tplDir, "tpl/*.tpl"),
)

View File

@@ -46,6 +46,7 @@ func NewClient(uaStr string) (r *R00tClient, err error) {
func NewServer(log logging.Logger, cliArgs *args.Args) (srv *Server, err error) {
var s Server
var origin string
var udsSockPerms args.UdsPerms
if log == nil {
@@ -62,6 +63,7 @@ func NewServer(log logging.Logger, cliArgs *args.Args) (srv *Server, err error)
args: cliArgs,
mux: http.NewServeMux(),
sock: nil,
corsOrigins: nil,
reloadChan: make(chan os.Signal),
stopChan: make(chan os.Signal),
}
@@ -73,6 +75,42 @@ func NewServer(log logging.Logger, cliArgs *args.Args) (srv *Server, err error)
s.mux.HandleFunc("/usage.html", s.handleUsage)
s.mux.HandleFunc("/favicon.ico", s.explicit404)
if cliArgs.CORS != nil && len(cliArgs.CORS) > 0 {
if s.corsOrigins == nil {
s.corsOrigins = make(map[string]struct{})
}
for _, origin = range cliArgs.CORS {
s.corsOrigins[strings.ToLower(origin)] = struct{}{}
}
}
if cliArgs.V4Url != nil {
if s.v4Url, err = url.Parse(*cliArgs.V4Url); err != nil {
s.log.Err("server.NewServer: Failed to parse IPv4 URI '%s': %v", *cliArgs.V4Url, err)
return
}
if s.corsOrigins == nil {
s.corsOrigins = make(map[string]struct{})
}
origin = strings.ToLower(fmt.Sprintf("%s://%s", s.v4Url.Scheme, s.v4Url.Host))
s.corsOrigins[origin] = struct{}{}
}
if cliArgs.V6Url != nil {
if s.v6Url, err = url.Parse(*cliArgs.V6Url); err != nil {
s.log.Err("server.NewServer: Failed to parse IPv6 URI '%s': %v", *cliArgs.V6Url, err)
return
}
if s.corsOrigins == nil {
s.corsOrigins = make(map[string]struct{})
}
origin = strings.ToLower(fmt.Sprintf("%s://%s", s.v6Url.Scheme, s.v6Url.Host))
s.corsOrigins[origin] = struct{}{}
}
if s.corsOrigins != nil {
s.log.Debug("server.NewServer: CORS origins: %#v", s.corsOrigins)
}
if s.listenUri, err = url.Parse(cliArgs.Listen.Listen); err != nil {
s.log.Err("server.NewServer: Failed to parse listener URI: %v", err)
return

View File

@@ -4,6 +4,48 @@ import (
`fmt`
)
func (p *Page) AltURL() (altUrl string) {
if !p.HasAltURL() {
return
}
if p.Info.IP.Is4() {
altUrl = p.srv.v6Url.String()
} else if p.Info.IP.Is6() {
altUrl = p.srv.v4Url.String()
}
return
}
func (p *Page) AltVer() (altVer string) {
if !p.HasAltURL() {
return
}
if p.Info.IP.Is4() {
altVer = "v6"
} else if p.Info.IP.Is6() {
altVer = "v4"
}
return
}
func (p *Page) HasAltURL() (hasAlt bool) {
if p.Info == nil {
return
}
hasAlt = (p.Info.IP.Is4() && p.srv.v6Url != nil) ||
(p.Info.IP.Is6() && p.srv.v4Url != nil)
return
}
func (p *Page) RenderIP(indent uint) (s string) {
s = fmt.Sprintf("<a href=\"https://ipinfo.io/%s\">%s</a>", p.Info.IP.String(), p.Info.IP.String())

View File

@@ -3,24 +3,24 @@ package server
import (
`encoding/json`
`encoding/xml`
"errors"
"fmt"
"net"
"net/http"
`errors`
`fmt`
`net`
`net/http`
`net/http/fcgi`
"net/netip"
"net/url"
"os"
"os/signal"
"strings"
"sync"
`net/netip`
`net/url`
`os`
`os/signal`
`strings`
`sync`
`syscall`
sysd "github.com/coreos/go-systemd/daemon"
"github.com/davecgh/go-spew/spew"
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"
`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.
@@ -267,8 +267,10 @@ func (s *Server) explicit404(resp http.ResponseWriter, req *http.Request) {
func (s *Server) handleDefault(resp http.ResponseWriter, req *http.Request) {
var err error
var ok bool
var page *Page
var uas []string
var origin string
var reqdMimes []string
var parsedUA *R00tClient
var nAP netip.AddrPort
@@ -280,13 +282,20 @@ func (s *Server) handleDefault(resp http.ResponseWriter, req *http.Request) {
var outerFmt string = mediaJSON
s.log.Debug("server.Server.handleDefault: Handling request:\n%s", spew.Sdump(req))
origin = req.Header.Get("Origin")
if s.corsOrigins != nil && len(s.corsOrigins) != 0 && origin != "" {
if _, ok = s.corsOrigins[origin]; ok {
resp.Header().Set("Access-Control-Allow-Origin", origin)
resp.Header().Add("Vary", "Origin")
}
}
resp.Header().Set("ClientInfo-Version", version.Ver.Short())
page = &Page{
Info: &R00tInfo{
Client: nil,
IP: nil,
IP: netip.Addr{},
Port: 0,
Headers: XmlHeaders(req.Header),
Req: req,
@@ -301,7 +310,7 @@ func (s *Server) handleDefault(resp http.ResponseWriter, req *http.Request) {
// First the client info.
remAddrPort = req.RemoteAddr
if s.isHttp && req.Header.Get(httpRealHdr) != "" {
// TODO: WHitelist explicit reverse proxy addr(s)?
// TODO: Whitelist explicit reverse proxy addr(s)?
remAddrPort = req.Header.Get(httpRealHdr)
req.Header.Del(httpRealHdr)
}
@@ -315,7 +324,7 @@ func (s *Server) handleDefault(resp http.ResponseWriter, req *http.Request) {
*/
err = nil
}
page.Info.IP = net.ParseIP(nAP.Addr().String())
page.Info.IP = nAP.Addr().Unmap()
page.Info.Port = nAP.Port()
}
if req.URL != nil {
@@ -547,6 +556,8 @@ func (s *Server) renderHTML(page *Page, resp http.ResponseWriter) (err error) {
var b []byte
page.srv = s
if page.RawFmt != nil {
switch *page.RawFmt {
case mediaHTML:

View File

@@ -2,9 +2,22 @@ package server
import (
`fmt`
`net/netip`
`strings`
`html/template`
)
func getIpver(a netip.Addr) (verStr string) {
if a.Is4() {
verStr = "v4"
} else if a.Is6() {
verStr = "v6"
}
return
}
func getTitle(subPage string) (title string) {
if subPage == "" || subPage == "index" {
@@ -16,3 +29,10 @@ func getTitle(subPage string) (title string) {
return
}
func safeUrl(urlStr string) (u template.URL) {
u = template.URL(urlStr)
return
}

View File

@@ -4,9 +4,35 @@
{{- $linkico := "🔗" }}
<h2 id="client">Client/Browser Information<a href="#client">{{ $linkico }}</a></h2>
<p>
<b>Your IP Address is <i><a href="https://ipinfo.io/{{ $page.Info.IP.String }}">{{ $page.Info.IP.String }}</a></i>.</b>
<b>Your IP{{ getIpver $page.Info.IP }} Address is <i><a href="https://ipinfo.io/{{ $page.Info.IP.String }}">{{ $page.Info.IP.String }}</a></i>.</b>
<br/>
<i>You are connecting with port <b>{{ $page.Info.Port }}</b> outbound.</i>
{{- if $page.HasAltURL }}
<div id="alt_addr"></div>
<script>
async function fetchAltAddr() {
try {
const resp = await fetch('{{ safeUrl $page.AltURL }}', {
headers: {
'Accept': 'application/json'
}
});
if (resp.ok) {
const dat = await resp.json();
const addr = dat.ip;
const infoDiv = document.getElementById('alt_addr');
infoDiv.innerHTML = `
<i>You also have IP{{ $page.AltVer }} address <b><a href="https://ipinfo.io/${addr}">${addr}</a></b>.</i>
<br/>
<i>Try loading <b><a href="{{ $page.AltURL }}">{{ $page.AltURL }}</a></b> for more information.</i>`;
}
} catch (error) {
console.info('Did not fetch alternate address: ', error);
}
}
fetchAltAddr()
</script>
{{- end }}
</p>
{{- if $page.Raw }}
<h3 id="client_raw">Raw Block ({{ $page.RawFmt }})<a href="#client_raw">{{ $linkico }}</a></h3>
@@ -42,7 +68,7 @@
</tbody>
</table>
</div>
{{- /*
{{- /*
<ul>
{{- $flds := $ua.ToMap }}

View File

@@ -1,19 +1,22 @@
package server
import (
`encoding/xml`
`net`
`net/http`
`net/url`
`os`
"encoding/xml"
"net"
"net/http"
"net/netip"
"net/url"
"os"
`github.com/mileusna/useragent`
`r00t2.io/clientinfo/args`
`r00t2.io/goutils/logging`
"github.com/mileusna/useragent"
"r00t2.io/clientinfo/args"
"r00t2.io/goutils/logging"
)
type outerRenderer func(page *Page, resp http.ResponseWriter) (err error)
type XmlHeaders map[string][]string
type (
outerRenderer func(page *Page, resp http.ResponseWriter) (err error)
XmlHeaders map[string][]string
)
// R00tInfo is the structure of data returned to the client.
type R00tInfo struct {
@@ -22,7 +25,7 @@ type R00tInfo struct {
// Client is the UA/Client info, if any passed by the client.
Client []*R00tClient `json:"ua,omitempty" xml:"ua,omitempty" yaml:"Client/User Agent,omitempty"`
// IP is the client IP address.
IP net.IP `json:"ip" xml:"ip,attr" yaml:"Client IP Address"`
IP netip.Addr `json:"ip" xml:"ip,attr" yaml:"Client IP Address"`
// Port is the client's port number.
Port uint16 `json:"port" xml:"port,attr" yaml:"Client Port"`
// Headers are the collection of the request headers sent by the client.
@@ -84,19 +87,23 @@ type Page struct {
Indent string
// DoIndent indicates if indenting was enabled.
DoIndent bool
srv *Server
}
type Server struct {
log logging.Logger
args *args.Args
listenUri *url.URL
isHttp bool
mux *http.ServeMux
sock net.Listener
doneChan chan bool
stopChan chan os.Signal
reloadChan chan os.Signal
isStopping bool
log logging.Logger
args *args.Args
listenUri *url.URL
isHttp bool
mux *http.ServeMux
sock net.Listener
doneChan chan bool
stopChan chan os.Signal
reloadChan chan os.Signal
v4Url *url.URL
v6Url *url.URL
corsOrigins map[string]struct{}
isStopping bool
}
// https://www.iana.org/assignments/media-types/media-types.xhtml