Support multiple Telebit client bindings
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
.env
|
.env
|
||||||
bin/
|
bin/
|
||||||
telebit-client.env
|
telebit-client.env
|
||||||
|
.telebit-clients/
|
||||||
|
|||||||
13
README.md
13
README.md
@@ -113,7 +113,7 @@ Create a new tunnel key for a subdomain:
|
|||||||
./telebitctl bind test
|
./telebitctl bind test
|
||||||
```
|
```
|
||||||
|
|
||||||
This creates `test.$DOMAIN` and writes `telebit-client.env`.
|
This creates `test.$DOMAIN` and writes `.telebit-clients/test.env`.
|
||||||
|
|
||||||
Run the tunnel to a local service:
|
Run the tunnel to a local service:
|
||||||
|
|
||||||
@@ -127,10 +127,13 @@ That forwards:
|
|||||||
https://test.$DOMAIN -> localhost:80
|
https://test.$DOMAIN -> localhost:80
|
||||||
```
|
```
|
||||||
|
|
||||||
`up` also installs the active client config to:
|
`up` uses a per-slug client config. If `.telebit-clients/test.env` already exists, it reuses that key and only updates the local port. If it does not exist, it binds `test` before starting the tunnel.
|
||||||
|
|
||||||
```text
|
Because each slug has its own env file, multiple tunnels can run at the same time:
|
||||||
~/.config/telebit/client.env
|
|
||||||
|
```sh
|
||||||
|
telebitctl up app 3000
|
||||||
|
telebitctl up api 8080
|
||||||
```
|
```
|
||||||
|
|
||||||
Rotate the client key:
|
Rotate the client key:
|
||||||
@@ -147,7 +150,7 @@ Release a subdomain:
|
|||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- `.env`, `telebit-client.env`, and `bin/` are ignored by git.
|
- `.env`, `.telebit-clients/`, `telebit-client.env`, and `bin/` are ignored by git.
|
||||||
- `SECRET` in `.env` is the management/relay master secret. Do not use it directly as a long-lived client key.
|
- `SECRET` in `.env` is the management/relay master secret. Do not use it directly as a long-lived client key.
|
||||||
- Client keys are one-time registration secrets. Use `bind` or `auth refresh` to create device-specific keys.
|
- Client keys are one-time registration secrets. Use `bind` or `auth refresh` to create device-specific keys.
|
||||||
- If `/ws` returns nginx `502`, nginx is probably still proxying to `https://127.0.0.1:8085`; it must use `http://127.0.0.1:8085`.
|
- If `/ws` returns nginx `502`, nginx is probably still proxying to `https://127.0.0.1:8085`; it must use `http://127.0.0.1:8085`.
|
||||||
|
|||||||
85
telebitctl
85
telebitctl
@@ -4,7 +4,7 @@ set -eu
|
|||||||
ROOT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
ROOT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||||
SERVER_ENV="${TELEBIT_SERVER_ENV:-$ROOT_DIR/.env}"
|
SERVER_ENV="${TELEBIT_SERVER_ENV:-$ROOT_DIR/.env}"
|
||||||
CLIENT_ENV="${TELEBIT_CLIENT_ENV:-$ROOT_DIR/telebit-client.env}"
|
CLIENT_ENV="${TELEBIT_CLIENT_ENV:-$ROOT_DIR/telebit-client.env}"
|
||||||
INSTALLED_CLIENT_ENV="${TELEBIT_INSTALLED_CLIENT_ENV:-$HOME/.config/telebit/client.env}"
|
CLIENT_ENV_DIR="${TELEBIT_CLIENT_ENV_DIR:-$ROOT_DIR/.telebit-clients}"
|
||||||
TELEBIT_BIN="${TELEBIT_BIN:-$ROOT_DIR/bin/telebit}"
|
TELEBIT_BIN="${TELEBIT_BIN:-$ROOT_DIR/bin/telebit}"
|
||||||
SIGNJWT_BIN="${SIGNJWT_BIN:-$ROOT_DIR/bin/signjwt}"
|
SIGNJWT_BIN="${SIGNJWT_BIN:-$ROOT_DIR/bin/signjwt}"
|
||||||
|
|
||||||
@@ -69,14 +69,36 @@ current_port() {
|
|||||||
sed -n 's#^LOCALS=https:[^:]*:\([0-9][0-9]*\).*#\1#p' "$CLIENT_ENV" | sed -n '1p'
|
sed -n 's#^LOCALS=https:[^:]*:\([0-9][0-9]*\).*#\1#p' "$CLIENT_ENV" | sed -n '1p'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
safe_slug() {
|
||||||
|
case "$1" in
|
||||||
|
""|*/*|*..*|.*|*-|-*|*[^A-Za-z0-9-]*)
|
||||||
|
die "Invalid slug: $1"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
client_env_path() {
|
||||||
|
slug="$1"
|
||||||
|
safe_slug "$slug"
|
||||||
|
printf '%s/%s.env\n' "$CLIENT_ENV_DIR" "$slug"
|
||||||
|
}
|
||||||
|
|
||||||
|
env_port() {
|
||||||
|
env_file="$1"
|
||||||
|
[ -f "$env_file" ] || return 1
|
||||||
|
sed -n 's#^LOCALS=https:[^:]*:\([0-9][0-9]*\).*#\1#p' "$env_file" | sed -n '1p'
|
||||||
|
}
|
||||||
|
|
||||||
write_client_env() {
|
write_client_env() {
|
||||||
slug="$1"
|
slug="$1"
|
||||||
shared_key="$2"
|
shared_key="$2"
|
||||||
port="${3:-$(current_port || true)}"
|
port="${3:-}"
|
||||||
install="${4:-false}"
|
env_file="${4:-$(client_env_path "$slug")}"
|
||||||
|
port="${port:-$(env_port "$env_file" || current_port || true)}"
|
||||||
port="${port:-80}"
|
port="${port:-80}"
|
||||||
|
|
||||||
cat > "$CLIENT_ENV" <<EOF
|
mkdir -p "$(dirname "$env_file")"
|
||||||
|
cat > "$env_file" <<EOF
|
||||||
TUNNEL_RELAY_URL=wss://$TUNNEL_DOMAIN/ws
|
TUNNEL_RELAY_URL=wss://$TUNNEL_DOMAIN/ws
|
||||||
AUTH_URL=https://$TUNNEL_DOMAIN/api
|
AUTH_URL=https://$TUNNEL_DOMAIN/api
|
||||||
ACME_HTTP_01_RELAY_URL=https://$TUNNEL_DOMAIN/api/acme-relay
|
ACME_HTTP_01_RELAY_URL=https://$TUNNEL_DOMAIN/api/acme-relay
|
||||||
@@ -89,24 +111,19 @@ SECRET=$shared_key
|
|||||||
LOCALS=https:$slug.$DOMAIN:$port
|
LOCALS=https:$slug.$DOMAIN:$port
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
if [ "$install" = "true" ]; then
|
cp "$env_file" "$CLIENT_ENV"
|
||||||
mkdir -p "$(dirname "$INSTALLED_CLIENT_ENV")"
|
printf 'Updated %s\n' "$env_file"
|
||||||
cp "$CLIENT_ENV" "$INSTALLED_CLIENT_ENV"
|
|
||||||
printf 'Updated %s and %s\n' "$CLIENT_ENV" "$INSTALLED_CLIENT_ENV"
|
|
||||||
else
|
|
||||||
printf 'Updated %s\n' "$CLIENT_ENV"
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
client_key() {
|
client_key() {
|
||||||
[ -f "$CLIENT_ENV" ] || return 1
|
env_file="${1:-$CLIENT_ENV}"
|
||||||
sed -n 's/^SECRET=//p' "$CLIENT_ENV" | sed -n '1p'
|
[ -f "$env_file" ] || return 1
|
||||||
|
sed -n 's/^SECRET=//p' "$env_file" | sed -n '1p'
|
||||||
}
|
}
|
||||||
|
|
||||||
bind_slug() {
|
create_device() {
|
||||||
load_server_env
|
slug="$1"
|
||||||
slug="${1:?bind requires a slug}"
|
token="$2"
|
||||||
token=$(admin_token)
|
|
||||||
response=$(curl -fsS -X POST "$MGMT_URL/devices" \
|
response=$(curl -fsS -X POST "$MGMT_URL/devices" \
|
||||||
-H "Authorization: Bearer $token" \
|
-H "Authorization: Bearer $token" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
@@ -114,16 +131,29 @@ bind_slug() {
|
|||||||
|
|
||||||
shared_key=$(printf '%s\n' "$response" | sed -n 's/.*"shared_key":"\([^"]*\)".*/\1/p')
|
shared_key=$(printf '%s\n' "$response" | sed -n 's/.*"shared_key":"\([^"]*\)".*/\1/p')
|
||||||
[ -n "$shared_key" ] || die "Could not parse shared_key from response: $response"
|
[ -n "$shared_key" ] || die "Could not parse shared_key from response: $response"
|
||||||
|
printf '%s\n' "$shared_key"
|
||||||
|
}
|
||||||
|
|
||||||
write_client_env "$slug" "$shared_key" "$(current_port || true)" false
|
bind_slug() {
|
||||||
|
load_server_env
|
||||||
|
slug="${1:?bind requires a slug}"
|
||||||
|
env_file=$(client_env_path "$slug")
|
||||||
|
token=$(admin_token)
|
||||||
|
shared_key=$(create_device "$slug" "$token")
|
||||||
|
write_client_env "$slug" "$shared_key" "$(env_port "$env_file" || current_port || true)" "$env_file"
|
||||||
printf 'Bound %s.%s\n' "$slug" "$DOMAIN"
|
printf 'Bound %s.%s\n' "$slug" "$DOMAIN"
|
||||||
}
|
}
|
||||||
|
|
||||||
release_slug() {
|
release_slug() {
|
||||||
load_server_env
|
load_server_env
|
||||||
slug="${1:?release requires a slug}"
|
slug="${1:?release requires a slug}"
|
||||||
|
env_file=$(client_env_path "$slug")
|
||||||
token=$(admin_token)
|
token=$(admin_token)
|
||||||
delete_device "$slug" "$token" true
|
delete_device "$slug" "$token" true
|
||||||
|
rm -f "$env_file"
|
||||||
|
if [ "$(current_slug || true)" = "$slug" ]; then
|
||||||
|
rm -f "$CLIENT_ENV"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
delete_device() {
|
delete_device() {
|
||||||
@@ -152,9 +182,12 @@ refresh_auth() {
|
|||||||
load_server_env
|
load_server_env
|
||||||
slug="${1:-$(current_slug || true)}"
|
slug="${1:-$(current_slug || true)}"
|
||||||
slug="${slug:-test}"
|
slug="${slug:-test}"
|
||||||
|
env_file=$(client_env_path "$slug")
|
||||||
token=$(admin_token)
|
token=$(admin_token)
|
||||||
delete_device "$slug" "$token" false >/dev/null 2>&1 || true
|
delete_device "$slug" "$token" false >/dev/null 2>&1 || true
|
||||||
bind_slug "$slug"
|
shared_key=$(create_device "$slug" "$token")
|
||||||
|
write_client_env "$slug" "$shared_key" "$(env_port "$env_file" || current_port || true)" "$env_file"
|
||||||
|
printf 'Bound %s.%s\n' "$slug" "$DOMAIN"
|
||||||
}
|
}
|
||||||
|
|
||||||
up_client() {
|
up_client() {
|
||||||
@@ -162,12 +195,18 @@ up_client() {
|
|||||||
need_bin "$TELEBIT_BIN"
|
need_bin "$TELEBIT_BIN"
|
||||||
slug="${1:?up requires a slug}"
|
slug="${1:?up requires a slug}"
|
||||||
port="${2:?up requires a local port}"
|
port="${2:?up requires a local port}"
|
||||||
key="$(client_key || true)"
|
env_file=$(client_env_path "$slug")
|
||||||
[ -n "$key" ] || die "No client key found. Run: ./telebitctl bind $slug"
|
key="$(client_key "$env_file" || true)"
|
||||||
write_client_env "$slug" "$key" "$port" true >/dev/null
|
|
||||||
|
if [ -z "$key" ]; then
|
||||||
|
token=$(admin_token)
|
||||||
|
key=$(create_device "$slug" "$token")
|
||||||
|
fi
|
||||||
|
|
||||||
|
write_client_env "$slug" "$key" "$port" "$env_file" >/dev/null
|
||||||
unset SECRET DOMAIN TUNNEL_DOMAIN MGMT_URL POSTGRES_PASSWORD TELEBIT_PORT MGMT_PORT VENDOR_ID
|
unset SECRET DOMAIN TUNNEL_DOMAIN MGMT_URL POSTGRES_PASSWORD TELEBIT_PORT MGMT_PORT VENDOR_ID
|
||||||
cd "$HOME"
|
cd "$HOME"
|
||||||
exec "$TELEBIT_BIN" --env "$INSTALLED_CLIENT_ENV"
|
exec "$TELEBIT_BIN" --env "$env_file"
|
||||||
}
|
}
|
||||||
|
|
||||||
build_client() {
|
build_client() {
|
||||||
|
|||||||
Reference in New Issue
Block a user