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 # FZF_API_KEY is required for a non-localhost listen address
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 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 - Bug fixes

View File

@ -793,14 +793,19 @@ 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[=[ADDR:]PORT]" .B "--listen[=[ADDR:]PORT]" "--listen-unsafe[=[ADDR:]PORT]"
Start HTTP server and listen on the given address. It allows external processes 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 to send actions to perform via POST method.
given as 0, fzf will automatically choose a port and export it as
\fBFZF_PORT\fR environment variable to the child processes. If - If the port number is omitted or given as 0, fzf will automatically choose
\fBFZF_API_KEY\fR environment variable is set, the server would require sending a port and export it as \fBFZF_PORT\fR environment variable to the child processes
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. - 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. e.g.
\fB# Start HTTP server on port 6266 \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 --print0 Print output delimited by ASCII NUL characters
--sync Synchronous search for multi-staged filtering --sync Synchronous search for multi-staged filtering
--listen[=[ADDR:]PORT] Start HTTP server to receive actions (POST /) --listen[=[ADDR:]PORT] Start HTTP server to receive actions (POST /)
(To allow remote process execution, use --listen-unsafe)
--version Display version information and exit --version Display version information and exit
Environment variables Environment variables
@ -334,7 +335,8 @@ type Options struct {
PreviewLabel labelOpts PreviewLabel labelOpts
Unicode bool Unicode bool
Tabstop int Tabstop int
ListenAddr *string ListenAddr *listenAddress
Unsafe bool
ClearOnExit bool ClearOnExit bool
Version bool Version bool
} }
@ -404,6 +406,7 @@ func defaultOptions() *Options {
Tabstop: 8, Tabstop: 8,
BorderLabel: labelOpts{}, BorderLabel: labelOpts{},
PreviewLabel: labelOpts{}, PreviewLabel: labelOpts{},
Unsafe: false,
ClearOnExit: true, ClearOnExit: true,
Version: false} 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)")) nextString(allArgs, &i, "padding required (TRBL / TB,RL / T,RL,B / T,R,B,L)"))
case "--tabstop": case "--tabstop":
opts.Tabstop = nextInt(allArgs, &i, "tab stop required") opts.Tabstop = nextInt(allArgs, &i, "tab stop required")
case "--listen": case "--listen", "--listen-unsafe":
given, addr := optionalNextString(allArgs, &i) given, str := optionalNextString(allArgs, &i)
if !given { addr := defaultListenAddr
addr = defaultListenAddr if given {
var err error
err, addr = parseListenAddress(str)
if err != nil {
errorExit(err.Error())
}
} }
opts.ListenAddr = &addr opts.ListenAddr = &addr
case "--no-listen": opts.Unsafe = arg == "--listen-unsafe"
case "--no-listen", "--no-listen-unsafe":
opts.ListenAddr = nil opts.ListenAddr = nil
opts.Unsafe = false
case "--clear": case "--clear":
opts.ClearOnExit = true opts.ClearOnExit = true
case "--no-clear": case "--no-clear":
@ -1930,7 +1940,19 @@ 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 {
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 { } 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 {

View File

@ -32,7 +32,6 @@ const (
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 {
@ -41,38 +40,47 @@ type httpServer struct {
responseChannel chan string 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) parts := strings.SplitN(address, ":", 3)
if len(parts) == 1 { if len(parts) == 1 {
parts = []string{"localhost", parts[0]} parts = []string{"localhost", parts[0]}
} }
if len(parts) != 2 { 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] portStr := parts[len(parts)-1]
port, err := strconv.Atoi(portStr) port, err := strconv.Atoi(portStr)
if err != nil || port < 0 || port > 65535 { 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 { if len(parts[0]) == 0 {
parts[0] = "localhost" 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") apiKey := os.Getenv("FZF_API_KEY")
if host != "localhost" && host != "127.0.0.1" && len(apiKey) == 0 { if !address.IsLocal() && len(apiKey) == 0 {
return fmt.Errorf("FZF_API_KEY is required for remote access"), port 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 { 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 { if port == 0 {
addr := listener.Addr().String() addr := listener.Addr().String()

View File

@ -235,8 +235,9 @@ type Terminal struct {
margin [4]sizeSpec margin [4]sizeSpec
padding [4]sizeSpec padding [4]sizeSpec
unicode bool unicode bool
listenAddr *string listenAddr *listenAddress
listenPort *int listenPort *int
listenUnsafe bool
borderShape tui.BorderShape borderShape tui.BorderShape
cleanExit bool cleanExit bool
paused bool paused bool
@ -436,6 +437,26 @@ const (
actResponse 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 { type placeholderFlags struct {
plus bool plus bool
preserveSpace bool preserveSpace bool
@ -661,6 +682,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
padding: opts.Padding, padding: opts.Padding,
unicode: opts.Unicode, unicode: opts.Unicode,
listenAddr: opts.ListenAddr, listenAddr: opts.ListenAddr,
listenUnsafe: opts.Unsafe,
borderShape: opts.BorderShape, borderShape: opts.BorderShape,
borderWidth: 1, borderWidth: 1,
borderLabel: nil, borderLabel: nil,
@ -3088,8 +3110,18 @@ func (t *Terminal) Loop() {
select { select {
case event = <-t.eventChan: case event = <-t.eventChan:
needBarrier = !event.Is(tui.Load, tui.One, tui.Zero) needBarrier = !event.Is(tui.Load, tui.One, tui.Zero)
case actions = <-t.serverInputChan: case serverActions := <-t.serverInputChan:
event = tui.Invalid.AsEvent() 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 needBarrier = false
} }
} }