mirror of
https://github.com/Llewellynvdm/fzf.git
synced 2024-11-18 02:55:11 +00:00
Add API Keys for fzf --listen (#3374)
This commit is contained in:
parent
3c09c77269
commit
c0435fdff4
14
CHANGELOG.md
14
CHANGELOG.md
@ -1,6 +1,20 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
0.42.1
|
||||
------
|
||||
- `--listen` server can be secured by setting `$FZF_API_KEY` environment
|
||||
variable.
|
||||
```sh
|
||||
export FZF_API_KEY="$(head -c 32 /dev/urandom | base64)"
|
||||
|
||||
# Server
|
||||
fzf --listen 6266
|
||||
|
||||
# Client
|
||||
curl localhost:6266 -H "x-api-key: $FZF_API_KEY" -d 'change-query(yo)'
|
||||
```
|
||||
|
||||
0.42.0
|
||||
------
|
||||
- Added new info style: `--info=right`
|
||||
|
@ -772,7 +772,9 @@ 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.
|
||||
\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.
|
||||
|
||||
e.g.
|
||||
\fB# Start HTTP server on port 6266
|
||||
@ -781,6 +783,10 @@ 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
|
||||
export FZF_API_KEY="$(head -c 32 /dev/urandom | base64)"
|
||||
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
|
||||
fzf --listen --bind 'start:execute-silent:echo $FZF_PORT > /tmp/fzf-port'
|
||||
\fR
|
||||
@ -800,6 +806,11 @@ this case make sure that the command is POSIX-compliant.
|
||||
.TP
|
||||
.B FZF_DEFAULT_OPTS
|
||||
Default options. e.g. \fBexport FZF_DEFAULT_OPTS="--extended --cycle"\fR
|
||||
.TP
|
||||
.B FZF_API_KEY
|
||||
Can be used to require an API key when using \fB--listen\fR option. If not set,
|
||||
no authentication will be required by the server. You can set this value if
|
||||
you need to protect against DNS rebinding and privilege escalation attacks.
|
||||
|
||||
.SH EXIT STATUS
|
||||
.BR 0 " Normal exit"
|
||||
|
@ -125,6 +125,7 @@ const usage = `usage: fzf [options]
|
||||
FZF_DEFAULT_COMMAND Default command to use when input is tty
|
||||
FZF_DEFAULT_OPTS Default options
|
||||
(e.g. '--layout=reverse --inline-info')
|
||||
FZF_API_KEY X-API-Key header for HTTP server (--listen)
|
||||
|
||||
`
|
||||
|
||||
|
@ -3,9 +3,11 @@ package fzf
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/subtle"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@ -15,10 +17,16 @@ 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
|
||||
)
|
||||
|
||||
type httpServer struct {
|
||||
apiKey []byte
|
||||
channel chan []*action
|
||||
}
|
||||
|
||||
func startHttpServer(port int, channel chan []*action) (error, int) {
|
||||
if port < 0 {
|
||||
return nil, port
|
||||
@ -41,6 +49,11 @@ func startHttpServer(port int, channel chan []*action) (error, int) {
|
||||
}
|
||||
}
|
||||
|
||||
server := httpServer{
|
||||
apiKey: []byte(os.Getenv("FZF_API_KEY")),
|
||||
channel: channel,
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
@ -51,7 +64,7 @@ func startHttpServer(port int, channel chan []*action) (error, int) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
conn.Write([]byte(handleHttpRequest(conn, channel)))
|
||||
conn.Write([]byte(server.handleHttpRequest(conn)))
|
||||
conn.Close()
|
||||
}
|
||||
listener.Close()
|
||||
@ -66,9 +79,14 @@ func startHttpServer(port int, channel chan []*action) (error, int) {
|
||||
// * No --listen: 2.8MB
|
||||
// * --listen with net/http: 5.7MB
|
||||
// * --listen w/o net/http: 3.3MB
|
||||
func handleHttpRequest(conn net.Conn, channel chan []*action) string {
|
||||
func (server *httpServer) handleHttpRequest(conn net.Conn) string {
|
||||
contentLength := 0
|
||||
apiKey := ""
|
||||
body := ""
|
||||
unauthorized := func(message string) string {
|
||||
message += "\n"
|
||||
return httpUnauthorized + fmt.Sprintf("Content-Length: %d%s", len(message), crlf+crlf+message)
|
||||
}
|
||||
bad := func(message string) string {
|
||||
message += "\n"
|
||||
return httpBadRequest + fmt.Sprintf("Content-Length: %d%s", len(message), crlf+crlf+message)
|
||||
@ -105,18 +123,27 @@ func handleHttpRequest(conn net.Conn, channel chan []*action) string {
|
||||
continue
|
||||
}
|
||||
pair := strings.SplitN(text, ":", 2)
|
||||
if len(pair) == 2 && strings.ToLower(pair[0]) == "content-length" {
|
||||
length, err := strconv.Atoi(strings.TrimSpace(pair[1]))
|
||||
if err != nil || length <= 0 || length > maxContentLength {
|
||||
return bad("invalid content length")
|
||||
if len(pair) == 2 {
|
||||
switch strings.ToLower(pair[0]) {
|
||||
case "content-length":
|
||||
length, err := strconv.Atoi(strings.TrimSpace(pair[1]))
|
||||
if err != nil || length <= 0 || length > maxContentLength {
|
||||
return bad("invalid content length")
|
||||
}
|
||||
contentLength = length
|
||||
case "x-api-key":
|
||||
apiKey = strings.TrimSpace(pair[1])
|
||||
}
|
||||
contentLength = length
|
||||
}
|
||||
case 2:
|
||||
body += text
|
||||
}
|
||||
}
|
||||
|
||||
if len(server.apiKey) != 0 && subtle.ConstantTimeCompare([]byte(apiKey), server.apiKey) != 1 {
|
||||
return unauthorized("invalid api key")
|
||||
}
|
||||
|
||||
if len(body) < contentLength {
|
||||
return bad("incomplete request")
|
||||
}
|
||||
@ -133,6 +160,6 @@ func handleHttpRequest(conn net.Conn, channel chan []*action) string {
|
||||
return bad("no action specified")
|
||||
}
|
||||
|
||||
channel <- actions
|
||||
server.channel <- actions
|
||||
return httpOk
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ UNSETS = %w[
|
||||
FZF_CTRL_T_COMMAND FZF_CTRL_T_OPTS
|
||||
FZF_ALT_C_COMMAND
|
||||
FZF_ALT_C_OPTS FZF_CTRL_R_OPTS
|
||||
FZF_API_KEY
|
||||
fish_history
|
||||
].freeze
|
||||
DEFAULT_TIMEOUT = 10
|
||||
@ -2750,6 +2751,26 @@ class TestGoFZF < TestBase
|
||||
end
|
||||
end
|
||||
|
||||
def test_listen_with_api_key
|
||||
post_uri = URI('http://localhost:6266')
|
||||
tmux.send_keys 'seq 10 | FZF_API_KEY=123abc fzf --listen 6266', :Enter
|
||||
tmux.until { |lines| assert_equal 10, lines.item_count }
|
||||
# Incorrect API Key
|
||||
[nil, { 'x-api-key' => '' }, { 'x-api-key' => '124abc' }].each do |headers|
|
||||
res = Net::HTTP.post(post_uri, 'change-query(yo)+reload(seq 100)+change-prompt:hundred> ', headers)
|
||||
assert_equal '401', res.code
|
||||
assert_equal 'Unauthorized', res.message
|
||||
assert_equal "invalid api key\n", res.body
|
||||
end
|
||||
# Valid API Key
|
||||
[{ 'x-api-key' => '123abc' }, { 'X-API-Key' => '123abc' }].each do |headers|
|
||||
res = Net::HTTP.post(post_uri, 'change-query(yo)+reload(seq 100)+change-prompt:hundred> ', headers)
|
||||
assert_equal '200', res.code
|
||||
tmux.until { |lines| assert_equal 100, lines.item_count }
|
||||
tmux.until { |lines| assert_equal 'hundred> yo', lines[-1] }
|
||||
end
|
||||
end
|
||||
|
||||
def test_toggle_alternative_preview_window
|
||||
tmux.send_keys "seq 10 | #{FZF} --bind space:toggle-preview --preview-window '<100000(hidden,up,border-none)' --preview 'echo /{}/{}/'", :Enter
|
||||
tmux.until { |lines| assert_equal 10, lines.item_count }
|
||||
|
Loading…
Reference in New Issue
Block a user