summaryrefslogtreecommitdiffstats
path: root/src/server.go
blob: f196edeedc3116e8df3e2da44a56d4bd32471c07 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
package fzf

import (
	"bufio"
	"bytes"
	"errors"
	"fmt"
	"net"
	"strconv"
	"strings"
)

const (
	crlf           = "\r\n"
	httpOk         = "HTTP/1.1 200 OK" + crlf
	httpBadRequest = "HTTP/1.1 400 Bad Request" + crlf
)

func startHttpServer(port int, channel chan []*action) {
	if port == 0 {
		return
	}

	listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
	if err != nil {
		return
	}

	go func() {
		for {
			conn, err := listener.Accept()
			if err != nil {
				if errors.Is(err, net.ErrClosed) {
					break
				} else {
					continue
				}
			}
			conn.Write([]byte(handleHttpRequest(conn, channel)))
			conn.Close()
		}
		listener.Close()
	}()
}

// 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
func handleHttpRequest(conn net.Conn, channel chan []*action) string {
	line := 0
	headerRead := false
	contentLength := 0
	body := ""
	bad := func(message string) string {
		message += "\n"
		return httpBadRequest + fmt.Sprintf("Content-Length: %d%s", len(message), crlf+crlf+message)
	}
	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
	})

	for scanner.Scan() {
		text := scanner.Text()
		if line == 0 && !strings.HasPrefix(text, "POST / HTTP") {
			return bad("invalid request method")
		}
		if text == crlf {
			headerRead = true
		}
		if !headerRead {
			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 {
					return bad("invalid content length")
				}
				contentLength = length
			}
		} else if contentLength <= 0 {
			break
		} else {
			body += text
		}
		line++
	}

	errorMessage := ""
	actions := parseSingleActionList(strings.TrimSpace(string(body)), func(message string) {
		errorMessage = message
	})
	if len(errorMessage) > 0 {
		return bad(errorMessage)
	}
	if len(actions) == 0 {
		return bad("no action specified")
	}

	channel <- actions
	return httpOk
}