summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJunegunn Choi <junegunn.c@gmail.com>2023-11-05 10:50:11 +0900
committerJunegunn Choi <junegunn.c@gmail.com>2023-11-05 10:53:46 +0900
commita8186531740326e4eace928b84f78d130e67c319 (patch)
treef2902e3208d4055270f5556ff0b17a004a7635b9
parent5c3b044740a31f3f3a69d5cf5ae0122696f73ceb (diff)
Add --listen-unsafe=ADDR to allow remote process execution (#3498)
-rw-r--r--CHANGELOG.md4
-rw-r--r--man/man1/fzf.119
-rw-r--r--src/options.go36
-rw-r--r--src/server.go50
-rw-r--r--src/terminal.go36
5 files changed, 108 insertions, 37 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e561df6a..b7423f6d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,6 +16,10 @@ CHANGELOG
# FZF_API_KEY is required for a non-localhost listen address
export FZF_API_KEY="$(head -c 32 /dev/urandom | base64)"
fzf --listen 0.0.0.0:6266
+
+ # To allow remote process execution, use `--listen-unsafe` instead
+ # (execute, reload, become, preview, change-preview, tranform-*, etc.)
+ fzf --listen-unsafe 0.0.0.0:6266
```
- Bug fixes
diff --git a/man/man1/fzf.1 b/man/man1/fzf.1
index 66bc678b..6efd8a12 100644
--- a/man/man1/fzf.1
+++ b/man/man1/fzf.1
@@ -793,14 +793,19 @@ ncurses finder only after the input stream is complete.
e.g. \fBfzf --multi | fzf --sync\fR
.RE
.TP
-.B "--listen[=[ADDR:]PORT]"
+.B "--listen[=[ADDR:]PORT]" "--listen-unsafe[=[ADDR:]PORT]"
Start HTTP server and listen on the given address. It allows external processes
-to send actions to perform via POST method. If the port number is omitted or
-given as 0, fzf will automatically choose a port and export it as
-\fBFZF_PORT\fR environment variable to the child processes. If
-\fBFZF_API_KEY\fR environment variable is set, the server would require sending
-an API key with the same value in the \fBx-api-key\fR HTTP header.
-\fBFZF_API_KEY\fR is required for a non-localhost listen address.
+to send actions to perform via POST method.
+
+- If the port number is omitted or given as 0, fzf will automatically choose
+a port and export it as \fBFZF_PORT\fR environment variable to the child processes
+
+- If \fBFZF_API_KEY\fR environment variable is set, the server would require
+sending an API key with the same value in the \fBx-api-key\fR HTTP header
+
+- \fBFZF_API_KEY\fR is required for a non-localhost listen address
+
+- To allow remote process execution, use \fB--listen-unsafe\fR
e.g.
\fB# Start HTTP server on port 6266
diff --git a/src/options.go b/src/options.go
index 4176292f..d2c26082 100644
--- a/src/options.go
+++ b/src/options.go
@@ -119,6 +119,7 @@ const usage = `usage: fzf [options]
--print0 Print output delimited by ASCII NUL characters
--sync Synchronous search for multi-staged filtering
--listen[=[ADDR:]PORT] Start HTTP server to receive actions (POST /)
+ (To allow remote process execution, use --listen-unsafe)
--version Display version information and exit
Environment variables
@@ -334,7 +335,8 @@ type Options struct {
PreviewLabel labelOpts
Unicode bool
Tabstop int
- ListenAddr *string
+ ListenAddr *listenAddress
+ Unsafe bool
ClearOnExit bool
Version bool
}
@@ -404,6 +406,7 @@ func defaultOptions() *Options {
Tabstop: 8,
BorderLabel: labelOpts{},
PreviewLabel: labelOpts{},
+ Unsafe: false,
ClearOnExit: true,
Version: false}
}
@@ -1832,14 +1835,21 @@ func parseOptions(opts *Options, allArgs []string) {
nextString(allArgs, &i, "padding required (TRBL / TB,RL / T,RL,B / T,R,B,L)"))
case "--tabstop":
opts.Tabstop = nextInt(allArgs, &i, "tab stop required")
- case "--listen":
- given, addr := optionalNextString(allArgs, &i)
- if !given {
- addr = defaultListenAddr
+ case "--listen", "--listen-unsafe":
+ given, str := optionalNextString(allArgs, &i)
+ addr := defaultListenAddr
+ if given {
+ var err error
+ err, addr = parseListenAddress(str)
+ if err != nil {
+ errorExit(err.Error())
+ }
}
opts.ListenAddr = &addr
- case "--no-listen":
+ opts.Unsafe = arg == "--listen-unsafe"
+ case "--no-listen", "--no-listen-unsafe":
opts.ListenAddr = nil
+ opts.Unsafe = false
case "--clear":
opts.ClearOnExit = true
case "--no-clear":
@@ -1930,7 +1940,19 @@ func parseOptions(opts *Options, allArgs []string) {
} else if match, value := optString(arg, "--tabstop="); match {
opts.Tabstop = atoi(value)
} else if match, value := optString(arg, "--listen="); match {
- opts.ListenAddr = &value
+ err, addr := parseListenAddress(value)
+ if err != nil {
+ errorExit(err.Error())
+ }
+ opts.ListenAddr = &addr
+ opts.Unsafe = false
+ } else if match, value := optString(arg, "--listen-unsafe="); match {
+ err, addr := parseListenAddress(value)
+ if err != nil {
+ errorExit(err.Error())
+ }
+ opts.ListenAddr = &addr
+ opts.Unsafe = true
} else if match, value := optString(arg, "--hscroll-off="); match {
opts.HscrollOff = atoi(value)
} else if match, value := optString(arg, "--scroll-off="); match {
diff --git a/src/server.go b/src/server.go
index 56fce30f..a52dcfde 100644
--- a/src/server.go
+++ b/src/server.go
@@ -26,13 +26,12 @@ type getParams struct {
}
const (
- crlf = "\r\n"
- httpOk = "HTTP/1.1 200 OK" + crlf
- httpBadRequest = "HTTP/1.1 400 Bad Request" + crlf
- httpUnauthorized = "HTTP/1.1 401 Unauthorized" + crlf
- httpReadTimeout = 10 * time.Second
- maxContentLength = 1024 * 1024
- defaultListenAddr = "localhost:0"
+ crlf = "\r\n"
+ httpOk = "HTTP/1.1 200 OK" + crlf
+ httpBadRequest = "HTTP/1.1 400 Bad Request" + crlf
+ httpUnauthorized = "HTTP/1.1 401 Unauthorized" + crlf
+ httpReadTimeout = 10 * time.Second
+ maxContentLength = 1024 * 1024
)
type httpServer struct {
@@ -41,38 +40,47 @@ type httpServer struct {
responseChannel chan string
}
-func parseListenAddress(address string) (error, string, int) {
+type listenAddress struct {
+ host string
+ port int
+}
+
+func (addr listenAddress) IsLocal() bool {
+ return addr.host == "localhost" || addr.host == "127.0.0.1"
+}
+
+var defaultListenAddr = listenAddress{"localhost", 0}
+
+func parseListenAddress(address string) (error, listenAddress) {
parts := strings.SplitN(address, ":", 3)
if len(parts) == 1 {
parts = []string{"localhost", parts[0]}
}
if len(parts) != 2 {
- return fmt.Errorf("invalid listen address: %s", address), "", 0
+ return fmt.Errorf("invalid listen address: %s", address), defaultListenAddr
}
portStr := parts[len(parts)-1]
port, err := strconv.Atoi(portStr)
if err != nil || port < 0 || port > 65535 {
- return fmt.Errorf("invalid listen port: %s", portStr), "", 0
+ return fmt.Errorf("invalid listen port: %s", portStr), defaultListenAddr
}
if len(parts[0]) == 0 {
parts[0] = "localhost"
}
- return nil, parts[0], port
+ return nil, listenAddress{parts[0], port}
}
-func startHttpServer(address string, actionChannel chan []*action, responseChannel chan string) (error, int) {
- err, host, port := parseListenAddress(address)
- if err != nil {
- return err, port
- }
-
+func startHttpServer(address listenAddress, actionChannel chan []*action, responseChannel chan string) (error, int) {
+ host := address.host
+ port := address.port
apiKey := os.Getenv("FZF_API_KEY")
- if host != "localhost" && host != "127.0.0.1" && len(apiKey) == 0 {
- return fmt.Errorf("FZF_API_KEY is required for remote access"), port
+ if !address.IsLocal() && len(apiKey) == 0 {
+ return fmt.Errorf("FZF_API_KEY is required to allow remote access"), port
}
- listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", host, port))
+ addrStr := fmt.Sprintf("%s:%d", host, port)
+ listener, err := net.Listen("tcp", addrStr)
if err != nil {
- return fmt.Errorf("failed to listen on %s", address), port
+ return fmt.Errorf("failed to listen on %s", addrStr), port
}
if port == 0 {
addr := listener.Addr().String()
diff --git a/src/terminal.go b/src/terminal.go
index 326c07cb..747001ef 100644
--- a/src/terminal.go
+++ b/src/terminal.go
@@ -235,8 +235,9 @@ type Terminal struct {
margin [4]sizeSpec
padding [4]sizeSpec
unicode bool
- listenAddr *string
+ listenAddr *listenAddress
listenPort *int
+ listenUnsafe bool
borderShape tui.BorderShape
cleanExit bool
paused bool
@@ -436,6 +437,26 @@ const (
actResponse
)
+func processExecution(action actionType) bool {
+ switch action {
+ case actTransformBorderLabel,
+ actTransformHeader,
+ actTransformPreviewLabel,
+ actTransformPrompt,
+ actTransformQuery,
+ actPreview,
+ actChangePreview,
+ actExecute,
+ actExecuteSilent,
+ actExecuteMulti,
+ actReload,
+ actReloadSync,
+ actBecome:
+ return true
+ }
+ return false
+}
+
type placeholderFlags struct {
plus bool
preserveSpace bool
@@ -661,6 +682,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
padding: opts.Padding,
unicode: opts.Unicode,
listenAddr: opts.ListenAddr,
+ listenUnsafe: opts.Unsafe,
borderShape: opts.BorderShape,
borderWidth: 1,
borderLabel: nil,
@@ -3088,8 +3110,18 @@ func (t *Terminal) Loop() {
select {
case event = <-t.eventChan:
needBarrier = !event.Is(tui.Load, tui.One, tui.Zero)
- case actions = <-t.serverInputChan:
+ case serverActions := <-t.serverInputChan:
event = tui.Invalid.AsEvent()
+ if t.listenAddr == nil || t.listenAddr.IsLocal() || t.listenUnsafe {
+ actions = serverActions
+ } else {
+ for _, action := range serverActions {
+ if !processExecution(action.t) {
+ actions = append(actions, action)
+ }
+ }
+ }
+
needBarrier = false
}
}