Allow accepting remote connections

Close #3498

  # FZF_API_KEY is required for a non-localhost listen address
  FZF_API_KEY=xxx fzf --listen 0.0.0.0:6266
This commit is contained in:
Junegunn Choi 2023-11-04 16:06:59 +09:00
parent 70c19ccf16
commit 3f78d76da1
No known key found for this signature in database
GPG Key ID: 254BC280FEF9C627
5 changed files with 72 additions and 40 deletions

View File

@ -11,6 +11,12 @@ CHANGELOG
fzf --preview='fzf-preview.sh {}' fzf --preview='fzf-preview.sh {}'
``` ```
- (Experimental) Sixel and Kitty image support now also available on Windows - (Experimental) Sixel and Kitty image support now also available on Windows
- HTTP server can be configured to accept remote connections
```sh
# 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
```
- Bug fixes - Bug fixes
0.43.0 0.43.0

View File

@ -793,14 +793,14 @@ ncurses finder only after the input stream is complete.
e.g. \fBfzf --multi | fzf --sync\fR e.g. \fBfzf --multi | fzf --sync\fR
.RE .RE
.TP .TP
.B "--listen[=HTTP_PORT]" .B "--listen[=[ADDR:]PORT]"
Start HTTP server on the given port. It allows external processes to send Start HTTP server and listen on the given address. It allows external processes
actions to perform via POST method. If the port number is omitted or given as to send actions to perform via POST method. If the port number is omitted or
0, fzf will choose the port automatically and export it as \fBFZF_PORT\fR given as 0, fzf will automatically choose a port and export it as
environment variable to the child processes started via \fBexecute\fR and \fBFZF_PORT\fR environment variable to the child processes. If
\fBexecute-silent\fR actions. If \fBFZF_API_KEY\fR environment variable is \fBFZF_API_KEY\fR environment variable is set, the server would require sending
set, the server would require sending an API key with the same value in the an API key with the same value in the \fBx-api-key\fR HTTP header.
\fBx-api-key\fR HTTP header. \fBFZF_API_KEY\fR is required for a non-localhost listen address.
e.g. e.g.
\fB# Start HTTP server on port 6266 \fB# Start HTTP server on port 6266
@ -812,8 +812,12 @@ e.g.
# Send action to the server # Send action to the server
curl -XPOST localhost:6266 -d 'reload(seq 100)+change-prompt(hundred> )' curl -XPOST localhost:6266 -d 'reload(seq 100)+change-prompt(hundred> )'
# Start HTTP server on port 6266 and send an authenticated action # Start HTTP server on port 6266 with remote connections allowed
# * Listening on non-localhost address requires using an API key
export FZF_API_KEY="$(head -c 32 /dev/urandom | base64)" export FZF_API_KEY="$(head -c 32 /dev/urandom | base64)"
fzf --listen 0.0.0.0:6266
# Send an authenticated action
curl -XPOST localhost:6266 -H "x-api-key: $FZF_API_KEY" -d 'change-query(yo)' curl -XPOST localhost:6266 -H "x-api-key: $FZF_API_KEY" -d 'change-query(yo)'
# Choose port automatically and export it as $FZF_PORT to the child process # Choose port automatically and export it as $FZF_PORT to the child process

View File

