aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorjet2tlf <jet2tlf@gmail.com>2024-06-03 16:48:36 +0000
committerjet2tlf <jet2tlf@gmail.com>2024-06-03 16:48:36 +0000
commitb3dd001bc8cc282c75105ba229e0c55e5a4b3252 (patch)
tree1351303952ba7ee16a494db4c069d38747bf3528
parent8a94110728cca144388958d4bd322045f8bfb9e4 (diff)
downloadhttp-server-go-b3dd001bc8cc282c75105ba229e0c55e5a4b3252.tar.gz
http-server-go-b3dd001bc8cc282c75105ba229e0c55e5a4b3252.zip
codecrafters submit [skip ci]
-rw-r--r--app/handler.go33
-rw-r--r--app/request.go83
-rw-r--r--app/response.go31
-rw-r--r--app/router.go82
-rw-r--r--app/server.go54
-rw-r--r--app/status.go24
6 files changed, 246 insertions, 61 deletions
diff --git a/app/handler.go b/app/handler.go
new file mode 100644
index 0000000..634e4a9
--- /dev/null
+++ b/app/handler.go
@@ -0,0 +1,33 @@
+package main
+
+import (
+ "regexp"
+ "strconv"
+ "strings"
+)
+
+func basePathHandler(request *Request) *Response {
+ return NewResponse(Ok, nil, nil)
+}
+
+func echoHandler(request *Request) *Response {
+ message := strings.Split(request.Target(), "/")[2]
+ return NewResponse(Ok, map[string]string{
+ "Content-Type": "text/plain",
+ "Content-Length": strconv.Itoa(len(message)),
+ }, []byte(message))
+}
+
+func userAgentHandler(request *Request) *Response {
+ header := request.Header("User-Agent")
+ return NewResponse(Ok, map[string]string{
+ "Content-Type": "text/plain",
+ "Content-Length": strconv.Itoa(len(header)),
+ }, []byte(header))
+}
+
+func Register(builder *RouterBuilder) {
+ builder.Add("GET", regexp.MustCompile("^/$"), basePathHandler)
+ builder.Add("GET", regexp.MustCompile("^/echo/[a-zA-Z]+$"), echoHandler)
+ builder.Add("GET", regexp.MustCompile("^/user-agent$"), userAgentHandler)
+}
diff --git a/app/request.go b/app/request.go
index a859154..8dd161b 100644
--- a/app/request.go
+++ b/app/request.go
@@ -4,34 +4,85 @@ import (
"bufio"
"bytes"
"fmt"
- "io"
)
+type requestHeaders struct {
+ headers map[string]string
+}
+
+type requestLine struct {
+ method string
+ target string
+ version string
+}
+
type Request struct {
- Method string
- Target string
- Version string
+ line requestLine
+ headers requestHeaders
}
-func NewRequest(reader io.Reader) (*Request, error) {
- scanner := bufio.NewScanner(reader)
+func newRequestHeaders(reader *bufio.Reader) (requestHeaders, error) {
+ request := requestHeaders{make(map[string]string)}
- ok := scanner.Scan()
- if !ok {
- err := scanner.Err()
- if err == nil {
- err = io.EOF
+ for {
+ buf, err := reader.ReadBytes('\n')
+ if err != nil {
+ return request, err
}
- return nil, err
+ if bytes.Equal(buf, []byte{'\r', '\n'}) {
+ return request, nil
+ }
+
+ buf = bytes.TrimSuffix(buf, []byte{'\r', '\n'})
+ header := bytes.Split(buf, []byte{':', ' '})
+
+ if len(header) != 2 {
+ return request, fmt.Errorf("invalid header line")
+ }
+
+ request.headers[string(header[0])] = string(header[1])
}
+}
- line := scanner.Bytes()
- elements := bytes.Split(line, []byte(" "))
+func newRequestLine(reader *bufio.Reader) (requestLine, error) {
+ buf, err := reader.ReadBytes('\n')
+ if err != nil {
+ return requestLine{}, err
+ }
+
+ buf = bytes.TrimSuffix(buf, []byte{'\r', '\n'})
+ elements := bytes.Split(buf, []byte{' '})
if len(elements) != 3 {
- return nil, fmt.Errorf("invalid request, excepted 3 elements, got %d", len(elements))
+ return requestLine{}, fmt.Errorf("invalid request, expected 3 elements, got %d", len(elements))
}
- return &Request{Method: string(elements[0]), Target: string(elements[1]), Version: string(elements[2])}, nil
+ return requestLine{string(elements[0]), string(elements[1]), string(elements[2])}, nil
+}
+
+func NewRequest(reader *bufio.Reader) (*Request, error) {
+ line, err := newRequestLine(reader)
+ if err != nil {
+ return nil, err
+ }
+
+ headers, err := newRequestHeaders(reader)
+ if err != nil {
+ return nil, err
+ }
+
+ return &Request{line, headers}, nil
+}
+
+func (r *Request) Method() string {
+ return r.line.method
+}
+
+func (r *Request) Target() string {
+ return r.line.target
+}
+
+func (r *Request) Header(name string) string {
+ return r.headers.headers[name]
}
diff --git a/app/response.go b/app/response.go
new file mode 100644
index 0000000..abc28d9
--- /dev/null
+++ b/app/response.go
@@ -0,0 +1,31 @@
+package main
+
+import "fmt"
+
+type Response struct {
+ status Status
+ headers map[string]string
+ body []byte
+}
+
+func (r *Response) StatusLine() []byte {
+ return []byte(fmt.Sprintf("HTTP/1.1 %d %s\r\n", r.status.Code(), r.status.Text()))
+}
+
+func (r *Response) Headers() []byte {
+ buf := make([]byte, 0)
+
+ for key, value := range r.headers {
+ buf = append(buf, fmt.Sprintf("%s: %s\r\n", key, value)...)
+ }
+
+ return append(buf, "\r\n"...)
+}
+
+func (r *Response) Body() []byte {
+ return r.body
+}
+
+func NewResponse(code Status, headers map[string]string, body []byte) *Response {
+ return &Response{code, headers, body}
+}
diff --git a/app/router.go b/app/router.go
new file mode 100644
index 0000000..4f563d7
--- /dev/null
+++ b/app/router.go
@@ -0,0 +1,82 @@
+package main
+
+import (
+ "bufio"
+ "io"
+ "net"
+ "regexp"
+)
+
+type Router struct {
+ routes []Route
+}
+
+func (r *Router) handler(request *Request) func(*Request) *Response {
+ for _, route := range r.routes {
+ if route.method == request.Method() && route.path.MatchString(request.Target()) {
+ return route.handler
+ }
+ }
+
+ return func(request *Request) *Response {
+ return NewResponse(NotFound, nil, nil)
+ }
+}
+
+func (r *Router) write(writer io.Writer, response *Response) (err error) {
+ _, err = writer.Write(response.StatusLine())
+ if err != nil {
+ return
+ }
+
+ _, err = writer.Write(response.Headers())
+ if err != nil {
+ return
+ }
+
+ _, err = writer.Write(response.Body())
+ if err != nil {
+ return
+ }
+
+ return
+}
+
+func (r *Router) Handle(conn net.Conn) error {
+ request, err := NewRequest(bufio.NewReader(conn))
+ if err != nil {
+ return err
+ }
+
+ return r.write(conn, r.handler(request)(request))
+}
+
+type Route struct {
+ method string
+ path *regexp.Regexp
+ handler func(*Request) *Response
+}
+
+type RouterBuilder struct {
+ routes []Route
+}
+
+func (r *RouterBuilder) Add(method string, path *regexp.Regexp, handler func(*Request) *Response) {
+ r.routes = append(r.routes, Route{
+ method: method,
+ path: path,
+ handler: handler,
+ })
+}
+
+func (r *RouterBuilder) Build() *Router {
+ return &Router{
+ routes: r.routes,
+ }
+}
+
+func NewRouterBuilder() *RouterBuilder {
+ return &RouterBuilder{
+ routes: make([]Route, 0),
+ }
+}
diff --git a/app/server.go b/app/server.go
index b4dd7ee..4a0c364 100644
--- a/app/server.go
+++ b/app/server.go
@@ -4,10 +4,17 @@ import (
"fmt"
"net"
"os"
- "strings"
)
+func createRouter() *Router {
+ builder := NewRouterBuilder()
+ Register(builder)
+ return builder.Build()
+}
+
func main() {
+ router := createRouter()
+
l, err := net.Listen("tcp", "0.0.0.0:4221")
if err != nil {
fmt.Println("Failed to bind to port 4221")
@@ -20,48 +27,5 @@ func main() {
os.Exit(1)
}
- request, err := NewRequest(conn)
- if err != nil {
- fmt.Println("Error getting target: ", err.Error())
- os.Exit(1)
- }
-
- if request.Target == "/" {
- _, err = conn.Write([]byte("HTTP/1.1 200 OK\r\n\r\n"))
- if err != nil {
- fmt.Println("Error connection write: ", err.Error())
- }
- } else if strings.HasPrefix(request.Target, "/echo/") {
- message := strings.SplitN(request.Target, "/", 3)[2]
-
- _, err = conn.Write([]byte("HTTP/1.1 200 OK\r\n"))
- if err != nil {
- fmt.Println("Error connection write: ", err.Error())
- }
-
- _, err = conn.Write([]byte("Content-Type: text/plain\r\n"))
- if err != nil {
- fmt.Println("Error connection write: ", err.Error())
- }
-
- _, err = conn.Write([]byte(fmt.Sprintf("Content-Length: %d\r\n", len(message))))
- if err != nil {
- fmt.Println("Error connection write: ", err.Error())
- }
-
- _, err = conn.Write([]byte("\r\n"))
- if err != nil {
- fmt.Println("Error connection write: ", err.Error())
- }
-
- _, err = conn.Write([]byte(message))
- if err != nil {
- fmt.Println("Error connection write: ", err.Error())
- }
- } else {
- _, err = conn.Write([]byte("HTTP/1.1 404 Not Found\r\n\r\n"))
- if err != nil {
- fmt.Println("Error connection write: ", err.Error())
- }
- }
+ router.Handle(conn)
}
diff --git a/app/status.go b/app/status.go
new file mode 100644
index 0000000..6a905c9
--- /dev/null
+++ b/app/status.go
@@ -0,0 +1,24 @@
+package main
+
+type status struct {
+ code int
+ text string
+}
+
+type Status interface {
+ Code() int
+ Text() string
+}
+
+var (
+ Ok Status = &status{200, "OK"}
+ NotFound Status = &status{404, "Not Found"}
+)
+
+func (s *status) Code() int {
+ return s.code
+}
+
+func (s *status) Text() string {
+ return s.text
+}