From 6d1bf72d9da6fa207e2986dbbfa6fb20d6e0d01e Mon Sep 17 00:00:00 2001 From: Fedorov Vladimir Date: Sun, 19 Oct 2025 13:05:29 +0700 Subject: [PATCH] add client and transport --- http/client.go | 107 ++++--------------- http/dns.go | 5 + http/header.go | 6 ++ http/{http.go => request.go} | 7 +- http/round_tripper.go | 5 + http/transport.go | 115 +++++++++++++++++++++ http/{client_test.go => transport_test.go} | 4 +- http/write_request.go | 32 ------ test_client/main.go | 16 ++- 9 files changed, 168 insertions(+), 129 deletions(-) create mode 100644 http/dns.go create mode 100644 http/header.go rename http/{http.go => request.go} (70%) create mode 100644 http/round_tripper.go create mode 100644 http/transport.go rename http/{client_test.go => transport_test.go} (95%) delete mode 100644 http/write_request.go diff --git a/http/client.go b/http/client.go index 04b42ec..3d52324 100644 --- a/http/client.go +++ b/http/client.go @@ -1,11 +1,6 @@ package http import ( - "bufio" - "errors" - "io" - "net" - "strconv" "strings" ) @@ -14,14 +9,12 @@ var ( SP = []byte(" ") ) -func getIP(domain string) string { - if domain == "test.ru" { - return "127.0.0.1:8081" - } - return "" +type Client struct { + DNS DNS + Transport RoundTripper } -func Do(method string, url string, headers []Header) (*Response, error) { +func (c *Client) Do(method string, url string, headers []Header) (*Response, error) { url = strings.TrimPrefix(url, "http://") url = strings.TrimPrefix(url, "https://") path := "/" @@ -33,87 +26,23 @@ func Do(method string, url string, headers []Header) (*Response, error) { path = "/" + path } - connectPath := getIP(arr[0]) - conn, err := net.Dial("tcp", connectPath) - // conn, err := tls.Dial("tcp", connectPath, nil) - if err != nil { - return nil, errors.New("connect " + connectPath) + connectPath := "" + if c.DNS != nil { + var err error + connectPath, err = c.DNS.GetIP(arr[0]) + if err != nil { + return nil, err + } } - defer conn.Close() - DebugWrap( - conn, + resp, err := c.Transport.RoundTrip( &Request{ - Method: method, - Path: path, - Protocol: "HTTP/1.0", - Headers: headers, + ConnectPath: connectPath, + Method: method, + Path: path, + Protocol: "HTTP/1.0", + Headers: headers, }, - WriteRequest, ) - return ReadResponse(conn) -} - -func ReadResponse(r io.Reader) (*Response, error) { - b := bufio.NewReader(r) - - if _, err := b.ReadString(' '); err != nil { - return nil, errors.New("read httpVersion") - } - statusCodeStr, err := b.ReadString(' ') - if err != nil { - 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 nil, errors.New("read header") - } - if headerStr == "\r\n" { - break - } - arr := strings.Split(headerStr, ": ") - value := strings.TrimSpace(arr[1]) - header := Header{Name: arr[0], Value: value} - if header.Name == "Content-Length" { - contentLengthStr = header.Value - } - headers = append(headers, header) - } - - length, err := strconv.Atoi(contentLengthStr) - if err != nil { - return nil, errors.New("read Content-Length") - } - - body := make([]byte, 0, length) - - for i := 0; i < length; i++ { - r, err := b.ReadByte() - if err != nil { - return nil, errors.New("read body") - } - body = append(body, r) - } - - return &Response{ - StatusCode: statusCode, - Headers: headers, - Body: body, - }, nil + return resp, err } diff --git a/http/dns.go b/http/dns.go new file mode 100644 index 0000000..f9cd704 --- /dev/null +++ b/http/dns.go @@ -0,0 +1,5 @@ +package http + +type DNS interface { + GetIP(domain string) (string, error) +} diff --git a/http/header.go b/http/header.go new file mode 100644 index 0000000..e4928ba --- /dev/null +++ b/http/header.go @@ -0,0 +1,6 @@ +package http + +type Header struct { + Name string + Value string +} diff --git a/http/http.go b/http/request.go similarity index 70% rename from http/http.go rename to http/request.go index a5ce2a7..07dd303 100644 --- a/http/http.go +++ b/http/request.go @@ -1,6 +1,8 @@ package http type Request struct { + ConnectPath string // ip + Method string Path string Protocol string @@ -9,8 +11,3 @@ type Request struct { Body string } - -type Header struct { - Name string - Value string -} diff --git a/http/round_tripper.go b/http/round_tripper.go new file mode 100644 index 0000000..43a68c7 --- /dev/null +++ b/http/round_tripper.go @@ -0,0 +1,5 @@ +package http + +type RoundTripper interface { + RoundTrip(*Request) (*Response, error) +} diff --git a/http/transport.go b/http/transport.go new file mode 100644 index 0000000..8ea5526 --- /dev/null +++ b/http/transport.go @@ -0,0 +1,115 @@ +package http + +import ( + "bufio" + "bytes" + "errors" + "fmt" + "io" + "net" + "strconv" + "strings" +) + +type HttpTransport struct{} + +func (t *HttpTransport) RoundTrip(req *Request) (*Response, error) { + conn, err := net.Dial("tcp", req.ConnectPath) + // conn, err := tls.Dial("tcp", connectPath, nil) + if err != nil { + return nil, errors.New("connect " + req.ConnectPath) + } + defer conn.Close() + + debugWrap(conn, req, writeRequest) + return readResponse(conn) +} + +func writeRequest(w io.Writer, req *Request) { + fmt.Fprintf(w, "%s %s %s\r\n", req.Method, req.Path, req.Protocol) + + for _, header := range req.Headers { + fmt.Fprintf(w, "%s: %s\r\n", header.Name, header.Value) + } + fmt.Fprintf(w, "\r\n") + + if len(req.Body) > 0 { + fmt.Fprintf(w, "%s\r\n", req.Body) + } +} + +func debugWrap(w io.Writer, req *Request, f func(w io.Writer, req *Request)) { + buffer := &bytes.Buffer{} + + f(buffer, req) + + fmt.Println("----- Debug Info -----") + fmt.Println(buffer.String()) + fmt.Println("----- Debug End Info -----") + + w.Write(buffer.Bytes()) +} + +func readResponse(r io.Reader) (*Response, error) { + b := bufio.NewReader(r) + + if _, err := b.ReadString(' '); err != nil { + return nil, errors.New("read httpVersion") + } + statusCodeStr, err := b.ReadString(' ') + if err != nil { + 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 nil, errors.New("read header") + } + if headerStr == "\r\n" { + break + } + arr := strings.Split(headerStr, ": ") + value := strings.TrimSpace(arr[1]) + header := Header{Name: arr[0], Value: value} + if header.Name == "Content-Length" { + contentLengthStr = header.Value + } + headers = append(headers, header) + } + + length, err := strconv.Atoi(contentLengthStr) + if err != nil { + return nil, errors.New("read Content-Length") + } + + body := make([]byte, 0, length) + + for i := 0; i < length; i++ { + r, err := b.ReadByte() + if err != nil { + return nil, errors.New("read body") + } + body = append(body, r) + } + + return &Response{ + StatusCode: statusCode, + Headers: headers, + Body: body, + }, nil +} diff --git a/http/client_test.go b/http/transport_test.go similarity index 95% rename from http/client_test.go rename to http/transport_test.go index 57d7183..6008fc7 100644 --- a/http/client_test.go +++ b/http/transport_test.go @@ -11,7 +11,7 @@ import ( func Test_WriteRequest(t *testing.T) { b := &strings.Builder{} - WriteRequest( + writeRequest( b, &Request{ Method: "GET", @@ -28,7 +28,7 @@ func Test_ReadResponse(t *testing.T) { []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) + r, err := readResponse(b) assert.Nil(t, err) assert.Equal(t, 200, r.StatusCode) diff --git a/http/write_request.go b/http/write_request.go deleted file mode 100644 index 841b008..0000000 --- a/http/write_request.go +++ /dev/null @@ -1,32 +0,0 @@ -package http - -import ( - "bytes" - "fmt" - "io" -) - -func WriteRequest(w io.Writer, req *Request) { - fmt.Fprintf(w, "%s %s %s\r\n", req.Method, req.Path, req.Protocol) - - for _, header := range req.Headers { - fmt.Fprintf(w, "%s: %s\r\n", header.Name, header.Value) - } - fmt.Fprintf(w, "\r\n") - - if len(req.Body) > 0 { - fmt.Fprintf(w, "%s\r\n", req.Body) - } -} - -func DebugWrap(w io.Writer, req *Request, f func(w io.Writer, req *Request)) { - buffer := &bytes.Buffer{} - - f(buffer, req) - - fmt.Println("----- Debug Info -----") - fmt.Println(buffer.String()) - fmt.Println("----- Debug End Info -----") - - w.Write(buffer.Bytes()) -} diff --git a/test_client/main.go b/test_client/main.go index 13f3650..c77f163 100644 --- a/test_client/main.go +++ b/test_client/main.go @@ -1,13 +1,27 @@ package main import ( + "errors" "fmt" "git.3crabs.ru/VLADIMIR/net/http" ) +type CustomDNS struct{} + +func (*CustomDNS) GetIP(domain string) (string, error) { + if domain == "test.ru" { + return "127.0.0.1:8081", nil + } + return "", errors.New("ip not found") +} + func main() { - r, err := http.Do( + client := http.Client{ + DNS: &CustomDNS{}, + Transport: &http.HttpTransport{}, + } + r, err := client.Do( "GET", "http://test.ru", []http.Header{