@ -118,7 +118,7 @@ const usage = `usage: fzf [options]
--read0 Read input delimited by ASCII NUL characters --read0 Read input delimited by ASCII NUL characters
--print0 Print output delimited by ASCII NUL characters --print0 Print output delimited by ASCII NUL characters
--sync Synchronous search for multi-staged filtering --sync Synchronous search for multi-staged filtering
--listen[=HTTP_PORT] Start HTTP server to receive actions (POST /) --listen[=[ADDR:]PORT] Start HTTP server to receive actions (POST /)
--version Display version information and exit --version Display version information and exit
Environment variables Environment variables
@ -334,7 +334,7 @@ type Options struct {
PreviewLabel labelOpts PreviewLabel labelOpts
Unicode bool Unicode bool
Tabstop int Tabstop int
ListenPort *int ListenAddr *string
ClearOnExit bool ClearOnExit bool
Version bool Version bool
} }
@ -1833,10 +1833,13 @@ func parseOptions(opts *Options, allArgs []string) {
case "--tabstop": case "--tabstop":
opts.Tabstop = nextInt(allArgs, &i, "tab stop required") opts.Tabstop = nextInt(allArgs, &i, "tab stop required")
case "--listen": case "--listen":
port := optionalNumeric(allArgs, &i, 0) given, addr := optionalNextString(allArgs, &i)
opts.ListenPort = &port if !given {
addr = defaultListenAddr
}
opts.ListenAddr = &addr
case "--no-listen": case "--no-listen":
opts.ListenPort = nil opts.ListenAddr = nil
case "--clear": case "--clear":
opts.ClearOnExit = true opts.ClearOnExit = true
case "--no-clear": case "--no-clear":
@ -1927,8 +1930,7 @@ func parseOptions(opts *Options, allArgs []string) {
} else if match, value := optString(arg, "--tabstop="); match { } else if match, value := optString(arg, "--tabstop="); match {
opts.Tabstop = atoi(value) opts.Tabstop = atoi(value)
} else if match, value := optString(arg, "--listen="); match { } else if match, value := optString(arg, "--listen="); match {
port := atoi(value) opts.ListenAddr = &value
opts.ListenPort = &port
} else if match, value := optString(arg, "--hscroll-off="); match { } else if match, value := optString(arg, "--hscroll-off="); match {
opts.HscrollOff = atoi(value) opts.HscrollOff = atoi(value)
} else if match, value := optString(arg, "--scroll-off="); match { } else if match, value := optString(arg, "--scroll-off="); match {
@ -1958,10 +1960,6 @@ func parseOptions(opts *Options, allArgs []string) {
errorExit("tab stop must be a positive integer") errorExit("tab stop must be a positive integer")
} }
if opts.ListenPort != nil && (*opts.ListenPort < 0 || *opts.ListenPort > 65535) {
errorExit("invalid listen port")
}
if len(opts.JumpLabels) == 0 { if len(opts.JumpLabels) == 0 {
errorExit("empty jump labels") errorExit("empty jump labels")
} }

View File

@ -26,12 +26,13 @@ type getParams struct {
} }
const ( const (
crlf = "\r\n" crlf = "\r\n"
httpOk = "HTTP/1.1 200 OK" + crlf httpOk = "HTTP/1.1 200 OK" + crlf
httpBadRequest = "HTTP/1.1 400 Bad Request" + crlf httpBadRequest = "HTTP/1.1 400 Bad Request" + crlf
httpUnauthorized = "HTTP/1.1 401 Unauthorized" + crlf httpUnauthorized = "HTTP/1.1 401 Unauthorized" + crlf
httpReadTimeout = 10 * time.Second httpReadTimeout = 10 * time.Second
maxContentLength = 1024 * 1024 maxContentLength = 1024 * 1024
defaultListenAddr = "localhost:0"
) )
type httpServer struct { type httpServer struct {
@ -40,30 +41,52 @@ type httpServer struct {
responseChannel chan string responseChannel chan string
} }
func startHttpServer(port int, actionChannel chan []*action, responseChannel chan string) (error, int) { func parseListenAddress(address string) (error, string, int) {
if port < 0 { parts := strings.SplitN(address, ":", 3)
return nil, port if len(parts) == 1 {
parts = []string{"localhost", parts[0]}
}
if len(parts) != 2 {
return fmt.Errorf("invalid listen address: %s", address), "", 0
}
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
}
if len(parts[0]) == 0 {
parts[0] = "localhost"
}
return nil, 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
} }
listener, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", 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
}
listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", host, port))
if err != nil { if err != nil {
return fmt.Errorf("port not available: %d", port), port return fmt.Errorf("failed to listen on %s", address), port
} }
if port == 0 { if port == 0 {
addr := listener.Addr().String() addr := listener.Addr().String()
parts := strings.SplitN(addr, ":", 2) parts := strings.Split(addr, ":")
if len(parts) < 2 { if len(parts) < 2 {
return fmt.Errorf("cannot extract port: %s", addr), port return fmt.Errorf("cannot extract port: %s", addr), port
} }
var err error if port, err := strconv.Atoi(parts[len(parts)-1]); err != nil {
port, err = strconv.Atoi(parts[1])
if err != nil {
return err, port return err, port
} }
} }
server := httpServer{ server := httpServer{
apiKey: []byte(os.Getenv("FZF_API_KEY")), apiKey: []byte(apiKey),
actionChannel: actionChannel, actionChannel: actionChannel,
responseChannel: responseChannel, responseChannel: responseChannel,
} }

View File

@ -235,6 +235,7 @@ type Terminal struct {
margin [4]sizeSpec margin [4]sizeSpec
padding [4]sizeSpec padding [4]sizeSpec
unicode bool unicode bool
listenAddr *string
listenPort *int listenPort *int
borderShape tui.BorderShape borderShape tui.BorderShape
cleanExit bool cleanExit bool
@ -586,7 +587,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
} }
var previewBox *util.EventBox var previewBox *util.EventBox
// We need to start previewer if HTTP server is enabled even when --preview option is not specified // We need to start previewer if HTTP server is enabled even when --preview option is not specified
if len(opts.Preview.command) > 0 || hasPreviewAction(opts) || opts.ListenPort != nil { if len(opts.Preview.command) > 0 || hasPreviewAction(opts) || opts.ListenAddr != nil {
previewBox = util.NewEventBox() previewBox = util.NewEventBox()
} }
var renderer tui.Renderer var renderer tui.Renderer
@ -659,7 +660,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
margin: opts.Margin, margin: opts.Margin,
padding: opts.Padding, padding: opts.Padding,
unicode: opts.Unicode, unicode: opts.Unicode,
listenPort: opts.ListenPort, listenAddr: opts.ListenAddr,
borderShape: opts.BorderShape, borderShape: opts.BorderShape,
borderWidth: 1, borderWidth: 1,
borderLabel: nil, borderLabel: nil,
@ -748,8 +749,8 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
_, t.hasLoadActions = t.keymap[tui.Load.AsEvent()] _, t.hasLoadActions = t.keymap[tui.Load.AsEvent()]
if t.listenPort != nil { if t.listenAddr != nil {
err, port := startHttpServer(*t.listenPort, t.serverInputChan, t.serverOutputChan) err, port := startHttpServer(*t.listenAddr, t.serverInputChan, t.serverOutputChan)
if err != nil { if err != nil {
errorExit(err.Error()) errorExit(err.Error())
} }