initial commit before refactor switch
This commit is contained in:
commit
db081e2699
49
.gitignore
vendored
Normal file
49
.gitignore
vendored
Normal file
@ -0,0 +1,49 @@
|
||||
*.7z
|
||||
*.bak
|
||||
*.deb
|
||||
*.jar
|
||||
*.rar
|
||||
*.run
|
||||
*.sig
|
||||
*.tar
|
||||
*.tar.bz2
|
||||
*.tar.gz
|
||||
*.tar.xz
|
||||
*.tbz
|
||||
*.tbz2
|
||||
*.tgz
|
||||
*.txz
|
||||
*.zip
|
||||
.*.swp
|
||||
.editix
|
||||
|
||||
# https://github.com/github/gitignore/blob/master/Go.gitignore
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
#*.test
|
||||
test.sh
|
||||
|
||||
# Built binary
|
||||
bin/*
|
||||
poc/*
|
||||
_poc/*
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# Example configs.
|
||||
_exampledata/
|
||||
|
||||
# Don't include rendered doc
|
||||
#/README.html
|
||||
|
||||
# Example code.
|
||||
_demo/
|
9
LICENSE
Normal file
9
LICENSE
Normal file
@ -0,0 +1,9 @@
|
||||
Copyright (c) 2024 Brent Saner.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
22
_extras/clientinfo.env
Normal file
22
_extras/clientinfo.env
Normal file
@ -0,0 +1,22 @@
|
||||
# Enable debug mode.
|
||||
#CINFO_DEBUG=1
|
||||
|
||||
# Use a different listening spec.
|
||||
# The default is 'unix:///var/run/clientinfo/fcgi.sock'
|
||||
##
|
||||
# The below example listens on UDS path /tmp/mysock.fcgi as FCGI.
|
||||
# Note the *three* slashes after the scheme. This is intentional and recommended,
|
||||
# otherwise the path will be treated as relative to current working directory.
|
||||
#CINFO_URI="unix:///tmp/mysock.fcgi"
|
||||
#
|
||||
# The below example listens on localhost port 4321 as FCGI.
|
||||
# TLS is currently not supported; use a reverse stream proxy to terminate if TLS is needed.
|
||||
#CINFO_URI="tcp://127.0.0.1:4321"
|
||||
#
|
||||
# The below example listens on localhost port 1234 as HTTP instead of FCGI.
|
||||
# HTTPS is currently not supported; use a reverse proxy to terminate if HTTPS is needed.
|
||||
# If reverse-proxying, BE SURE to set (NOT add) a header "X-ClientInfo-RealIP"
|
||||
# with a value in the form of:
|
||||
# * "<Client IP>:<Client Port>" (IPv4)
|
||||
# * "[<Client IP>]:<Client Port>" (IPv6)
|
||||
#CINFO_URI="http://127.0.0.1:1234"
|
22
_extras/clientinfo.service
Normal file
22
_extras/clientinfo.service
Normal file
@ -0,0 +1,22 @@
|
||||
# This file goes in /etc/systemd/system/.
|
||||
# DO NOT PLACE IT IN /usr/lib/systemd/system/ unless you are a packager for a Linux distribution.
|
||||
# After it's in place (or updated), run:
|
||||
# systemctl daemon-reload
|
||||
[Unit]
|
||||
Description=Return Client Request Information
|
||||
|
||||
[Service]
|
||||
Type=notify
|
||||
# These may also be "httpd", "nginx", etc.
|
||||
User=http
|
||||
Group=htttp
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
# Not required, but you may create this file and specify the associated commandline argument env vars.
|
||||
# This allows for easily changing the listen URI, debug mode, etc. without changing the systemd unit.
|
||||
# See clientinfo.env for an example.
|
||||
EnvironmentFile=-/etc/default/clientinfo
|
||||
ExecStart=/usr/local/bin/clientinfo
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
25
_extras/fastcgi.inc
Normal file
25
_extras/fastcgi.inc
Normal file
@ -0,0 +1,25 @@
|
||||
|
||||
fastcgi_param QUERY_STRING $query_string;
|
||||
fastcgi_param REQUEST_METHOD $request_method;
|
||||
fastcgi_param CONTENT_TYPE $content_type;
|
||||
fastcgi_param CONTENT_LENGTH $content_length;
|
||||
|
||||
fastcgi_param SCRIPT_NAME $fastcgi_script_name;
|
||||
fastcgi_param REQUEST_URI $request_uri;
|
||||
fastcgi_param DOCUMENT_URI $document_uri;
|
||||
fastcgi_param DOCUMENT_ROOT $document_root;
|
||||
fastcgi_param SERVER_PROTOCOL $server_protocol;
|
||||
fastcgi_param REQUEST_SCHEME $scheme;
|
||||
fastcgi_param HTTPS $https if_not_empty;
|
||||
|
||||
fastcgi_param GATEWAY_INTERFACE CGI/1.1;
|
||||
fastcgi_param SERVER_SOFTWARE nginx/$nginx_version;
|
||||
|
||||
fastcgi_param REMOTE_ADDR $remote_addr;
|
||||
fastcgi_param REMOTE_PORT $remote_port;
|
||||
fastcgi_param SERVER_ADDR $server_addr;
|
||||
fastcgi_param SERVER_PORT $server_port;
|
||||
fastcgi_param SERVER_NAME $server_name;
|
||||
|
||||
# PHP only, required if PHP was built with --enable-force-cgi-redirect
|
||||
fastcgi_param REDIRECT_STATUS 200;
|
74
_extras/nginx.conf
Normal file
74
_extras/nginx.conf
Normal file
@ -0,0 +1,74 @@
|
||||
worker_processes 1;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
default_type application/octet-stream;
|
||||
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
sendfile on;
|
||||
keepalive_timeout 65;
|
||||
|
||||
server {
|
||||
listen [::]:80 ipv6only=off default_server;
|
||||
server_name localhost _;
|
||||
|
||||
access_log /var/log/nginx/access.log main;
|
||||
error_log /var/log/nginx/debug.log debug;
|
||||
error_log /var/log/nginx/error.log;
|
||||
|
||||
# You MUST set this for dual-stack setups!
|
||||
set $client_ip_port "$remote_addr:$remote_port";
|
||||
if ($remote_addr ~ "^.+:.+$") {
|
||||
set $client_ip_port "[$remote_addr]:$remote_port";
|
||||
}
|
||||
|
||||
location / {
|
||||
# For UDS FastCGI (the default URI).
|
||||
# UDS FastCGI is the recommended setup, as it provides the most accuracy,
|
||||
# requires the least amount of configuration, and is the most performant.
|
||||
# Yes, Virginia, even more performant than localhost network ports.
|
||||
include fastcgi.inc;
|
||||
fastcgi_pass unix:/var/run/clientinfo/fcgi.sock;
|
||||
#
|
||||
# For TCP FastCGI. (See the clientinfo.env file for a corresponding URI.)
|
||||
#include fastcgi.inc;
|
||||
#fastcgi_pass 127.0.0.1:4321;
|
||||
#
|
||||
# For HTTP. (See the clientinfo.env file for a corresponding URI.)
|
||||
# BE SURE to explicitly set (NOT add) the X-ClientInfo-RealIP header
|
||||
# if reverse-proxying, and enclose in brackets if IPv6!
|
||||
# (FastCGI via fastcgi.inc does this automatically for fastcgi_pass.)
|
||||
# Note that in dual-stack NGINX (as configured here), the $client_ip_port
|
||||
# will use IPv6-mapped IPv4 (https://www.rfc-editor.org/rfc/rfc5156.html#section-2.2)
|
||||
# for IPv4 clients, which looks like e.g. "[::ffff:<IP>]:<Port>".
|
||||
# FastCGI will use the expected "<IP>:<Port>" format for IPv4 clients.
|
||||
#proxy_set_header X-ClientInfo-RealIP "$client_ip_port";
|
||||
#proxy_http_version 1.1;
|
||||
#proxy_pass http://127.0.0.1:1234;
|
||||
}
|
||||
|
||||
# Alternatively, if you only wanted to return the client's IP,
|
||||
# you don't even need ClientInfo, it can be returned directly.
|
||||
# See https://nginx.org/en/docs/varindex.html
|
||||
location /ip {
|
||||
return 200 "$remote_addr";
|
||||
}
|
||||
# Likewise, ...
|
||||
location /ipport {
|
||||
return 200 "$client_ip_port";
|
||||
}
|
||||
# And so forth.
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
}
|
||||
}
|
75
args/args_funcs.go
Normal file
75
args/args_funcs.go
Normal file
@ -0,0 +1,75 @@
|
||||
package args
|
||||
|
||||
import (
|
||||
`errors`
|
||||
`os/user`
|
||||
`strconv`
|
||||
)
|
||||
|
||||
// ModesAndOwners returns the evaluated file UID, file GID, dir UID, dir GID, file mode, and dir mode for a UDS socket.
|
||||
func (a *Args) ModesAndOwners() (perms UdsPerms, err error) {
|
||||
|
||||
var idInt int
|
||||
var uGid int
|
||||
var u *user.User
|
||||
var g *user.Group
|
||||
var nErr *strconv.NumError = new(strconv.NumError)
|
||||
|
||||
perms.FMode = a.SockMode
|
||||
perms.DMode = a.SockDirMode
|
||||
|
||||
// UID is always this user
|
||||
if u, err = user.Current(); err != nil {
|
||||
return
|
||||
}
|
||||
if perms.UID, err = strconv.Atoi(u.Uid); err != nil {
|
||||
return
|
||||
}
|
||||
if g, err = user.LookupGroupId(u.Gid); err != nil {
|
||||
return
|
||||
}
|
||||
if uGid, err = strconv.Atoi(g.Gid); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
perms.FGID = uGid
|
||||
if a.SockGrp != nil {
|
||||
// First try a direct GID.
|
||||
if idInt, err = strconv.Atoi(*a.SockGrp); err != nil {
|
||||
if errors.As(err, &nErr) {
|
||||
err = nil
|
||||
// And then try a group name.
|
||||
if g, err = user.LookupGroup(*a.SockGrp); err != nil {
|
||||
return
|
||||
}
|
||||
if idInt, err = strconv.Atoi(g.Gid); err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
perms.FGID = idInt
|
||||
}
|
||||
perms.DGID = uGid
|
||||
if a.SockDirGrp != nil {
|
||||
// First try a direct GID.
|
||||
if idInt, err = strconv.Atoi(*a.SockDirGrp); err != nil {
|
||||
if errors.As(err, &nErr) {
|
||||
err = nil
|
||||
// And then try a group name.
|
||||
if g, err = user.LookupGroup(*a.SockDirGrp); err != nil {
|
||||
return
|
||||
}
|
||||
if idInt, err = strconv.Atoi(g.Gid); err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
perms.DGID = idInt
|
||||
}
|
||||
|
||||
return
|
||||
}
|
28
args/types.go
Normal file
28
args/types.go
Normal file
@ -0,0 +1,28 @@
|
||||
package args
|
||||
|
||||
import (
|
||||
`io/fs`
|
||||
)
|
||||
|
||||
type Args struct {
|
||||
Version bool `short:"v" long:"version" description:"Print the version and exit."`
|
||||
DetailVersion bool `short:"V" long:"detail" description:"Print detailed version info and exit."`
|
||||
DoDebug bool `env:"CINFO_DEBUG" short:"d" long:"debug" description:"If specified, enable debug logging. This may log a LOT of information."`
|
||||
SockMode fs.FileMode `env:"CINFO_FMODE" short:"m" long:"fmode" default:"0o0600" description:"If using a UDS, set the socket file to this permission. This should probably be either 0o0600 or 0o0660."`
|
||||
SockDirMode fs.FileMode `env:"CINFO_DMODE" short:"M" long:"dmode" default:"0o0700" description:"If using a UDS, attempt to set the directory containing the socket to use this permission. This should probably be either 0o0700 or 0o0770."`
|
||||
SockGrp *string `env:"CINFO_FGRP" short:"g" long:"fgroup" description:"If specified and using a UDS, attempt to set the socket to this GID/group name. (If unspecified, the default is current user's primary group.)"`
|
||||
SockDirGrp *string `env:"CINFO_DGRP" short:"G" long:"dgroup" description:"If specified and using a UDS, attempt to set the directory containing the socket to this GID/group name. (If unspecified, the default is current user's primary group.)"`
|
||||
Listen ListenArgs `positional-args:"true"`
|
||||
}
|
||||
|
||||
type ListenArgs struct {
|
||||
Listen string `env:"CINFO_URI" positional-arg-name:"LISTEN_URI" default:"unix:///var/run/clientinfo/fcgi.sock" description:"The specification to listen on.\nIf the scheme is 'unix', a FastCGI UDS/IPC socket is used (default); any host, query parameters, etc. component is ignored and the URI path is used to specify the socket.\nIf 'tcp', a FastCGI socket over TCP is opened on the <host:port>.\nIf 'http', an HTTP listener is opened on the <host:port>; any path, query parameters, etc. components are ignored.\nHTTPS is unsupported; terminate with a reverse proxy. All other schemes will cause a fatal error.\nThe default is 'unix:///var/run/clientinfo/fcgi.sock'." validate:"required,uri"`
|
||||
}
|
||||
|
||||
type UdsPerms struct {
|
||||
UID int
|
||||
FGID int
|
||||
DGID int
|
||||
FMode fs.FileMode
|
||||
DMode fs.FileMode
|
||||
}
|
87
build.sh
Executable file
87
build.sh
Executable file
@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
# This is not portable. It has bashisms.
|
||||
|
||||
BUILD_TIME="$(date '+%s')"
|
||||
BUILD_USER="$(whoami)"
|
||||
BUILD_SUDO_USER="${SUDO_USER}"
|
||||
BUILD_HOST="$(hostname)"
|
||||
|
||||
# Check to make sure git is available.
|
||||
if ! command -v git &> /dev/null;
|
||||
then
|
||||
echo "Git is not available; automatic version handling unsupported."
|
||||
echo "You must build by calling 'go build' directly in the respective directories."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check git directory/repository.
|
||||
if ! git rev-parse --is-inside-work-tree &>/dev/null;
|
||||
then
|
||||
echo "Not running inside a git work tree; automatic version handling unsupported/build script unsupported."
|
||||
echo "You must build by calling 'go build' directly in the respective directories instead."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# If it has a tag in the path of the current HEAD that matches a version string...
|
||||
# I wish git describe supported regex. It does not; only globs. Gross.
|
||||
# If there's a bug anywhere, it's here.
|
||||
if git describe --tags --abbrev=0 --match "v[0-9]*" HEAD &> /dev/null;
|
||||
then
|
||||
# It has a tag we can use.
|
||||
CURRENT_VER="$(git describe --tags --abbrev=0 --match "v[0-9]*" HEAD)"
|
||||
COMMITS_SINCE="$(git rev-list --count ${CURRENT_VER}..HEAD)"
|
||||
else
|
||||
# No tag available.
|
||||
CURRENT_VER=""
|
||||
COMMITS_SINCE=""
|
||||
fi
|
||||
|
||||
# If it's dirty (staged but not committed or unstaged files)...
|
||||
if ! git diff-index --quiet HEAD;
|
||||
then
|
||||
# It's dirty.
|
||||
IS_DIRTY="1"
|
||||
else
|
||||
# It's clean.
|
||||
IS_DIRTY="0"
|
||||
fi
|
||||
|
||||
# Get the commit hash of the *most recent* commit in the path of current HEAD...
|
||||
CURRENT_HASH="$(git rev-parse --verify HEAD)"
|
||||
# The same as above, but abbreviated.
|
||||
CURRENT_SHORT="$(git rev-parse --verify --short HEAD)"
|
||||
|
||||
# Get the module name.
|
||||
MODPATH="$(sed -n -re 's@^\s*module\s+(.*)(//.*)?$@\1@p' go.mod)"
|
||||
|
||||
# Build the ldflags string.
|
||||
# BEHOLD! BASH WITCHCRAFT.
|
||||
LDFLAGS_STR="\
|
||||
-X '${MODPATH}/version.sourceControl=git' \
|
||||
-X '${MODPATH}/version.version=${CURRENT_VER}' \
|
||||
-X '${MODPATH}/version.commitHash=${CURRENT_HASH}' \
|
||||
-X '${MODPATH}/version.commitShort=${CURRENT_SHORT}' \
|
||||
-X '${MODPATH}/version.numCommitsAfterTag=${COMMITS_SINCE}' \
|
||||
-X '${MODPATH}/version.isDirty=${IS_DIRTY}' \
|
||||
-X '${MODPATH}/version.buildTime=${BUILD_TIME}' \
|
||||
-X '${MODPATH}/version.buildUser=${BUILD_USER}' \
|
||||
-X '${MODPATH}/version.buildSudoUser=${BUILD_SUDO_USER}' \
|
||||
-X '${MODPATH}/version.buildHost=${BUILD_HOST}'"
|
||||
|
||||
# And finally build.
|
||||
mkdir -p ./bin/
|
||||
export CGO_ENABLED=0
|
||||
|
||||
cmd="clientinfo"
|
||||
# Linux
|
||||
echo -n "Building ./bin/${cmd}..."
|
||||
go build \
|
||||
-o "./bin/${cmd}" \
|
||||
-ldflags \
|
||||
"${LDFLAGS_STR}" \
|
||||
cmd/${cmd}/*.go
|
||||
echo " Done."
|
||||
|
||||
echo "Build complete."
|
18
cmd/clientinfo/consts.go
Normal file
18
cmd/clientinfo/consts.go
Normal file
@ -0,0 +1,18 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
`log`
|
||||
|
||||
sysdUtil `github.com/coreos/go-systemd/util`
|
||||
`github.com/go-playground/validator/v10`
|
||||
)
|
||||
|
||||
var (
|
||||
isSystemd bool = sysdUtil.IsRunningSystemd()
|
||||
validate *validator.Validate = validator.New(validator.WithRequiredStructEnabled())
|
||||
)
|
||||
|
||||
const (
|
||||
logFlags int = log.LstdFlags | log.Lmsgprefix
|
||||
logFlagsDebug int = logFlags | log.Llongfile
|
||||
)
|
118
cmd/clientinfo/main.go
Normal file
118
cmd/clientinfo/main.go
Normal file
@ -0,0 +1,118 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
`errors`
|
||||
`fmt`
|
||||
`log`
|
||||
`os`
|
||||
|
||||
`github.com/creasty/defaults`
|
||||
`github.com/davecgh/go-spew/spew`
|
||||
`github.com/jessevdk/go-flags`
|
||||
`r00t2.io/clientinfo/args`
|
||||
`r00t2.io/clientinfo/server`
|
||||
`r00t2.io/clientinfo/version`
|
||||
`r00t2.io/goutils/logging`
|
||||
`r00t2.io/sysutils/envs`
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
var err error
|
||||
var logger *logging.MultiLogger
|
||||
var logFlagsRuntime int = logFlags
|
||||
var srv *server.Server
|
||||
var args *args.Args = new(args.Args)
|
||||
var flagsErr *flags.Error = new(flags.Error)
|
||||
var parser *flags.Parser = flags.NewParser(args, flags.Default)
|
||||
|
||||
if _, err = parser.Parse(); err != nil {
|
||||
switch {
|
||||
case errors.As(err, &flagsErr):
|
||||
switch {
|
||||
// These print their relevant messages by themselves.
|
||||
case errors.Is(
|
||||
flagsErr.Type,
|
||||
flags.ErrHelp,
|
||||
),
|
||||
errors.Is(
|
||||
flagsErr.Type,
|
||||
flags.ErrCommandRequired,
|
||||
),
|
||||
errors.Is(
|
||||
flagsErr.Type,
|
||||
flags.ErrRequired,
|
||||
):
|
||||
return
|
||||
default:
|
||||
log.Panicln(err)
|
||||
}
|
||||
default:
|
||||
log.Panicln(err)
|
||||
}
|
||||
}
|
||||
|
||||
if version.Ver, err = version.Version(); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
|
||||
// If args.Version or args.DetailVersion are true, just print them and exit.
|
||||
if args.DetailVersion || args.Version {
|
||||
if args.Version {
|
||||
fmt.Println(version.Ver.Short())
|
||||
return
|
||||
} else if args.DetailVersion {
|
||||
fmt.Println(version.Ver.Detail())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// We want to set up logging before anything else.
|
||||
if args.DoDebug {
|
||||
logFlagsRuntime = logFlagsDebug
|
||||
}
|
||||
logger = logging.GetMultiLogger(args.DoDebug, "ClientInfo")
|
||||
if err = logger.AddDefaultLogger(
|
||||
"default",
|
||||
logFlagsRuntime,
|
||||
"/var/log/clientinfo/clientinfo.log", "~/logs/clientinfo.log",
|
||||
); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
if err = logger.Setup(); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
logger.Info("main: ClientInfo version %v", version.Ver.Short())
|
||||
logger.Debug("main: ClientInfo version (extended):\n%v", version.Ver.Detail())
|
||||
defer logger.Shutdown()
|
||||
|
||||
// TODO: WORKAROUND: https://github.com/jessevdk/go-flags/issues/408
|
||||
if envs.HasEnv("CINFO_URI") {
|
||||
args.Listen.Listen = os.Getenv("CINFO_URI")
|
||||
}
|
||||
|
||||
if err = defaults.Set(args); err != nil {
|
||||
logger.Err("main: Failed to set CLI arg defaults: %v", err)
|
||||
log.Panicln(err)
|
||||
}
|
||||
|
||||
logger.Debug("main: Initialized with args:\n%v", spew.Sdump(args))
|
||||
|
||||
if err = validate.Struct(args); err != nil {
|
||||
logger.Err("main: Received error when validating args: %v", err)
|
||||
log.Panicln(err)
|
||||
}
|
||||
|
||||
if srv, err = server.NewServer(logger, args); err != nil {
|
||||
logger.Err("main: Received error when creating server: %v", err)
|
||||
log.Panicln(err)
|
||||
}
|
||||
logger.Debug("main: Starting server.")
|
||||
if err = srv.Run(); err != nil {
|
||||
logger.Err("main: Received error when running server: %v", err)
|
||||
log.Panicln(err)
|
||||
}
|
||||
defer srv.Close()
|
||||
|
||||
logger.Debug("main: Exiting.")
|
||||
}
|
30
go.mod
Normal file
30
go.mod
Normal file
@ -0,0 +1,30 @@
|
||||
module r00t2.io/clientinfo
|
||||
|
||||
go 1.23.3
|
||||
|
||||
require (
|
||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
|
||||
github.com/creasty/defaults v1.8.0
|
||||
github.com/davecgh/go-spew v1.1.1
|
||||
github.com/go-playground/validator/v10 v10.23.0
|
||||
github.com/goccy/go-yaml v1.15.7
|
||||
github.com/jessevdk/go-flags v1.6.1
|
||||
github.com/mileusna/useragent v1.3.5
|
||||
golang.org/x/mod v0.22.0
|
||||
r00t2.io/goutils v1.7.1
|
||||
r00t2.io/sysutils v1.12.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/djherbis/times v1.6.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.7 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
golang.org/x/crypto v0.30.0 // indirect
|
||||
golang.org/x/net v0.32.0 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
)
|
66
go.sum
Normal file
66
go.sum
Normal file
@ -0,0 +1,66 @@
|
||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU=
|
||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/creasty/defaults v1.8.0 h1:z27FJxCAa0JKt3utc0sCImAEb+spPucmKoOdLHvHYKk=
|
||||
github.com/creasty/defaults v1.8.0/go.mod h1:iGzKe6pbEHnpMPtfDXZEr0NVxWnPTjb1bbDy08fPzYM=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
||||
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||
github.com/gabriel-vasile/mimetype v1.4.7 h1:SKFKl7kD0RiPdbht0s7hFtjl489WcQ1VyPW8ZzUMYCA=
|
||||
github.com/gabriel-vasile/mimetype v1.4.7/go.mod h1:GDlAgAyIRT27BhFl53XNAFtfjzOkLaF35JdEG0P7LtU=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o=
|
||||
github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/goccy/go-yaml v1.15.7 h1:L7XuKpd/A66X4w/dlk08lVfiIADdy79a1AzRoIefC98=
|
||||
github.com/goccy/go-yaml v1.15.7/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4=
|
||||
github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/mileusna/useragent v1.3.5 h1:SJM5NzBmh/hO+4LGeATKpaEX9+b4vcGg2qXGLiNGDws=
|
||||
github.com/mileusna/useragent v1.3.5/go.mod h1:3d8TOmwL/5I8pJjyVDteHtgDGcefrFUX4ccGOMKNYYc=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY=
|
||||
golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
|
||||
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI=
|
||||
golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
r00t2.io/goutils v1.7.1 h1:Yzl9rxX1sR9WT0FcjK60qqOgBoFBOGHYKZVtReVLoQc=
|
||||
r00t2.io/goutils v1.7.1/go.mod h1:9ObJI9S71wDLTOahwoOPs19DhZVYrOh4LEHmQ8SW4Lk=
|
||||
r00t2.io/sysutils v1.1.1 h1:q2P5u50HIIRk6muCPo1Gpapy6sNT4oaB1l2O/C/mi3A=
|
||||
r00t2.io/sysutils v1.1.1/go.mod h1:Wlfi1rrJpoKBOjWiYM9rw2FaiZqraD6VpXyiHgoDo/o=
|
||||
r00t2.io/sysutils v1.12.0 h1:Ce3qUOyLixE1ZtFT/+SVwOT5kSkzg5+l1VloGeGugrU=
|
||||
r00t2.io/sysutils v1.12.0/go.mod h1:bNTKNBk9MnUhj9coG9JBNicSi5FrtJHEM645um85pyw=
|
88
server/consts.go
Normal file
88
server/consts.go
Normal file
@ -0,0 +1,88 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
`embed`
|
||||
`encoding/json`
|
||||
`encoding/xml`
|
||||
`html/template`
|
||||
`os`
|
||||
`syscall`
|
||||
|
||||
sysdUtil `github.com/coreos/go-systemd/util`
|
||||
`github.com/goccy/go-yaml`
|
||||
)
|
||||
|
||||
const (
|
||||
convertTag string = "uaField"
|
||||
prettyTag string = "renderName"
|
||||
baseTitle string = "r00t^2 Client Info Revealer"
|
||||
titleSep string = " || "
|
||||
xmlHdrElem string = "header"
|
||||
xmlHdrElemName string = "name"
|
||||
xmlHdrVal string = "value"
|
||||
nilUaFieldStr string = "(N/A)"
|
||||
trueUaFieldStr string = "Yes"
|
||||
falseUaFieldStr string = "No"
|
||||
dfltIndent string = " "
|
||||
httpRealHdr string = "X-ClientInfo-RealIP"
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed "tpl"
|
||||
tplDir embed.FS
|
||||
tpl *template.Template = template.Must(
|
||||
template.New("").
|
||||
Funcs(
|
||||
template.FuncMap{
|
||||
"getTitle": getTitle,
|
||||
},
|
||||
).ParseFS(tplDir, "tpl/*.tpl"),
|
||||
)
|
||||
)
|
||||
|
||||
// Signal traps
|
||||
var (
|
||||
stopSigs []os.Signal = []os.Signal{
|
||||
syscall.SIGQUIT,
|
||||
os.Interrupt,
|
||||
syscall.SIGTERM,
|
||||
}
|
||||
reloadSigs []os.Signal = []os.Signal{
|
||||
syscall.SIGHUP,
|
||||
// We also add stopSigs so we trigger the Reload loop to close. TODO.
|
||||
syscall.SIGQUIT,
|
||||
os.Interrupt,
|
||||
syscall.SIGTERM,
|
||||
}
|
||||
isSystemd bool = sysdUtil.IsRunningSystemd()
|
||||
)
|
||||
|
||||
// media/MIME types
|
||||
const (
|
||||
mediaJSON string = "application/json"
|
||||
mediaXML string = "application/xml"
|
||||
mediaYAML string = "application/yaml"
|
||||
mediaHTML string = "text/html"
|
||||
// TODO: plain/text? CSV? TOML?
|
||||
)
|
||||
|
||||
var (
|
||||
// mediaNoIndent covers everything (except HTML).
|
||||
mediaNoIndent map[string]func(obj any) (b []byte, err error) = map[string]func(obj any) (b []byte, err error){
|
||||
mediaJSON: json.Marshal,
|
||||
mediaXML: xml.Marshal,
|
||||
mediaYAML: yaml.Marshal,
|
||||
// HTML is handled explicitly.
|
||||
}
|
||||
// mediaIndent only contains MIME types that support configured indents.
|
||||
mediaIndent map[string]func(obj any, pfx string, indent string) (b []byte, err error) = map[string]func(obj any, pfx string, indent string) (b []byte, err error){
|
||||
mediaJSON: json.MarshalIndent,
|
||||
mediaXML: xml.MarshalIndent,
|
||||
}
|
||||
okAcceptMime []string = []string{
|
||||
mediaJSON,
|
||||
mediaXML,
|
||||
mediaYAML,
|
||||
mediaHTML,
|
||||
}
|
||||
)
|
17
server/errs.go
Normal file
17
server/errs.go
Normal file
@ -0,0 +1,17 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
`errors`
|
||||
)
|
||||
|
||||
var (
|
||||
ErrEmptyUA error = errors.New("empty user agent string")
|
||||
ErrIncompatFieldType error = errors.New("a field type was passed that is incompatible with the target type")
|
||||
ErrInvalidAccept error = errors.New("an Accept header was encountered that does not conform to RFC 9110§12.5.1/IANA format")
|
||||
ErrInvalidScheme error = errors.New("invalid scheme for listener; must be 'unix', 'tcp', or 'http'")
|
||||
ErrNoArgs error = errors.New("no args.Args passed to server creation")
|
||||
ErrPtrNeeded error = errors.New("structs passed to reflection must be pointers")
|
||||
ErrStructNeeded error = errors.New("pointers passed to reflection must point to structs")
|
||||
ErrUnhandledField error = errors.New("unhandled field type passed to reflection")
|
||||
ErrUnsupportedMIME error = errors.New("unsupported MIME type(s)")
|
||||
)
|
430
server/funcs.go
Normal file
430
server/funcs.go
Normal file
@ -0,0 +1,430 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
`fmt`
|
||||
`net`
|
||||
`net/http`
|
||||
`net/url`
|
||||
`os`
|
||||
`path/filepath`
|
||||
`reflect`
|
||||
`sort`
|
||||
`strings`
|
||||
|
||||
`github.com/mileusna/useragent`
|
||||
`r00t2.io/clientinfo/args`
|
||||
`r00t2.io/goutils/logging`
|
||||
`r00t2.io/goutils/multierr`
|
||||
`r00t2.io/sysutils/paths`
|
||||
)
|
||||
|
||||
// NewClient returns a R00tClient from a UA string.
|
||||
func NewClient(uaStr string) (r *R00tClient, err error) {
|
||||
|
||||
var newR R00tClient
|
||||
var ua useragent.UserAgent
|
||||
|
||||
if strings.TrimSpace(uaStr) == "" {
|
||||
err = ErrEmptyUA
|
||||
return
|
||||
}
|
||||
|
||||
ua = useragent.Parse(uaStr)
|
||||
|
||||
if err = reflectClient(&ua, &newR); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
newR.ua = &ua
|
||||
|
||||
r = &newR
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// NewServer returns a Server ready to use. Be sure to call Close to free up resources when done.
|
||||
func NewServer(log logging.Logger, cliArgs *args.Args) (srv *Server, err error) {
|
||||
|
||||
var s Server
|
||||
var udsSockPerms args.UdsPerms
|
||||
|
||||
if log == nil {
|
||||
log = &logging.NullLogger{}
|
||||
}
|
||||
if cliArgs == nil {
|
||||
err = ErrNoArgs
|
||||
log.Err("server.NewServer: Received error creating server: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
s = Server{
|
||||
log: log,
|
||||
args: cliArgs,
|
||||
mux: http.NewServeMux(),
|
||||
sock: nil,
|
||||
reloadChan: make(chan os.Signal),
|
||||
stopChan: make(chan os.Signal),
|
||||
}
|
||||
|
||||
s.mux.HandleFunc("/", s.handleDefault)
|
||||
s.mux.HandleFunc("/about", s.handleAbout)
|
||||
s.mux.HandleFunc("/about.html", s.handleAbout)
|
||||
s.mux.HandleFunc("/usage", s.handleUsage)
|
||||
s.mux.HandleFunc("/usage.html", s.handleUsage)
|
||||
s.mux.HandleFunc("/favicon.ico", s.explicit404)
|
||||
|
||||
if s.listenUri, err = url.Parse(cliArgs.Listen.Listen); err != nil {
|
||||
s.log.Err("server.NewServer: Failed to parse listener URI: %v", err)
|
||||
return
|
||||
}
|
||||
s.listenUri.Scheme = strings.ToLower(s.listenUri.Scheme)
|
||||
|
||||
switch s.listenUri.Scheme {
|
||||
case "unix":
|
||||
if udsSockPerms, err = cliArgs.ModesAndOwners(); err != nil {
|
||||
s.log.Err("server.NewServer: Failed to parse unix socket permissions: %v", err)
|
||||
return
|
||||
}
|
||||
if err = paths.RealPath(&s.listenUri.Path); err != nil {
|
||||
s.log.Err("server.NewServer: Failed to canonize/resolve socket path '%s': %v", s.listenUri.Path, err)
|
||||
return
|
||||
}
|
||||
// Cleanup any stale socket.
|
||||
if err = s.cleanup(true); err != nil {
|
||||
s.log.Err("server.NewServer: Failed to cleanup for 'unix' listener: %v", err)
|
||||
return
|
||||
}
|
||||
if err = os.MkdirAll(filepath.Dir(s.listenUri.Path), udsSockPerms.DMode); err != nil {
|
||||
s.log.Err("server.NewServer: Received error creating socket directory '%s': %v", filepath.Dir(s.listenUri.Path), err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = os.Chmod(filepath.Dir(s.listenUri.Path), udsSockPerms.DMode); err != nil {
|
||||
s.log.Err("server.NewServer: Received error chmodding socket directory '%s': %v", filepath.Dir(s.listenUri.Path), err)
|
||||
return
|
||||
}
|
||||
if err = os.Chown(filepath.Dir(s.listenUri.Path), udsSockPerms.UID, udsSockPerms.DGID); err != nil {
|
||||
s.log.Err("server.NewServer: Received error chowning socket directory '%s': %v", filepath.Dir(s.listenUri.Path), err)
|
||||
return
|
||||
}
|
||||
if s.listenUri, err = url.Parse(
|
||||
fmt.Sprintf(
|
||||
"%s://%s",
|
||||
s.listenUri.Scheme, s.listenUri.Path,
|
||||
),
|
||||
); err != nil {
|
||||
s.log.Err("server.NewServer: Failed to re-parse listener URI: %v", err)
|
||||
return
|
||||
}
|
||||
if s.sock, err = net.Listen("unix", s.listenUri.Path); err != nil {
|
||||
s.log.Err("server.NewServer: Failed to open socket on '%s': %v", s.listenUri.Path, err)
|
||||
}
|
||||
if err = os.Chmod(s.listenUri.Path, udsSockPerms.FMode); err != nil {
|
||||
s.log.Err("server.NewServer: Received error chmodding socket '%s': %v", filepath.Dir(s.listenUri.Path), err)
|
||||
return
|
||||
}
|
||||
if err = os.Chown(s.listenUri.Path, udsSockPerms.UID, udsSockPerms.FGID); err != nil {
|
||||
s.log.Err("server.NewServer: Received error chowning socket '%s': %v", filepath.Dir(s.listenUri.Path), err)
|
||||
return
|
||||
}
|
||||
case "http", "tcp":
|
||||
s.isHttp = s.listenUri.Scheme == "http"
|
||||
if err = s.cleanup(true); err != nil {
|
||||
s.log.Err("server.NewServer: Failed to cleanup for '%s' listener: %v", strings.ToUpper(s.listenUri.Scheme), err)
|
||||
return
|
||||
}
|
||||
if s.listenUri, err = url.Parse(
|
||||
fmt.Sprintf(
|
||||
"%s://%s%s",
|
||||
s.listenUri.Scheme, s.listenUri.Host, s.listenUri.Path,
|
||||
),
|
||||
); err != nil {
|
||||
s.log.Err("server.NewServer: Failed to re-parse listener URI: %v", err)
|
||||
return
|
||||
}
|
||||
if s.sock, err = net.Listen("tcp", s.listenUri.Host); err != nil {
|
||||
s.log.Err("server.NewServer: Failed to open %s socket on '%s': %v", strings.ToUpper(s.listenUri.Scheme), s.listenUri.Host, err)
|
||||
return
|
||||
}
|
||||
default:
|
||||
s.log.Err("server.NewServer: Unsupported scheme: %v", s.listenUri.Scheme)
|
||||
err = ErrInvalidScheme
|
||||
return
|
||||
}
|
||||
cliArgs.Listen.Listen = s.listenUri.String()
|
||||
|
||||
srv = &s
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
decideParseAccept takes the slice returned from parseAccept, runs parseAccept on it,
|
||||
and chooses based on what MIME types are supported by this program.
|
||||
err will be an ErrUnsupportedMIME if no supported MIME type is found.
|
||||
If parsed is nil or empty, format will be defFormat and err will be nil.
|
||||
*/
|
||||
func decideParseAccept(parsed []*parsedMIME, defFormat string) (format string, err error) {
|
||||
|
||||
var customFmtFound bool
|
||||
|
||||
if parsed == nil || len(parsed) == 0 {
|
||||
format = defFormat
|
||||
return
|
||||
}
|
||||
|
||||
for _, pf := range parsed {
|
||||
switch pf.MIME {
|
||||
case "*/*": // Client explicitly accept anything
|
||||
format = defFormat
|
||||
customFmtFound = true
|
||||
case "application/*": // Use JSON
|
||||
format = mediaJSON
|
||||
customFmtFound = true
|
||||
case "text/*": // Use HTML
|
||||
format = mediaHTML
|
||||
customFmtFound = true
|
||||
case mediaHTML, mediaJSON, mediaXML, mediaYAML:
|
||||
format = pf.MIME
|
||||
customFmtFound = true
|
||||
}
|
||||
if customFmtFound {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !customFmtFound {
|
||||
format = defFormat
|
||||
err = ErrUnsupportedMIME
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
reflectClient takes a src and dst and attempts to set/convert src to dst. It is *VERY STRICT*.
|
||||
It is expected that src does NOT use pointers.
|
||||
...This is pretty much just custom-made for converting a useragent.UserAgent to a R00tClient.
|
||||
Don't use it for anything else.
|
||||
*/
|
||||
func reflectClient(src, dst any) (err error) {
|
||||
|
||||
var dstField reflect.StructField
|
||||
var dstFieldVal reflect.Value
|
||||
var srcFieldVal reflect.Value
|
||||
var srcField string
|
||||
var ok bool
|
||||
var intVal *int
|
||||
var strVal *string
|
||||
var boolVal *bool
|
||||
var srcVal reflect.Value = reflect.ValueOf(src)
|
||||
var dstVal reflect.Value = reflect.ValueOf(dst)
|
||||
|
||||
// Both must be ptrs to a struct
|
||||
if srcVal.Kind() != reflect.Ptr || dstVal.Kind() != reflect.Ptr {
|
||||
err = ErrPtrNeeded
|
||||
return
|
||||
}
|
||||
|
||||
srcVal = srcVal.Elem()
|
||||
dstVal = dstVal.Elem()
|
||||
|
||||
/*
|
||||
Now that we have the underlying type/value of the ptr above,
|
||||
check for structs.
|
||||
*/
|
||||
if srcVal.Kind() != reflect.Struct || dstVal.Kind() != reflect.Struct {
|
||||
err = ErrStructNeeded
|
||||
return
|
||||
}
|
||||
|
||||
for i := 0; i < dstVal.NumField(); i++ {
|
||||
dstField = dstVal.Type().Field(i)
|
||||
dstFieldVal = dstVal.Field(i)
|
||||
|
||||
// Skip unexported
|
||||
if !dstFieldVal.CanSet() {
|
||||
continue
|
||||
}
|
||||
srcField = dstField.Tag.Get(convertTag)
|
||||
// Skip explicitly skipped (<convertTag>:"-")
|
||||
if srcField == "-" {
|
||||
continue
|
||||
}
|
||||
// If no explicit field name is present, set it to the dst field name.
|
||||
if _, ok = dstField.Tag.Lookup(convertTag); !ok {
|
||||
srcField = dstField.Name
|
||||
}
|
||||
// Get the value from src
|
||||
srcFieldVal = srcVal.FieldByName(srcField)
|
||||
// Skip invalid...
|
||||
if !srcFieldVal.IsValid() {
|
||||
continue
|
||||
}
|
||||
// And zero-value.
|
||||
if reflect.DeepEqual(srcFieldVal.Interface(), reflect.Zero(srcFieldVal.Type()).Interface()) {
|
||||
continue
|
||||
}
|
||||
// Structs need to recurse.
|
||||
if dstFieldVal.Kind() == reflect.Ptr && dstFieldVal.Type().Elem().Kind() == reflect.Struct {
|
||||
// Ensure we don't have a nil ptr
|
||||
if dstFieldVal.IsNil() {
|
||||
dstFieldVal.Set(reflect.New(dstFieldVal.Type().Elem()))
|
||||
}
|
||||
// And recurse into it.
|
||||
if err = reflectClient(srcFieldVal.Addr().Interface(), dstFieldVal.Interface()); err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// Everything else gets assigned here.
|
||||
switch dstFieldVal.Kind() {
|
||||
case reflect.Bool:
|
||||
if srcFieldVal.Kind() == reflect.Bool {
|
||||
dstFieldVal.Set(reflect.ValueOf(srcFieldVal.Interface().(bool)))
|
||||
} else {
|
||||
err = ErrIncompatFieldType
|
||||
return
|
||||
}
|
||||
case reflect.String:
|
||||
if srcFieldVal.Kind() == reflect.String {
|
||||
dstFieldVal.Set(reflect.ValueOf(srcFieldVal.Interface().(string)))
|
||||
} else {
|
||||
err = ErrIncompatFieldType
|
||||
return
|
||||
}
|
||||
case reflect.Int:
|
||||
if srcFieldVal.Kind() == reflect.Int {
|
||||
dstFieldVal.Set(reflect.ValueOf(srcFieldVal.Interface().(int)))
|
||||
} else {
|
||||
err = ErrIncompatFieldType
|
||||
return
|
||||
}
|
||||
case reflect.Ptr:
|
||||
// Pointers to above
|
||||
switch dstFieldVal.Type().Elem().Kind() {
|
||||
case reflect.Bool:
|
||||
if srcFieldVal.Kind() == reflect.Bool {
|
||||
boolVal = new(bool)
|
||||
*boolVal = srcFieldVal.Interface().(bool)
|
||||
dstFieldVal.Set(reflect.ValueOf(boolVal))
|
||||
} else {
|
||||
err = ErrIncompatFieldType
|
||||
return
|
||||
}
|
||||
case reflect.String:
|
||||
if srcFieldVal.Kind() == reflect.String {
|
||||
strVal = new(string)
|
||||
*strVal = srcFieldVal.Interface().(string)
|
||||
dstFieldVal.Set(reflect.ValueOf(strVal))
|
||||
} else {
|
||||
err = ErrIncompatFieldType
|
||||
return
|
||||
}
|
||||
case reflect.Int:
|
||||
if srcFieldVal.Kind() == reflect.Int {
|
||||
intVal = new(int)
|
||||
*intVal = srcFieldVal.Interface().(int)
|
||||
dstFieldVal.Set(reflect.ValueOf(intVal))
|
||||
} else {
|
||||
err = ErrIncompatFieldType
|
||||
return
|
||||
}
|
||||
default:
|
||||
err = ErrUnhandledField
|
||||
return
|
||||
}
|
||||
default:
|
||||
err = ErrUnhandledField
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// parseAccept parses an Accept header as per RFC 9110 § 12.5.1.
|
||||
func parseAccept(hdrVal string) (parsed []*parsedMIME, err error) {
|
||||
|
||||
var mimes []string
|
||||
var parts []string
|
||||
var params []string
|
||||
var paramsLen int
|
||||
var kv []string
|
||||
var mt *parsedMIME
|
||||
var mErr *multierr.MultiError = multierr.NewMultiError(nil)
|
||||
|
||||
if hdrVal == "" {
|
||||
return
|
||||
}
|
||||
|
||||
mimes = strings.Split(hdrVal, ",")
|
||||
|
||||
for _, mime := range mimes {
|
||||
mt = &parsedMIME{
|
||||
MIME: "",
|
||||
Weight: 1.0, // between 0.0 and 1.0
|
||||
Params: nil,
|
||||
}
|
||||
mime = strings.TrimSpace(mime)
|
||||
// Split into []string{<type>[, <param>, ...]}
|
||||
parts = strings.Split(mime, ";")
|
||||
if parts == nil || len(parts) < 1 {
|
||||
mErr.AddError(ErrInvalidAccept)
|
||||
continue
|
||||
}
|
||||
if parts[0] == "" {
|
||||
mErr.AddError(ErrInvalidAccept)
|
||||
continue
|
||||
}
|
||||
if len(strings.Split(parts[0], "/")) != 2 {
|
||||
mErr.AddError(ErrInvalidAccept)
|
||||
continue
|
||||
}
|
||||
mt.MIME = strings.TrimSpace(parts[0])
|
||||
if len(parts) > 1 {
|
||||
// Parameters were provided. We don't really use them except `q`, but...
|
||||
params = parts[1:]
|
||||
paramsLen = len(params)
|
||||
for idx, param := range params {
|
||||
param = strings.TrimSpace(param)
|
||||
kv = strings.SplitN(param, "=", 2)
|
||||
if len(kv) != 2 {
|
||||
mErr.AddError(ErrInvalidAccept)
|
||||
continue
|
||||
}
|
||||
if kv[0] == "q" && idx == paramsLen-1 {
|
||||
// It's the weight. RFC's pretty clear it's the last param.
|
||||
fmt.Sscanf(kv[1], "%f", &mt.Weight)
|
||||
if mt.Weight > 1.0 || mt.Weight < 0.0 {
|
||||
mErr.AddError(ErrInvalidAccept)
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
if mt.Params == nil {
|
||||
mt.Params = make(map[string]string)
|
||||
}
|
||||
mt.Params[kv[0]] = kv[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
parsed = append(parsed, mt)
|
||||
}
|
||||
|
||||
// Now sort by weight (descending).
|
||||
sort.SliceStable(
|
||||
parsed,
|
||||
func(i, j int) (isBefore bool) {
|
||||
isBefore = parsed[i].Weight > parsed[j].Weight
|
||||
return
|
||||
},
|
||||
)
|
||||
|
||||
if !mErr.IsEmpty() {
|
||||
err = mErr
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
12
server/funcs_page.go
Normal file
12
server/funcs_page.go
Normal file
@ -0,0 +1,12 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
`fmt`
|
||||
)
|
||||
|
||||
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())
|
||||
|
||||
return
|
||||
}
|
88
server/funcs_r00tclient.go
Normal file
88
server/funcs_r00tclient.go
Normal file
@ -0,0 +1,88 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
`reflect`
|
||||
`strings`
|
||||
)
|
||||
|
||||
/*
|
||||
ToMap generates and returns a map representation of a R00tClient.
|
||||
|
||||
Keys by default use the YAML tag for the name.
|
||||
If they are specified with the tag `renderName:"-"`, they are skipped.
|
||||
If they are specified with the tag `renderName:"Foo"`, the string "Foo" will
|
||||
be used as the key instead.
|
||||
Only bools, strings, and pointers thereof are allowed.
|
||||
|
||||
m will never be nil, but may be empty.
|
||||
|
||||
Currently err will always be nil but is specified for future API compatibility.
|
||||
It should be handled by callers for future-proofing, as it may not always be nil
|
||||
in the future.
|
||||
*/
|
||||
func (r *R00tClient) ToMap() (m map[string]string, err error) {
|
||||
|
||||
var ok bool
|
||||
var tagVal string
|
||||
var field reflect.StructField
|
||||
var fieldVal reflect.Value
|
||||
var rootVal reflect.Value
|
||||
|
||||
m = make(map[string]string)
|
||||
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
rootVal = reflect.ValueOf(r).Elem()
|
||||
|
||||
for i := 0; i < rootVal.NumField(); i++ {
|
||||
field = rootVal.Type().Field(i)
|
||||
fieldVal = rootVal.Field(i)
|
||||
|
||||
// Only exported.
|
||||
if field.PkgPath != "" {
|
||||
continue
|
||||
}
|
||||
// Get the key name.
|
||||
tagVal = field.Tag.Get(prettyTag)
|
||||
if tagVal == "-" {
|
||||
continue
|
||||
}
|
||||
if _, ok = field.Tag.Lookup(prettyTag); !ok {
|
||||
tagVal = field.Tag.Get("yaml")
|
||||
if tagVal == "" || strings.HasPrefix(tagVal, "-") {
|
||||
// Use the field name itself. YOLO
|
||||
tagVal = field.Name
|
||||
} else {
|
||||
tagVal = strings.Split(tagVal, ",")[0]
|
||||
}
|
||||
}
|
||||
switch fieldVal.Kind() {
|
||||
case reflect.Bool:
|
||||
if fieldVal.Interface().(bool) {
|
||||
m[tagVal] = trueUaFieldStr
|
||||
} else {
|
||||
m[tagVal] = falseUaFieldStr
|
||||
}
|
||||
case reflect.String:
|
||||
m[tagVal] = fieldVal.String()
|
||||
case reflect.Ptr:
|
||||
if fieldVal.IsNil() {
|
||||
m[tagVal] = nilUaFieldStr
|
||||
} else {
|
||||
switch fieldVal.Type().Elem().Kind() {
|
||||
case reflect.Bool:
|
||||
if fieldVal.Elem().Bool() {
|
||||
m[tagVal] = trueUaFieldStr
|
||||
} else {
|
||||
m[tagVal] = falseUaFieldStr
|
||||
}
|
||||
case reflect.String:
|
||||
m[tagVal] = fieldVal.Elem().String()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
860
server/funcs_server.go
Normal file
860
server/funcs_server.go
Normal file
@ -0,0 +1,860 @@
|
||||
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
|
||||
}
|
95
server/funcs_test.go
Normal file
95
server/funcs_test.go
Normal file
@ -0,0 +1,95 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
`encoding/json`
|
||||
`encoding/xml`
|
||||
`fmt`
|
||||
"testing"
|
||||
|
||||
`github.com/davecgh/go-spew/spew`
|
||||
`github.com/goccy/go-yaml`
|
||||
)
|
||||
|
||||
func TestNewClient(t *testing.T) {
|
||||
|
||||
var err error
|
||||
var b []byte
|
||||
var r *R00tClient
|
||||
|
||||
for _, s := range []string{
|
||||
"Mozilla/5.0 " +
|
||||
"(X11; Linux x86_64) " +
|
||||
"AppleWebKit/537.36 " +
|
||||
"(KHTML, like Gecko) " +
|
||||
"Chrome/131.0.0.0 " +
|
||||
"Safari/537.36", // Chrome
|
||||
"Mozilla/5.0 " +
|
||||
"(X11; Linux x86_64; rv:133.0) " +
|
||||
"Gecko/20100101 " +
|
||||
"Firefox/133.0", // Firefox
|
||||
"curl/8.11.0", // Curl
|
||||
"Wget/1.25.0", // Wget
|
||||
} {
|
||||
t.Logf("Raw UA: '%s'\n\n", s)
|
||||
if r, err = NewClient(s); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if b, err = json.Marshal(r); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fmt.Println(string(b))
|
||||
t.Logf("R00tClient:\n%s\n\n\n", spew.Sdump(r))
|
||||
}
|
||||
}
|
||||
|
||||
func TestExplicitContent(t *testing.T) {
|
||||
|
||||
var b []byte
|
||||
var err error
|
||||
var r *R00tClient = &R00tClient{
|
||||
ClientVer: &Ver{
|
||||
Major: 1,
|
||||
Minor: 2,
|
||||
Patch: 3,
|
||||
},
|
||||
OSVer: &Ver{
|
||||
Major: 9,
|
||||
Minor: 8,
|
||||
Patch: 7,
|
||||
},
|
||||
URL: new(string),
|
||||
String: new(string),
|
||||
Name: new(string),
|
||||
ClientVerStr: new(string),
|
||||
OS: new(string),
|
||||
OsVerStr: new(string),
|
||||
Dev: new(string),
|
||||
IsMobile: false,
|
||||
IsTablet: false,
|
||||
IsDesktop: false,
|
||||
IsBot: false,
|
||||
}
|
||||
|
||||
*r.URL = "https://datatracker.ietf.org/doc/html/rfc2324.html"
|
||||
*r.String = "(COMPLETE USER AGENT STRING)"
|
||||
*r.Name = "coffee_pot"
|
||||
*r.ClientVerStr = "1.2.3"
|
||||
*r.OS = "JavaOS"
|
||||
*r.OsVerStr = "9.8.7"
|
||||
*r.Dev = "mocha-latte"
|
||||
|
||||
if b, err = json.MarshalIndent(r, "", " "); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fmt.Println(string(b))
|
||||
|
||||
if b, err = xml.Marshal(r); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fmt.Println(string(b))
|
||||
|
||||
if b, err = yaml.Marshal(r); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fmt.Println(string(b))
|
||||
}
|
18
server/funcs_tpl.go
Normal file
18
server/funcs_tpl.go
Normal file
@ -0,0 +1,18 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
`fmt`
|
||||
`strings`
|
||||
)
|
||||
|
||||
func getTitle(subPage string) (title string) {
|
||||
|
||||
if subPage == "" || subPage == "index" {
|
||||
title = baseTitle
|
||||
return
|
||||
}
|
||||
|
||||
title = fmt.Sprintf("%s%s%s", baseTitle, titleSep, strings.ToTitle(subPage))
|
||||
|
||||
return
|
||||
}
|
160
server/funcs_xmlheaders.go
Normal file
160
server/funcs_xmlheaders.go
Normal file
@ -0,0 +1,160 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
`encoding/xml`
|
||||
`errors`
|
||||
`io`
|
||||
)
|
||||
|
||||
/*
|
||||
MarshalXML encodes an XmlHeaders as XML in the following format:
|
||||
|
||||
(<headers>)
|
||||
<header name="SomeHeader">
|
||||
<value>SomeValue</value>
|
||||
</header>
|
||||
<header name="SomeMultiValueHeader">
|
||||
<value>Foo</value>
|
||||
<value>Bar</value>
|
||||
</header>
|
||||
(</headers>)
|
||||
|
||||
For the above example, the field should be specified as `xml:"headers"`.
|
||||
However, the parent element name may be whatever you wish.
|
||||
*/
|
||||
func (x XmlHeaders) MarshalXML(e *xml.Encoder, start xml.StartElement) (err error) {
|
||||
|
||||
var curKey string
|
||||
var vals []string
|
||||
var val string
|
||||
var hdr xml.StartElement
|
||||
var child xml.StartElement
|
||||
// TODO: Does xml.EncodeElement properly escape?
|
||||
// var escKBuf *bytes.Buffer
|
||||
// var escVBuf *bytes.Buffer
|
||||
|
||||
// All values are []string, so we don't need any fancy parsing or switching or the like.
|
||||
// We do need to make sure we escape, though.
|
||||
|
||||
if err = e.EncodeToken(start); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if x != nil && len(x) > 0 {
|
||||
// escKBuf = new(bytes.Buffer)
|
||||
// escVBuf = new(bytes.Buffer)
|
||||
for curKey, vals = range x {
|
||||
// escKBuf.Reset()
|
||||
// if err = xml.EscapeText(escKBuf, []byte(curKey)); err != nil {
|
||||
// return
|
||||
// }
|
||||
hdr = xml.StartElement{
|
||||
Name: xml.Name{
|
||||
Local: xmlHdrElem,
|
||||
},
|
||||
Attr: []xml.Attr{
|
||||
xml.Attr{
|
||||
Name: xml.Name{
|
||||
Local: xmlHdrElemName,
|
||||
},
|
||||
// Value: escKBuf.String(),
|
||||
Value: curKey,
|
||||
},
|
||||
},
|
||||
}
|
||||
if err = e.EncodeToken(hdr); err != nil {
|
||||
return
|
||||
}
|
||||
for _, val = range vals {
|
||||
// escVBuf.Reset()
|
||||
// if err = xml.EscapeText(escVBuf, []byte(val)); err != nil {
|
||||
// return
|
||||
// }
|
||||
child = xml.StartElement{
|
||||
Name: xml.Name{
|
||||
Local: xmlHdrVal,
|
||||
},
|
||||
}
|
||||
// if err = e.EncodeElement(escVBuf.String(), child); err != nil {
|
||||
if err = e.EncodeElement(val, child); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
if err = e.EncodeToken(hdr.End()); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err = e.EncodeToken(start.End()); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// UnmarshalXML populates an XMLHeaders from an XML representation. See MarshalXML for example XML.
|
||||
func (x *XmlHeaders) UnmarshalXML(d *xml.Decoder, start xml.StartElement) (err error) {
|
||||
|
||||
var tok xml.Token
|
||||
var xm XmlHeaders
|
||||
var hdrNm string
|
||||
var vals []string
|
||||
var val *string
|
||||
var nameFound bool
|
||||
|
||||
for {
|
||||
if tok, err = d.Token(); err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
err = nil
|
||||
break
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
switch elem := tok.(type) {
|
||||
case xml.StartElement:
|
||||
switch elem.Name.Local {
|
||||
case xmlHdrElem:
|
||||
nameFound = false
|
||||
vals = nil
|
||||
for _, a := range elem.Attr {
|
||||
if a.Name.Local == xmlHdrElemName {
|
||||
nameFound = true
|
||||
hdrNm = a.Value
|
||||
break
|
||||
}
|
||||
}
|
||||
if !nameFound {
|
||||
continue
|
||||
}
|
||||
case xmlHdrVal:
|
||||
if !nameFound {
|
||||
continue
|
||||
}
|
||||
if vals == nil {
|
||||
vals = make([]string, 0, 1)
|
||||
}
|
||||
val = new(string)
|
||||
if err = d.DecodeElement(val, &elem); err != nil {
|
||||
return
|
||||
}
|
||||
vals = append(vals, *val)
|
||||
}
|
||||
case xml.EndElement:
|
||||
if elem.Name.Local != xmlHdrElem {
|
||||
continue
|
||||
}
|
||||
if xm == nil {
|
||||
xm = make(XmlHeaders)
|
||||
}
|
||||
xm[hdrNm] = vals
|
||||
}
|
||||
}
|
||||
|
||||
if xm != nil {
|
||||
*x = xm
|
||||
}
|
||||
|
||||
return
|
||||
}
|
45
server/tpl/about.html.tpl
Normal file
45
server/tpl/about.html.tpl
Normal file
@ -0,0 +1,45 @@
|
||||
{{- /*gotype: r00t2.io/clientinfo/server.Page*/ -}}
|
||||
{{- define "about" }}
|
||||
{{- $page := . -}}
|
||||
{{- $linkico := "🔗" }}
|
||||
{{ template "meta.top" $page }}
|
||||
<div class="jumbotron">
|
||||
<h1>About</h1>
|
||||
</div>
|
||||
<p>
|
||||
This is a tool to reveal certain information about your connection that the server sees.
|
||||
Note that all of this information you see is <i>sent by your client</i>;
|
||||
there was no probing/scanning or the like done from the server this site is hosted on.
|
||||
</p>
|
||||
<p>
|
||||
If you don't like this info being available to server administrators of the websites
|
||||
you visit you may want to consider:
|
||||
<ul>
|
||||
<li><a href="https://www.torproject.org/">hiding your client IP address</a></li>
|
||||
<li>
|
||||
<a href="https://panopticlick.eff.org/self-defense">hiding your browser's metadata, which can be done via browser plugins such as:</a>
|
||||
<ul>
|
||||
<li><a href="https://www.eff.org/privacybadger">Privacy Badger</a></li>
|
||||
<li><a href="https://addons.mozilla.org/en-US/firefox/addon/modify-headers/">Modify Headers</a></li>
|
||||
<li><a href="https://www.requestly.in/">Requestly</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
There are, of course, many other plugins/methods but as always, due diligence is required when finding the right plugin for you.
|
||||
Be sure to read multiple reviews.
|
||||
Some plugins/extensions even disguise your browser as an entirely different operating system, OS version, etc.
|
||||
Feel free to check back on this site after enabling them to test! (You may need to reset your browser's cache.)
|
||||
</p>
|
||||
<p>
|
||||
If you would like to view the <i>server</i> headers, then you can:
|
||||
<ul>
|
||||
<li>use a service such as <a href="https://securityheaders.io">SecurityHeaders.io</a></li>
|
||||
<li>use the <code>--head</code> (<code>-i</code>, <code>-X HEAD</code>, <code>--request HEAD</code>, etc.; all do the same thing) argument to <b><code>curl</code></b></li>
|
||||
<li>use the <code>-v</code> (<code>--verbose</code>) argument to <b><code>curl</code></b></li>
|
||||
<li>use your browser's built-in developer console (<a href="https://firefox-source-docs.mozilla.org/devtools-user/">on Firefox</a>, <a href="https://developer.chrome.com/docs/devtools/open">on Chrome and Chrome-based browsers</a>)</li>
|
||||
</ul>
|
||||
There are additionally some extensions/plugins that offer this in a directly-accessible button on the toolbar.
|
||||
</p>
|
||||
<br />
|
||||
{{- template "meta.bottom" $page }}
|
||||
{{- end }}
|
14
server/tpl/index.html.tpl
Normal file
14
server/tpl/index.html.tpl
Normal file
@ -0,0 +1,14 @@
|
||||
{{- /*gotype: r00t2.io/clientinfo/server.Page*/ -}}
|
||||
{{- define "index" }}
|
||||
{{- $page := . -}}
|
||||
{{- $linkico := "🔗" }}
|
||||
{{- template "meta.top" $page }}
|
||||
<div class="jumbotron">
|
||||
<h1>Client Info Revealer</h1>
|
||||
<p class="lead">A tool to reveal client-identifying data sent to webservers</p>
|
||||
</div>
|
||||
<div>
|
||||
{{- template "meta.info" $page }}
|
||||
</div>
|
||||
{{- template "meta.bottom" $page }}
|
||||
{{- end }}
|
11
server/tpl/meta.bottom.html.tpl
Normal file
11
server/tpl/meta.bottom.html.tpl
Normal file
@ -0,0 +1,11 @@
|
||||
{{- /*gotype: r00t2.io/clientinfo/server.Page*/ -}}
|
||||
{{- define "meta.bottom" -}}
|
||||
{{- $page := . -}}
|
||||
{{- $linkico := "🔗" }}
|
||||
<footer class="footer">
|
||||
<p><sub>See <a href="https://pkg.go.dev/r00t2.io/clientinfo">https://pkg.go.dev/r00t2.io/clientinfo</a> for more information on this program.</sub></p>
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
{{- end -}}
|
55
server/tpl/meta.info.html.tpl
Normal file
55
server/tpl/meta.info.html.tpl
Normal file
@ -0,0 +1,55 @@
|
||||
{{- /*gotype: r00t2.io/clientinfo/server.Page*/ -}}
|
||||
{{- define "meta.info" -}}
|
||||
{{- $page := . -}}
|
||||
{{- $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>
|
||||
<br/>
|
||||
<i>You are connecting with port <b>{{ $page.Info.Port }}</b> outbound.</i>
|
||||
</p>
|
||||
{{- if $page.Raw }}
|
||||
<h3 id="client_raw">Raw Block ({{ $page.RawFmt }})<a href="#client_raw">{{ $linkico }}</a></h3>
|
||||
<p>
|
||||
{{- if $page.DoRawIndent }}
|
||||
<pre>{{ $page.Raw }}</pre>
|
||||
{{- else }}
|
||||
<code>{{ $page.Raw }}</code>
|
||||
{{- end }}
|
||||
</p>
|
||||
{{- end }}
|
||||
<h3 id="client_ua">User Agent Information<a href="#client_ua">{{ $linkico }}</a></h3>
|
||||
<p>This is information that your browser sends to identify itself.</p>
|
||||
<p>
|
||||
{{- range $idx, $ua := $page.Info.Client }}
|
||||
User Agent ({{ $idx }}):
|
||||
<ul>
|
||||
{{- $flds := $ua.ToMap }}
|
||||
{{- range $fld, $str := $flds }}
|
||||
<li><b>{{ $fld }}:</b> {{ $str }}</li>
|
||||
{{- end }}
|
||||
</ul>
|
||||
{{- end }}
|
||||
</p>
|
||||
<h3 id="client_hdrs">Request Headers<a href="#client_hdrs">{{ $linkico }}</a></h3>
|
||||
<p>
|
||||
These are headers sent along with the request your browser sends for the page's content.
|
||||
Note that some headers may have multiple values.
|
||||
</p>
|
||||
<p>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Header</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
{{- range $hdrNm, $hdrVals := $page.Info.Req.Header }}
|
||||
<tr>
|
||||
{{- range $val := $hdrVals }}
|
||||
<td>{{ $hdrNm }}</td>
|
||||
<td>{{ $val }}</td>
|
||||
{{- end }}
|
||||
</tr>
|
||||
{{- end }}
|
||||
</table>
|
||||
</p>
|
||||
{{- end }}
|
53
server/tpl/meta.top.html.tpl
Normal file
53
server/tpl/meta.top.html.tpl
Normal file
@ -0,0 +1,53 @@
|
||||
{{- /*gotype: r00t2.io/clientinfo/server.Page*/ -}}
|
||||
{{- define "meta.top" -}}
|
||||
{{- $page := . -}}
|
||||
{{- $linkico := "🔗" -}}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>{{ getTitle $page.PageType }}</title>
|
||||
<!-- Bootstrap core CSS -->
|
||||
<!--
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
|
||||
-->
|
||||
<!-- Tachyons -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/tachyons@4.12.0/css/tachyons.min.css"/>
|
||||
<!--
|
||||
Custom styles for Bootstrap
|
||||
-->
|
||||
<!--
|
||||
<link href="https://getbootstrap.com/examples/jumbotron-narrow/jumbotron-narrow.css"
|
||||
rel="stylesheet">
|
||||
-->
|
||||
<!--
|
||||
<link href="https://getbootstrap.com/docs/4.0/examples/offcanvas/offcanvas.css" rel="stylesheet">
|
||||
-->
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header clearfix">
|
||||
<nav>
|
||||
<ul class="nav nav-pills pull-right">
|
||||
<li role="presentation"><a href="/">Home</a></li>
|
||||
<li role="presentation"><a href="/about">About</a></li>
|
||||
<li role="presentation"><a href="/usage">Usage</a></li>
|
||||
<ul role="presentation">
|
||||
<li><a href="/?mime=application/json&indent">JSON</a></li>
|
||||
<li><a href="/?mime=application/xml&indent">XML</a></li>
|
||||
<li><a href="/?mime=application/yaml">YAML</a></li>
|
||||
<li><a href="/?mime=text/html">HTML (This Page)</a></li>
|
||||
</ul>
|
||||
<!--
|
||||
the following opens in a new tab/window/whatever.
|
||||
the line after opens in the same tab/window/etc.
|
||||
-->
|
||||
<!--
|
||||
<li role="presentation"><a href="https://r00t2.io/" target="_blank">r00t^2</a></li>
|
||||
-->
|
||||
<li role="presentation"><a href="https://r00t2.io/">r00t^2</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
{{- end }}
|
95
server/tpl/usage.html.tpl
Normal file
95
server/tpl/usage.html.tpl
Normal file
@ -0,0 +1,95 @@
|
||||
{{- /*gotype: r00t2.io/clientinfo/server.Page*/ -}}
|
||||
{{- define "usage" }}
|
||||
{{- $page := . -}}
|
||||
{{- $linkico := "🔗" }}
|
||||
{{- template "meta.top" $page }}
|
||||
<div class="jumbotron">
|
||||
<h1>Usage</h1>
|
||||
</div>
|
||||
<h2 id="usage_params">Parameters<a href="#usage_params">{{ $linkico }}</a></h2>
|
||||
<p>
|
||||
You can control how the <a href="/">main page</a> displays/renders.
|
||||
By default, it will try to "guess" what you want; e.g. if you access it in Chrome, it will return HTML but if you fetch via Curl, you'll get raw JSON
|
||||
(or your specified data format; see below). If the classification of client can't be determined and an <code>Accept</code> wasn't specified,
|
||||
a fallback to text-mode (by default <code>application/json</code>) will be returned.
|
||||
<br/>
|
||||
|
||||
You can force a specific raw output by specifying the <a href="https://www.iana.org/assignments/media-types/media-types.xhtml">MIME type</a> via
|
||||
<a href="https://www.rfc-editor.org/rfc/rfc9110.html#section-12.5.1">the <code>Accept</code> header (RFC 9110 § 12.5.1)</a>, which may be one of:
|
||||
<ul>
|
||||
<li><code>application/json</code> for <a href="https://www.rfc-editor.org/rfc/rfc8259.html">JSON</a></li>
|
||||
<li><code>application/xml</code> for <a href="https://www.rfc-editor.org/rfc/rfc7303.html">XML</a></li>
|
||||
<li><code>application/yaml</code> for <a href="https://www.rfc-editor.org/rfc/rfc9512.html">YAML</a></li>
|
||||
<li><code>text/html</code> for <a href="https://www.rfc-editor.org/rfc/rfc2854.html">HTML</a></li>
|
||||
</ul>
|
||||
For example: <code>Accept: application/json</code> will return JSON.
|
||||
<br/>
|
||||
|
||||
If unspecified and it is a text-mode client (e.g. Curl), the default is <code>application/json</code>.
|
||||
<code>text/html</code> may be used to force an HTML response from a text-only client,
|
||||
just as one of the <code>application/*</code> MIME types above may be used to force that "raw" text MIME type for a "graphical" browser client.
|
||||
The specification as defined by <a href="https://www.rfc-editor.org/rfc/rfc9110.html#section-12.5.1">RFC 9110 § 12.5.1</a> is completely
|
||||
valid to pass and will be parsed without error (provided the header value is RFC-compliant and IANA-compliant),
|
||||
though note that <code>application/xml</code> and <code>text/html</code>'s <code>charset</code> parameter will be entirely ignored;
|
||||
the returned XML/HTML is <b>always</b> Unicode (with UTF-8 encoding).
|
||||
<br/>
|
||||
|
||||
If no selectable MIME type is provided but an <code>Accept</code> was given, an error will be returned; specifically, a
|
||||
<a href="https://www.rfc-editor.org/rfc/rfc9110.html#section-15.5.7"><code>406</code> status code (RFC 9110 § 15.5.7)</a>.
|
||||
In this case, supported MIME types will be returned in the response's <code>Accept</code> header.
|
||||
<br/>
|
||||
|
||||
Note that <a href="https://lynx.invisible-island.net/">Lynx</a> and <a href="http://elinks.or.cz/">Elinks</a> are considered "graphical"
|
||||
browsers by this program as they are HTML-centric.
|
||||
</p>
|
||||
<p id="usage_params_mod">
|
||||
The following parameters control/modify behavior.<a href="#usage_params_mod">{{ $linkico }}</a>
|
||||
<ul>
|
||||
<li>
|
||||
<b>mime:</b> Specify an explicit MIME type via URL instead of the <code>Accept</code> header as specified above.
|
||||
<ul>
|
||||
<li>This should only be used by clients in which it is impossible or particularly cumbersome to modify/specify headers.
|
||||
<code>Accept</code> is more performant.</li>
|
||||
<li>Only the first supported instance of this parameter will be used.</li>
|
||||
<li>Any of the defined MIME types above may be specified (e.g. <code>?mime=application/json</code>).</li>
|
||||
<li>If both this URL query parameter and the <code>Accept</code> header is specified, the URL query takes preference.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<b>include:</b> Include a <code><code></code> (or <code><pre></code>, depending on if indentation is needed/requested) block in the HTML for the specified MIME type as well.</li>
|
||||
<ul>
|
||||
<li>Only the first supported instance of this parameter will be used.</li>
|
||||
<li>
|
||||
The value <b>must</b> conform to the same rules/specifications as the <code>mime</code> parameter/<code>Accept</code> header.
|
||||
<ul>
|
||||
<li><code>include</code> may <b>not</b> be <code>text/html</code>; it will be ignored if this is set. Just learn to <code>ctrl+u</code>.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Only used if the evaluated return is HTML, ignored otherwise.</li>
|
||||
<li>Indentation can be specified via the <b>indent</b> parameter below (since indentation is otherwise meaningless to HTML returns).</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<b>indent:</b> Enable/specify indentation for JSON and XML output; ignored for others.
|
||||
<ul>
|
||||
<li>The default is to not indent. (Commonly referred to as "condensed" or "compressed" JSON/XML.)</li>
|
||||
<li>Only the first specified instance of this parameter will be used.</li>
|
||||
<li>If specified with a string value, use that string as each indent.
|
||||
<ul>
|
||||
<li>Be mindful of URL query parameter encoding,
|
||||
per <a href="https://www.rfc-editor.org/rfc/rfc3986.html#section-3.4">RFC 3986 § 3.4</a>
|
||||
and <a href="https://www.rfc-editor.org/rfc/rfc8820.html#section-2.4">RFC 8820 § 2.4</a></li>
|
||||
<li>For quick reference and as an example, to indent with a <a href="https://asciiref.dev/#c9">tab</a>
|
||||
(<code>\t</code>, <code>0x09</code>) for each level, use <code>?indent=%09</code></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>If indentation is specified without any value (<code>?indent</code>), the default is two
|
||||
<a href="https://asciiref.dev/#c32">spaces</a> (<code>0x20</code>); this would be represented
|
||||
as <code>?indent=%20%20</code></li>
|
||||
<li><code>?indent=</code> (no value specified) is equal to <code>?indent</code>.</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</p>
|
||||
{{- template "meta.bottom" $page }}
|
||||
{{- end }}
|
107
server/types.go
Normal file
107
server/types.go
Normal file
@ -0,0 +1,107 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
`encoding/xml`
|
||||
`net`
|
||||
`net/http`
|
||||
`net/url`
|
||||
`os`
|
||||
|
||||
`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
|
||||
|
||||
// R00tInfo is the structure of data returned to the client.
|
||||
type R00tInfo struct {
|
||||
// XMLName is the element name/namespace of this object ("info").
|
||||
XMLName xml.Name `json:"-" xml:"info" yaml:"-"`
|
||||
// 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"`
|
||||
// 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.
|
||||
Headers XmlHeaders `json:"headers" xml:"reqHeaders" yaml:"Request Headers"`
|
||||
// Req contains the original request. It is not rendered but may be used for templating.
|
||||
Req *http.Request `json:"-" xml:"-" yaml:"-"`
|
||||
}
|
||||
|
||||
// R00tClient is the UA/Client info, if any passed by the client.
|
||||
type R00tClient struct {
|
||||
// XMLName is the element name/namespace of this object ("ua").
|
||||
XMLName xml.Name `json:"-" xml:"ua" yaml:"-" uaField:"-" renderName:"-"`
|
||||
// String contains the entire UA string.
|
||||
String *string `json:"str,omitempty" xml:",chardata" yaml:"String"`
|
||||
// ClientVer is a parsed version structure of the client version (see ClientVerStr for the combined string).
|
||||
ClientVer *Ver `json:"ver,omitempty" xml:"version,omitempty" yaml:"Client Version,omitempty" uaField:"VersionNo" renderName:"-"`
|
||||
// OSVer is the parsed OS version info of the client (see OsVersionStr for the combined string).
|
||||
OSVer *Ver `json:"os_ver,omitempty" xml:"osVersion,omitempty" yaml:"Operating System Version,omitempty" uaField:"OSVersionNo" renderName:"-"`
|
||||
// URL, if any, is the URL of the client.
|
||||
URL *string `json:"url,omitempty" xml:"url,attr,omitempty" yaml:"URL,omitempty"`
|
||||
// Name is the client software name.
|
||||
Name *string `json:"name,omitempty" xml:"name,attr,omitempty" yaml:"Program/Name,omitempty"`
|
||||
// ClientVerStr contains the full version as a string (see also Clientversion).
|
||||
ClientVerStr *string `json:"ver_str,omitempty" xml:"verStr,attr,omitempty" yaml:"Client Version String,omitempty" uaField:"Version" renderName:"Client Version"`
|
||||
// OS is the operating system of the client.
|
||||
OS *string `json:"os,omitempty" xml:"os,attr,omitempty" yaml:"Operating System,omitempty"`
|
||||
// OsVerStr is the version of the operating system of the client.
|
||||
OsVerStr *string `json:"os_ver_str,omitempty" xml:"osVerStr,attr,omitempty" yaml:"Operating System Version String,omitempty" uaField:"OSVersion" renderName:"Operating System Version"`
|
||||
// Dev is the device type.
|
||||
Dev *string `json:"dev,omitempty" xml:"dev,attr,omitempty" yaml:"Device,omitempty" uaField:"Device"`
|
||||
// IsMobile is true if this is a mobile device.
|
||||
IsMobile bool `json:"mobile" xml:"mobile,attr" yaml:"Is Mobile" uaField:"Mobile"`
|
||||
// sTablet is true if this is a tablet.
|
||||
IsTablet bool `json:"tablet" xml:"tablet,attr" yaml:"Is Tablet" uaField:"Tablet"`
|
||||
// IsDesktop is true if this is a desktop/laptop.
|
||||
IsDesktop bool `json:"desktop" xml:"desktop,attr" yaml:"Is Desktop" uaField:"Desktop"`
|
||||
// IsBot is true if this is a bot.
|
||||
IsBot bool `json:"bot" xml:"bot,attr" yaml:"Is Bot" uaField:"Bot"`
|
||||
ua *useragent.UserAgent
|
||||
}
|
||||
|
||||
type Ver struct {
|
||||
// XMLName xml.Name `json:"-" xml:"version" yaml:"-"`
|
||||
Major int `json:"major" xml:"maj,attr" yaml:"Major"`
|
||||
Minor int `json:"minor" xml:"min,attr" yaml:"Minor"`
|
||||
Patch int `json:"patch" xml:"patch,attr" yaml:"Patch"`
|
||||
}
|
||||
|
||||
// Page is only used for HTML rendering.
|
||||
type Page struct {
|
||||
Info *R00tInfo
|
||||
// e.g. "index.html.tpl"; populated by handler
|
||||
PageType string
|
||||
// Nil unless `?include=` specified, otherwise a block of text to be wrapped in <code>...</code>.
|
||||
Raw *string
|
||||
// RawFmt is the MIME type for Raw, if `?include=` enabled/specified.
|
||||
RawFmt *string
|
||||
// Indent specifies the indentation string.
|
||||
Indent string
|
||||
// DoIndent indicates if indenting was enabled.
|
||||
DoIndent bool
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// https://www.iana.org/assignments/media-types/media-types.xhtml
|
||||
type parsedMIME struct {
|
||||
MIME string
|
||||
Weight float32 // Technically a param (q; "qualifier"?), but copied and converted here for easier sorting.
|
||||
Params map[string]string
|
||||
}
|
32
version/consts.go
Normal file
32
version/consts.go
Normal file
@ -0,0 +1,32 @@
|
||||
package version
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
)
|
||||
|
||||
/*
|
||||
These variables are automatically handled by the build script.
|
||||
|
||||
DO NOT MODIFY THESE VARIABLES.
|
||||
Refer to /build.sh for how these are generated at build time and populated.
|
||||
*/
|
||||
var (
|
||||
sourceControl string = "git"
|
||||
version string = "(unknown)"
|
||||
commitHash string
|
||||
commitShort string
|
||||
numCommitsAfterTag string
|
||||
isDirty string
|
||||
buildTime string
|
||||
buildUser string
|
||||
buildSudoUser string
|
||||
buildHost string
|
||||
)
|
||||
|
||||
var (
|
||||
patchRe *regexp.Regexp = regexp.MustCompile(`^(?P<patch>[0-9+])(?P<pre>-[0-9A-Za-z.-]+)?(?P<build>\+[0-9A-Za-z.-]+)?$`)
|
||||
patchReIsolated *regexp.Regexp = regexp.MustCompile(`^([0-9]+)(?:[-+](.*)?)?$`)
|
||||
)
|
||||
|
||||
// Ver is populated by main() from the build script and used in other places.
|
||||
var Ver *BuildInfo
|
144
version/funcs.go
Normal file
144
version/funcs.go
Normal file
@ -0,0 +1,144 @@
|
||||
package version
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/mod/semver"
|
||||
)
|
||||
|
||||
// Version returns the build information. See build.sh.
|
||||
func Version() (b *BuildInfo, err error) {
|
||||
|
||||
var n int
|
||||
var s string
|
||||
var sb strings.Builder
|
||||
var ok bool
|
||||
var canonical string
|
||||
var build strings.Builder
|
||||
// Why a map?
|
||||
// I forget but I had a reason for it once upon a time.
|
||||
var raw map[string]string = map[string]string{
|
||||
"sourceControl": sourceControl,
|
||||
"tag": version,
|
||||
"hash": commitHash,
|
||||
"shortHash": commitShort,
|
||||
"postTagCommits": numCommitsAfterTag,
|
||||
"dirty": isDirty,
|
||||
"time": buildTime,
|
||||
"user": buildUser,
|
||||
"sudoUser": buildSudoUser,
|
||||
"host": buildHost,
|
||||
}
|
||||
var i BuildInfo = BuildInfo{
|
||||
SourceControl: raw["sourceControl"],
|
||||
TagVersion: raw["tag"],
|
||||
// PostTagCommits: 0,
|
||||
CommitHash: raw["hash"],
|
||||
CommitId: raw["shortHash"],
|
||||
BuildUser: raw["user"],
|
||||
RealBuildUser: raw["sudoUser"],
|
||||
// BuildTime: time.Time{},
|
||||
BuildHost: raw["host"],
|
||||
Dirty: false,
|
||||
isDefined: false,
|
||||
raw: raw,
|
||||
}
|
||||
|
||||
if s, ok = raw["postTagCommits"]; ok && strings.TrimSpace(s) != "" {
|
||||
if n, err = strconv.Atoi(s); err == nil {
|
||||
i.PostTagCommits = uint(n)
|
||||
}
|
||||
}
|
||||
|
||||
if s, ok = raw["time"]; ok && strings.TrimSpace(s) != "" {
|
||||
if n, err = strconv.Atoi(s); err == nil {
|
||||
i.BuildTime = time.Unix(int64(n), 0).UTC()
|
||||
}
|
||||
}
|
||||
|
||||
switch strings.ToLower(raw["dirty"]) {
|
||||
case "1":
|
||||
i.Dirty = true
|
||||
case "0", "":
|
||||
i.Dirty = false
|
||||
}
|
||||
|
||||
// Build the short form. We use this for both BuildInfo.short and BuildInfo.verSplit.
|
||||
if i.TagVersion == "" {
|
||||
sb.WriteString(i.SourceControl)
|
||||
} else {
|
||||
sb.WriteString(i.TagVersion)
|
||||
}
|
||||
/*
|
||||
Now the mess. In order to conform to SemVer 2.0 (the spec this code targets):
|
||||
|
||||
1.) MAJOR.
|
||||
2.) MINOR.
|
||||
3.) PATCH
|
||||
4.) -PRERELEASE (OPTIONAL)
|
||||
(git commit, if building against a commit made past 1-3. Always included if untagged.)
|
||||
5.) +BUILDINFO (OPTIONAL)
|
||||
("+x[.y]", where x is # of commits past 4, or tag commit if 4 is empty. 0 is valid.
|
||||
y is optional, and is the string "dirty" if it is a "dirty" build - that is, uncommitted/unstaged changes.
|
||||
if x and y would be 0 and empty, respectively, then 5 is not included.)
|
||||
|
||||
1-3 are already written, or the source control software used if not.
|
||||
|
||||
Technically 4 and 5 are only included if 3 is present. We force patch to 0 if it's a tagged release and patch isn't present --
|
||||
so this is not relevant.
|
||||
*/
|
||||
// PRERELEASE
|
||||
if i.TagVersion == "" || i.PostTagCommits > 0 {
|
||||
// We use the full commit hash for git versions, short identifier for tagged releases.
|
||||
if i.TagVersion == "" {
|
||||
i.Pre = i.CommitHash
|
||||
} else {
|
||||
i.Pre = i.CommitId
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("-%v", i.Pre))
|
||||
}
|
||||
// BUILD
|
||||
if i.PostTagCommits > 0 || i.Dirty {
|
||||
build.WriteString(strconv.Itoa(int(i.PostTagCommits)))
|
||||
if i.Dirty {
|
||||
build.WriteString(".dirty")
|
||||
}
|
||||
i.Build = build.String()
|
||||
sb.WriteString(fmt.Sprintf("+%v", i.Build))
|
||||
}
|
||||
|
||||
i.short = sb.String()
|
||||
if semver.IsValid(i.short) {
|
||||
// DON'T DO THIS. It strips the prerelease and build info.
|
||||
// i.short = semver.Canonical(i.short)
|
||||
// Do this instead.
|
||||
canonical = semver.Canonical(i.short)
|
||||
// Numeric versions...
|
||||
if n, err = strconv.Atoi(strings.TrimPrefix(semver.Major(canonical), "v")); err != nil {
|
||||
err = nil
|
||||
} else {
|
||||
i.Major = uint(n)
|
||||
}
|
||||
if n, err = strconv.Atoi(strings.Split(semver.MajorMinor(canonical), ".")[1]); err != nil {
|
||||
err = nil
|
||||
} else {
|
||||
i.Minor = uint(n)
|
||||
}
|
||||
if n, err = strconv.Atoi(patchReIsolated.FindStringSubmatch(strings.Split(canonical, ".")[2])[1]); err != nil {
|
||||
err = nil
|
||||
} else {
|
||||
i.Patch = uint(n)
|
||||
}
|
||||
// The other tag assignments were performed above.
|
||||
}
|
||||
// The default is 0 for the numerics, so no big deal.
|
||||
|
||||
i.isDefined = true
|
||||
|
||||
b = &i
|
||||
|
||||
return
|
||||
}
|
103
version/funcs_buildinfo.go
Normal file
103
version/funcs_buildinfo.go
Normal file
@ -0,0 +1,103 @@
|
||||
package version
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/mod/semver"
|
||||
)
|
||||
|
||||
// Detail returns a multiline string containing every possible piece of information we collect.
|
||||
func (b *BuildInfo) Detail() (ver string) {
|
||||
|
||||
var sb strings.Builder
|
||||
|
||||
sb.WriteString(fmt.Sprintf("%v\n\n", b.short))
|
||||
sb.WriteString(fmt.Sprintf("====\nSource Control: %v\n", b.SourceControl))
|
||||
if b.TagVersion != "" {
|
||||
if b.PostTagCommits > 0 {
|
||||
sb.WriteString(fmt.Sprintf("Version Base: %v\nCommit Hash: %v\n", b.TagVersion, b.CommitHash))
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf("Version: %v\n", b.TagVersion))
|
||||
}
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf("Version: (Unversioned)\nCommit Hash: %v\n", b.CommitHash))
|
||||
}
|
||||
|
||||
// Post-commits
|
||||
if b.TagVersion != "" {
|
||||
sb.WriteString(fmt.Sprintf("# of Commits Since %v: %v\n", b.TagVersion, b.PostTagCommits))
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf("# of Commits Since %v: %v\n", b.CommitId, b.PostTagCommits))
|
||||
}
|
||||
|
||||
sb.WriteString("Uncommitted/Unstaged Changes: ")
|
||||
if b.Dirty {
|
||||
sb.WriteString("yes (dirty/monkeypatched build)\n")
|
||||
} else {
|
||||
sb.WriteString("no (clean build)\n")
|
||||
}
|
||||
|
||||
if b.TagVersion != "" {
|
||||
sb.WriteString(
|
||||
fmt.Sprintf(
|
||||
"====\nMajor: %v\nMinor: %v\nPatch: %v\n",
|
||||
b.Major, b.Minor, b.Patch,
|
||||
),
|
||||
)
|
||||
}
|
||||
sb.WriteString("====\n")
|
||||
sb.WriteString(b.Meta())
|
||||
|
||||
ver = sb.String()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Short returns a uniquely identifiable version string.
|
||||
func (b *BuildInfo) Short() (ver string) {
|
||||
|
||||
ver = b.short
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Meta returns the build/compile-time info.
|
||||
func (b *BuildInfo) Meta() (meta string) {
|
||||
|
||||
var sb strings.Builder
|
||||
|
||||
if b.RealBuildUser != b.BuildUser && b.RealBuildUser != "" {
|
||||
sb.WriteString(fmt.Sprintf("Real Build User: %v\n", b.RealBuildUser))
|
||||
sb.WriteString(fmt.Sprintf("Sudo Build User: %v\n", b.BuildUser))
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf("Build User: %v\n", b.BuildUser))
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("Build Time: %v\nBuild Host: %v\n", b.BuildTime, b.BuildHost))
|
||||
|
||||
meta = sb.String()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// getReMap gets a regex map of map[pattern]match.
|
||||
func (b *BuildInfo) getReMap() (matches map[string]string) {
|
||||
|
||||
var s string = b.Short()
|
||||
var sections []string
|
||||
|
||||
if !semver.IsValid(s) {
|
||||
return
|
||||
}
|
||||
|
||||
sections = strings.Split(s, ".")
|
||||
|
||||
// The split should contain everything in the third element.
|
||||
// Or, if using a "simplified" semver, the last element.
|
||||
matches = make(map[string]string)
|
||||
for idx, str := range patchRe.FindStringSubmatch(sections[len(sections)-1]) {
|
||||
matches[patchRe.SubexpNames()[idx]] = str
|
||||
}
|
||||
|
||||
return
|
||||
}
|
51
version/types.go
Normal file
51
version/types.go
Normal file
@ -0,0 +1,51 @@
|
||||
package version
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// BuildInfo contains nativized version information.
|
||||
type BuildInfo struct {
|
||||
// TagVersion is the most recent tag name on the current branch.
|
||||
TagVersion string
|
||||
// PostTagCommits is the number of commits after BuildInfo.TagVersion's commit on the current branch.
|
||||
PostTagCommits uint
|
||||
// CommitHash is the full commit hash.
|
||||
CommitHash string
|
||||
// CommitId is the "short" version of BuildInfo.CommitHash.
|
||||
CommitId string
|
||||
// BuildUser is the user the program was compiled under.
|
||||
BuildUser string
|
||||
// If compiled under sudo, BuildInfo.RealBuildUser is the user that called sudo.
|
||||
RealBuildUser string
|
||||
// BuildTime is the time and date of the program's build time.
|
||||
BuildTime time.Time
|
||||
// BuildHost is the host the binary was compiled on.
|
||||
BuildHost string
|
||||
// Dirty specifies if the source was "dirty" (uncommitted/unstaged etc. files) at the time of compilation.
|
||||
Dirty bool
|
||||
// SourceControl is the source control version used. Only relevant if not a "clean" build or untagged.
|
||||
SourceControl string
|
||||
// Major is the major version, expressed as an uint per spec.
|
||||
Major uint
|
||||
// Minor is the minor version, expressed as an uint per spec.
|
||||
Minor uint
|
||||
// Patch is the patch version, expressed as an uint per spec.
|
||||
Patch uint
|
||||
// Pre
|
||||
Pre string
|
||||
// Build
|
||||
Build string
|
||||
// isDefined specifies if this version was retrieved from the built-in values.
|
||||
isDefined bool
|
||||
// raw is the raw variable values.
|
||||
raw map[string]string
|
||||
/*
|
||||
verSplit is a slice of []string{Major, Minor, Patch, PreRelease, Build}
|
||||
|
||||
If using an actual point release, PreRelease and Build are probably blank.
|
||||
*/
|
||||
verSplit [5]string
|
||||
// short is the condensed version of verSplit.
|
||||
short string
|
||||
}
|
Loading…
Reference in New Issue
Block a user