diff --git a/cmd/externals/externals.go b/cmd/externals/externals.go index b96a9f2..107bd7e 100644 --- a/cmd/externals/externals.go +++ b/cmd/externals/externals.go @@ -3,26 +3,21 @@ package main import ( "http-parser/internal/http" "log/slog" - "net" ) func main() { - conn, err := net.Dial("tcp", "example.com:80") + c, err := http.NewHttpClient("example.com:80") if err != nil { - slog.Error("Unable to dial", "err", err) + slog.Error("Error while creating client", "err", err) return } - c := http.HttpClient{ - Conn: conn, - } - - resp, err := c.Get("example.com") + resp, err := c.Get("/") if err != nil { slog.Error("Error getting data from uri", "err", err) return } - slog.Info("received data", "data", resp) + slog.Info("received data", "body", string(resp.Body)) } diff --git a/cmd/reference/main.go b/cmd/reference/main.go new file mode 100644 index 0000000..4a50237 --- /dev/null +++ b/cmd/reference/main.go @@ -0,0 +1,16 @@ +package main + +import ( + "log/slog" + "net/http" +) + +func main() { + res, err := http.Get("http://example.com") + if err != nil { + slog.Error("error when fetching", "err", err) + return + } + + slog.Info("got response", "res", res) +} diff --git a/internal/http/http.go b/internal/http/http.go index 88c2a1e..bddff49 100644 --- a/internal/http/http.go +++ b/internal/http/http.go @@ -19,6 +19,7 @@ var ErrUnsupportedHTTPVersion = errors.New("unsupported HTTP version") var ErrIncompleteStatusLine = errors.New("incomplete StatusLine. Needs 3 parts") var ErrStatusCodeOutsideOfRange = errors.New("statuscode is outside of allowed range 100-599") var ErrConnectionIsNil = errors.New("expected connection to be set, was nil") +var ErrLocationNotFoundOnRedirect = errors.New("expected Location header to be sent on redirect") var ErrInvalidHeaderFormat = errors.New("invalid header format detected. Expected format: \"key: value\"") @@ -30,6 +31,7 @@ type Client interface { type HttpClient struct { net.Conn + address string } func NewHttpClient(address string) (HttpClient, error) { @@ -38,7 +40,7 @@ func NewHttpClient(address string) (HttpClient, error) { return HttpClient{}, err } - return HttpClient{dial}, nil + return HttpClient{dial, address}, nil } var supported_methods = []string{"GET", "HEAD"} @@ -48,6 +50,7 @@ func (hc *HttpClient) Do(request Request) (Response, error) { return Response{}, ErrImplementationDoesNotSupportMethod } + slog.Debug("Do: Performing request to URI", "uri", request.Uri, "address", hc.RemoteAddr()) written, err := hc.Write([]byte(request.ToRaw())) if err != nil { slog.Error("Error while writing to connection", "err", err) @@ -56,7 +59,23 @@ func (hc *HttpClient) Do(request Request) (Response, error) { slog.Debug("Written bytes", "written", written) - return parseResponse(hc) + resp, err := parseResponse(hc) + if err != nil { + return Response{}, err + } + + if resp.redirected() { + loc, err := resp.location() + if err != nil { + return Response{}, ErrLocationNotFoundOnRedirect + } + + req := request + req.setLocation(loc) + return hc.Do(req) + } + + return resp, nil } func (hc *HttpClient) Get(uri string) (Response, error) { @@ -64,6 +83,7 @@ func (hc *HttpClient) Get(uri string) (Response, error) { Version: HTTP11, Method: "GET", Uri: uri, + Host: hc.address, } return hc.Do(request) @@ -75,22 +95,18 @@ func (hc *HttpClient) Head(uri string) (Response, error) { Version: HTTP11, Method: "HEAD", Uri: uri, + Host: hc.address, } return hc.Do(request) } func Raw_http_parsing_docker_socket(docker_socket string) (Response, error) { - - socket, err := dial(docker_socket) - + client, err := NewHttpClient(docker_socket) if err != nil { - slog.Error("Unable to connect to socket", "err", err) - return Response{}, nil + return Response{}, err } - client := HttpClient{socket} - defer client.Close() return client.Get("http://localhost/containers/json") } @@ -159,6 +175,10 @@ func parseResponse(conn io.Reader) (Response, error) { _, err = limit_reader.Read(buf) + if err == io.EOF { + return resp, nil + } + if err != nil { return Response{}, nil } diff --git a/internal/http/http_test.go b/internal/http/http_test.go index 6db784d..9110cc9 100644 --- a/internal/http/http_test.go +++ b/internal/http/http_test.go @@ -19,7 +19,37 @@ func XyzHandler(w http.ResponseWriter, r *http.Request) { } if r.Method == "HEAD" { - w.WriteHeader(200) + w.WriteHeader(HttpStatusCodeOK) + } + + w.WriteHeader(HttpStatusCodeNotFound) +} + +func PermanentlyMovedHandler(w http.ResponseWriter, r *http.Request) { + if r.Method == "GET" { + w.Header().Add("Location", "/redirecttarget") + w.WriteHeader(HttpStatusCodeMovedPermanently) + } + + w.WriteHeader(HttpStatusCodeNotFound) +} + +func TemporarilyMovedHandler(w http.ResponseWriter, r *http.Request) { + if r.Method == "GET" { + w.Header().Add("Location", "/redirecttarget") + w.WriteHeader(HttpStatusCodeTemporaryRedirect) + } + + w.WriteHeader(HttpStatusCodeNotFound) +} + +func TargetHandler(w http.ResponseWriter, r *http.Request) { + if r.Method == "GET" { + resp := "RedirectTargetHandler" + w.Header().Add("Content-Type", "text/plain") + w.WriteHeader(HttpStatusCodeOK) + + w.Write(([]byte(resp))) } } @@ -34,6 +64,9 @@ func BuildServer(t *testing.T) (*httptest.Server, string) { t.Helper() mux := http.NewServeMux() mux.Handle("/", MiddleWare(http.HandlerFunc(XyzHandler))) + mux.Handle("/movedpermanently", MiddleWare(http.HandlerFunc(PermanentlyMovedHandler))) + mux.Handle("/movedtemporarily", MiddleWare(http.HandlerFunc(TemporarilyMovedHandler))) + mux.Handle("/redirecttarget", MiddleWare(http.HandlerFunc(TargetHandler))) server := httptest.NewServer(mux) url := strings.Split(server.URL, "://")[1] @@ -90,7 +123,6 @@ func TestHead(t *testing.T) { } func TestDo(t *testing.T) { - return request := Request{ Method: "INVALID", } @@ -104,6 +136,58 @@ func TestDo(t *testing.T) { } } +func TestRedirect(t *testing.T) { + t.Run("moved permanently", func(t *testing.T) { + server, url := BuildServer(t) + http_client, err := NewHttpClient(url) + + if err != nil { + t.Errorf("unexpected error when creating http client %s", err) + } + defer server.Close() + resp, err := http_client.Get("/movedpermanently") + + if err != nil { + t.Errorf("Unexpected error %s", err) + } + + if !resp.Ok() { + t.Errorf("got %d want %d", resp.StatusLine.StatusCode, HttpStatusCodeOK) + } + + body := string(resp.Body) + + if body != "RedirectTargetHandler" { + t.Errorf("got %s want %s", body, "RedirectTargetHandler") + } + }) + + t.Run("moved temporarily", func(t *testing.T) { + server, url := BuildServer(t) + http_client, err := NewHttpClient(url) + + if err != nil { + t.Errorf("unexpected error when creating http client %s", err) + } + defer server.Close() + resp, err := http_client.Get("/movedtemporarily") + + if err != nil { + t.Errorf("Unexpected error %s", err) + } + + if !resp.Ok() { + t.Errorf("got %d want %d", resp.StatusLine.StatusCode, HttpStatusCodeOK) + } + + body := string(resp.Body) + + if body != "RedirectTargetHandler" { + t.Errorf("got %s want %s", body, "RedirectTargetHandler") + } + }) +} + func TestParseResponse(t *testing.T) { t.Run("successfull response", func(t *testing.T) { responseRaw := "HTTP/1.1 200 OK\nHost: localhost\r\n\r\n" @@ -117,8 +201,8 @@ func TestParseResponse(t *testing.T) { t.Errorf("got %s want %s", resp.StatusLine.HttpVersion, HTTP11) } - if resp.StatusLine.StatusCode != 200 { - t.Errorf("got %d want %d", resp.StatusLine.StatusCode, 200) + if resp.StatusLine.StatusCode != HttpStatusCodeOK { + t.Errorf("got %d want %d", resp.StatusLine.StatusCode, HttpStatusCodeOK) } if resp.StatusLine.ReasonPhrase != "OK" { diff --git a/internal/http/request.go b/internal/http/request.go index efef8a0..c4a47fa 100644 --- a/internal/http/request.go +++ b/internal/http/request.go @@ -1,16 +1,23 @@ package http -import "fmt" +import ( + "fmt" +) type Request struct { Method string Uri string Version string + Host string requestHeader string } func (r Request) ToRaw() string { - host := "Host: localhost" + host := fmt.Sprintf("Host: %s", r.Host) return fmt.Sprintf("%s %s %s\n%s%s", r.Method, r.Uri, r.Version, host, EndOfMessage) } + +func (r *Request) setLocation(location string) { + r.Uri = location +} diff --git a/internal/http/response.go b/internal/http/response.go index b11a432..e7f9515 100644 --- a/internal/http/response.go +++ b/internal/http/response.go @@ -2,6 +2,7 @@ package http import ( "errors" + "slices" "strconv" ) @@ -22,6 +23,11 @@ var ErrNoContentTypefound = errors.New("no content type") var ErrHeaderNotFound = errors.New("header not found") var ErrInvalidContentLengthFormat = errors.New("invalid content lenght format") +var redirectCodes = []int{ + HttpStatusCodeTemporaryRedirect, + HttpStatusCodeMovedPermanently, +} + func (r Response) ContentType() (string, error) { res, ok := r.Headers["Content-Type"] @@ -62,3 +68,17 @@ func (r Response) ContentLength() (int64, error) { return val, nil } + +func (r Response) location() (string, error) { + res, ok := r.Headers["Location"] + + if !ok { + return "", ErrHeaderNotFound + } + + return res, nil +} + +func (r Response) redirected() bool { + return slices.Contains(redirectCodes, int(r.StatusLine.StatusCode)) +}