initial commit
This commit is contained in:
commit
010643757e
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/
|
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="gobroke"
|
||||
# Linux
|
||||
echo -n "Building ./bin/${cmd}..."
|
||||
go build \
|
||||
-o "./bin/${cmd}" \
|
||||
-ldflags \
|
||||
"${LDFLAGS_STR}" \
|
||||
cmd/${cmd}/*.go
|
||||
echo " Done."
|
||||
|
||||
echo "Build complete."
|
31
cachedb/_static/cache.schema.sql
Normal file
31
cachedb/_static/cache.schema.sql
Normal file
@ -0,0 +1,31 @@
|
||||
PRAGMA foreign_keys=OFF;
|
||||
BEGIN TRANSACTION;
|
||||
CREATE TABLE tunnels (
|
||||
tun_id INTEGER NOT NULL PRIMARY KEY,
|
||||
cksum_crc32 INTEGER NOT NULL,
|
||||
"desc" TEXT,
|
||||
server_v4 TEXT NOT NULL,
|
||||
current_client_v4 TEXT NOT NULL,
|
||||
tunnel_server_v6 TEXT NOT NULL,
|
||||
tunnel_client_v6 TEXT NOT NULL,
|
||||
prefix_64 TEXT NOT NULL,
|
||||
prefix_48 TEXT,
|
||||
rdns_1 TEXT,
|
||||
rdns_2 TEXT,
|
||||
rdns_3 TEXT,
|
||||
rdns_4 TEXT,
|
||||
rdns_5 TEXT,
|
||||
created INTEGER NOT NULL,
|
||||
checked INTEGER NOT NULL,
|
||||
updated INTEGER
|
||||
);
|
||||
CREATE TABLE client_ips (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
tun_id INTEGER NOT NULL,
|
||||
client_ip INTEGER NOT NULL,
|
||||
when_set INTEGER NOT NULL, when_fetched INTEGER,
|
||||
CONSTRAINT client_ips_tunnels_FK FOREIGN KEY (tun_id) REFERENCES tunnels(tun_id) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
INSERT INTO sqlite_sequence VALUES('client_ips',0);
|
||||
COMMIT;
|
||||
PRAGMA foreign_keys=ON;
|
15
cachedb/consts.go
Normal file
15
cachedb/consts.go
Normal file
@ -0,0 +1,15 @@
|
||||
package cachedb
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed "_static/cache.schema.sql"
|
||||
schemaBytes []byte
|
||||
)
|
||||
|
||||
const (
|
||||
SelectTunnels string = ""
|
||||
SelectTunnelById string = ""
|
||||
)
|
35
cachedb/funcs.go
Normal file
35
cachedb/funcs.go
Normal file
@ -0,0 +1,35 @@
|
||||
package cachedb
|
||||
|
||||
import (
|
||||
`os`
|
||||
`path/filepath`
|
||||
|
||||
`r00t2.io/sysutils/paths`
|
||||
)
|
||||
|
||||
/*
|
||||
NewCache returns a Cache from path to SQLite file `db` (or ":memory:" for an in-memory one).
|
||||
|
||||
It will be created if it doesn't exist for persistent caches.
|
||||
*/
|
||||
func NewCache(db string) (c *Cache, err error) {
|
||||
|
||||
var exists bool
|
||||
|
||||
switch db {
|
||||
case ":memory:":
|
||||
default:
|
||||
if exists, err = paths.RealPathExists(&db); err != nil {
|
||||
return
|
||||
}
|
||||
if !exists {
|
||||
if err = os.MkdirAll(filepath.Dir(db), 0700); err != nil {
|
||||
return
|
||||
}
|
||||
if err = os.WriteFile()
|
||||
}
|
||||
}
|
||||
// TODO
|
||||
|
||||
return
|
||||
}
|
27
cachedb/types.go
Normal file
27
cachedb/types.go
Normal file
@ -0,0 +1,27 @@
|
||||
package cachedb
|
||||
|
||||
import (
|
||||
`time`
|
||||
|
||||
`github.com/jmoiron/sqlx`
|
||||
`r00t2.io/gobroke/tunnelbroker`
|
||||
)
|
||||
|
||||
type Cache struct {
|
||||
db *sqlx.DB
|
||||
}
|
||||
|
||||
type TunnelDB struct {
|
||||
*tunnelbroker.Tunnel
|
||||
Created time.Time `db:"created"`
|
||||
Checked time.Time `db:"checked"`
|
||||
Updated time.Time `db:"updated"`
|
||||
}
|
||||
|
||||
type ClientIpDB struct {
|
||||
ID uint64 `db:"id"`
|
||||
TunID uint64 `db:"tun_id"`
|
||||
*tunnelbroker.FetchedIP
|
||||
Set time.Time `db:"when_set"`
|
||||
Fetched time.Time `db:"when_fetched"`
|
||||
}
|
1
cmd/gobroke/main.go
Normal file
1
cmd/gobroke/main.go
Normal file
@ -0,0 +1 @@
|
||||
package gobroke
|
82
conf/_testdata/test.json
Normal file
82
conf/_testdata/test.json
Normal file
@ -0,0 +1,82 @@
|
||||
{
|
||||
"default_username": "default_user",
|
||||
"1tun": true,
|
||||
"cache_db": "/var/cache/gobroke.db",
|
||||
"cache_perms": {
|
||||
"file": {
|
||||
"user": "",
|
||||
"group": "",
|
||||
"mode": 384
|
||||
},
|
||||
"dir": {
|
||||
"user": "",
|
||||
"group": "",
|
||||
"mode": 448
|
||||
}
|
||||
},
|
||||
"tunnels": [
|
||||
{
|
||||
"tun_id": 123,
|
||||
"addr": "203.0.113.1",
|
||||
"mtu": 1450,
|
||||
"username": "specific_user",
|
||||
"update_key": "abcdef",
|
||||
"cfg_tpls": [
|
||||
{
|
||||
"tpl": "/etc/gobroke/tpl/dnsmasq/ra_dhcpv6.conf.tpl",
|
||||
"dest": "/etc/dnsmasq.d/ra_dhcpv6.conf",
|
||||
"perms": {
|
||||
"file": {
|
||||
"user": "",
|
||||
"group": "",
|
||||
"mode": 384
|
||||
},
|
||||
"dir": {
|
||||
"user": "",
|
||||
"group": "",
|
||||
"mode": 448
|
||||
}
|
||||
},
|
||||
"cmds": [
|
||||
{
|
||||
"bin": "/usr/local/bin/somecmd",
|
||||
"args": [
|
||||
"-f", "foo"
|
||||
],
|
||||
"isol8_env": false,
|
||||
"env": [
|
||||
"SOMEENV=SOMEVAL"
|
||||
],
|
||||
"on_change": true,
|
||||
"is_tpl": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tpl": "/etc/gobroke/tpl/stat.tpl",
|
||||
"dest": "/tmp/gobroke.dump"
|
||||
}
|
||||
],
|
||||
"cmds": [
|
||||
{
|
||||
"bin": "systemctl",
|
||||
"args": [
|
||||
"restart",
|
||||
"someservice"
|
||||
],
|
||||
"on_change": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tun_id": 456,
|
||||
"username": "specific_user",
|
||||
"update_key": "defghi"
|
||||
}
|
||||
],
|
||||
"cmds": [
|
||||
{
|
||||
"bin": "/usr/local/bin/alltunsprogram"
|
||||
}
|
||||
]
|
||||
}
|
289
conf/_testdata/test.toml
Normal file
289
conf/_testdata/test.toml
Normal file
@ -0,0 +1,289 @@
|
||||
# DefaultUsername specifies the default username to use for
|
||||
# authenticating to tunnelbroker.net.
|
||||
# It is optional, as the username can be specified for each Tunnel,
|
||||
# but at least one or the other *must* be provided.
|
||||
# This makes it easier if you have multiple tunnels under the same account
|
||||
# (as possible in higher levels of HE IPv6 certification).
|
||||
# If a username is specified in Tunnel.Username, it will be used.
|
||||
# If not (and, of course, DefaultUsername is specified), then
|
||||
# DefaultUsername will be used for that Tunnel.
|
||||
DefaultUsername = "default_user"
|
||||
|
||||
# If SingleTunnel is true, each Tunnel below will be run in order instead of
|
||||
# concurrently.
|
||||
# If there is any concern about race conditions (e.g. the same service being
|
||||
# restarted by multiple tunnels, etc.), then it is HIGHLY RECOMMENDED
|
||||
# you set this to true.
|
||||
SingleTunnel = true
|
||||
|
||||
# CacheDbPath is entirely optional.
|
||||
# If not provided, results will be cached in RAM (and thus lost on reboot
|
||||
# or program termination/restart).
|
||||
# (This can be explicitly specified by using the value ':memory:'.)
|
||||
# If provided, it should be a path to a file to use as a SQLite3 database
|
||||
# that holds cached information.
|
||||
# The information that is cached contains only:
|
||||
# * each Tunnel.TunnelID
|
||||
# * the associated tunnelbroker.FetchedTunnel
|
||||
# * a CRC32 of all configuration (as defined in this file) for that Tunnel
|
||||
# The UpdateKey and other configuration defined here (aside from
|
||||
# Tunnel.TunnelID, and Tunnel.ExplicitClientIP if specified) are
|
||||
# NOT stored.
|
||||
# Any tunnel present in a persistent cache DB but *not* defined in the
|
||||
# running GoBroke config will be removed.
|
||||
# Note that the cache DB primary key is based on the Tunnel.TunnelID,
|
||||
# as one cannot define multiple client endpoints for the same tunnel.
|
||||
CacheDbPath = '/var/cache/gobroke.db'
|
||||
|
||||
# CacheDbPerms specify the permissions for CacheDbPath.
|
||||
# This directive is completely optional, and is
|
||||
# ignored if CacheDbPath is ":memory:" (or unspecified).
|
||||
# If not specified (and CacheDbPath is persistent),
|
||||
# then the runtime user's umask and effective UID/GID
|
||||
# is used if creating a new database file.
|
||||
# If the file exists and permissions are defined, they will
|
||||
# be enforced.
|
||||
# If the file exists but no permissions are defined, they
|
||||
# will be left as-is.
|
||||
[CacheDbPerms]
|
||||
# Permissions are/may be defined for both the file being written
|
||||
# and the parent directory (see below).
|
||||
[CacheDbPerms.File]
|
||||
# The User is optional.
|
||||
# If unspecified, the default behavir mentioned above is performed.
|
||||
# If specified as an empty string, the runtime EUID is enforced.
|
||||
# Otherwise it may be a username or a UID (checked in that order).
|
||||
User = ""
|
||||
# Group is also optional, and follows the same logic except
|
||||
# for EGID/groupnames/GIDs.
|
||||
Group = ""
|
||||
# Mode is optional also.
|
||||
# It *must* be equal to the octal mode bits (e.g. it must be an
|
||||
# unsigned integer), but may be represented in multiple ways.
|
||||
# e.g.:
|
||||
# Mode = 0o0600
|
||||
# Mode = 0o600
|
||||
# Mode = 0x0180
|
||||
# Mode = 0x180
|
||||
# Mode = 0b110000000
|
||||
# Mode = 384
|
||||
# All evaluate to the exact same value in TOML:
|
||||
# https://toml.io/en/v1.0.0#integer
|
||||
# For consistency with `chmod(1)`, it is recommended to use the
|
||||
# octal representation (0o0600 or 0o600 above).
|
||||
# If you need help determining what number you should actually use,
|
||||
# you can use the calculator here:
|
||||
# https://rubendougall.co.uk/projects/permissions-calculator/
|
||||
# (source: https://github.com/Ruben9922/permissions-calculator )
|
||||
# (Supports "special" bits)
|
||||
# or here:
|
||||
# https://wintelguy.com/permissions-calc.pl
|
||||
# (beware of ads)
|
||||
# (provides an explanation of the bits)
|
||||
# Or see https://en.wikipedia.org/wiki/Chmod
|
||||
Mode = 0o0600
|
||||
# Dir permissions specifiy permissions/ownership of the parent directory of the cache DB.
|
||||
# The same rules, logic, behavior, etc. as in CacheDbPerms.File apply here.
|
||||
[CacheDbPerms.Dir]
|
||||
User = ""
|
||||
Group = ""
|
||||
Mode = 0o0700
|
||||
|
||||
|
||||
#############
|
||||
## Tunnels ##
|
||||
#############
|
||||
|
||||
# Each Tunnel represents a single tunnelbroker.net tunnel configuration.
|
||||
# Note that each Tunnel is run concurrently. If this is undesired due to
|
||||
# potential race conditions, set the root-level directive SingleTunnel
|
||||
# to true.
|
||||
[[Tunnel]]
|
||||
# The TunnelID can be found by logging into https://tunnelbroker.net/ and,
|
||||
# at the "Main Page" that loads when logging in, clicking on the desired
|
||||
# tunnel name.
|
||||
# The tunnel ID is then displayed in both the URL bar:
|
||||
# https://tunnelbroker.net/tunnel_detail.php?tid=<TunnelID>
|
||||
# And as the first line on the first tab ("IPv6 Tunnel" tab),
|
||||
# labeled "Tunnel ID".
|
||||
TunnelID = 123
|
||||
# If you wish to use a different or explicit "Client IPv4 address",
|
||||
# this can be specified via ExplicitClientIP.
|
||||
# If it is empty or is not specified, the public IP of this host will be determined
|
||||
# via an external service.
|
||||
# This *must* be an IPv4 address (if specified).
|
||||
ExplicitClientIP = '203.0.113.1'
|
||||
# If you have specified a custom MTU under the "Advanced" tab for this tunnel,
|
||||
# you can set this value here.
|
||||
# If you have not set a custom one, leave this option unspecified;
|
||||
# the default (and maximum allowed), 1480 MTU, will be used in that case.
|
||||
MTU = 1450
|
||||
# The Username field is optional IF DefaultUsername was specified.
|
||||
# This also allows you to specify tunnels from different accounts
|
||||
# by providing a tunnel-specific username.
|
||||
Username = "specific_user"
|
||||
# The UpdateKey can be found under the "Advanced" tab on your tunnelbroker.net
|
||||
# tunnel's page, labeled "Update Key".
|
||||
# Your real token is likely to be a bit longer and more random.
|
||||
# This token is used to not only update the client-side tunnel IP but also to
|
||||
# query the HE Tunnelbroker "API" (it's really just a single endpoint)
|
||||
# to get the tunnel configuration.
|
||||
UpdateKey = "abcdef"
|
||||
|
||||
|
||||
######################
|
||||
## Config Templates ##
|
||||
######################
|
||||
|
||||
# Each ConfigTemplate consists of a path to a template file and a destination
|
||||
# file at the bere minimum. In addition, Commands may be provided.
|
||||
# Any paths leading up to Destination that don't exist will (attempt to be)
|
||||
# created.
|
||||
# The template is always rendered in memory, but the destination is only written
|
||||
# if:
|
||||
# * The Destination doesn't exist
|
||||
# * The Destination differs from the buffered rendering of the template
|
||||
# Commands are optional, and are a list of commands to be run.
|
||||
# Their running may be restricted to only if the tunnel information/IP
|
||||
# information has changed, always run, or the inverse of all conditions.
|
||||
[[Tunnel.ConfigTemplate]]
|
||||
# Template points to where the template file can be found.
|
||||
# It must be in a Golang text/template syntax/format; see:
|
||||
# https://pkg.go.dev/text/template
|
||||
# Refer to the library's definition of the tunnelbroker.FetchedTunnel struct;
|
||||
# this is the object that is passed to the template.
|
||||
Template = "/etc/gobroke/tpl/dnsmasq/ra_dhcpv6.conf.tpl"
|
||||
# Destination is the file to write to.
|
||||
# It will only be written to if:
|
||||
# * The path does not exist
|
||||
# * The path exists but is different from the in-memory rendered buffer
|
||||
# An attempt will be made to create any leading components that are not
|
||||
# present.
|
||||
# It is recommended to enforce permissions/ownership of these via the
|
||||
# Commands.
|
||||
Destination = "/etc/dnsmasq.d/ra_dhcpv6.conf"
|
||||
|
||||
|
||||
#################################
|
||||
## Config Template Permissions ##
|
||||
#################################
|
||||
|
||||
# Permissions can be defined for the Destionation file.
|
||||
# They are completely optional, in which case the default umask, user,
|
||||
# group, etc. for the runtime user will be used, and permissions/ownership
|
||||
# will not be enforced for existing Destination files.
|
||||
# It follows the same syntax, logic, behavior, etc. as CacheDbPerms.
|
||||
[[Tunnel.ConfigTemplate.Permissions]]
|
||||
[[Tunnel.ConfigTemplate.Permissions.File]]
|
||||
User = ""
|
||||
Group = ""
|
||||
Mode = 0o0600
|
||||
[[Tunnel.ConfigTemplate.Permissions.Dir]]
|
||||
User = ""
|
||||
Group = ""
|
||||
Mode = 0o0700
|
||||
|
||||
|
||||
##############################
|
||||
## Config Template Commands ##
|
||||
##############################
|
||||
|
||||
# Commands are a collection of commands to run as part of this template
|
||||
# run.
|
||||
# Multiple Commands may be specified; they will be run in the order specified.
|
||||
# The below Command would be equivalent to:
|
||||
# SOMEENV=SOMEVAL /usr/local/bin/somecmd -f foo
|
||||
# on the shell.
|
||||
[[Tunnel.ConfigTemplate.Command]]
|
||||
# ProgramPath should be the absolute path to the binary to run.
|
||||
# It behaves as an (os/)exec.Cmd.Path (https://pkg.go.dev/os/exec#Cmd),
|
||||
# It is recommended to use an absolute path.
|
||||
ProgramPath = '/usr/local/bin/somecmd'
|
||||
# Args are optional for a Command.
|
||||
# They should conform to the rules for (os/)exec.Cmd.Args.
|
||||
Args = [
|
||||
'-f', 'foo',
|
||||
]
|
||||
# If IsolatedEnv is false (the default), the runtime environment variables
|
||||
# will be applied to the command.
|
||||
# If true, *only* the EnvVars, if specified, will be used for the spawned
|
||||
# command (an empty environment will be used if IsolateEnv is true and
|
||||
# no EnvVars are specified).
|
||||
IsolatedEnv = false
|
||||
# If provided, EnvVars can be used to add/replace environment variables.
|
||||
# They should conform to the rules for (os/)exec.Cmd.Env.
|
||||
# Whether they are added to/selectively replace or completely replace
|
||||
# the current runtime environment variables depends on how IsolateEnv
|
||||
# is configured.
|
||||
EnvVars = [
|
||||
'SOMEENV=SOMEVAL',
|
||||
]
|
||||
# If OnChange is true, this Command will run *only if SOMETHING CHANGED*.
|
||||
# (e.g. a /48 was added to the tunnel, the client IP is different, etc.)
|
||||
# If false, this Command will run *only if NOTHING CHANGED*.
|
||||
# If unspecified, the default is to always run this command regardless
|
||||
# of change status.
|
||||
# The very first (successful) run of a Tunnel is considered a "change",
|
||||
# as is writing out this template to disk as a new file.
|
||||
OnChange = true
|
||||
# By default, this Command will be run literally/as-is.
|
||||
# However, in some cases it may be useful to dynamically template out
|
||||
# commands to run.
|
||||
# If IsTemplate is set to true, then this Command.ProgramPath, each
|
||||
# of the Command.Args, and each of the Command.EnvVars will be
|
||||
# treated as Golang text/template strings as well, and will also
|
||||
# be passed a tunnelbroker.FetchedTunnel.
|
||||
# Note that if IsolateEnv is false, runtime/inherited environment
|
||||
# variables will *not* be templated.
|
||||
# It is recommended to not enable this unless necessary as it can add
|
||||
# a non-negligible amount of resource overhead/execution time.
|
||||
IsTemplate = false
|
||||
|
||||
#######################################################################
|
||||
|
||||
# Multiple ConfigTemplates may be specified.
|
||||
[[Tunnel.ConfigTemplate]]
|
||||
Template = "/etc/gobroke/tpl/stat.tpl"
|
||||
Destination = "/tmp/gobroke.dump"
|
||||
|
||||
|
||||
#####################
|
||||
## Tunnel Commands ##
|
||||
#####################
|
||||
|
||||
# Each Tunnel also supports its *own* commands. The syntax, spcification,
|
||||
# behavior, etc. is the same as the Tunnel.ConfigTemplate.Command.
|
||||
# These are executed after all Tunnel.ConfigTemplate (if any) are executed.
|
||||
# This is particularly useful for consolidating service restarts.
|
||||
[[Tunnel.Command]]
|
||||
ProgramPath = 'systemctl'
|
||||
Args = [
|
||||
'restart',
|
||||
'someservice',
|
||||
]
|
||||
# OnChange in a Tunnel.Command is scoped to any updates of the tunnel
|
||||
# and any changes in ANY of the Tunnel.ConfigTemplate specified
|
||||
# for this Tunnel (if true and ConfigTemplate were specified).
|
||||
OnChange = true
|
||||
|
||||
###############################################################################
|
||||
|
||||
# Multiple tunnel configurations are supported as well.
|
||||
[[Tunnel]]
|
||||
TunnelID = 456
|
||||
Username = "specific_user"
|
||||
UpdateKey = "defghi"
|
||||
|
||||
|
||||
######################
|
||||
## General Commands ##
|
||||
######################
|
||||
|
||||
# Command items may be specified at the root level as well.
|
||||
# The syntax is like all other Commands items, with two exceptions:
|
||||
# * There is no templating performed...
|
||||
# * As such, there is no IsTemplate directive for these.
|
||||
# A root-level Command is run after all tunnels complete.
|
||||
# The OnChange directive is true if any Tunnels result in any changes.
|
||||
[[Command]]
|
||||
ProgramPath = "/usr/local/bin/alltunpsrogram"
|
68
conf/_testdata/test.xml
Normal file
68
conf/_testdata/test.xml
Normal file
@ -0,0 +1,68 @@
|
||||
<!--
|
||||
See the example TOML for detailed comments and explanations.
|
||||
-->
|
||||
<config defaultUser="default_user"
|
||||
oneTun="true"
|
||||
cacheDb="/var/cache/gobroke.db">
|
||||
<cachePerms>
|
||||
<file user=""
|
||||
group=""
|
||||
mode="384"/>
|
||||
<dir user=""
|
||||
group=""
|
||||
mode="448"/>
|
||||
</cachePerms>
|
||||
<tunnels>
|
||||
<tunnel id="123"
|
||||
addr="203.0.113.1"
|
||||
mtu="1450"
|
||||
username="specific_user"
|
||||
key="abcdef">
|
||||
<config>
|
||||
<tpl tpl="/etc/gobroke/tpl/dnsmasq/ra_dhcpv6.conf.tpl"
|
||||
dest="/etc/dnsmasq.d/ra_dhcpv6.conf">
|
||||
<perms>
|
||||
<file user=""
|
||||
group=""
|
||||
mode="384"/>
|
||||
<dir user=""
|
||||
group=""
|
||||
mode="448"/>
|
||||
</perms>
|
||||
<cmds>
|
||||
<cmd bin="/usr/local/bin/somecmd"
|
||||
isol8Env="false"
|
||||
onChange="true"
|
||||
isTpl="false">
|
||||
<args>
|
||||
<arg>-f</arg>
|
||||
<arg>foo</arg>
|
||||
</args>
|
||||
<envs>
|
||||
<env>SOMEENV=SOMEVAL</env>
|
||||
</envs>
|
||||
</cmd>
|
||||
</cmds>
|
||||
</tpl>
|
||||
<tpl tpl="/etc/gobroke/tpl/stat.tpl"
|
||||
dest="/tmp/gobroke.dump"/>
|
||||
</config>
|
||||
<commands>
|
||||
<cmd bin="systemctl"
|
||||
onChange="true">
|
||||
<args>
|
||||
<arg>restart</arg>
|
||||
<arg>someservice</arg>
|
||||
</args>
|
||||
</cmd>
|
||||
</commands>
|
||||
</tunnel>
|
||||
<tunnel id="456"
|
||||
mtu="1480"
|
||||
username="specific_user"
|
||||
key="defghi"/>
|
||||
</tunnels>
|
||||
<commands>
|
||||
<cmd bin="/usr/local/bin/alltunsprogram" isol8Env="false"/>
|
||||
</commands>
|
||||
</config>
|
59
conf/_testdata/test.yml
Normal file
59
conf/_testdata/test.yml
Normal file
@ -0,0 +1,59 @@
|
||||
# See the example TOML for detailed comments and explanations.
|
||||
Default Username: default_user
|
||||
|
||||
NoGoTunnel: true
|
||||
|
||||
Cache Database Path: /var/cache/gobroke.db
|
||||
|
||||
Cache Database Permissions:
|
||||
File:
|
||||
User: ''
|
||||
Group: ''
|
||||
Mode: 384
|
||||
Directory:
|
||||
User: ''
|
||||
Group: ''
|
||||
Mode: 448
|
||||
|
||||
Tunnels:
|
||||
- Tunnel ID: 123
|
||||
Explicit Client IP Address: 203.0.113.1
|
||||
MTU: 1450
|
||||
Username: specific_user
|
||||
Update Key: abcdef
|
||||
Configuration File Templates:
|
||||
- Template File Path: /etc/gobroke/tpl/dnsmasq/ra_dhcpv6.conf.tpl
|
||||
Destination File Path: /etc/dnsmasq.d/ra_dhcpv6.conf
|
||||
Permissions and Ownership:
|
||||
File:
|
||||
User: ''
|
||||
Group: ''
|
||||
Mode: 384
|
||||
Directory:
|
||||
User: ''
|
||||
Group: ''
|
||||
Mode: 448
|
||||
Commands:
|
||||
- Program Path: /usr/local/bin/somecmd
|
||||
Arguments:
|
||||
- '-f'
|
||||
- 'foo'
|
||||
Isolated Environment: false
|
||||
Environment Variables:
|
||||
- SOMEENV=SOMEVAL
|
||||
On Change: true
|
||||
Is Template: false
|
||||
- Template File Path: /etc/gobroke/tpl/stat.tpl
|
||||
Destination File Path: /tmp/gobroke.dump
|
||||
Commands:
|
||||
- Program Path: systemctl
|
||||
Arguments:
|
||||
- restart
|
||||
- someservice
|
||||
On Change: true
|
||||
- Tunnel ID: 456
|
||||
Username: specific_user
|
||||
Update Key: defghi
|
||||
|
||||
Commands:
|
||||
- Program Path: /usr/local/bin/alltunsprogram
|
9
conf/consts.go
Normal file
9
conf/consts.go
Normal file
@ -0,0 +1,9 @@
|
||||
package conf
|
||||
|
||||
import (
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
var (
|
||||
validate *validator.Validate = validator.New(validator.WithRequiredStructEnabled())
|
||||
)
|
10
conf/errs.go
Normal file
10
conf/errs.go
Normal file
@ -0,0 +1,10 @@
|
||||
package conf
|
||||
|
||||
import (
|
||||
`errors`
|
||||
)
|
||||
|
||||
var (
|
||||
ErrMissingUser error = errors.New("at least one tunnel is missing a username and no default username was provided")
|
||||
ErrUnkownSyntax error = errors.New("unknown/unsupported configuration syntax")
|
||||
)
|
75
conf/funcs.go
Normal file
75
conf/funcs.go
Normal file
@ -0,0 +1,75 @@
|
||||
package conf
|
||||
|
||||
import (
|
||||
`encoding/json`
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/creasty/defaults"
|
||||
"github.com/goccy/go-yaml"
|
||||
"r00t2.io/sysutils/paths"
|
||||
)
|
||||
|
||||
func NewConfig(path string) (cfg *Config, err error) {
|
||||
|
||||
var b []byte
|
||||
|
||||
if err = paths.RealPath(&path); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
cfg, err = NewConfigFromBytes(b)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func NewConfigFromBytes(b []byte) (cfg *Config, err error) {
|
||||
|
||||
if err = json.Unmarshal(b, &cfg); err != nil {
|
||||
if err = yaml.Unmarshal(b, &cfg); err != nil {
|
||||
if err = toml.Unmarshal(b, &cfg); err != nil {
|
||||
if err = toml.Unmarshal(b, &cfg); err != nil {
|
||||
err = ErrUnkownSyntax
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err = defaults.Set(cfg); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if cfg.CacheDB != ":memory:" {
|
||||
if err = paths.RealPath(&cfg.CacheDB); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err = validate.Struct(cfg); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, t := range cfg.Tunnels {
|
||||
t.cfg = cfg
|
||||
if t.Username == nil {
|
||||
if cfg.Username == nil {
|
||||
err = ErrMissingUser
|
||||
return
|
||||
} else {
|
||||
t.Username = cfg.Username
|
||||
}
|
||||
}
|
||||
if t.TemplateConfigs != nil && len(t.TemplateConfigs) > 0 {
|
||||
for _, tpl := range t.TemplateConfigs {
|
||||
if err = paths.RealPath(&tpl.Template); err != nil {
|
||||
return
|
||||
}
|
||||
if err = paths.RealPath(&tpl.Dest); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
50
conf/funcs_test.go
Normal file
50
conf/funcs_test.go
Normal file
@ -0,0 +1,50 @@
|
||||
package conf
|
||||
|
||||
import (
|
||||
`embed`
|
||||
`encoding/xml`
|
||||
`fmt`
|
||||
"testing"
|
||||
|
||||
`github.com/BurntSushi/toml`
|
||||
`github.com/goccy/go-yaml`
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed "_testdata"
|
||||
testData embed.FS
|
||||
)
|
||||
|
||||
func TestConf(t *testing.T) {
|
||||
var err error
|
||||
var cfg *Config
|
||||
var b []byte
|
||||
|
||||
if b, err = testData.ReadFile("_testdata/test.json"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cfg, err = NewConfigFromBytes(b); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
/*
|
||||
spew.Dump(cfg)
|
||||
return
|
||||
*/
|
||||
|
||||
if b, err = toml.Marshal(cfg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fmt.Println(string(b))
|
||||
|
||||
if b, err = xml.MarshalIndent(cfg, "", " "); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fmt.Println(string(b))
|
||||
|
||||
if b, err = yaml.Marshal(cfg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fmt.Println(string(b))
|
||||
|
||||
}
|
1
conf/funcs_tunnel.go
Normal file
1
conf/funcs_tunnel.go
Normal file
@ -0,0 +1 @@
|
||||
package conf
|
129
conf/types.go
Normal file
129
conf/types.go
Normal file
@ -0,0 +1,129 @@
|
||||
package conf
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
`net`
|
||||
`os`
|
||||
|
||||
`r00t2.io/gobroke/tplCmd`
|
||||
)
|
||||
|
||||
// Config represents a configuration file.
|
||||
type Config struct {
|
||||
XMLName xml.Name `json:"-" toml:"-" xml:"config" yaml:"-" `
|
||||
/*
|
||||
Username, if provided, will be the default username used for any Tunnel that does not specify one (via Tunnel.Username).
|
||||
This should be the username used when authenticating to tunnelbroker.net.
|
||||
It is optional, as the username can be specified/overridden for each Tunnel, but at least one or the other *must* be provided.
|
||||
This makes it easier if you have multiple tunnels under the same account.
|
||||
If a username is specified in Tunnel.Username, it will be used.
|
||||
If not (and, of course, Config.Username is specified), then Config.Username will be used for that Tunnel.
|
||||
*/
|
||||
Username *string `json:"default_username,omitempty" toml:"DefaultUsername,omitempty" xml:"defaultUser,attr,omitempty" yaml:"Default Username,omitempty"`
|
||||
// SingleTunnel, if true, will suppress goroutine-management of tunnels and instead execute them sequentially instead.
|
||||
SingleTunnel bool `json:"1tun,omitempty" toml:"SingleTunnel,omitempty" xml:"oneTun,attr,omitempty" yaml:"NoGoTunnel,omitempty"`
|
||||
// CacheDB, if specified, is a path to a SQLite3 DB on-disk to make cached information persistent across reboots.
|
||||
CacheDB string `json:"cache_db,omitempty" toml:"CacheDbPath,omitempty" xml:"cacheDb,attr,omitempty" yaml:"Cache Database Path,omitempty" default:":memory:" validate:"omitempty,filepath|eq=:memory:"`
|
||||
// CacheDbPerms specifies the optional permissions for the file and parent directory for CacheDB; only used if persistent cache.
|
||||
CacheDbPerms *Perms `json:"cache_perms,omitempty" toml:"CacheDbPerms,omitempty" xml:"cachePerms,omitempty" yaml:"Cache Database Permissions,omitempty"`
|
||||
// Tunnels contains one or more tunnel configurations.
|
||||
Tunnels []*Tunnel `json:"tunnels" toml:"Tunnel" xml:"tunnels>tunnel" yaml:"Tunnels" validate:"required"`
|
||||
/*
|
||||
Cmds are executed, in order, *after* all Tunnel configurations have been run.
|
||||
Unlike in Tunnel and ConfigTemplate, no templating on these commands is performed.
|
||||
*/
|
||||
Cmds []tplCmd.Cmd `json:"cmds,omitempty" toml:"Command,omitempty" xml:"commands>cmd,omitempty" yaml:"Commands,omitempty"`
|
||||
}
|
||||
|
||||
// Tunnel represents a single tunnel configuration from tunnelbroker.net.
|
||||
type Tunnel struct {
|
||||
XMLName xml.Name `json:"-" toml:"-" xml:"tunnel" yaml:"-"`
|
||||
/*
|
||||
TunnelID can be found by logging into https://tunnelbroker.net/ and, at the "Main Page" that loads
|
||||
when logging in, clicking on the desired tunnel name. The tunnel ID is then displayed in both the URL bar:
|
||||
https://tunnelbroker.net/tunnel_detail.php?tid=<TunnelID>
|
||||
And as the first line on the first tab ("IPv6 Tunnel" tab), labeled "Tunnel ID".
|
||||
*/
|
||||
TunnelID uint `json:"tun_id" toml:"TunnelID" xml:"id,attr" yaml:"Tunnel ID" validate:"required,ge=1"`
|
||||
/*
|
||||
ExplicitAddr, if provided, will be used as the tunnelbroker.FetchedTunnel.CurrentIPv4.
|
||||
If not provided, this will be fetched dynamically from an external source.
|
||||
*/
|
||||
ExplicitAddr *net.IP `json:"addr,omitempty" toml:"ExplicitClientIP,omitempty" xml:"addr,attr,omitempty" yaml:"Explicit Client IP Address,omitempty"`
|
||||
/*
|
||||
MTU should be specified if you have defined a custom one (under the "Advanced" tab for this tunnel at tunnlebroker.net).
|
||||
If you did not change this, the default is 1480 (the maximum allowed), and the default value of this struct field
|
||||
on configuration parsing will reflect this.
|
||||
*/
|
||||
MTU uint `json:"mtu,omitempty" toml:"MTU,omitempty" xml:"mtu,attr,omitempty" yaml:"MTU,omitempty" default:"1480" validate:"required,gt=0,le=1480"`
|
||||
/*
|
||||
Username field is optional IF DefaultUsername was specified.
|
||||
This also allows you to specify tunnels from different accounts
|
||||
by providing a tunnel-specific username.
|
||||
*/
|
||||
Username *string `json:"username,omitempty" toml:"Username,omitempty" xml:"username,attr,omitempty" yaml:"Username,omitempty"`
|
||||
/*
|
||||
UpdateKey can be found under the "Advanced" tab on your tunnelbroker.net tunnel's page, labeled "Update Key".
|
||||
This token is used to not only update the client-side tunnel IP but also to query the HE Tunnelbroker "API"
|
||||
(it's really just a single endpoint) to get the tunnel information necessary for local configuration.
|
||||
*/
|
||||
UpdateKey string `json:"update_key" toml:"UpdateKey" xml:"key,attr" yaml:"Update Key" validate:"required"`
|
||||
// TemplateConfgs is optional. It holds templates that will be executed in order given. See ConfigTemplate.
|
||||
TemplateConfigs []ConfigTemplate `json:"cfg_tpls" toml:"ConfigTemplate" xml:"config>tpl" yaml:"Configuration File Templates"`
|
||||
/*
|
||||
Cmds are executed, in order, *after* all tunnel updates/fetching and the templating has completed (if any specified).
|
||||
Each command will also have tunnelbroker.FetchedTunnel templated to it like TemplateConfigs/ConfigTemplate.Commands,
|
||||
so they may be templated as necessary.
|
||||
*/
|
||||
Cmds []tplCmd.TemplateCmd `json:"cmds,omitempty" toml:"Command,omitempty" xml:"commands>cmd,omitempty" yaml:"Commands,omitempty"`
|
||||
// cfg is the parent Config.
|
||||
cfg *Config
|
||||
}
|
||||
|
||||
/*
|
||||
ConfigTemplate allows the templating of configuration files, etc. from the tunnel information.
|
||||
Templates are executed *after* the IP update (if an update was necessary), but are always *run*.
|
||||
ConfigTemplate.Dest will only be written to if:
|
||||
|
||||
* The file does not exist (yet), or
|
||||
* The templated content differs from the file on disk
|
||||
*/
|
||||
type ConfigTemplate struct {
|
||||
XMLName xml.Name `json:"-" toml:"-" xml:"tpl" yaml:"-"`
|
||||
/*
|
||||
Template is the path to the template file on disk.
|
||||
It must follow the syntax, rules, etc. of a Golang (text/)template.Template (https://pkg.go.dev/text/template#Template).
|
||||
The struct passed to it is a tunnelbroker.FetchedTunnel.
|
||||
*/
|
||||
Template string `json:"tpl" toml:"Template" xml:"tpl,attr" yaml:"Template File Path" validate:"required,filepath"`
|
||||
// Dest contains the filepath that the Template should be written out to.
|
||||
Dest string `json:"dest" toml:"Destination" xml:"dest,attr" yaml:"Destination File Path" validate:"required,filepath"`
|
||||
// Perms allows specifying permissions/ownerships, if the curent user has the capability to do so.
|
||||
Perms *Perms `json:"perms,omitempty" toml:"Permissions,omitempty" xml:"perms,omitempty" yaml:"Permissions and Ownership,omitempty"`
|
||||
// Commands specifiies commands to run after this ConfigTemplate run.
|
||||
Commands []tplCmd.TemplateCmd `json:"cmds,omitempty" toml:"Command,omitempty" xml:"cmds>cmd,omitempty" yaml:"Commands,omitempty"`
|
||||
}
|
||||
|
||||
type Perms struct {
|
||||
// File specifies the desired permissions/ownership of the target file.
|
||||
File *PermSpec `json:"file,omitempty" toml:"File,omitempty" xml:"file,omitempty" yaml:"File,omitempty"`
|
||||
// ParentDir specifies the desired permissions/ownership of the parent ("dirname") of File.
|
||||
ParentDir *PermSpec `json:"dir,omitempty" toml:"Dir,omitempty" xml:"dir,omitempty" yaml:"Directory,omitempty"`
|
||||
}
|
||||
|
||||
type PermSpec struct {
|
||||
/*
|
||||
User is the username or UID (tried in that order) to chown.
|
||||
If specified as an empty string, the current/runtime UID will be used.
|
||||
If unspecified, UID will not be enforced.
|
||||
*/
|
||||
User *string `json:"user,omitempty" toml:"User,omitempty" xml:"user,attr,omitempty" yaml:"User,omitempty"`
|
||||
/*
|
||||
Group is the groupname or GID (tried in that order) to chown.
|
||||
If specified as an empty string, the current/runtime GID will be used.
|
||||
If unspecified, GID will not be enforced.
|
||||
*/
|
||||
Group *string `json:"group,omitempty" toml:"Group,omitempty" xml:"group,attr,omitempty" yaml:"Group,omitempty"`
|
||||
// Mode is the permission mode bitset. If unspecified, mode will not be enforced.
|
||||
Mode *os.FileMode `json:"mode,omitempty" toml:"Mode,omitempty" xml:"mode,attr,omitempty" yaml:"Mode,omitempty"`
|
||||
}
|
27
go.mod
Normal file
27
go.mod
Normal file
@ -0,0 +1,27 @@
|
||||
module r00t2.io/gobroke
|
||||
|
||||
go 1.23.3
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.4.0
|
||||
github.com/creasty/defaults v1.8.0
|
||||
github.com/go-playground/validator/v10 v10.23.0
|
||||
github.com/go-resty/resty/v2 v2.16.2
|
||||
github.com/goccy/go-yaml v1.15.7
|
||||
github.com/jmoiron/sqlx v1.4.0
|
||||
r00t2.io/sysutils v1.12.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/djherbis/times v1.6.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
golang.org/x/crypto v0.25.0 // indirect
|
||||
golang.org/x/net v0.27.0 // indirect
|
||||
golang.org/x/sync v0.9.0 // indirect
|
||||
golang.org/x/sys v0.26.0 // indirect
|
||||
golang.org/x/text v0.16.0 // indirect
|
||||
r00t2.io/goutils v1.7.1 // indirect
|
||||
)
|
61
go.sum
Normal file
61
go.sum
Normal file
@ -0,0 +1,61 @@
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
|
||||
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
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/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/go-resty/resty/v2 v2.16.2 h1:CpRqTjIzq/rweXUt9+GxzzQdlkqMdt8Lm/fuK/CAbAg=
|
||||
github.com/go-resty/resty/v2 v2.16.2/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU=
|
||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
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/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
|
||||
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
|
||||
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/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
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.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
|
||||
golang.org/x/sync v0.9.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.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
|
||||
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
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/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=
|
34
tplCmd/funcs_cmd.go
Normal file
34
tplCmd/funcs_cmd.go
Normal file
@ -0,0 +1,34 @@
|
||||
package tplCmd
|
||||
|
||||
import (
|
||||
`os`
|
||||
`os/exec`
|
||||
)
|
||||
|
||||
/*
|
||||
ToCmd returns an (os/)exec.Cmd from a Cmd.
|
||||
|
||||
err will always be nil for now, but is still returned and should be handled
|
||||
for future-proofing.
|
||||
*/
|
||||
func (c *Cmd) ToCmd() (cmd *exec.Cmd, err error) {
|
||||
|
||||
var envs []string
|
||||
|
||||
if c.Args != nil && len(c.Args) > 0 {
|
||||
cmd = exec.Command(c.Program, c.Args...)
|
||||
} else {
|
||||
cmd = exec.Command(c.Program)
|
||||
}
|
||||
|
||||
if !c.IsolateEnv {
|
||||
envs = os.Environ()
|
||||
}
|
||||
if c.EnvVars != nil && len(c.EnvVars) > 0 {
|
||||
envs = append(envs, c.EnvVars...)
|
||||
}
|
||||
|
||||
cmd.Env = envs
|
||||
|
||||
return
|
||||
}
|
73
tplCmd/funcs_templatecmd.go
Normal file
73
tplCmd/funcs_templatecmd.go
Normal file
@ -0,0 +1,73 @@
|
||||
package tplCmd
|
||||
|
||||
import (
|
||||
`bytes`
|
||||
`os`
|
||||
`os/exec`
|
||||
`text/template`
|
||||
)
|
||||
|
||||
// ToCmd returns an (os/)exec.Cmd from a TemplateCmd. t should be a tunnelbroker.FetchedTunnel, generally.
|
||||
func (c *TemplateCmd) ToCmd(t any) (cmd *exec.Cmd, err error) {
|
||||
|
||||
var progName string
|
||||
var envs []string
|
||||
var tpl *template.Template
|
||||
var args []string
|
||||
var buf *bytes.Buffer = new(bytes.Buffer)
|
||||
|
||||
buf.Reset()
|
||||
if tpl, err = template.New("").Parse(c.Program); err != nil {
|
||||
return
|
||||
}
|
||||
if err = tpl.Execute(buf, t); err != nil {
|
||||
return
|
||||
}
|
||||
progName = buf.String()
|
||||
|
||||
if c.Args != nil && len(c.Args) > 0 {
|
||||
args = make([]string, len(c.Args))
|
||||
for idx, arg := range c.Args {
|
||||
buf.Reset()
|
||||
if tpl, err = template.New("").Parse(arg); err != nil {
|
||||
return
|
||||
}
|
||||
if err = tpl.Execute(buf, t); err != nil {
|
||||
return
|
||||
}
|
||||
args[idx] = buf.String()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if c.EnvVars != nil && len(c.EnvVars) > 0 {
|
||||
envs = make([]string, len(c.EnvVars))
|
||||
for idx, env := range c.EnvVars {
|
||||
buf.Reset()
|
||||
if tpl, err = template.New("").Parse(env); err != nil {
|
||||
return
|
||||
}
|
||||
if err = tpl.Execute(buf, t); err != nil {
|
||||
return
|
||||
}
|
||||
envs[idx] = buf.String()
|
||||
}
|
||||
}
|
||||
if !c.IsolateEnv {
|
||||
if envs != nil && len(envs) > 0 {
|
||||
envs = append(os.Environ(), envs...)
|
||||
} else {
|
||||
envs = os.Environ()
|
||||
}
|
||||
}
|
||||
|
||||
if args != nil && len(args) > 0 {
|
||||
cmd = exec.Command(progName, args...)
|
||||
} else {
|
||||
cmd = exec.Command(progName)
|
||||
}
|
||||
|
||||
cmd.Env = envs
|
||||
|
||||
return
|
||||
}
|
21
tplCmd/types.go
Normal file
21
tplCmd/types.go
Normal file
@ -0,0 +1,21 @@
|
||||
package tplCmd
|
||||
|
||||
import (
|
||||
`encoding/xml`
|
||||
)
|
||||
|
||||
// Cmd is a command that is executed without any templating.
|
||||
type Cmd struct {
|
||||
XMLName xml.Name `json:"-" toml:"-" xml:"cmd" yaml:"-"`
|
||||
Program string `json:"bin" toml:"ProgramPath" xml:"bin,attr" yaml:"Program Path" validate:"required,file"`
|
||||
Args []string `json:"args" toml:"Argument" xml:"args>arg" yaml:"Arguments"`
|
||||
IsolateEnv bool `json:"isol8_env" toml:"IsolatedEnv" xml:"isol8Env,attr" yaml:"Isolated Environment"`
|
||||
EnvVars []string `json:"env" toml:"EnvVars" xml:"envs>env" yaml:"Environment Variables"`
|
||||
OnChanges *bool `json:"on_change,omitempty" toml:"OnChange,omitempty" xml:"onChange,attr,omitempty" yaml:"On Change,omitempty"`
|
||||
}
|
||||
|
||||
// TemplateCmd is a command that supports templating. (It's in its own package to avoid a cyclic dependency.)
|
||||
type TemplateCmd struct {
|
||||
*Cmd `yaml:",inline"`
|
||||
IsTemplate bool `json:"is_tpl" toml:"IsTemplate" xml:"isTpl,attr" yaml:"Is Template"`
|
||||
}
|
23
tunnelbroker/consts.go
Normal file
23
tunnelbroker/consts.go
Normal file
@ -0,0 +1,23 @@
|
||||
package tunnelbroker
|
||||
|
||||
const (
|
||||
wanIpUrl string = "https://c4.r00t2.io/ip"
|
||||
// https://forums.he.net/index.php?topic=3153.0
|
||||
// If no TID is provided, all tunnels are returned.
|
||||
/*
|
||||
All-tunnels mode can *only* use the account's password,
|
||||
the TID-specified can *only* use the Update Key.
|
||||
*/
|
||||
infoBaseUrl string = "https://tunnelbroker.net/tunnelInfo.php"
|
||||
updateBaseUrl string = "https://ipv4.tunnelbroker.net/nic/update"
|
||||
infoTidParam string = "tid"
|
||||
updateTidParam string = "hostname"
|
||||
/*
|
||||
NOTE:
|
||||
This parameter is only required if the client's WAN IP
|
||||
does not match the desired tunnel address.
|
||||
If left off, it defaults to client IP (as seen by the webserver).
|
||||
*/
|
||||
updateIpParam string = "myip"
|
||||
noTunBody string = "No tunnels found"
|
||||
)
|
9
tunnelbroker/funcs.go
Normal file
9
tunnelbroker/funcs.go
Normal file
@ -0,0 +1,9 @@
|
||||
package tunnelbroker
|
||||
|
||||
// NewClient reuturns a Client.
|
||||
func NewClient() (c *Client, err error) {
|
||||
|
||||
// TODO
|
||||
|
||||
return
|
||||
}
|
49
tunnelbroker/types.go
Normal file
49
tunnelbroker/types.go
Normal file
@ -0,0 +1,49 @@
|
||||
package tunnelbroker
|
||||
|
||||
import (
|
||||
`encoding/xml`
|
||||
`net`
|
||||
`net/netip`
|
||||
|
||||
`github.com/go-resty/resty/v2`
|
||||
`r00t2.io/gobroke/conf`
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
tunCfg *conf.Config
|
||||
myAddr net.IP
|
||||
}
|
||||
|
||||
type TunnelList struct {
|
||||
XMLName xml.Name `json:"-" xml:"tunnels" yaml:"-"`
|
||||
Tunnels []*Tunnel `json:"tunnels" xml:"tunnel" yaml:"Tunnels"`
|
||||
}
|
||||
|
||||
type Tunnel struct {
|
||||
XMLName xml.Name `json:"-" xml:"tunnel" yaml:"-"`
|
||||
ID uint `json:"id" xml:"id,attr" yaml:"ID" db:"tun_id"`
|
||||
Description string `json:"desc" xml:"description" yaml:"Description" db:"desc"`
|
||||
ServerIPv4 net.IP `json:"tgt_v4" xml:"serverv4" yaml:"IPv4 Tunnel Target" db:"server_v4"`
|
||||
ClientIPv4 net.IP `json:"client_v4" xml:"clientv4" yaml:"Configured IPv4 Client Address" db:"current_client_v4"`
|
||||
ServerIPv6 net.IP `json:"server_v6" xml:"serverv6" yaml:"IPv6 Endpoint" db:"tunnel_server_v6"`
|
||||
ClientIPv6 net.IP `json:"client_v6" xml:"clientv6" yaml:"IPv6 Tunnel Client Address" db:"tunnel_client_v6"`
|
||||
Routed64 netip.Prefix `json:"routed_64" xml:"routed64" yaml:"Routed /64" db:"prefix_64"`
|
||||
Routed48 *netip.Prefix `json:"routed_48,omitempty" xml:"routed48,omitempty" yaml:"Routed /48,omitempty" db:"prefix_48"`
|
||||
RDNS1 *string `json:"rdns_1,omitempty" xml:"rdns1,omitempty" yaml:"RDNS #1,omitempty" db:"rdns_1"`
|
||||
RDNS2 *string `json:"rdns_2,omitempty" xml:"rdns2,omitempty" yaml:"RDNS #2,omitempty" db:"rdns_2"`
|
||||
RDNS3 *string `json:"rdns_3,omitempty" xml:"rdns3,omitempty" yaml:"RDNS #3,omitempty" db:"rdns_3"`
|
||||
RDNS4 *string `json:"rdns_4,omitempty" xml:"rdns4,omitempty" yaml:"RDNS #4,omitempty" db:"rdns_4"`
|
||||
RDNS5 *string `json:"rdns_5,omitempty" xml:"rdns5,omitempty" yaml:"RDNS #5,omitempty" db:"rdns_5"`
|
||||
client *Client
|
||||
heClient *resty.Client
|
||||
tunCfg *conf.Tunnel
|
||||
}
|
||||
|
||||
type FetchedIP struct {
|
||||
NewClientIPv4 net.IP `json:"new_client_v4" xml:"newClientv4,attr" yaml:"Evaluated IPv4 Client Address" db:"client_ip"`
|
||||
}
|
||||
|
||||
type FetchedTunnel struct {
|
||||
*Tunnel
|
||||
*FetchedIP
|
||||
}
|
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