Remove token from client
This commit is contained in:
24
README.md
24
README.md
@@ -28,12 +28,9 @@ go build -o luxtools-client .
|
|||||||
./luxtools-client
|
./luxtools-client
|
||||||
```
|
```
|
||||||
|
|
||||||
On startup, if you don’t pass `-token`, the server generates a random token and prints it to the logs. Put that token into the Dokuwiki plugin configuration.
|
|
||||||
|
|
||||||
### Flags
|
### Flags
|
||||||
|
|
||||||
- `-listen` (default `127.0.0.1:8765`): listen address (`host:port`). Must be loopback.
|
- `-listen` (default `127.0.0.1:8765`): listen address (`host:port`). Must be loopback.
|
||||||
- `-token` (default empty): shared secret token. If empty, a token is generated and logged at startup.
|
|
||||||
- `-allow` (repeatable): allowed path prefix. If you specify one or more, the requested path must start with one of them.
|
- `-allow` (repeatable): allowed path prefix. If you specify one or more, the requested path must start with one of them.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
@@ -42,9 +39,6 @@ Examples:
|
|||||||
# Listen on a different local port
|
# Listen on a different local port
|
||||||
./luxtools-client -listen 127.0.0.1:9000
|
./luxtools-client -listen 127.0.0.1:9000
|
||||||
|
|
||||||
# Use an explicit token (recommended for stable config)
|
|
||||||
./luxtools-client -token "your-shared-secret"
|
|
||||||
|
|
||||||
# Restrict opens to your home directory (repeat -allow as needed)
|
# Restrict opens to your home directory (repeat -allow as needed)
|
||||||
./luxtools-client -allow "$HOME"
|
./luxtools-client -allow "$HOME"
|
||||||
```
|
```
|
||||||
@@ -53,7 +47,6 @@ Examples:
|
|||||||
|
|
||||||
The scripts below install (or update) the client as a service that starts automatically with the system.
|
The scripts below install (or update) the client as a service that starts automatically with the system.
|
||||||
They assume the client binary already exists in the same folder as the scripts.
|
They assume the client binary already exists in the same folder as the scripts.
|
||||||
During install/update, the scripts prompt you for the shared token (press Enter to keep the current token, if already configured).
|
|
||||||
|
|
||||||
### Linux (systemd)
|
### Linux (systemd)
|
||||||
|
|
||||||
@@ -79,7 +72,7 @@ Uninstall:
|
|||||||
./uninstall-linux.sh
|
./uninstall-linux.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
Keep token/config on uninstall:
|
Keep config on uninstall:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./uninstall-linux.sh --keep-config
|
./uninstall-linux.sh --keep-config
|
||||||
@@ -89,7 +82,7 @@ Notes:
|
|||||||
|
|
||||||
- Installs to `~/.local/share/luxtools-client/luxtools-client`
|
- Installs to `~/.local/share/luxtools-client/luxtools-client`
|
||||||
- Creates a systemd *user* unit at `~/.config/systemd/user/luxtools-client.service`
|
- Creates a systemd *user* unit at `~/.config/systemd/user/luxtools-client.service`
|
||||||
- Stores config (including the token) in `~/.config/luxtools-client/luxtools-client.env`
|
- Stores config in `~/.config/luxtools-client/luxtools-client.env`
|
||||||
|
|
||||||
### Windows (Scheduled Task at logon)
|
### Windows (Scheduled Task at logon)
|
||||||
|
|
||||||
@@ -116,7 +109,7 @@ Uninstall:
|
|||||||
uninstall-windows-task.bat
|
uninstall-windows-task.bat
|
||||||
```
|
```
|
||||||
|
|
||||||
Keep token/config on uninstall:
|
Keep config on uninstall:
|
||||||
|
|
||||||
```bat
|
```bat
|
||||||
uninstall-windows-task.bat --keep-config
|
uninstall-windows-task.bat --keep-config
|
||||||
@@ -125,18 +118,11 @@ uninstall-windows-task.bat --keep-config
|
|||||||
Notes:
|
Notes:
|
||||||
|
|
||||||
- Installs to `%LOCALAPPDATA%\luxtools-client\luxtools-client.exe`
|
- Installs to `%LOCALAPPDATA%\luxtools-client\luxtools-client.exe`
|
||||||
- Stores config (including the token) in `%LOCALAPPDATA%\luxtools-client\config.json`
|
- Stores config in `%LOCALAPPDATA%\luxtools-client\config.json`
|
||||||
- Re-running the install script updates the EXE in place and restarts the task.
|
- Re-running the install script updates the EXE in place and restarts the task.
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
### Auth
|
|
||||||
|
|
||||||
Requests must include the token using the `X-Luxtools-Token` header.
|
|
||||||
|
|
||||||
- Header: `X-Luxtools-Token: <token>`
|
|
||||||
- For `GET /open`, a `token=...` query parameter is also accepted as a fallback.
|
|
||||||
|
|
||||||
### `GET /health`
|
### `GET /health`
|
||||||
|
|
||||||
Returns JSON:
|
Returns JSON:
|
||||||
@@ -164,7 +150,6 @@ Example:
|
|||||||
```bash
|
```bash
|
||||||
curl -sS -X POST \
|
curl -sS -X POST \
|
||||||
-H 'Content-Type: application/json' \
|
-H 'Content-Type: application/json' \
|
||||||
-H 'X-Luxtools-Token: your-shared-secret' \
|
|
||||||
--data '{"path":"/tmp"}' \
|
--data '{"path":"/tmp"}' \
|
||||||
http://127.0.0.1:8765/open
|
http://127.0.0.1:8765/open
|
||||||
```
|
```
|
||||||
@@ -177,7 +162,6 @@ Example:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -i \
|
curl -i \
|
||||||
-H 'X-Luxtools-Token: your-shared-secret' \
|
|
||||||
'http://127.0.0.1:8765/open?path=/tmp'
|
'http://127.0.0.1:8765/open?path=/tmp'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ Usage: $0 [--listen host:port] [--allow <path>]...
|
|||||||
|
|
||||||
Installs/updates ${SERVICE_NAME} as a systemd *user* service (runs under your current user).
|
Installs/updates ${SERVICE_NAME} as a systemd *user* service (runs under your current user).
|
||||||
- Re-running updates the installed binary and restarts the service.
|
- Re-running updates the installed binary and restarts the service.
|
||||||
- A stable token is stored in ${ENV_FILE} (created on first install).
|
- Config is stored in ${ENV_FILE} (created on first install).
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--listen host:port Listen address (default: ${DEFAULT_LISTEN})
|
--listen host:port Listen address (default: ${DEFAULT_LISTEN})
|
||||||
@@ -74,48 +74,19 @@ cp "$SRC_BIN" "$TMP_BIN"
|
|||||||
chmod 0755 "$TMP_BIN" || true
|
chmod 0755 "$TMP_BIN" || true
|
||||||
|
|
||||||
if [[ -f "$ENV_FILE" ]]; then
|
if [[ -f "$ENV_FILE" ]]; then
|
||||||
# Preserve existing config (especially TOKEN).
|
# Preserve existing config.
|
||||||
# shellcheck disable=SC1090
|
# shellcheck disable=SC1090
|
||||||
source "$ENV_FILE" || true
|
source "$ENV_FILE" || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
CURRENT_TOKEN="${TOKEN:-}"
|
|
||||||
SUGGESTED_TOKEN=""
|
|
||||||
if [[ -z "${CURRENT_TOKEN}" ]]; then
|
|
||||||
if command -v openssl >/dev/null 2>&1; then
|
|
||||||
SUGGESTED_TOKEN="$(openssl rand -base64 32 | tr '+/' '-_' | tr -d '=\n\r')"
|
|
||||||
else
|
|
||||||
SUGGESTED_TOKEN="$(head -c 32 /dev/urandom | base64 | tr '+/' '-_' | tr -d '=\n\r')"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo
|
|
||||||
if [[ -n "${CURRENT_TOKEN}" ]]; then
|
|
||||||
echo "A token is already configured. Press Enter to keep it, or paste a new one."
|
|
||||||
else
|
|
||||||
echo "No token configured yet. Press Enter to use a generated token, or paste your own."
|
|
||||||
fi
|
|
||||||
read -r -s -p "Token: " TOKEN_INPUT
|
|
||||||
echo
|
|
||||||
|
|
||||||
if [[ -n "${TOKEN_INPUT}" ]]; then
|
|
||||||
TOKEN="${TOKEN_INPUT}"
|
|
||||||
elif [[ -n "${CURRENT_TOKEN}" ]]; then
|
|
||||||
TOKEN="${CURRENT_TOKEN}"
|
|
||||||
else
|
|
||||||
TOKEN="${SUGGESTED_TOKEN}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
cat >"$ENV_FILE" <<EOF
|
cat >"$ENV_FILE" <<EOF
|
||||||
# ${SERVICE_NAME} environment
|
# ${SERVICE_NAME} environment
|
||||||
# Keep this file to preserve your shared token across updates.
|
|
||||||
LISTEN="${LISTEN}"
|
LISTEN="${LISTEN}"
|
||||||
TOKEN="${TOKEN}"
|
|
||||||
ALLOW_ARGS="${ALLOW_ARGS}"
|
ALLOW_ARGS="${ALLOW_ARGS}"
|
||||||
EOF
|
EOF
|
||||||
chmod 0640 "$ENV_FILE"
|
chmod 0640 "$ENV_FILE"
|
||||||
|
|
||||||
# Best-effort tighten config perms for single-user token storage.
|
# Best-effort tighten config perms.
|
||||||
chmod 0600 "$ENV_FILE" || true
|
chmod 0600 "$ENV_FILE" || true
|
||||||
|
|
||||||
install -m 0755 -D "$TMP_BIN" "$BIN_PATH"
|
install -m 0755 -D "$TMP_BIN" "$BIN_PATH"
|
||||||
@@ -127,7 +98,7 @@ Description=luxtools-client (local folder opener helper)
|
|||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
EnvironmentFile=%h/.config/luxtools-client/luxtools-client.env
|
EnvironmentFile=%h/.config/luxtools-client/luxtools-client.env
|
||||||
ExecStart=/bin/sh -lc '%h/.local/share/luxtools-client/luxtools-client -listen "$LISTEN" -token "$TOKEN" $ALLOW_ARGS'
|
ExecStart=/bin/sh -lc '%h/.local/share/luxtools-client/luxtools-client -listen "$LISTEN" $ALLOW_ARGS'
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
RestartSec=1
|
RestartSec=1
|
||||||
|
|
||||||
@@ -147,7 +118,6 @@ echo "Installed/updated ${SERVICE_NAME}."
|
|||||||
echo "- Binary: ${BIN_PATH}"
|
echo "- Binary: ${BIN_PATH}"
|
||||||
echo "- Unit: ${UNIT_FILE}"
|
echo "- Unit: ${UNIT_FILE}"
|
||||||
echo "- Config: ${ENV_FILE}"
|
echo "- Config: ${ENV_FILE}"
|
||||||
echo "Token (set this in the plugin config): ${TOKEN}"
|
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "View logs with: journalctl --user -u ${SERVICE_NAME} -f"
|
echo "View logs with: journalctl --user -u ${SERVICE_NAME} -f"
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ Usage:
|
|||||||
|
|
||||||
Installs/updates $ServiceName as a Windows Scheduled Task (per-user, runs at logon).
|
Installs/updates $ServiceName as a Windows Scheduled Task (per-user, runs at logon).
|
||||||
- Re-running updates the installed binary and restarts the task.
|
- Re-running updates the installed binary and restarts the task.
|
||||||
- A stable token is stored in: %LOCALAPPDATA%\$ServiceName\config.json
|
- Config is stored in: %LOCALAPPDATA%\$ServiceName\config.json
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--listen host:port Listen address (default: 127.0.0.1:8765)
|
--listen host:port Listen address (default: 127.0.0.1:8765)
|
||||||
@@ -34,14 +34,6 @@ function Quote-Arg([string]$s) {
|
|||||||
return $s
|
return $s
|
||||||
}
|
}
|
||||||
|
|
||||||
function New-Base64UrlToken([int]$numBytes = 32) {
|
|
||||||
$bytes = New-Object byte[] $numBytes
|
|
||||||
[System.Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($bytes)
|
|
||||||
$b64 = [Convert]::ToBase64String($bytes)
|
|
||||||
# Base64URL without padding
|
|
||||||
return ($b64.TrimEnd('=') -replace '\+','-' -replace '/','_')
|
|
||||||
}
|
|
||||||
|
|
||||||
function Stop-ExistingInstance([string]$exePath) {
|
function Stop-ExistingInstance([string]$exePath) {
|
||||||
try {
|
try {
|
||||||
if (Get-Command Stop-ScheduledTask -ErrorAction SilentlyContinue) {
|
if (Get-Command Stop-ScheduledTask -ErrorAction SilentlyContinue) {
|
||||||
@@ -106,37 +98,9 @@ $configPath = Join-Path $installDir "config.json"
|
|||||||
|
|
||||||
New-Item -ItemType Directory -Force -Path $installDir | Out-Null
|
New-Item -ItemType Directory -Force -Path $installDir | Out-Null
|
||||||
|
|
||||||
# Load existing config if present (preserve TOKEN across updates).
|
|
||||||
$existingToken = $null
|
|
||||||
if (Test-Path -LiteralPath $configPath) {
|
|
||||||
try {
|
|
||||||
$cfg = Get-Content -LiteralPath $configPath -Raw | ConvertFrom-Json
|
|
||||||
if ($cfg -and $cfg.Token) { $existingToken = [string]$cfg.Token }
|
|
||||||
} catch {
|
|
||||||
# ignore malformed config; will regenerate
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$tokenSuggested = if ($existingToken) { "" } else { New-Base64UrlToken }
|
|
||||||
Write-Host ""
|
|
||||||
if ($existingToken) {
|
|
||||||
Write-Host "A token is already configured. Press Enter to keep it, or paste a new one."
|
|
||||||
} else {
|
|
||||||
Write-Host "No token configured yet. Press Enter to use a generated token, or paste your own."
|
|
||||||
}
|
|
||||||
|
|
||||||
$tokenInput = Read-Host -Prompt "Token" # not secure, but matches Linux behavior
|
|
||||||
$tokenInput = ("" + $tokenInput).Trim()
|
|
||||||
|
|
||||||
$token = if ($tokenInput.Length -gt 0) { $tokenInput } elseif ($existingToken) { $existingToken } else { $tokenSuggested }
|
|
||||||
if (-not $token -or $token.Trim().Length -eq 0) {
|
|
||||||
throw "Failed to determine token"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Persist config.
|
# Persist config.
|
||||||
$config = [ordered]@{
|
$config = [ordered]@{
|
||||||
Listen = $Listen
|
Listen = $Listen
|
||||||
Token = $token
|
|
||||||
Allow = @($Allow)
|
Allow = @($Allow)
|
||||||
}
|
}
|
||||||
($config | ConvertTo-Json -Depth 4) | Set-Content -LiteralPath $configPath -Encoding UTF8
|
($config | ConvertTo-Json -Depth 4) | Set-Content -LiteralPath $configPath -Encoding UTF8
|
||||||
@@ -156,7 +120,7 @@ try {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Build the argument string for the scheduled task.
|
# Build the argument string for the scheduled task.
|
||||||
$argList = @('-listen', $Listen, '-token', $token)
|
$argList = @('-listen', $Listen)
|
||||||
foreach ($p in $Allow) {
|
foreach ($p in $Allow) {
|
||||||
if ($p -and $p.Trim().Length -gt 0) {
|
if ($p -and $p.Trim().Length -gt 0) {
|
||||||
$argList += @('-allow', $p)
|
$argList += @('-allow', $p)
|
||||||
@@ -188,7 +152,6 @@ Write-Host "Installed/updated $ServiceName (Scheduled Task)."
|
|||||||
Write-Host "- Binary: $exePath"
|
Write-Host "- Binary: $exePath"
|
||||||
Write-Host "- Task: $TaskName"
|
Write-Host "- Task: $TaskName"
|
||||||
Write-Host "- Config: $configPath"
|
Write-Host "- Config: $configPath"
|
||||||
Write-Host "Token (set this in the plugin config): $token"
|
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host "To view task status: schtasks /Query /TN $TaskName /V"
|
Write-Host "To view task status: schtasks /Query /TN $TaskName /V"
|
||||||
Write-Host "To run it manually: schtasks /Run /TN $TaskName"
|
Write-Host "To run it manually: schtasks /Run /TN $TaskName"
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ func isKDESession() bool {
|
|||||||
// tabs.
|
// tabs.
|
||||||
//
|
//
|
||||||
// Note: On Plasma Wayland, reliably forcing the window to the foreground is
|
// Note: On Plasma Wayland, reliably forcing the window to the foreground is
|
||||||
// gated by XDG activation tokens; we do a best-effort activation call but it may
|
// gated by XDG activation mechanisms; we do a best-effort activation call but it may
|
||||||
// be ignored by the compositor.
|
// be ignored by the compositor.
|
||||||
func openFolderKDEDBus(path string) error {
|
func openFolderKDEDBus(path string) error {
|
||||||
conn, err := dbus.SessionBus()
|
conn, err := dbus.SessionBus()
|
||||||
|
|||||||
53
main.go
53
main.go
@@ -1,8 +1,6 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
@@ -45,7 +43,6 @@ func main() {
|
|||||||
errLog := log.New(os.Stderr, "ERROR: ", log.LstdFlags)
|
errLog := log.New(os.Stderr, "ERROR: ", log.LstdFlags)
|
||||||
|
|
||||||
listen := flag.String("listen", "127.0.0.1:8765", "listen address (host:port), should be loopback")
|
listen := flag.String("listen", "127.0.0.1:8765", "listen address (host:port), should be loopback")
|
||||||
token := flag.String("token", "", "shared secret token; if empty, requests are allowed without authentication")
|
|
||||||
var allowed allowList
|
var allowed allowList
|
||||||
flag.Var(&allowed, "allow", "allowed path prefix (repeatable); if none, any path is allowed")
|
flag.Var(&allowed, "allow", "allowed path prefix (repeatable); if none, any path is allowed")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
@@ -54,15 +51,6 @@ func main() {
|
|||||||
errLog.Fatalf("refusing to listen on non-loopback address: %s", *listen)
|
errLog.Fatalf("refusing to listen on non-loopback address: %s", *listen)
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.TrimSpace(*token) == "" {
|
|
||||||
generated, err := generateToken()
|
|
||||||
if err != nil {
|
|
||||||
errLog.Fatalf("failed to generate token: %v", err)
|
|
||||||
}
|
|
||||||
*token = generated
|
|
||||||
infoLog.Printf("generated token (set this in the plugin config): %s", *token)
|
|
||||||
}
|
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -100,16 +88,6 @@ func main() {
|
|||||||
|
|
||||||
rawPath = req.Path
|
rawPath = req.Path
|
||||||
|
|
||||||
if !checkToken(r, *token) {
|
|
||||||
// Allow token to be supplied via query string for GET fallback.
|
|
||||||
qt := strings.TrimSpace(r.URL.Query().Get("token"))
|
|
||||||
if qt == "" || !subtleStringEqual(qt, strings.TrimSpace(*token)) {
|
|
||||||
errLog.Printf("/open unauthorized method=%s path=%q headerToken=%t queryToken=%t dur=%s", r.Method, rawPath, strings.TrimSpace(r.Header.Get("X-Luxtools-Token")) != "", qt != "", time.Since(start))
|
|
||||||
writeJSON(w, http.StatusUnauthorized, openResponse{OK: false, Message: "unauthorized"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
target, err := normalizePath(req.Path)
|
target, err := normalizePath(req.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errLog.Printf("/open bad-path method=%s path=%q err=%v dur=%s", r.Method, rawPath, err, time.Since(start))
|
errLog.Printf("/open bad-path method=%s path=%q err=%v dur=%s", r.Method, rawPath, err, time.Since(start))
|
||||||
@@ -165,14 +143,6 @@ func isLoopbackListenAddr(addr string) bool {
|
|||||||
return ip.IsLoopback()
|
return ip.IsLoopback()
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateToken() (string, error) {
|
|
||||||
b := make([]byte, 32)
|
|
||||||
if _, err := rand.Read(b); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return base64.RawURLEncoding.EncodeToString(b), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func withCORS(w http.ResponseWriter, r *http.Request) {
|
func withCORS(w http.ResponseWriter, r *http.Request) {
|
||||||
origin := r.Header.Get("Origin")
|
origin := r.Header.Get("Origin")
|
||||||
if origin != "" {
|
if origin != "" {
|
||||||
@@ -182,28 +152,7 @@ func withCORS(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
}
|
}
|
||||||
w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS")
|
w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS")
|
||||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, X-Luxtools-Token")
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||||
}
|
|
||||||
|
|
||||||
func checkToken(r *http.Request, required string) bool {
|
|
||||||
required = strings.TrimSpace(required)
|
|
||||||
if required == "" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
got := r.Header.Get("X-Luxtools-Token")
|
|
||||||
got = strings.TrimSpace(got)
|
|
||||||
return got != "" && subtleStringEqual(got, required)
|
|
||||||
}
|
|
||||||
|
|
||||||
func subtleStringEqual(a, b string) bool {
|
|
||||||
if len(a) != len(b) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
var v byte
|
|
||||||
for i := 0; i < len(a); i++ {
|
|
||||||
v |= a[i] ^ b[i]
|
|
||||||
}
|
|
||||||
return v == 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizePath(input string) (string, error) {
|
func normalizePath(input string) (string, error) {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Usage: $0 [--keep-config]
|
|||||||
Uninstalls ${SERVICE_NAME} systemd *user* service and removes installed files.
|
Uninstalls ${SERVICE_NAME} systemd *user* service and removes installed files.
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--keep-config Keep ${CONFIG_DIR} (token/config) on disk.
|
--keep-config Keep ${CONFIG_DIR} (config) on disk.
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user