Skip to content

Commit

Permalink
Add --listen-unsafe=ADDR to allow remote process execution (#3498)
Browse files Browse the repository at this point in the history
  • Loading branch information
junegunn committed Nov 5, 2023
1 parent 5c3b044 commit a818653
Show file tree
Hide file tree
Showing 5 changed files with 108 additions and 37 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
19 changes: 12 additions & 7 deletions man/man1/fzf.1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 29 additions & 7 deletions src/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -334,7 +335,8 @@ type Options struct {
PreviewLabel labelOpts
Unicode bool
Tabstop int
ListenAddr *string
ListenAddr *listenAddress
Unsafe bool
ClearOnExit bool
Version bool
}
Expand Down Expand Up @@ -404,6 +406,7 @@ func defaultOptions() *Options {
Tabstop: 8,
BorderLabel: labelOpts{},
PreviewLabel: labelOpts{},
Unsafe: false,
ClearOnExit: true,
Version: false}
}
Expand Down Expand Up @@ -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":
Expand Down Expand Up @@ -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 {
Expand Down
50 changes: 29 additions & 21 deletions src/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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()
Expand Down
36 changes: 34 additions & 2 deletions src/terminal.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
}
}
Expand Down

0 comments on commit a818653

Please sign in to comment.