2022-12-20 16:28:36 +09:00
|
|
|
package fzf
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bufio"
|
|
|
|
"bytes"
|
2023-07-20 23:42:09 +09:00
|
|
|
"crypto/subtle"
|
2022-12-20 16:28:36 +09:00
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"net"
|
2023-07-20 23:42:09 +09:00
|
|
|
"os"
|
2023-09-18 00:51:40 +09:00
|
|
|
"regexp"
|
2022-12-20 16:28:36 +09:00
|
|
|
"strconv"
|
|
|
|
"strings"
|
2022-12-22 20:44:49 +09:00
|
|
|
"time"
|
2022-12-20 16:28:36 +09:00
|
|
|
)
|
|
|
|
|
2023-09-18 00:51:40 +09:00
|
|
|
var getRegex *regexp.Regexp
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
getRegex = regexp.MustCompile(`^GET /(?:\?([a-z0-9=&]+))? HTTP`)
|
|
|
|
}
|
|
|
|
|
|
|
|
type getParams struct {
|
|
|
|
limit int
|
|
|
|
offset int
|
|
|
|
}
|
|
|
|
|
2022-12-20 16:28:36 +09:00
|
|
|
const (
|
2023-11-05 10:50:11 +09:00
|
|
|
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
|
2024-01-21 22:58:18 +09:00
|
|
|
httpUnavailable = "HTTP/1.1 503 Service Unavailable" + crlf
|
2023-11-05 10:50:11 +09:00
|
|
|
httpReadTimeout = 10 * time.Second
|
2024-03-21 19:01:44 +09:00
|
|
|
channelTimeout = 2 * time.Second
|
2024-01-21 22:58:18 +09:00
|
|
|
jsonContentType = "Content-Type: application/json" + crlf
|
2023-11-05 10:50:11 +09:00
|
|
|
maxContentLength = 1024 * 1024
|
2022-12-20 16:28:36 +09:00
|
|
|
)
|
|
|
|
|
2023-07-20 23:42:09 +09:00
|
|
|
type httpServer struct {
|
2024-06-14 21:33:42 +09:00
|
|
|
apiKey []byte
|
|
|
|
actionChannel chan []*action
|
|
|
|
getHandler func(getParams) string
|
2023-07-20 23:42:09 +09:00
|
|
|
}
|
|
|
|
|
2023-11-05 10:50:11 +09:00
|
|
|
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}
|
|
|
|
|
2024-03-04 12:56:41 +09:00
|
|
|
func parseListenAddress(address string) (listenAddress, error) {
|
2023-11-04 16:06:59 +09:00
|
|
|
parts := strings.SplitN(address, ":", 3)
|
|
|
|
if len(parts) == 1 {
|
|
|
|
parts = []string{"localhost", parts[0]}
|
2022-12-20 16:28:36 +09:00
|
|
|
}
|
2023-11-04 16:06:59 +09:00
|
|
|
if len(parts) != 2 {
|
2024-03-04 12:56:41 +09:00
|
|
|
return defaultListenAddr, fmt.Errorf("invalid listen address: %s", address)
|
2023-11-04 16:06:59 +09:00
|
|
|
}
|
|
|
|
portStr := parts[len(parts)-1]
|
|
|
|
port, err := strconv.Atoi(portStr)
|
|
|
|
if err != nil || port < 0 || port > 65535 {
|
2024-03-04 12:56:41 +09:00
|
|
|
return defaultListenAddr, fmt.Errorf("invalid listen port: %s", portStr)
|
2023-11-04 16:06:59 +09:00
|
|
|
}
|
|
|
|
if len(parts[0]) == 0 {
|
|
|
|
parts[0] = "localhost"
|
|
|
|
}
|
2024-03-04 12:56:41 +09:00
|
|
|
return listenAddress{parts[0], port}, nil
|
2023-11-04 16:06:59 +09:00
|
|
|
}
|
2022-12-20 16:28:36 +09:00
|
|
|
|
2024-06-14 21:33:42 +09:00
|
|
|
func startHttpServer(address listenAddress, actionChannel chan []*action, getHandler func(getParams) string) (net.Listener, int, error) {
|
2023-11-05 10:50:11 +09:00
|
|
|
host := address.host
|
|
|
|
port := address.port
|
2023-11-04 16:06:59 +09:00
|
|
|
apiKey := os.Getenv("FZF_API_KEY")
|
2023-11-05 10:50:11 +09:00
|
|
|
if !address.IsLocal() && len(apiKey) == 0 {
|
2024-05-07 01:06:42 +09:00
|
|
|
return nil, port, errors.New("FZF_API_KEY is required to allow remote access")
|
2023-11-04 16:06:59 +09:00
|
|
|
}
|
2023-11-05 10:50:11 +09:00
|
|
|
addrStr := fmt.Sprintf("%s:%d", host, port)
|
|
|
|
listener, err := net.Listen("tcp", addrStr)
|
2022-12-20 16:28:36 +09:00
|
|
|
if err != nil {
|
2024-05-07 01:06:42 +09:00
|
|
|
return nil, port, fmt.Errorf("failed to listen on %s", addrStr)
|
2023-03-19 15:42:47 +09:00
|
|
|
}
|
|
|
|
if port == 0 {
|
|
|
|
addr := listener.Addr().String()
|
2023-11-04 16:06:59 +09:00
|
|
|
parts := strings.Split(addr, ":")
|
2023-03-19 15:42:47 +09:00
|
|
|
if len(parts) < 2 {
|
2024-05-07 01:06:42 +09:00
|
|
|
return nil, port, fmt.Errorf("cannot extract port: %s", addr)
|
2023-03-19 15:42:47 +09:00
|
|
|
}
|
2023-11-04 16:27:24 +09:00
|
|
|
var err error
|
|
|
|
port, err = strconv.Atoi(parts[len(parts)-1])
|
|
|
|
if err != nil {
|
2024-05-07 01:06:42 +09:00
|
|
|
return nil, port, err
|
2023-03-19 15:42:47 +09:00
|
|
|
}
|
2022-12-20 16:28:36 +09:00
|
|
|
}
|
|
|
|
|
2023-07-20 23:42:09 +09:00
|
|
|
server := httpServer{
|
2024-06-14 21:33:42 +09:00
|
|
|
apiKey: []byte(apiKey),
|
|
|
|
actionChannel: actionChannel,
|
|
|
|
getHandler: getHandler,
|
2023-07-20 23:42:09 +09:00
|
|
|
}
|
|
|
|
|
2022-12-20 16:28:36 +09:00
|
|
|
go func() {
|
|
|
|
for {
|
|
|
|
conn, err := listener.Accept()
|
|
|
|
if err != nil {
|
|
|
|
if errors.Is(err, net.ErrClosed) {
|
2024-05-07 01:06:42 +09:00
|
|
|
return
|
2022-12-20 16:28:36 +09:00
|
|
|
}
|
2024-05-07 01:06:42 +09:00
|
|
|
continue
|
2022-12-20 16:28:36 +09:00
|
|
|
}
|
2023-07-20 23:42:09 +09:00
|
|
|
conn.Write([]byte(server.handleHttpRequest(conn)))
|
2022-12-20 16:28:36 +09:00
|
|
|
conn.Close()
|
|
|
|
}
|
|
|
|
}()
|
2022-12-21 13:02:25 +09:00
|
|
|
|
2024-05-07 01:06:42 +09:00
|
|
|
return listener, port, nil
|
2022-12-20 16:28:36 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
// Here we are writing a simplistic HTTP server without using net/http
|
|
|
|
// package to reduce the size of the binary.
|
|
|
|
//
|
|
|
|
// * No --listen: 2.8MB
|
|
|
|
// * --listen with net/http: 5.7MB
|
|
|
|
// * --listen w/o net/http: 3.3MB
|
2023-07-20 23:42:09 +09:00
|
|
|
func (server *httpServer) handleHttpRequest(conn net.Conn) string {
|
2022-12-20 16:28:36 +09:00
|
|
|
contentLength := 0
|
2023-07-20 23:42:09 +09:00
|
|
|
apiKey := ""
|
2022-12-20 16:28:36 +09:00
|
|
|
body := ""
|
2023-09-03 16:30:35 +09:00
|
|
|
answer := func(code string, message string) string {
|
2023-07-20 23:42:09 +09:00
|
|
|
message += "\n"
|
2023-09-03 16:30:35 +09:00
|
|
|
return code + fmt.Sprintf("Content-Length: %d%s", len(message), crlf+crlf+message)
|
|
|
|
}
|
|
|
|
unauthorized := func(message string) string {
|
|
|
|
return answer(httpUnauthorized, message)
|
2023-07-20 23:42:09 +09:00
|
|
|
}
|
2022-12-20 16:28:36 +09:00
|
|
|
bad := func(message string) string {
|
2023-09-03 16:30:35 +09:00
|
|
|
return answer(httpBadRequest, message)
|
|
|
|
}
|
|
|
|
good := func(message string) string {
|
2024-01-21 22:58:18 +09:00
|
|
|
return answer(httpOk+jsonContentType, message)
|
2022-12-20 16:28:36 +09:00
|
|
|
}
|
2022-12-22 20:44:49 +09:00
|
|
|
conn.SetReadDeadline(time.Now().Add(httpReadTimeout))
|
2022-12-20 16:28:36 +09:00
|
|
|
scanner := bufio.NewScanner(conn)
|
|
|
|
scanner.Split(func(data []byte, atEOF bool) (int, []byte, error) {
|
|
|
|
found := bytes.Index(data, []byte(crlf))
|
|
|
|
if found >= 0 {
|
|
|
|
token := data[:found+len(crlf)]
|
|
|
|
return len(token), token, nil
|
|
|
|
}
|
|
|
|
if atEOF || len(body)+len(data) >= contentLength {
|
|
|
|
return 0, data, bufio.ErrFinalToken
|
|
|
|
}
|
|
|
|
return 0, nil, nil
|
|
|
|
})
|
|
|
|
|
2022-12-22 20:44:49 +09:00
|
|
|
section := 0
|
2022-12-20 16:28:36 +09:00
|
|
|
for scanner.Scan() {
|
|
|
|
text := scanner.Text()
|
2022-12-22 20:44:49 +09:00
|
|
|
switch section {
|
|
|
|
case 0:
|
2023-09-18 00:51:40 +09:00
|
|
|
getMatch := getRegex.FindStringSubmatch(text)
|
|
|
|
if len(getMatch) > 0 {
|
2024-06-14 21:33:42 +09:00
|
|
|
response := server.getHandler(parseGetParams(getMatch[1]))
|
|
|
|
if len(response) > 0 {
|
2024-01-21 22:58:18 +09:00
|
|
|
return good(response)
|
|
|
|
}
|
2024-06-14 21:33:42 +09:00
|
|
|
return answer(httpUnavailable+jsonContentType, `{"error":"timeout"}`)
|
2023-09-03 16:30:35 +09:00
|
|
|
} else if !strings.HasPrefix(text, "POST / HTTP") {
|
2022-12-25 19:53:53 +09:00
|
|
|
return bad("invalid request method")
|
2022-12-22 20:44:49 +09:00
|
|
|
}
|
|
|
|
section++
|
|
|
|
case 1:
|
|
|
|
if text == crlf {
|
|
|
|
if contentLength == 0 {
|
|
|
|
return bad("content-length header missing")
|
|
|
|
}
|
|
|
|
section++
|
|
|
|
continue
|
|
|
|
}
|
2022-12-20 16:28:36 +09:00
|
|
|
pair := strings.SplitN(text, ":", 2)
|
2023-07-20 23:42:09 +09:00
|
|
|
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])
|
2022-12-20 16:28:36 +09:00
|
|
|
}
|
|
|
|
}
|
2022-12-22 20:44:49 +09:00
|
|
|
case 2:
|
2022-12-20 16:28:36 +09:00
|
|
|
body += text
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-20 23:42:09 +09:00
|
|
|
if len(server.apiKey) != 0 && subtle.ConstantTimeCompare([]byte(apiKey), server.apiKey) != 1 {
|
|
|
|
return unauthorized("invalid api key")
|
|
|
|
}
|
|
|
|
|
2022-12-22 20:44:49 +09:00
|
|
|
if len(body) < contentLength {
|
|
|
|
return bad("incomplete request")
|
|
|
|
}
|
|
|
|
body = body[:contentLength]
|
|
|
|
|
2024-05-07 01:06:42 +09:00
|
|
|
actions, err := parseSingleActionList(strings.Trim(string(body), "\r\n"))
|
|
|
|
if err != nil {
|
|
|
|
return bad(err.Error())
|
2022-12-20 16:28:36 +09:00
|
|
|
}
|
|
|
|
if len(actions) == 0 {
|
|
|
|
return bad("no action specified")
|
|
|
|
}
|
2022-12-25 19:53:53 +09:00
|
|
|
|
2024-03-21 19:01:44 +09:00
|
|
|
select {
|
|
|
|
case server.actionChannel <- actions:
|
|
|
|
case <-time.After(channelTimeout):
|
|
|
|
return httpUnavailable + crlf
|
|
|
|
}
|
2023-12-16 07:15:00 +01:00
|
|
|
return httpOk + crlf
|
2022-12-20 16:28:36 +09:00
|
|
|
}
|
2023-09-18 00:51:40 +09:00
|
|
|
|
|
|
|
func parseGetParams(query string) getParams {
|
|
|
|
params := getParams{limit: 100, offset: 0}
|
|
|
|
for _, pair := range strings.Split(query, "&") {
|
|
|
|
parts := strings.SplitN(pair, "=", 2)
|
|
|
|
if len(parts) == 2 {
|
|
|
|
switch parts[0] {
|
2024-03-04 21:06:09 +09:00
|
|
|
case "limit", "offset":
|
|
|
|
if val, err := strconv.Atoi(parts[1]); err == nil {
|
|
|
|
if parts[0] == "limit" {
|
|
|
|
params.limit = val
|
|
|
|
} else {
|
|
|
|
params.offset = val
|
|
|
|
}
|
|
|
|
}
|
2023-09-18 00:51:40 +09:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return params
|
|
|
|
}
|