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 {}'
```
- (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
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
.RE
.TP
.B "--listen[=HTTP_PORT]"
Start HTTP server on the given port. It allows external processes to send
actions to perform via POST method. If the port number is omitted or given as
0, fzf will choose the port automatically and export it as \fBFZF_PORT\fR
environment variable to the child processes started via \fBexecute\fR and
\fBexecute-silent\fR actions. 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.
.B "--listen[=[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.
e.g.
\fB# Start HTTP server on port 6266
@ -812,8 +812,12 @@ e.g.
# Send action to the server
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)"
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)'
# 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
--print0 Print output delimited by ASCII NUL characters
--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
Environment variables
@ -334,7 +334,7 @@ type Options struct {
PreviewLabel labelOpts
Unicode bool
Tabstop int
ListenPort *int
ListenAddr *string
ClearOnExit bool
Version bool
}
@ -1833,10 +1833,13 @@ func parseOptions(opts *Options, allArgs []string) {
case "--tabstop":
opts.Tabstop = nextInt(allArgs, &i, "tab stop required")
case "--listen":
port := optionalNumeric(allArgs, &i, 0)
opts.ListenPort = &port
given, addr := optionalNextString(allArgs, &i)
if !given {
addr = defaultListenAddr
}
opts.ListenAddr = &addr
case "--no-listen":
opts.ListenPort = nil
opts.ListenAddr = nil
case "--clear":
opts.ClearOnExit = true
case "--no-clear":
@ -1927,8 +1930,7 @@ 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 {
port := atoi(value)
opts.ListenPort = &port
opts.ListenAddr = &value
} else if match, value := optString(arg, "--hscroll-off="); match {
opts.HscrollOff = atoi(value)
} 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")
}
if opts.ListenPort != nil && (*opts.ListenPort < 0 || *opts.ListenPort > 65535) {
errorExit("invalid listen port")
}
if len(opts.JumpLabels) == 0 {
errorExit("empty jump labels")
}

View File

@ -26,12 +26,13 @@ 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
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"
)
type httpServer struct {
@ -40,30 +41,52 @@ type httpServer struct {
responseChannel chan string
}
func startHttpServer(port int, actionChannel chan []*action, responseChannel chan string) (error, int) {
if port < 0 {
return nil, port
func parseListenAddress(address string) (error, string, int) {
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
}
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 {
return fmt.Errorf("port not available: %d", port), port
return fmt.Errorf("failed to listen on %s", address), port
}
if port == 0 {
addr := listener.Addr().String()
parts := strings.SplitN(addr, ":", 2)
parts := strings.Split(addr, ":")
if len(parts) < 2 {
return fmt.Errorf("cannot extract port: %s", addr), port
}
var err error
port, err = strconv.Atoi(parts[1])
if err != nil {
if port, err := strconv.Atoi(parts[len(parts)-1]); err != nil {
return err, port
}
}
server := httpServer{
apiKey: []byte(os.Getenv("FZF_API_KEY")),
apiKey: []byte(apiKey),
actionChannel: actionChannel,
responseChannel: responseChannel,
}

View File

@ -235,6 +235,7 @@ type Terminal struct {
margin [4]sizeSpec
padding [4]sizeSpec
unicode bool
listenAddr *string
listenPort *int
borderShape tui.BorderShape
cleanExit bool
@ -586,7 +587,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
}
var previewBox *util.EventBox
// 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()
}
var renderer tui.Renderer
@ -659,7 +660,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
margin: opts.Margin,
padding: opts.Padding,
unicode: opts.Unicode,
listenPort: opts.ListenPort,
listenAddr: opts.ListenAddr,
borderShape: opts.BorderShape,
borderWidth: 1,
borderLabel: nil,
@ -748,8 +749,8 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
_, t.hasLoadActions = t.keymap[tui.Load.AsEvent()]
if t.listenPort != nil {
err, port := startHttpServer(*t.listenPort, t.serverInputChan, t.serverOutputChan)
if t.listenAddr != nil {
err, port := startHttpServer(*t.listenAddr, t.serverInputChan, t.serverOutputChan)
if err != nil {
errorExit(err.Error())
}