fzf/src/server.go

178 lines
4.2 KiB
Go
Raw Normal View History

package fzf
import (
"bufio"
"bytes"
2023-07-20 14:42:09 +00:00
"crypto/subtle"
"errors"
"fmt"
"net"
2023-07-20 14:42:09 +00:00
"os"
"strconv"
"strings"
"time"
)
const (
crlf = "\r\n"
httpOk = "HTTP/1.1 200 OK" + crlf
httpBadRequest = "HTTP/1.1 400 Bad Request" + crlf
2023-07-20 14:42:09 +00:00
httpUnauthorized = "HTTP/1.1 401 Unauthorized" + crlf
httpReadTimeout = 10 * time.Second
maxContentLength = 1024 * 1024
)
2023-07-20 14:42:09 +00:00
type httpServer struct {
apiKey []byte
actionChannel chan []*action
responseChannel chan string
2023-07-20 14:42:09 +00:00
}
func startHttpServer(port int, actionChannel chan []*action, responseChannel chan string) (error, int) {
if port < 0 {
return nil, port
}
2022-12-31 14:13:14 +00:00
listener, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", port))
if err != nil {
return fmt.Errorf("port not available: %d", port), port
}
if port == 0 {
addr := listener.Addr().String()
parts := strings.SplitN(addr, ":", 2)
if len(parts) < 2 {
return fmt.Errorf("cannot extract port: %s", addr), port
}
var err error
port, err = strconv.Atoi(parts[1])
if err != nil {
return err, port
}
}
2023-07-20 14:42:09 +00:00
server := httpServer{
apiKey: []byte(os.Getenv("FZF_API_KEY")),
actionChannel: actionChannel,
responseChannel: responseChannel,
2023-07-20 14:42:09 +00:00
}
go func() {
for {
conn, err := listener.Accept()
if err != nil {
if errors.Is(err, net.ErrClosed) {
break
} else {
continue
}
}
2023-07-20 14:42:09 +00:00
conn.Write([]byte(server.handleHttpRequest(conn)))
conn.Close()
}
listener.Close()
}()
return nil, port
}
// 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 14:42:09 +00:00
func (server *httpServer) handleHttpRequest(conn net.Conn) string {
contentLength := 0
2023-07-20 14:42:09 +00:00
apiKey := ""
body := ""
answer := func(code string, message string) string {
2023-07-20 14:42:09 +00:00
message += "\n"
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 14:42:09 +00:00
}
bad := func(message string) string {
return answer(httpBadRequest, message)
}
good := func(message string) string {
return answer(httpOk+"Content-Type: application/json"+crlf, message)
}
conn.SetReadDeadline(time.Now().Add(httpReadTimeout))
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
})
section := 0
for scanner.Scan() {
text := scanner.Text()
switch section {
case 0:
// TODO: Parameter support e.g. "GET /?limit=100 HTTP"
if strings.HasPrefix(text, "GET / HTTP") {
server.actionChannel <- []*action{{t: actResponse}}
response := <-server.responseChannel
return good(response)
} else if !strings.HasPrefix(text, "POST / HTTP") {
return bad("invalid request method")
}
section++
case 1:
if text == crlf {
if contentLength == 0 {
return bad("content-length header missing")
}
section++
continue
}
pair := strings.SplitN(text, ":", 2)
2023-07-20 14:42:09 +00: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])
}
}
case 2:
body += text
}
}
2023-07-20 14:42:09 +00:00
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")
}
body = body[:contentLength]
errorMessage := ""
actions := parseSingleActionList(strings.Trim(string(body), "\r\n"), func(message string) {
errorMessage = message
})
if len(errorMessage) > 0 {
return bad(errorMessage)
}
if len(actions) == 0 {
return bad("no action specified")
}
server.actionChannel <- actions
return httpOk
}