diff options
| -rw-r--r-- | app/handler.go | 33 | ||||
| -rw-r--r-- | app/request.go | 83 | ||||
| -rw-r--r-- | app/response.go | 31 | ||||
| -rw-r--r-- | app/router.go | 82 | ||||
| -rw-r--r-- | app/server.go | 54 | ||||
| -rw-r--r-- | app/status.go | 24 |
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 +} |