feat: open proxy token file
This commit is contained in:
38
README.md
38
README.md
@@ -18,17 +18,17 @@ 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 **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
|
||||
server, so nothing is exposed on the network.
|
||||
|
||||
Per argument, the client does the cheapest thing that works:
|
||||
|
||||
| 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. |
|
||||
| **URL** (`https://…`, `mailto:…`) | Forwarded as a string; host opens it. |
|
||||
| Anything else | Forwarded as a string; host's `open` decides. |
|
||||
| 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. |
|
||||
| **URL** (`https://…`, `mailto:…`) | Forwarded as a string; host opens it. |
|
||||
| Anything else | Forwarded as a string; host's `open` decides. |
|
||||
|
||||
> **Best effort, by design.** A copied `report.html` that references sibling
|
||||
> assets (`./style.css`, images) won't find them on the host — only the single
|
||||
@@ -46,7 +46,8 @@ Produces one static binary used for both roles.
|
||||
|
||||
```sh
|
||||
# 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
|
||||
```
|
||||
@@ -65,14 +66,14 @@ to the GUI.
|
||||
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.
|
||||
|
||||
2. If you set a token on the host, set the same one on the VM (e.g. in your
|
||||
shell rc):
|
||||
2. If you set a token on the host, set the same one on the VM (e.g. via a
|
||||
secret-managed file):
|
||||
|
||||
```sh
|
||||
export OPEN_PROXY_TOKEN=<same value as host>
|
||||
export OPEN_PROXY_TOKEN_FILE=~/.config/open-proxy-token
|
||||
```
|
||||
|
||||
## 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
|
||||
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.
|
||||
|
||||
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
|
||||
the host too (e.g. a box that is both host and VM) without recursing.
|
||||
|
||||
@@ -127,11 +128,12 @@ open ./diagram.png # copied to host, opens in host image viewer
|
||||
|
||||
## Configuration
|
||||
|
||||
| Variable | Default | Meaning |
|
||||
| -------------------- | ------------------ | ---------------------------------------------------- |
|
||||
| `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_MAXSIZE` | `104857600` (100M) | Max file transfer size in bytes (server-side). |
|
||||
| Variable | Default | Meaning |
|
||||
| ----------------------- | ------------------ | ------------------------------------------------------------------------- |
|
||||
| `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_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). |
|
||||
|
||||
## Security notes
|
||||
|
||||
|
||||
@@ -29,9 +29,15 @@ func runClient(args []string, openerName string) int {
|
||||
return 2
|
||||
}
|
||||
|
||||
token, err := envToken()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "open: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
c := &client{
|
||||
base: "http://" + envAddr(),
|
||||
token: envToken(),
|
||||
token: token,
|
||||
}
|
||||
|
||||
exit := 0
|
||||
|
||||
5
main.go
5
main.go
@@ -56,8 +56,9 @@ Usage:
|
||||
|
||||
Environment:
|
||||
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_MAXSIZE max file transfer size in bytes (default 104857600)
|
||||
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)
|
||||
|
||||
Tip: symlink the binary to "open" and/or "xdg-open" early on the VM's $PATH.
|
||||
When the host is unreachable, it falls back to the real opener on $PATH.
|
||||
|
||||
24
proto.go
24
proto.go
@@ -1,8 +1,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -20,8 +22,26 @@ func envAddr() string {
|
||||
return defaultAddr
|
||||
}
|
||||
|
||||
func envToken() string {
|
||||
return os.Getenv("OPEN_PROXY_TOKEN")
|
||||
func envToken() (string, error) {
|
||||
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 {
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
Reference in New Issue
Block a user