feat(server): Adds server configuration management commands and metric monitoring functionality.

- Add a new `server config` command to display server configuration.
- Supports displaying the full token via the --full flag.
- Add the metrics-token configuration option for monitoring access control.
- Integrate Prometheus metrics monitoring system
- Add the /metrics endpoint to provide monitoring data in Prometheus format.
- Add detailed metric collection for tunnels, connections, traffic, etc.
- Add a link to the metrics endpoint on the homepage
refactor: Refactor the token display logic to support full display options.
- Refactor the token mask logic in the configuration display
- Supports controlling the token display method via the configFull flag.
build: Update dependency versions
- Updated github.com/spf13/cobra from v1.10.1 to v1.10.2
- Updated golang.org/x/crypto from v0.45.0 to v0.46.0
- Updated golang.org/x/net from v0.47.0 to v0.48.0
- Update golang.org/x/sys from v0.38.0 to v0.39.0
- Added several new indirect dependency packages, including Prometheus-related components.
- Update the versions of several existing dependency packages.
This commit is contained in:
Gouryella
2026-01-03 16:50:28 +08:00
parent fa92896d7e
commit 11ca454659
13 changed files with 480 additions and 54 deletions

3
.gitignore vendored
View File

@@ -53,4 +53,5 @@ certs/
.drip-server.env
benchmark-results/
drip-linux-amd64
./drip
./drip
drip

34
go.mod
View File

@@ -7,29 +7,41 @@ require (
github.com/goccy/go-json v0.10.5
github.com/gorilla/websocket v1.5.3
github.com/hashicorp/yamux v0.1.2
github.com/spf13/cobra v1.10.1
github.com/spf13/cobra v1.10.2
go.uber.org/zap v1.27.1
golang.org/x/crypto v0.45.0
golang.org/x/net v0.47.0
golang.org/x/sys v0.38.0
golang.org/x/crypto v0.46.0
golang.org/x/net v0.48.0
golang.org/x/sys v0.39.0
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
github.com/charmbracelet/x/ansi v0.8.0 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/charmbracelet/colorprofile v0.4.1 // indirect
github.com/charmbracelet/x/ansi v0.11.3 // indirect
github.com/charmbracelet/x/cellbuf v0.0.14 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/clipperhouse/displaywidth v0.6.1 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/prometheus/client_golang v1.23.2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.4 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/stretchr/testify v1.11.1 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/text v0.31.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/text v0.32.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
)

56
go.sum
View File

