summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJunegunn Choi <junegunn.c@gmail.com>2023-11-04 16:06:59 +0900
committerJunegunn Choi <junegunn.c@gmail.com>2023-11-04 16:19:16 +0900
commit3f78d76da1f114d4e0e50f8ed3ed19fda980342d (patch)
tree51e449291e2edf79f351a71220e734ce77224221
parent70c19ccf16a0f8ef2d0ef8ef44f69dd72aa210b1 (diff)
downloadfzf-3f78d76da1f114d4e0e50f8ed3ed19fda980342d.tar.gz
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
-rw-r--r--CHANGELOG.md6
-rw-r--r--man/man1/fzf.122
-rw-r--r--src/options.go20
-rw-r--r--src/server.go55
-rw-r--r--src/terminal.go9
5 files changed, 72 insertions, 40 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a45122cc..e561df6a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/man/man1/fzf.1 b/man/man1/fzf.1
index 309be90e..66bc678b 100644
--- a/man/man1/fzf.1
+++ b/man/man1/fzf.1
@@ -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
diff --git a/src/options.go b/src/options.go
index b4f74e95..4176292f 100644
--- a/src/options.go
+++ b/src/options.go
@@ -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")
}
diff --git a/src/server.go b/src/server.go
index 8fc605ac..a89c5e08 100644
--- a/src/server.go
+++ b/src/server.go
@@ -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
+}
- listener, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", port))
+func startHttpServer(address string, actionChannel chan []*action, responseChannel chan string) (error, int) {
+ err, host, port := parseListenAddress(address)
+ if err != nil {
+ return err, 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,
}
diff --git a/src/terminal.go b/src/terminal.go
index e4ff304c..326c07cb 100644
--- a/src/terminal.go
+++ b/src/terminal.go
@@ -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())
}