Skip to content

Commit

Permalink
feat: Add redirect support (MovedPermanently && MovedTemporarily)
Browse files Browse the repository at this point in the history
  • Loading branch information
randuck-dev committed Nov 11, 2023
1 parent 12f274d commit 4d9d4db
Show file tree
Hide file tree
Showing 6 changed files with 166 additions and 24 deletions.
13 changes: 4 additions & 9 deletions cmd/externals/externals.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
16 changes: 16 additions & 0 deletions cmd/reference/main.go
Original file line number Diff line number Diff line change
@@ -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)
}
38 changes: 29 additions & 9 deletions internal/http/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -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\"")

Expand All @@ -30,6 +31,7 @@ type Client interface {

type HttpClient struct {
net.Conn
address string
}

func NewHttpClient(address string) (HttpClient, error) {
Expand All @@ -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"}
Expand All @@ -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)
Expand All @@ -56,14 +59,31 @@ 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) {
request := Request{
Version: HTTP11,
Method: "GET",
Uri: uri,
Host: hc.address,
}

return hc.Do(request)
Expand All @@ -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")
}
Expand Down Expand Up @@ -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
}
Expand Down
92 changes: 88 additions & 4 deletions internal/http/http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
}
}

Expand All @@ -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]
Expand Down Expand Up @@ -90,7 +123,6 @@ func TestHead(t *testing.T) {
}

func TestDo(t *testing.T) {
return
request := Request{
Method: "INVALID",
}
Expand All @@ -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"
Expand All @@ -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" {
Expand Down
11 changes: 9 additions & 2 deletions internal/http/request.go
Original file line number Diff line number Diff line change
@@ -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
}
20 changes: 20 additions & 0 deletions internal/http/response.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package http

import (
"errors"
"slices"
"strconv"
)

Expand All @@ -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"]
Expand Down Expand Up @@ -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))
}

0 comments on commit 4d9d4db

Please sign in to comment.