feat: open proxy token file
This commit is contained in:
22
README.md
22
README.md
@@ -18,14 +18,14 @@ open on your laptop instead of failing in a headless shell.
|
|||||||
|
|
||||||
- The **host** runs `open-proxy serve`, listening on `127.0.0.1:7777`.
|
- The **host** runs `open-proxy serve`, listening on `127.0.0.1:7777`.
|
||||||
- The **VM** runs the `open-proxy open` client, which connects to
|
- The **VM** runs the `open-proxy open` client, which connects to
|
||||||
`127.0.0.1:7777` *on the VM*.
|
`127.0.0.1:7777` _on the VM_.
|
||||||
- An SSH **reverse tunnel** maps the VM's `127.0.0.1:7777` back to the host's
|
- An SSH **reverse tunnel** maps the VM's `127.0.0.1:7777` back to the host's
|
||||||
server, so nothing is exposed on the network.
|
server, so nothing is exposed on the network.
|
||||||
|
|
||||||
Per argument, the client does the cheapest thing that works:
|
Per argument, the client does the cheapest thing that works:
|
||||||
|
|
||||||
| Argument | Behavior |
|
| Argument | Behavior |
|
||||||
| ---------------------------- | --------------------------------------------------------------- |
|
| --------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
|
||||||
| Existing **regular file** | Streams the bytes to the host; host writes it to a temp dir (keeping the filename/extension) and opens that copy. |
|
| Existing **regular file** | Streams the bytes to the host; host writes it to a temp dir (keeping the filename/extension) and opens that copy. |
|
||||||
| **URL** (`https://…`, `mailto:…`) | Forwarded as a string; host opens it. |
|
| **URL** (`https://…`, `mailto:…`) | Forwarded as a string; host opens it. |
|
||||||
| Anything else | Forwarded as a string; host's `open` decides. |
|
| Anything else | Forwarded as a string; host's `open` decides. |
|
||||||
@@ -46,7 +46,8 @@ Produces one static binary used for both roles.
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
# Optional but recommended if the VM is multi-user: require a shared secret.
|
# Optional but recommended if the VM is multi-user: require a shared secret.
|
||||||
export OPEN_PROXY_TOKEN=$(openssl rand -hex 16)
|
openssl rand -hex 16 > ~/.config/open-proxy-token
|
||||||
|
export OPEN_PROXY_TOKEN_FILE=~/.config/open-proxy-token
|
||||||
|
|
||||||
open-proxy serve # listens on 127.0.0.1:7777
|
open-proxy serve # listens on 127.0.0.1:7777
|
||||||
```
|
```
|
||||||
@@ -65,14 +66,14 @@ to the GUI.
|
|||||||
ln -sf open-proxy ~/.local/bin/open # convenience / macOS habit
|
ln -sf open-proxy ~/.local/bin/open # convenience / macOS habit
|
||||||
```
|
```
|
||||||
|
|
||||||
`~/.local/bin` must come *before* `/usr/bin` in `$PATH`. The binary checks
|
`~/.local/bin` must come _before_ `/usr/bin` in `$PATH`. The binary checks
|
||||||
`argv[0]`, so a symlink named `xdg-open` or `open` runs in client mode.
|
`argv[0]`, so a symlink named `xdg-open` or `open` runs in client mode.
|
||||||
|
|
||||||
2. If you set a token on the host, set the same one on the VM (e.g. in your
|
2. If you set a token on the host, set the same one on the VM (e.g. via a
|
||||||
shell rc):
|
secret-managed file):
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
export OPEN_PROXY_TOKEN=<same value as host>
|
export OPEN_PROXY_TOKEN_FILE=~/.config/open-proxy-token
|
||||||
```
|
```
|
||||||
|
|
||||||
## The tunnel
|
## The tunnel
|
||||||
@@ -110,10 +111,10 @@ fail — it falls back to the **real** opener. It walks `$PATH` for the next
|
|||||||
`xdg-open`/`open` that isn't this binary and runs that, so a shadowed VM still
|
`xdg-open`/`open` that isn't this binary and runs that, so a shadowed VM still
|
||||||
behaves sanely offline.
|
behaves sanely offline.
|
||||||
|
|
||||||
Fallback fires only on a *connection* failure. An HTTP rejection (e.g. a 403
|
Fallback fires only on a _connection_ failure. An HTTP rejection (e.g. a 403
|
||||||
from a token mismatch) is surfaced as an error, not silently opened locally.
|
from a token mismatch) is surfaced as an error, not silently opened locally.
|
||||||
|
|
||||||
Both the fallback and the host's own opener resolve the *real* `xdg-open`/`open`
|
Both the fallback and the host's own opener resolve the _real_ `xdg-open`/`open`
|
||||||
by skipping this binary on `$PATH`, so it's safe to shadow the opener names on
|
by skipping this binary on `$PATH`, so it's safe to shadow the opener names on
|
||||||
the host too (e.g. a box that is both host and VM) without recursing.
|
the host too (e.g. a box that is both host and VM) without recursing.
|
||||||
|
|
||||||
@@ -128,9 +129,10 @@ open ./diagram.png # copied to host, opens in host image viewer
|
|||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
| Variable | Default | Meaning |
|
| Variable | Default | Meaning |
|
||||||
| -------------------- | ------------------ | ---------------------------------------------------- |
|
| ----------------------- | ------------------ | ------------------------------------------------------------------------- |
|
||||||
| `OPEN_PROXY_ADDR` | `127.0.0.1:7777` | Client target / server listen address. |
|
| `OPEN_PROXY_ADDR` | `127.0.0.1:7777` | Client target / server listen address. |
|
||||||
| `OPEN_PROXY_TOKEN` | _(unset)_ | Shared secret; if set on the server, clients must match. |
|
| `OPEN_PROXY_TOKEN` | _(unset)_ | Shared secret; if set on the server, clients must match. |
|
||||||
|
| `OPEN_PROXY_TOKEN_FILE` | _(unset)_ | File containing the shared secret, used when `OPEN_PROXY_TOKEN` is unset. |
|
||||||
| `OPEN_PROXY_MAXSIZE` | `104857600` (100M) | Max file transfer size in bytes (server-side). |
|
| `OPEN_PROXY_MAXSIZE` | `104857600` (100M) | Max file transfer size in bytes (server-side). |
|
||||||
|
|
||||||
## Security notes
|
## Security notes
|
||||||
|
|||||||
@@ -29,9 +29,15 @@ func runClient(args []string, openerName string) int {
|
|||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
token, err := envToken()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "open: %v\n", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
c := &client{
|
c := &client{
|
||||||
base: "http://" + envAddr(),
|
base: "http://" + envAddr(),
|
||||||
token: envToken(),
|
token: token,
|
||||||
}
|
}
|
||||||
|
|
||||||
exit := 0
|
exit := 0
|
||||||
|
|||||||
1
main.go
1
main.go
@@ -57,6 +57,7 @@ Usage:
|
|||||||
Environment:
|
Environment:
|
||||||
OPEN_PROXY_ADDR client target / server listen address (default 127.0.0.1:7777)
|
OPEN_PROXY_ADDR client target / server listen address (default 127.0.0.1:7777)
|
||||||
OPEN_PROXY_TOKEN shared secret; if set on the server, clients must match
|
OPEN_PROXY_TOKEN shared secret; if set on the server, clients must match
|
||||||
|
OPEN_PROXY_TOKEN_FILE file containing the shared secret, used when OPEN_PROXY_TOKEN is unset
|
||||||
OPEN_PROXY_MAXSIZE max file transfer size in bytes (default 104857600)
|
OPEN_PROXY_MAXSIZE max file transfer size in bytes (default 104857600)
|
||||||
|
|
||||||
Tip: symlink the binary to "open" and/or "xdg-open" early on the VM's $PATH.
|
Tip: symlink the binary to "open" and/or "xdg-open" early on the VM's $PATH.
|
||||||
|
|||||||
24
proto.go
24
proto.go
@@ -1,8 +1,10 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -20,8 +22,26 @@ func envAddr() string {
|
|||||||
return defaultAddr
|
return defaultAddr
|
||||||
}
|
}
|
||||||
|
|
||||||
func envToken() string {
|
func envToken() (string, error) {
|
||||||
return os.Getenv("OPEN_PROXY_TOKEN")
|
if v := os.Getenv("OPEN_PROXY_TOKEN"); v != "" {
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
path := os.Getenv("OPEN_PROXY_TOKEN_FILE")
|
||||||
|
if path == "" {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("read OPEN_PROXY_TOKEN_FILE %q: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
token := strings.TrimRight(string(b), "\r\n")
|
||||||
|
if token == "" {
|
||||||
|
return "", fmt.Errorf("OPEN_PROXY_TOKEN_FILE %q is empty", path)
|
||||||
|
}
|
||||||
|
return token, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func envMaxSize() int64 {
|
func envMaxSize() int64 {
|
||||||
|
|||||||
48
proto_test.go
Normal file
48
proto_test.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEnvTokenPrefersTokenEnv(t *testing.T) {
|
||||||
|
t.Setenv("OPEN_PROXY_TOKEN", "env-token")
|
||||||
|
t.Setenv("OPEN_PROXY_TOKEN_FILE", filepath.Join(t.TempDir(), "missing"))
|
||||||
|
|
||||||
|
token, err := envToken()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("envToken() error = %v", err)
|
||||||
|
}
|
||||||
|
if token != "env-token" {
|
||||||
|
t.Fatalf("envToken() = %q, want %q", token, "env-token")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnvTokenReadsTokenFile(t *testing.T) {
|
||||||
|
path := filepath.Join(t.TempDir(), "token")
|
||||||
|
if err := os.WriteFile(path, []byte("file-token\n"), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Setenv("OPEN_PROXY_TOKEN_FILE", path)
|
||||||
|
|
||||||
|
token, err := envToken()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("envToken() error = %v", err)
|
||||||
|
}
|
||||||
|
if token != "file-token" {
|
||||||
|
t.Fatalf("envToken() = %q, want %q", token, "file-token")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnvTokenErrorsOnEmptyTokenFile(t *testing.T) {
|
||||||
|
path := filepath.Join(t.TempDir(), "token")
|
||||||
|
if err := os.WriteFile(path, []byte("\n"), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Setenv("OPEN_PROXY_TOKEN_FILE", path)
|
||||||
|
|
||||||
|
if token, err := envToken(); err == nil {
|
||||||
|
t.Fatalf("envToken() = %q, nil error; want error", token)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,7 +19,13 @@ func runServer(argv []string) int {
|
|||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
|
|
||||||
srv := &server{token: envToken(), maxSize: envMaxSize()}
|
token, err := envToken()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "open-proxy serve: %v\n", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
srv := &server{token: token, maxSize: envMaxSize()}
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
Reference in New Issue
Block a user