diff --git a/go.mod b/go.mod index 3a39dec..e3bf4c4 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,11 @@ module git.3crabs.ru/VLADIMIR/net go 1.20 + +require github.com/stretchr/testify v1.8.4 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..8cf6655 --- /dev/null +++ b/go.sum @@ -0,0 +1,9 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/http/client.go b/http/client.go index f60e4e0..13853df 100644 --- a/http/client.go +++ b/http/client.go @@ -2,15 +2,22 @@ package http import ( "bufio" + "crypto/tls" "errors" "fmt" "io" - "net" "strconv" "strings" ) -func Do(method string, url string) (*Response, error) { +var ( + CRLF = []byte("\r\n") + SP = []byte(" ") +) + +func Do(method string, url string, headers []*Header) (*Response, error) { + url = strings.TrimPrefix(url, "http://") + url = strings.TrimPrefix(url, "https://") path := "/" arr := strings.Split(url, "/") if len(arr) == 2 { @@ -20,56 +27,93 @@ func Do(method string, url string) (*Response, error) { path = "/" + path } - conn, err := net.Dial("tcp", arr[0]) + connectPath := arr[0] + // conn, err := net.Dial("tcp", connectPath) + conn, err := tls.Dial("tcp", connectPath, nil) if err != nil { - return nil, errors.New("connect " + arr[0]) + return nil, errors.New("connect " + connectPath) } defer conn.Close() - WriteRequest(conn, method, path, "HTTP/1.0") + WriteRequest(conn, method, arr[0], path, "HTTP/1.0", headers) return ReadResponse(conn) } -func WriteRequest(w io.Writer, method string, path string, httpVersion string) { - fmt.Fprintf(w, "%s %s %s\r\n\r\n", method, path, httpVersion) +func WriteRequest( + w io.Writer, + method string, + host string, + path string, + httpVersion string, + headers []*Header, +) { + io.WriteString(w, method) + w.Write(SP) + io.WriteString(w, path) + w.Write(SP) + io.WriteString(w, httpVersion) + w.Write(CRLF) + + for _, header := range headers { + WriteHeader(w, header.Name, header.Value) + } + + // headers end + w.Write(CRLF) + + // todo: body +} + +func WriteHeader(w io.Writer, name string, value string) { + fmt.Fprintf(w, "%s:%s", name, value) + w.Write(CRLF) } func ReadResponse(r io.Reader) (*Response, error) { - response := &Response{} - b := bufio.NewReader(r) - status, err := b.ReadString('\n') - if err != nil { - return response, errors.New("read status") + if _, err := b.ReadString(' '); err != nil { + return nil, errors.New("read httpVersion") } - arr := strings.Split(status, " ") - response.StatusCode, err = strconv.Atoi(arr[1]) + statusCodeStr, err := b.ReadString(' ') if err != nil { - return response, errors.New("read status code") + return nil, errors.New("read statusCode") + } + if _, err := b.ReadString('\r'); err != nil { + return nil, errors.New("read statusName") + } + if _, err := b.ReadString('\n'); err != nil { + return nil, errors.New("read LF") + } + + statusCode, err := strconv.Atoi(statusCodeStr[:len(statusCodeStr)-1]) + if err != nil { + return nil, errors.New("read statusCode") } headerStr := "" + contentLengthStr := "" + headers := []*Header{} for { headerStr, err = b.ReadString('\n') if err != nil { - return response, errors.New("read header") + return nil, errors.New("read header") } if headerStr == "\r\n" { break } arr := strings.Split(headerStr, ": ") value := strings.TrimSpace(arr[1]) - response.Headers = append(response.Headers, &Header{Name: arr[0], Value: value}) + header := &Header{Name: arr[0], Value: value} + if header.Name == "Content-Length" { + contentLengthStr = header.Value + } + headers = append(headers, header) } - contentTypeHeader, err := response.GetHeader("Content-Length") + length, err := strconv.Atoi(contentLengthStr) if err != nil { - return response, errors.New("get Content-Length") - } - length, err := strconv.Atoi(contentTypeHeader.Value) - if err != nil { - return response, errors.New("read Content-Length") + return nil, errors.New("read Content-Length") } body := make([]byte, 0, length) @@ -77,11 +121,14 @@ func ReadResponse(r io.Reader) (*Response, error) { for i := 0; i < length; i++ { r, err := b.ReadByte() if err != nil { - return response, errors.New("read body") + return nil, errors.New("read body") } body = append(body, r) } - response.Body = body - return response, nil + return &Response{ + StatusCode: statusCode, + Headers: headers, + Body: body, + }, nil } diff --git a/http/client_test.go b/http/client_test.go new file mode 100644 index 0000000..786c32c --- /dev/null +++ b/http/client_test.go @@ -0,0 +1,39 @@ +package http + +import ( + "bytes" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_WriteRequest(t *testing.T) { + b := &strings.Builder{} + + WriteRequest(b, "GET", "example.ru", "/", "HTTP/1.0", nil) + + assert.Equal(t, "GET / HTTP/1.0\r\n\r\n", b.String()) +} + +func Test_ReadResponse(t *testing.T) { + b := bytes.NewBuffer( + []byte("HTTP/1.1 200 OK\r\nDate: Sat, 03 Feb 2024 16:40:29 GMT\r\nContent-Length: 12\r\nContent-Type: text/plain; charset=utf-8\r\n\r\nHello World!"), + ) + + r, err := ReadResponse(b) + assert.Nil(t, err) + + assert.Equal(t, 200, r.StatusCode) + + assert.Equal(t, "Date", r.Headers[0].Name) + assert.Equal(t, "Sat, 03 Feb 2024 16:40:29 GMT", r.Headers[0].Value) + + assert.Equal(t, "Content-Length", r.Headers[1].Name) + assert.Equal(t, "12", r.Headers[1].Value) + + assert.Equal(t, "Content-Type", r.Headers[2].Name) + assert.Equal(t, "text/plain; charset=utf-8", r.Headers[2].Value) + + assert.Equal(t, []byte("Hello World!"), r.Body) +} diff --git a/test_client/main.go b/test_client/main.go index 608fad3..db517bf 100644 --- a/test_client/main.go +++ b/test_client/main.go @@ -7,9 +7,21 @@ import ( ) func main() { - r, err := http.Do("GET", "127.0.0.1:8081") + r, err := http.Do( + "GET", + "https://3crabs.ru:443", + []*http.Header{ + {Name: "Host", Value: "3crabs.ru"}, + {Name: "User-Agent", Value: "3crabs/0.0.1"}, + {Name: "Accept", Value: "*/*"}, + }, + ) if err != nil { panic(err) } - fmt.Println(r.StatusCode, string(r.Body)) + fmt.Println(r.StatusCode) + for _, header := range r.Headers { + fmt.Println(header) + } + fmt.Println(string(r.Body)) } diff --git a/test_server/main.go b/test_server/main.go index 3faa1d0..914b6b4 100644 --- a/test_server/main.go +++ b/test_server/main.go @@ -7,6 +7,7 @@ import ( func main() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + fmt.Println(r.Header) fmt.Fprintf(w, "Hello World!") w.WriteHeader(200) })