mirror of
https://github.com/Llewellynvdm/fzf.git
synced 2025-02-02 20:18:31 +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
|
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
|
0.42.0
|
||||||
------
|
------
|
||||||
- Added new info style: `--info=right`
|
- 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
|
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
|
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
|
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.
|
e.g.
|
||||||
\fB# Start HTTP server on port 6266
|
\fB# Start HTTP server on port 6266
|
||||||
@ -781,6 +783,10 @@ e.g.
|
|||||||
# Send action to the server
|
# Send action to the server
|
||||||
curl -XPOST localhost:6266 -d 'reload(seq 100)+change-prompt(hundred> )'
|
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
|
# 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'
|
fzf --listen --bind 'start:execute-silent:echo $FZF_PORT > /tmp/fzf-port'
|
||||||
\fR
|
\fR
|
||||||
@ -800,6 +806,11 @@ this case make sure that the command is POSIX-compliant.
|
|||||||
.TP
|
.TP
|
||||||
.B FZF_DEFAULT_OPTS
|
.B FZF_DEFAULT_OPTS
|
||||||
Default options. e.g. \fBexport FZF_DEFAULT_OPTS="--extended --cycle"\fR
|
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
|
.SH EXIT STATUS
|
||||||
.BR 0 " Normal exit"
|
.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_COMMAND Default command to use when input is tty
|
||||||
FZF_DEFAULT_OPTS Default options
|
FZF_DEFAULT_OPTS Default options
|
||||||
(e.g. '--layout=reverse --inline-info')
|
(e.g. '--layout=reverse --inline-info')
|
||||||
|
FZF_API_KEY X-API-Key header for HTTP server (--listen)
|
||||||
|
|
||||||
`
|
`
|
||||||
|
|
||||||
|
@ -3,9 +3,11 @@ package fzf
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"crypto/subtle"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@ -15,10 +17,16 @@ const (
|
|||||||
crlf = "\r\n"
|
crlf = "\r\n"
|
||||||
httpOk = "HTTP/1.1 200 OK" + crlf
|
httpOk = "HTTP/1.1 200 OK" + crlf
|
||||||
httpBadRequest = "HTTP/1.1 400 Bad Request" + crlf
|
httpBadRequest = "HTTP/1.1 400 Bad Request" + crlf
|
||||||
|
httpUnauthorized = "HTTP/1.1 401 Unauthorized" + crlf
|
||||||
httpReadTimeout = 10 * time.Second
|
httpReadTimeout = 10 * time.Second
|
||||||
maxContentLength = 1024 * 1024
|
maxContentLength = 1024 * 1024
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type httpServer struct {
|
||||||
|
apiKey []byte
|
||||||
|
channel chan []*action
|
||||||
|
}
|
||||||
|
|
||||||
func startHttpServer(port int, channel chan []*action) (error, int) {
|
func startHttpServer(port int, channel chan []*action) (error, int) {
|
||||||
if port < 0 {
|
if port < 0 {
|
||||||
return nil, port
|
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() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
conn, err := listener.Accept()
|
conn, err := listener.Accept()
|
||||||
@ -51,7 +64,7 @@ func startHttpServer(port int, channel chan []*action) (error, int) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
conn.Write([]byte(handleHttpRequest(conn, channel)))
|
conn.Write([]byte(server.handleHttpRequest(conn)))
|
||||||
conn.Close()
|
conn.Close()
|
||||||
}
|
}
|
||||||
listener.Close()
|
listener.Close()
|
||||||
@ -66,9 +79,14 @@ func startHttpServer(port int, channel chan []*action) (error, int) {
|
|||||||
// * No --listen: 2.8MB
|
// * No --listen: 2.8MB
|
||||||
// * --listen with net/http: 5.7MB
|
// * --listen with net/http: 5.7MB
|
||||||
// * --listen w/o net/http: 3.3MB
|
// * --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
|
contentLength := 0
|
||||||
|
apiKey := ""
|
||||||
body := ""
|
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 {
|
bad := func(message string) string {
|
||||||
message += "\n"
|
message += "\n"
|
||||||
return httpBadRequest + fmt.Sprintf("Content-Length: %d%s", len(message), crlf+crlf+message)
|
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
|
continue
|
||||||
}
|
}
|
||||||
pair := strings.SplitN(text, ":", 2)
|
pair := strings.SplitN(text, ":", 2)
|
||||||
if len(pair) == 2 && strings.ToLower(pair[0]) == "content-length" {
|
if len(pair) == 2 {
|
||||||
|
switch strings.ToLower(pair[0]) {
|
||||||
|
case "content-length":
|
||||||
length, err := strconv.Atoi(strings.TrimSpace(pair[1]))
|
length, err := strconv.Atoi(strings.TrimSpace(pair[1]))
|
||||||
if err != nil || length <= 0 || length > maxContentLength {
|
if err != nil || length <= 0 || length > maxContentLength {
|
||||||
return bad("invalid content length")
|
return bad("invalid content length")
|
||||||
}
|
}
|
||||||
contentLength = length
|
contentLength = length
|
||||||
|
case "x-api-key":
|
||||||
|
apiKey = strings.TrimSpace(pair[1])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case 2:
|
case 2:
|
||||||
body += text
|
body += text
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(server.apiKey) != 0 && subtle.ConstantTimeCompare([]byte(apiKey), server.apiKey) != 1 {
|
||||||
|
return unauthorized("invalid api key")
|
||||||
|
}
|
||||||
|
|
||||||
if len(body) < contentLength {
|
if len(body) < contentLength {
|
||||||
return bad("incomplete request")
|
return bad("incomplete request")
|
||||||
}
|
}
|
||||||
@ -133,6 +160,6 @@ func handleHttpRequest(conn net.Conn, channel chan []*action) string {
|
|||||||
return bad("no action specified")
|
return bad("no action specified")
|
||||||
}
|
}
|
||||||
|
|
||||||
channel <- actions
|
server.channel <- actions
|
||||||
return httpOk
|
return httpOk
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ UNSETS = %w[
|
|||||||
FZF_CTRL_T_COMMAND FZF_CTRL_T_OPTS
|
FZF_CTRL_T_COMMAND FZF_CTRL_T_OPTS
|
||||||
FZF_ALT_C_COMMAND
|
FZF_ALT_C_COMMAND
|
||||||
FZF_ALT_C_OPTS FZF_CTRL_R_OPTS
|
FZF_ALT_C_OPTS FZF_CTRL_R_OPTS
|
||||||
|
FZF_API_KEY
|
||||||
fish_history
|
fish_history
|
||||||
].freeze
|
].freeze
|
||||||
DEFAULT_TIMEOUT = 10
|
DEFAULT_TIMEOUT = 10
|
||||||
@ -2750,6 +2751,26 @@ class TestGoFZF < TestBase
|
|||||||
end
|
end
|
||||||
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
|
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.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 }
|
tmux.until { |lines| assert_equal 10, lines.item_count }
|
||||||
|
Loading…
x
Reference in New Issue
Block a user