From 2c9394c1f6d590b686cbb450725573801d0d4aaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E8=8B=B1=E6=9D=B0?= <2635879218@qq.com> Date: Mon, 12 Aug 2024 16:47:07 +0800 Subject: [PATCH] WIP(x/http/client/get): Introducing textproto for header & implementing custom header --- x/http/_demo/headers/headers.go | 45 ++++++++++++++++++++++ x/http/client.go | 5 +++ x/http/header.go | 67 ++++++++++++++++++++++++++++++++- x/http/request.go | 33 ++++++++++++++-- x/http/response.go | 14 ------- x/http/transport.go | 25 +++++++++--- 6 files changed, 164 insertions(+), 25 deletions(-) create mode 100644 x/http/_demo/headers/headers.go diff --git a/x/http/_demo/headers/headers.go b/x/http/_demo/headers/headers.go new file mode 100644 index 0000000..98cda79 --- /dev/null +++ b/x/http/_demo/headers/headers.go @@ -0,0 +1,45 @@ +package main + +import ( + "fmt" + "io" + + "github.com/goplus/llgo/x/http" +) + +func main() { + client := &http.Client{} + req, err := http.NewRequest("GET", "https://jsonplaceholder.typicode.com/comments?postId=1", nil) + if err != nil { + println(err.Error()) + return + } + + //req.Header.Set("accept", "*/*") + //req.Header.Set("accept-encoding", "identity") + //req.Header.Set("cache-control", "no-cache") + //req.Header.Set("pragma", "no-cache") + //req.Header.Set("priority", "u=0, i") + //req.Header.Set("referer", "https://jsonplaceholder.typicode.com/") + //req.Header.Set("sec-ch-ua", "\"Not)A;Brand\";v=\"99\", \"Google Chrome\";v=\"127\", \"Chromium\";v=\"127\"") + //req.Header.Set("sec-ch-ua-mobile", "?0") + //req.Header.Set("sec-ch-ua-platform", "\"macOS\"") + //req.Header.Set("sec-fetch-dest", "document") + //req.Header.Set("sec-fetch-mode", "navigate") + //req.Header.Set("sec-fetch-site", "same-origin") + //req.Header.Set("sec-fetch-user", "?1") + //req.Header.Set("upgrade-insecure-requests", "1") + //req.Header.Set("user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36") + + resp, err := client.Do(req) + if err != nil { + println(err.Error()) + return + } + body, err := io.ReadAll(resp.Body) + if err != nil { + println(err.Error()) + return + } + fmt.Println(string(body)) +} diff --git a/x/http/client.go b/x/http/client.go index 9ce8506..177b089 100644 --- a/x/http/client.go +++ b/x/http/client.go @@ -37,6 +37,11 @@ func (c *Client) Do(req *Request) (*Response, error) { } func (c *Client) do(req *Request) (*Response, error) { + // Add user-defined request headers to hyper.Request + err := req.setHeaders() + if err != nil { + return nil, err + } return c.send(req, c.Timeout) } diff --git a/x/http/header.go b/x/http/header.go index 4710854..aac05ce 100644 --- a/x/http/header.go +++ b/x/http/header.go @@ -4,11 +4,74 @@ import ( "fmt" "github.com/goplus/llgo/c" + "github.com/goplus/llgo/x/textproto" "github.com/goplus/llgoexamples/rust/hyper" ) +// A Header represents the key-value pairs in an HTTP header. +// +// The keys should be in canonical form, as returned by +// CanonicalHeaderKey. type Header map[string][]string +// Add adds the key, value pair to the header. +// It appends to any existing values associated with key. +// The key is case insensitive; it is canonicalized by +// CanonicalHeaderKey. +func (h Header) Add(key, value string) { + textproto.MIMEHeader(h).Add(key, value) +} + +// Set sets the header entries associated with key to the +// single element value. It replaces any existing values +// associated with key. The key is case insensitive; it is +// canonicalized by textproto.CanonicalMIMEHeaderKey. +// To use non-canonical keys, assign to the map directly. +func (h Header) Set(key, value string) { + textproto.MIMEHeader(h).Set(key, value) +} + +// Get gets the first value associated with the given key. If +// there are no values associated with the key, Get returns "". +// It is case insensitive; textproto.CanonicalMIMEHeaderKey is +// used to canonicalize the provided key. Get assumes that all +// keys are stored in canonical form. To use non-canonical keys, +// access the map directly. +func (h Header) Get(key string) string { + return textproto.MIMEHeader(h).Get(key) +} + +// Values returns all values associated with the given key. +// It is case insensitive; textproto.CanonicalMIMEHeaderKey is +// used to canonicalize the provided key. To use non-canonical +// keys, access the map directly. +// The returned slice is not a copy. +func (h Header) Values(key string) []string { + return textproto.MIMEHeader(h).Values(key) +} + +// get is like Get, but key must already be in CanonicalHeaderKey form. +func (h Header) get(key string) string { + if v := h[key]; len(v) > 0 { + return v[0] + } + return "" +} + +// has reports whether h has the provided key defined, even if it's +// set to 0-length slice. +func (h Header) has(key string) bool { + _, ok := h[key] + return ok +} + +// Del deletes the values associated with key. +// The key is case insensitive; it is canonicalized by +// CanonicalHeaderKey. +func (h Header) Del(key string) { + textproto.MIMEHeader(h).Del(key) +} + // AppendToResponseHeader (HeadersForEachCallback) prints each header to the console func AppendToResponseHeader(userdata c.Pointer, name *uint8, nameLen uintptr, value *uint8, valueLen uintptr) c.Int { resp := (*Response)(userdata) @@ -18,8 +81,8 @@ func AppendToResponseHeader(userdata c.Pointer, name *uint8, nameLen uintptr, va if resp.Header == nil { resp.Header = make(map[string][]string) } - resp.Header[nameStr] = append(resp.Header[nameStr], valueStr) - //c.Printf(c.Str("%.*s: %.*s\n"), int(nameLen), name, int(valueLen), value) + resp.Header.Add(nameStr, valueStr) + //resp.Header[nameStr] = append(resp.Header[nameStr], valueStr) return hyper.IterContinue } diff --git a/x/http/request.go b/x/http/request.go index 2e04939..933ae3b 100644 --- a/x/http/request.go +++ b/x/http/request.go @@ -28,13 +28,16 @@ func NewRequest(method, urlStr string, body io.Reader) (*Request, error) { if err != nil { return nil, err } - return &Request{ + request := &Request{ Method: method, URL: parseURL, Req: req, Host: parseURL.Hostname(), + Header: make(Header), timeout: 0, - }, nil + } + request.Header.Set("Host", request.Host) + return request, nil } func newHyperRequest(method string, URL *url.URL) (*hyper.Request, error) { @@ -49,11 +52,33 @@ func newHyperRequest(method string, URL *url.URL) (*hyper.Request, error) { if req.SetURI((*uint8)(&[]byte(uri)[0]), c.Strlen(c.AllocaCStr(uri))) != hyper.OK { return nil, fmt.Errorf("error setting uri %s\n", uri) } - // Set the request headers reqHeaders := req.Headers() if reqHeaders.Set((*uint8)(&[]byte("Host")[0]), c.Strlen(c.Str("Host")), (*uint8)(&[]byte(host)[0]), c.Strlen(c.AllocaCStr(host))) != hyper.OK { - return nil, fmt.Errorf("error setting headers\n") + return nil, fmt.Errorf("error setting header: Host: %s\n", host) } + return req, nil } + +// setHeaders sets the headers of the request +func (req *Request) setHeaders() error { + headers := req.Req.Headers() + for key, values := range req.Header { + valueLen := len(values) + if valueLen > 1 { + for _, value := range values { + if headers.Add((*uint8)(&[]byte(key)[0]), c.Strlen(c.AllocaCStr(key)), (*uint8)(&[]byte(value)[0]), c.Strlen(c.AllocaCStr(value))) != hyper.OK { + return fmt.Errorf("error adding header %s: %s\n", key, value) + } + } + } else if valueLen == 1 { + if headers.Set((*uint8)(&[]byte(key)[0]), c.Strlen(c.AllocaCStr(key)), (*uint8)(&[]byte(values[0])[0]), c.Strlen(c.AllocaCStr(values[0]))) != hyper.OK { + return fmt.Errorf("error setting header %s: %s\n", key, values[0]) + } + } else { + return fmt.Errorf("error setting header %s: empty value\n", key) + } + } + return nil +} diff --git a/x/http/response.go b/x/http/response.go index 2f3a641..e69c273 100644 --- a/x/http/response.go +++ b/x/http/response.go @@ -11,17 +11,3 @@ type Response struct { Body io.ReadCloser ContentLength int64 } - -// AppendToResponseBody (BodyForEachCallback) appends the body to the response -//func AppendToResponseBody(userdata c.Pointer, chunk *hyper.Buf) c.Int { -// resp := (*Response)(userdata) -// len := chunk.Len() -// buf := unsafe.Slice((*byte)(chunk.Bytes()), len) -// _, err := resp.respBodyWriter.Write(buf) -// resp.ContentLength += int64(len) -// if err != nil { -// fmt.Printf("Failed to write response body: %v\n", err) -// return hyper.IterBreak -// } -// return hyper.IterContinue -//} diff --git a/x/http/transport.go b/x/http/transport.go index 1eed648..ec5ae02 100644 --- a/x/http/transport.go +++ b/x/http/transport.go @@ -184,6 +184,7 @@ func (pc *persistConn) roundTrip(req *Request) (*Response, error) { rc := <-pc.reqch // blocking // Free the resources FreeResources(nil, nil, nil, nil, pc, rc) + return nil, fmt.Errorf("request timeout\n") } select { case re := <-resc: @@ -297,12 +298,26 @@ func (pc *persistConn) readWriteLoop(loop *libuv.Loop) { response.Body, bodyWriter = io.Pipe() - // TODO(spongehah) Replace header operations with using the textproto package - lengthSlice := response.Header["content-length"] - if lengthSlice == nil { - response.ContentLength = 0 + //// TODO(spongehah) Replace header operations with using the textproto package + //lengthSlice := response.Header["content-length"] + //if lengthSlice == nil { + // response.ContentLength = -1 + //} else { + // contentLength := response.Header["content-length"][0] + // length, err := strconv.Atoi(contentLength) + // if err != nil { + // rc.ch <- responseAndError{err: fmt.Errorf("failed to parse content-length")} + // // Free the resources + // FreeResources(task, respBody, bodyWriter, exec, pc, rc) + // return + // } + // response.ContentLength = int64(length) + //} + + contentLength := response.Header.Get("content-length") + if contentLength == "" { + response.ContentLength = -1 } else { - contentLength := response.Header["content-length"][0] length, err := strconv.Atoi(contentLength) if err != nil { rc.ch <- responseAndError{err: fmt.Errorf("failed to parse content-length")}