@@ -1,15 +1,33 @@
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
github.com/charmbracelet/x/ansi v0.11.3 h1:6DcVaqWI82BBVM/atTyq6yBoRLZFBsnoDoX9GCu2YOI=
github.com/charmbracelet/x/ansi v0.11.3/go.mod h1:yI7Zslym9tCJcedxz5+WBq+eUGMJT0bM06Fqy1/Y4dI=
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/cellbuf v0.0.14 h1:iUEMryGyFTelKW3THW4+FfPgi4fkmKnnaLOXuc+/Kj4=
github.com/charmbracelet/x/cellbuf v0.0.14/go.mod h1:P447lJl49ywBbil/KjCk2HexGh4tEY9LH0/1QrZZ9rA=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
github.com/clipperhouse/displaywidth v0.6.1 h1:/zMlAezfDzT2xy6acHBzwIfyu2ic0hgkT83UX5EY2gY=
github.com/clipperhouse/displaywidth v0.6.1/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
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=
@@ -23,20 +41,40 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
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/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc=
github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
@@ -50,17 +88,35 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@@ -117,16 +117,17 @@ func runConfigShow(_ *cobra.Command, _ []string) error {
var displayToken string
if cfg.Token != "" {
tokenLen := len(cfg.Token)
if tokenLen <= 3 {
// For very short tokens, just show asterisks
displayToken = "***"
} else if tokenLen > 10 {
// For long tokens, show first 3 and last 3 characters
displayToken = cfg.Token[:3] + "***" + cfg.Token[tokenLen-3:]
if configFull {
displayToken = cfg.Token
} else {
// For medium tokens (4-10 chars), show first 3 characters
displayToken = cfg.Token[:3] + "***"
tokenLen := len(cfg.Token)
if tokenLen <= 3 {
displayToken = "***"
} else if tokenLen > 10 {
displayToken = cfg.Token[:3] + "***" + cfg.Token[tokenLen-3:]
} else {
displayToken = cfg.Token[:3] + "***"
}
}
} else {
displayToken = ""

View File

@@ -21,16 +21,17 @@ import (
)
var (
serverPort int
serverPublicPort int
serverDomain string
serverAuthToken string
serverDebug bool
serverTCPPortMin int
serverTCPPortMax int
serverTLSCert string
serverTLSKey string
serverPprofPort int
serverPort int
serverPublicPort int
serverDomain string
serverAuthToken string
serverMetricsToken string
serverDebug bool
serverTCPPortMin int
serverTCPPortMax int
serverTLSCert string
serverTLSKey string
serverPprofPort int
)
var serverCmd = &cobra.Command{
@@ -48,6 +49,7 @@ func init() {
serverCmd.Flags().IntVar(&serverPublicPort, "public-port", getEnvInt("DRIP_PUBLIC_PORT", 0), "Public port to display in URLs (env: DRIP_PUBLIC_PORT)")
serverCmd.Flags().StringVarP(&serverDomain, "domain", "d", getEnvString("DRIP_DOMAIN", constants.DefaultDomain), "Server domain (env: DRIP_DOMAIN)")
serverCmd.Flags().StringVarP(&serverAuthToken, "token", "t", getEnvString("DRIP_TOKEN", ""), "Authentication token (env: DRIP_TOKEN)")
serverCmd.Flags().StringVar(&serverMetricsToken, "metrics-token", getEnvString("DRIP_METRICS_TOKEN", ""), "Metrics and stats token (env: DRIP_METRICS_TOKEN)")
serverCmd.Flags().BoolVar(&serverDebug, "debug", false, "Enable debug logging")
serverCmd.Flags().IntVar(&serverTCPPortMin, "tcp-port-min", getEnvInt("DRIP_TCP_PORT_MIN", constants.DefaultTCPPortMin), "Minimum TCP tunnel port (env: DRIP_TCP_PORT_MIN)")
serverCmd.Flags().IntVar(&serverTCPPortMax, "tcp-port-max", getEnvInt("DRIP_TCP_PORT_MAX", constants.DefaultTCPPortMax), "Maximum TCP tunnel port (env: DRIP_TCP_PORT_MAX)")
@@ -130,7 +132,7 @@ func runServer(_ *cobra.Command, _ []string) error {
listenAddr := fmt.Sprintf("0.0.0.0:%d", serverPort)
httpHandler := proxy.NewHandler(tunnelManager, logger, serverDomain, serverAuthToken)
httpHandler := proxy.NewHandler(tunnelManager, logger, serverDomain, serverAuthToken, serverMetricsToken)
listener := tcp.NewListener(listenAddr, tlsConfig, serverAuthToken, tunnelManager, logger, portAllocator, serverDomain, displayPort, httpHandler)

View File

@@ -0,0 +1,176 @@
package cli
import (
"fmt"
"os"
"drip/internal/shared/constants"
"github.com/spf13/cobra"
)
var (
serverConfigFull bool
)
var serverConfigCmd = &cobra.Command{
Use: "config",
Short: "Manage server configuration",
Long: "Display and manage Drip server configuration",
}
var serverConfigShowCmd = &cobra.Command{
Use: "show",
Short: "Show server configuration",
Long: "Display the current Drip server configuration from environment variables and flags",
RunE: runServerConfigShow,
}
func init() {
serverConfigCmd.AddCommand(serverConfigShowCmd)
serverConfigShowCmd.Flags().BoolVar(&serverConfigFull, "full", false, "Show full tokens (not masked)")
serverCmd.AddCommand(serverConfigCmd)
}
func runServerConfigShow(_ *cobra.Command, _ []string) error {
// Read configuration from environment variables and defaults
port := getEnvInt("DRIP_PORT", 8443)
publicPort := getEnvInt("DRIP_PUBLIC_PORT", 0)
domain := getEnvString("DRIP_DOMAIN", constants.DefaultDomain)
token := getEnvString("DRIP_TOKEN", "")
metricsToken := getEnvString("DRIP_METRICS_TOKEN", "")
tlsCert := getEnvString("DRIP_TLS_CERT", "")
tlsKey := getEnvString("DRIP_TLS_KEY", "")
tcpPortMin := getEnvInt("DRIP_TCP_PORT_MIN", constants.DefaultTCPPortMin)
tcpPortMax := getEnvInt("DRIP_TCP_PORT_MAX", constants.DefaultTCPPortMax)
pprofPort := getEnvInt("DRIP_PPROF_PORT", 0)
if publicPort == 0 {
publicPort = port
}
// Mask tokens if not showing full
displayToken := maskToken(token, serverConfigFull)
displayMetricsToken := maskToken(metricsToken, serverConfigFull)
// Print configuration
fmt.Println()
fmt.Println("╔══════════════════════════════════════════════════════════════╗")
fmt.Println("║ Drip Server Configuration ║")
fmt.Println("╚══════════════════════════════════════════════════════════════╝")
fmt.Println()
// Server settings
fmt.Println("📡 Server Settings:")
fmt.Printf(" Domain: %s\n", colorValue(domain))
fmt.Printf(" Port: %s\n", colorValue(fmt.Sprintf("%d", port)))
if publicPort != port {
fmt.Printf(" Public Port: %s\n", colorValue(fmt.Sprintf("%d", publicPort)))
}
fmt.Println()
// Authentication
fmt.Println("🔐 Authentication:")
if token != "" {
fmt.Printf(" Auth Token: %s\n", colorValue(displayToken))
} else {
fmt.Printf(" Auth Token: %s\n", colorWarning("(not set)"))
}
if metricsToken != "" {
fmt.Printf(" Metrics Token: %s\n", colorValue(displayMetricsToken))
} else {
fmt.Printf(" Metrics Token: %s\n", colorWarning("(not set)"))
}
fmt.Println()
// TLS Configuration
fmt.Println("🔒 TLS Configuration:")
if tlsCert != "" {
certStatus := checkFileExists(tlsCert)
fmt.Printf(" Certificate: %s %s\n", colorValue(tlsCert), certStatus)
} else {
fmt.Printf(" Certificate: %s\n", colorError("(not set)"))
}
if tlsKey != "" {
keyStatus := checkFileExists(tlsKey)
fmt.Printf(" Private Key: %s %s\n", colorValue(tlsKey), keyStatus)
} else {
fmt.Printf(" Private Key: %s\n", colorError("(not set)"))
}
fmt.Println()
// TCP Tunnel Ports
fmt.Println("🌐 TCP Tunnel Port Range:")
fmt.Printf(" Min Port: %s\n", colorValue(fmt.Sprintf("%d", tcpPortMin)))
fmt.Printf(" Max Port: %s\n", colorValue(fmt.Sprintf("%d", tcpPortMax)))
fmt.Printf(" Available Ports: %s\n", colorValue(fmt.Sprintf("%d", tcpPortMax-tcpPortMin+1)))
fmt.Println()
// Performance
if pprofPort > 0 {
fmt.Println("⚡ Performance:")
fmt.Printf(" Pprof Port: %s\n", colorValue(fmt.Sprintf("%d", pprofPort)))
fmt.Println()
}
// Configuration sources
fmt.Println("📋 Configuration Sources:")
fmt.Println(" Environment variables (DRIP_*)")
fmt.Println(" Command-line flags")
fmt.Println(" Config file: /etc/drip/server.env")
fmt.Println()
// Endpoints
fmt.Println("🔗 Server Endpoints:")
fmt.Printf(" Main: https://%s:%d\n", domain, publicPort)
fmt.Printf(" Health: https://%s:%d/health\n", domain, publicPort)
fmt.Printf(" Stats: https://%s:%d/stats\n", domain, publicPort)
fmt.Printf(" Metrics: https://%s:%d/metrics\n", domain, publicPort)
fmt.Println()
if !serverConfigFull && (token != "" || metricsToken != "") {
fmt.Println("💡 Tip: Use --full flag to show complete tokens")
fmt.Println()
}
return nil
}
func maskToken(token string, showFull bool) string {
if token == "" {
return ""
}
if showFull {
return token
}
tokenLen := len(token)
if tokenLen <= 8 {
return "***"
}
// Show first 4 and last 4 characters
return token[:4] + "***" + token[tokenLen-4:]
}
func checkFileExists(path string) string {
if _, err := os.Stat(path); err == nil {
return colorSuccess("✓")
}
return colorError("✗ (not found)")
}
func colorValue(s string) string {
return fmt.Sprintf("\033[36m%s\033[0m", s) // Cyan
}
func colorSuccess(s string) string {
return fmt.Sprintf("\033[32m%s\033[0m", s) // Green
}
func colorWarning(s string) string {
return fmt.Sprintf("\033[33m%s\033[0m", s) // Yellow
}
func colorError(s string) string {
return fmt.Sprintf("\033[31m%s\033[0m", s) // Red
}

View File

@@ -0,0 +1,106 @@
package metrics
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
var (
// Tunnel metrics
TunnelCount = promauto.NewGauge(prometheus.GaugeOpts{
Name: "drip_tunnel_count",
Help: "Current number of active tunnels",
})
TunnelRegistrations = promauto.NewCounter(prometheus.CounterOpts{
Name: "drip_tunnel_registrations_total",
Help: "Total number of tunnel registrations",
})
TunnelRegistrationFailures = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "drip_tunnel_registration_failures_total",
Help: "Total number of failed tunnel registrations",
}, []string{"reason"})
TunnelsByIP = promauto.NewGaugeVec(prometheus.GaugeOpts{
Name: "drip_tunnels_by_ip",
Help: "Number of tunnels per client IP",
}, []string{"ip"})
// Connection metrics
ActiveConnections = promauto.NewGauge(prometheus.GaugeOpts{
Name: "drip_active_connections",
Help: "Current number of active TCP connections",
})
TotalConnections = promauto.NewCounter(prometheus.CounterOpts{
Name: "drip_connections_total",
Help: "Total number of connections handled",
})
// Traffic metrics
BytesReceived = promauto.NewCounter(prometheus.CounterOpts{
Name: "drip_bytes_received_total",
Help: "Total bytes received",
})
BytesSent = promauto.NewCounter(prometheus.CounterOpts{
Name: "drip_bytes_sent_total",
Help: "Total bytes sent",
})
RequestsTotal = promauto.NewCounter(prometheus.CounterOpts{
Name: "drip_requests_total",
Help: "Total number of HTTP requests handled",
})
// Per-tunnel metrics
TunnelBytesReceived = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "drip_tunnel_bytes_received_total",
Help: "Total bytes received per tunnel",
}, []string{"tunnel_id", "subdomain", "type"})
TunnelBytesSent = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "drip_tunnel_bytes_sent_total",
Help: "Total bytes sent per tunnel",
}, []string{"tunnel_id", "subdomain", "type"})
TunnelActiveConnections = promauto.NewGaugeVec(prometheus.GaugeOpts{
Name: "drip_tunnel_active_connections",
Help: "Current number of active connections per tunnel",
}, []string{"tunnel_id", "subdomain", "type"})
// Rate limiting metrics
RateLimitRejections = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "drip_rate_limit_rejections_total",
Help: "Total number of rate limit rejections",
}, []string{"type", "ip"})
// System metrics
PanicTotal = promauto.NewCounter(prometheus.CounterOpts{
Name: "drip_panic_total",
Help: "Total number of panics recovered",
})
WorkerPoolSize = promauto.NewGauge(prometheus.GaugeOpts{
Name: "drip_worker_pool_size",
Help: "Current worker pool size",
})
WorkerPoolActiveWorkers = promauto.NewGauge(prometheus.GaugeOpts{
Name: "drip_worker_pool_active_workers",
Help: "Current number of active workers",
})
// HTTP proxy metrics
HTTPRequestDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{
Name: "drip_http_request_duration_seconds",
Help: "HTTP request duration in seconds",
Buckets: prometheus.DefBuckets,
}, []string{"method", "status"})
HTTPRequestsInFlight = promauto.NewGauge(prometheus.GaugeOpts{
Name: "drip_http_requests_in_flight",
Help: "Current number of HTTP requests being processed",
})
)

