Add --listen-unsafe=ADDR to allow remote process execution (#3498)

This commit is contained in:
Junegunn Choi 2023-11-05 10:50:11 +09:00
parent 5c3b044740
commit a818653174
No known key found for this signature in database
GPG Key ID: 254BC280FEF9C627
5 changed files with 108 additions and 37 deletions

View File

@ -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

View File

@ -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

View File

@ -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 {

View File

@ -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()

View File

@ -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
}
}