diff --git a/CHANGELOG.md b/CHANGELOG.md index e561df6..b7423f6 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 66bc678..6efd8a1 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 4176292..d2c2608 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 56fce30..a52dcfd 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 326c07c..747001e 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 } }