View File

@@ -20,6 +20,7 @@ import (
"drip/internal/shared/pool"
"drip/internal/shared/protocol"
"github.com/prometheus/client_golang/prometheus/promhttp"
"go.uber.org/zap"
)
@@ -33,18 +34,20 @@ var bufioReaderPool = sync.Pool{
const openStreamTimeout = 3 * time.Second
type Handler struct {
manager *tunnel.Manager
logger *zap.Logger
domain string
authToken string
manager *tunnel.Manager
logger *zap.Logger
domain string
authToken string
metricsToken string
}
func NewHandler(manager *tunnel.Manager, logger *zap.Logger, domain string, authToken string) *Handler {
func NewHandler(manager *tunnel.Manager, logger *zap.Logger, domain string, authToken string, metricsToken string) *Handler {
return &Handler{
manager: manager,
logger: logger,
domain: domain,
authToken: authToken,
manager: manager,
logger: logger,
domain: domain,
authToken: authToken,
metricsToken: metricsToken,
}
}
@@ -57,6 +60,10 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.serveStats(w, r)
return
}
if r.URL.Path == "/metrics" {
h.serveMetrics(w, r)
return
}
subdomain := h.extractSubdomain(r.Host)
if subdomain == "" {
@@ -346,7 +353,7 @@ func (h *Handler) serveHomePage(w http.ResponseWriter, r *http.Request) {
<code>drip http 3000</code><br><br>
<code>drip https 443</code><br><br>
<code>drip tcp 5432</code>
<p><a href="/health">Health Check</a> | <a href="/stats">Statistics</a></p>
<p><a href="/health">Health Check</a> | <a href="/stats">Statistics</a> | <a href="/metrics">Prometheus Metrics</a></p>
</body>
</html>`
@@ -375,7 +382,7 @@ func (h *Handler) serveHealth(w http.ResponseWriter, r *http.Request) {
}
func (h *Handler) serveStats(w http.ResponseWriter, r *http.Request) {
if h.authToken != "" {
if h.metricsToken != "" {
// Only accept token via Authorization header (Bearer token)
// URL query parameters are insecure (logged, cached, visible in browser history)
var token string
@@ -384,9 +391,9 @@ func (h *Handler) serveStats(w http.ResponseWriter, r *http.Request) {
token = strings.TrimPrefix(authHeader, "Bearer ")
}
if token != h.authToken {
if token != h.metricsToken {
w.Header().Set("WWW-Authenticate", `Bearer realm="stats"`)
http.Error(w, "Unauthorized: provide token via 'Authorization: Bearer <token>' header", http.StatusUnauthorized)
http.Error(w, "Unauthorized: provide metrics token via 'Authorization: Bearer <token>' header", http.StatusUnauthorized)
return
}
}
@@ -426,6 +433,26 @@ func (h *Handler) serveStats(w http.ResponseWriter, r *http.Request) {
w.Write(data)
}
func (h *Handler) serveMetrics(w http.ResponseWriter, r *http.Request) {
if h.metricsToken != "" {
// Only accept token via Authorization header (Bearer token)
var token string
authHeader := r.Header.Get("Authorization")
if strings.HasPrefix(authHeader, "Bearer ") {
token = strings.TrimPrefix(authHeader, "Bearer ")
}
if token != h.metricsToken {
w.Header().Set("WWW-Authenticate", `Bearer realm="metrics"`)
http.Error(w, "Unauthorized: provide metrics token via 'Authorization: Bearer <token>' header", http.StatusUnauthorized)
return
}
}
// Serve Prometheus metrics
promhttp.Handler().ServeHTTP(w, r)
}
type bufferedReadWriteCloser struct {
*bufio.Reader
net.Conn

View File

@@ -10,6 +10,7 @@ import (
"sync"
"time"
"drip/internal/server/metrics"
"drip/internal/server/tunnel"
"drip/internal/shared/pool"
"drip/internal/shared/recovery"
@@ -56,6 +57,9 @@ func NewListener(address string, tlsConfig *tls.Config, authToken string, manage
panicMetrics := recovery.NewPanicMetrics(logger, nil)
recoverer := recovery.NewRecoverer(logger, panicMetrics)
// Initialize worker pool metrics
metrics.WorkerPoolSize.Set(float64(workers))
return &Listener{
address: address,
tlsConfig: tlsConfig,
@@ -237,11 +241,17 @@ func (l *Listener) handleConnection(netConn net.Conn) {
l.connections[connID] = conn
l.connMu.Unlock()
// Update connection metrics
metrics.TotalConnections.Inc()
metrics.ActiveConnections.Inc()
defer func() {
l.connMu.Lock()
delete(l.connections, connID)
l.connMu.Unlock()
metrics.ActiveConnections.Dec()
if !conn.IsHandedOff() {
netConn.Close()
}

View File

@@ -6,6 +6,7 @@ import (
"sync/atomic"
"time"
"drip/internal/server/metrics"
"drip/internal/shared/protocol"
"github.com/gorilla/websocket"
"go.uber.org/zap"
@@ -144,6 +145,8 @@ func (c *Connection) AddBytesIn(n int64) {
return
}
c.bytesIn.Add(n)
metrics.BytesReceived.Add(float64(n))
metrics.TunnelBytesReceived.WithLabelValues(c.Subdomain, c.Subdomain, c.GetTunnelType().String()).Add(float64(n))
}
func (c *Connection) AddBytesOut(n int64) {
@@ -151,6 +154,8 @@ func (c *Connection) AddBytesOut(n int64) {
return
}
c.bytesOut.Add(n)
metrics.BytesSent.Add(float64(n))
metrics.TunnelBytesSent.WithLabelValues(c.Subdomain, c.Subdomain, c.GetTunnelType().String()).Add(float64(n))
}
func (c *Connection) GetBytesIn() int64 {
@@ -163,12 +168,14 @@ func (c *Connection) GetBytesOut() int64 {
func (c *Connection) IncActiveConnections() {
c.activeConnections.Add(1)
metrics.TunnelActiveConnections.WithLabelValues(c.Subdomain, c.Subdomain, c.GetTunnelType().String()).Inc()
}
func (c *Connection) DecActiveConnections() {
if v := c.activeConnections.Add(-1); v < 0 {
c.activeConnections.Store(0)
}
metrics.TunnelActiveConnections.WithLabelValues(c.Subdomain, c.Subdomain, c.GetTunnelType().String()).Dec()
}
func (c *Connection) GetActiveConnections() int64 {

View File

@@ -7,6 +7,7 @@ import (
"sync/atomic"
"time"
"drip/internal/server/metrics"
"drip/internal/shared/utils"
"github.com/gorilla/websocket"
"go.uber.org/zap"
@@ -176,6 +177,7 @@ func (m *Manager) RegisterWithIP(conn *websocket.Conn, customSubdomain string, r
zap.Int64("current", current),
zap.Int("max", m.maxTunnels),
)
metrics.TunnelRegistrationFailures.WithLabelValues("max_tunnels").Inc()
return "", ErrTooManyTunnels
}
if m.tunnelCount.CompareAndSwap(current, current+1) {
@@ -199,6 +201,8 @@ func (m *Manager) RegisterWithIP(conn *websocket.Conn, customSubdomain string, r
zap.String("ip", remoteIP),
zap.Int("limit", m.rateLimit),
)
metrics.RateLimitRejections.WithLabelValues("registration", remoteIP).Inc()
metrics.TunnelRegistrationFailures.WithLabelValues("rate_limit").Inc()
return "", ErrRateLimitExceeded
}
@@ -211,11 +215,13 @@ func (m *Manager) RegisterWithIP(conn *websocket.Conn, customSubdomain string, r
zap.Int("current", currentPerIP),
zap.Int("max", m.maxTunnelsPerIP),
)
metrics.TunnelRegistrationFailures.WithLabelValues("max_per_ip").Inc()
return "", ErrTooManyPerIP
}
// Reserve per-IP slot while still holding the lock
m.tunnelsByIP[remoteIP]++
metrics.TunnelsByIP.WithLabelValues(remoteIP).Set(float64(m.tunnelsByIP[remoteIP]))
m.ipMu.Unlock()
}
@@ -293,6 +299,10 @@ func (m *Manager) RegisterWithIP(conn *websocket.Conn, customSubdomain string, r
zap.Int64("total_tunnels", m.tunnelCount.Load()),
)
// Update Prometheus metrics
metrics.TunnelRegistrations.Inc()
metrics.TunnelCount.Set(float64(m.tunnelCount.Load()))
return subdomain, nil
}
@@ -321,6 +331,9 @@ func (m *Manager) Unregister(subdomain string) {
m.tunnelsByIP[remoteIP]--
if m.tunnelsByIP[remoteIP] == 0 {
delete(m.tunnelsByIP, remoteIP)
metrics.TunnelsByIP.DeleteLabelValues(remoteIP)
} else {
metrics.TunnelsByIP.WithLabelValues(remoteIP).Set(float64(m.tunnelsByIP[remoteIP]))
}
}
m.ipMu.Unlock()
@@ -330,6 +343,9 @@ func (m *Manager) Unregister(subdomain string) {
zap.String("subdomain", subdomain),
zap.Int64("total_tunnels", m.tunnelCount.Load()),
)
// Update Prometheus metrics
metrics.TunnelCount.Set(float64(m.tunnelCount.Load()))
}
// Get retrieves a tunnel connection by subdomain

View File

@@ -7,6 +7,7 @@ import (
"sync/atomic"
"time"
"drip/internal/server/metrics"
"go.uber.org/zap"
)
@@ -39,6 +40,7 @@ func NewPanicMetrics(logger *zap.Logger, alerter Alerter) *PanicMetrics {
func (pm *PanicMetrics) RecordPanic(location string, panicValue interface{}) {
atomic.AddUint64(&pm.totalPanics, 1)
metrics.PanicTotal.Inc()
pm.mu.Lock()

View File

@@ -59,8 +59,9 @@ MSG_EN=(
["enter_domain"]="Enter your domain (e.g., tunnel.example.com)"
["domain_required"]="Domain is required"
["enter_port"]="Enter server port"
["enter_token"]="Enter authentication token (leave empty to generate)"
["token_generated"]="Token generated"
["enter_token"]="Enter authentication token (leave empty to auto-generate)"
["token_generated"]="Authentication token generated"
["metrics_token_generated"]="Metrics token generated"
["enter_cert_path"]="Enter TLS certificate path (public key)"
["enter_key_path"]="Enter TLS private key path"
["cert_not_found"]="Certificate file not found"
@@ -94,7 +95,8 @@ MSG_EN=(
["install_complete"]="Installation completed!"
["client_info"]="Client connection info"
["server_addr"]="Server"
["token_label"]="Token"
["token_label"]="Auth Token"
["metrics_token_label"]="Metrics Token"
["service_commands"]="Service management commands"
["cmd_start"]="Start service"
["cmd_stop"]="Stop service"
@@ -149,7 +151,8 @@ MSG_ZH=(
["domain_required"]="域名是必填项"
["enter_port"]="输入服务器端口"
["enter_token"]="输入认证令牌(留空自动生成)"
["token_generated"]="令牌已生成"
["token_generated"]="认证令牌已生成"
["metrics_token_generated"]="监控令牌已生成"
["enter_cert_path"]="输入 TLS 证书路径(公钥)"
["enter_key_path"]="输入 TLS 私钥路径"
["cert_not_found"]="证书文件未找到"
@@ -183,7 +186,8 @@ MSG_ZH=(
["install_complete"]="安装完成!"
["client_info"]="客户端连接信息"
["server_addr"]="服务器"
["token_label"]="令牌"
["token_label"]="认证令牌"
["metrics_token_label"]="监控令牌"
["service_commands"]="服务管理命令"
["cmd_start"]="启动服务"
["cmd_stop"]="停止服务"
@@ -546,7 +550,7 @@ install_binary() {
# Configuration
# ============================================================================
generate_token() {
# Generate a random 32-character token
# Generate a random 32-character token (16 bytes = 32 hex chars)
if command -v openssl &> /dev/null; then
openssl rand -hex 16
else
@@ -583,7 +587,7 @@ configure_server() {
read -p "$(msg enter_tcp_max) [$DEFAULT_TCP_PORT_MAX]: " TCP_PORT_MAX < /dev/tty
TCP_PORT_MAX="${TCP_PORT_MAX:-$DEFAULT_TCP_PORT_MAX}"
# Token
# Authentication token (user can provide or auto-generate)
echo ""
read -p "$(msg enter_token): " TOKEN < /dev/tty
if [[ -z "$TOKEN" ]]; then
@@ -591,6 +595,10 @@ configure_server() {
print_success "$(msg token_generated): $TOKEN"
fi
# Metrics token (always auto-generated)
METRICS_TOKEN=$(generate_token)
print_success "$(msg metrics_token_generated): $METRICS_TOKEN"
# TLS certificate selection
print_panel "$(msg cert_option_title)" \
"${GREEN}1)${NC} $(msg cert_option_certbot)" \
@@ -946,6 +954,7 @@ DRIP_PORT=${PORT}
DRIP_PUBLIC_PORT=${PUBLIC_PORT}
DRIP_DOMAIN=${DOMAIN}
DRIP_TOKEN=${TOKEN}
DRIP_METRICS_TOKEN=${METRICS_TOKEN}
# TLS certificate paths
DRIP_TLS_CERT=${CERT_PATH}
@@ -995,8 +1004,9 @@ show_completion() {
print_panel "$(msg install_complete)"
echo -e "${CYAN}$(msg client_info):${NC}"
echo -e " ${BOLD}$(msg server_addr):${NC} ${DOMAIN}:${PORT}"
echo -e " ${BOLD}$(msg token_label):${NC} ${TOKEN}"
echo -e " ${BOLD}$(msg server_addr):${NC} ${DOMAIN}:${PORT}"
echo -e " ${BOLD}$(msg token_label):${NC} ${TOKEN}"
echo -e " ${BOLD}$(msg metrics_token_label):${NC} ${METRICS_TOKEN}"
echo ""
echo -e "${CYAN}$(msg service_commands):${NC}"