Skip to content

Commit

Permalink
feat(httereq): build http request
Browse files Browse the repository at this point in the history
  • Loading branch information
ahuigo committed Jul 15, 2024
1 parent 0399479 commit cf99d9b
Show file tree
Hide file tree
Showing 9 changed files with 593 additions and 0 deletions.
File renamed without changes.
24 changes: 24 additions & 0 deletions demo/httpreq/curl_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package httpreq

import (
"strings"
"testing"

"github.com/ahuigo/gohttptool/httpreq"
)

func TestCurl(t *testing.T) {
curl, err := httpreq.R().
SetParams(map[string]string{"p": "1"}).
AddCookieKV("count", "1").
AddFileHeader("file", "test.txt", []byte("hello world")).
ToCurl()
if err != nil {
t.Fatal(err)
}
if !strings.HasPrefix(curl, "curl ") {
t.Fatal("bad curl: ", curl)
}else{
t.Log("curl: ", curl)
}
}
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
module github.com/ahuigo/gohttptool

go 1.22.1

require github.com/pkg/errors v0.9.1
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
88 changes: 88 additions & 0 deletions httpreq/curl.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package httpreq

import (
"bytes"
"io"
"net/http"
"net/http/cookiejar"

"net/url"
"strings"

"github.com/ahuigo/gohttptool/shell"
)

func (r *request) FromCurl(curl string) {

}
func (r *request) ToCurl() (curl string, err error) {
if httpreq, err := r.ToRequest(); err != nil {
return "", err
} else {
curl := buildCurlRequest(httpreq, nil)
return curl, nil
}
}

func buildCurlRequest(req *http.Request, httpCookiejar http.CookieJar) (curl string) {
// 1. Generate curl raw headers
curl = "curl -X " + req.Method + " "
// req.Host + req.URL.Path + "?" + req.URL.RawQuery + " " + req.Proto + " "
headers := dumpCurlHeaders(req)
for _, kv := range *headers {
curl += `-H ` + shell.Quote(kv[0]+": "+kv[1]) + ` `
}

// 2. Generate curl cookies
if cookieJar, ok := httpCookiejar.(*cookiejar.Jar); ok {
cookies := cookieJar.Cookies(req.URL)
if len(cookies) > 0 {
curl += ` -H ` + shell.Quote(dumpCurlCookies(cookies)) + " "
}
}

// 3. Generate curl body
if req.Body != nil {
buf, _ := io.ReadAll(req.Body)
req.Body = io.NopCloser(bytes.NewBuffer(buf)) // important!!
curl += `-d ` + shell.Quote(string(buf))
}

urlString := shell.Quote(req.URL.String())
if urlString == "''" {
urlString = "'http://unexecuted-request'"
}
curl += " " + urlString
return curl
}

// dumpCurlCookies dumps cookies to curl format
func dumpCurlCookies(cookies []*http.Cookie) string {
sb := strings.Builder{}
sb.WriteString("Cookie: ")
for _, cookie := range cookies {
sb.WriteString(cookie.Name + "=" + url.QueryEscape(cookie.Value) + "&")
}
return strings.TrimRight(sb.String(), "&")
}

// dumpCurlHeaders dumps headers to curl format
func dumpCurlHeaders(req *http.Request) *[][2]string {
headers := [][2]string{}
for k, vs := range req.Header {
for _, v := range vs {
headers = append(headers, [2]string{k, v})
}
}
n := len(headers)
for i := 0; i < n; i++ {
for j := n - 1; j > i; j-- {
jj := j - 1
h1, h2 := headers[j], headers[jj]
if h1[0] < h2[0] {
headers[jj], headers[j] = headers[j], headers[jj]
}
}
}
return &headers
}
143 changes: 143 additions & 0 deletions httpreq/req-builder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package httpreq

import (
"bytes"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/url"
"os"
"strings"

"github.com/pkg/errors"
)

func (session *request) ToRequest() (*http.Request, error) {
var dataType = ContentType(session.rawreq.Header.Get("Content-Type"))
var origurl = session.url
if len(session.files) > 0 || len(session.fileHeaders) > 0 {
dataType = ContentTypeFormData
}

URL, err := session.buildURLParams(origurl)
if err != nil {
return nil, err
}
if URL.Scheme == "" || URL.Host == "" {
err = &url.Error{Op: "parse", URL: origurl, Err: fmt.Errorf("failed")}
return nil, err
}

switch dataType {
case ContentTypeFormEncode:
if len(session.datas) > 0 {
formEncodeValues := session.buildFormEncode(session.datas)
session.setBodyFormEncode(formEncodeValues)
}
case ContentTypeFormData:
// multipart/form-data
session.buildFilesAndForms()
}

if session.rawreq.Body == nil && session.rawreq.Method != "GET" {
session.rawreq.Body = http.NoBody
}

session.rawreq.URL = URL

return session.rawreq, nil
}

// build post Form encode
func (session *request) buildFormEncode(datas map[string]string) (Forms url.Values) {
Forms = url.Values{}
for key, value := range datas {
Forms.Add(key, value)
}
return Forms
}

// set form urlencode
func (session *request) setBodyFormEncode(Forms url.Values) {
data := Forms.Encode()
session.rawreq.Body = io.NopCloser(strings.NewReader(data))
session.rawreq.ContentLength = int64(len(data))
}

func (r *request) buildURLParams(userURL string) (*url.URL, error) {
params := r.params
paramsArray := r.paramsList
if strings.HasPrefix(userURL, "/") {
userURL = "http://localhost" + userURL
}else if userURL == ""{
userURL = "http://unknown"
}
parsedURL, err := url.Parse(userURL)

if err != nil {
return nil, err
}

values := parsedURL.Query()

for key, value := range params {
values.Set(key, value)
}
for key, vals := range paramsArray {
for _, v := range vals {
values.Add(key, v)
}
}
parsedURL.RawQuery = values.Encode()
return parsedURL, nil
}

func (r *request) buildFilesAndForms() error {
files := r.files
datas := r.datas
filesHeaders := r.fileHeaders
//handle file multipart
var b bytes.Buffer
w := multipart.NewWriter(&b)

for k, v := range datas {
w.WriteField(k, v)
}

for field, path := range files {
part, err := w.CreateFormFile(field, path)
if err != nil {
fmt.Printf("Upload %s failed!", path)
panic(err)
}
file, err := os.Open(path)
if err != nil {
err = errors.WithMessagef(err, "Open %s", path)
return err
}
_, err = io.Copy(part, file)
if err != nil {
return err
}
}
for field, fileheader := range filesHeaders {
part, err := w.CreateFormFile(field, fileheader.Filename)
if err != nil {
fmt.Printf("Upload %s failed!", field)
panic(err)
}
_, err = io.Copy(part, bytes.NewReader([]byte(fileheader.content)))
if err != nil {
return err
}
}

w.Close()
// set file header example:
// "Content-Type": "multipart/form-data; boundary=------------------------7d87eceb5520850c",
r.rawreq.Body = io.NopCloser(bytes.NewReader(b.Bytes()))
r.rawreq.ContentLength = int64(b.Len())
r.rawreq.Header.Set("Content-Type", w.FormDataContentType())
return nil
}
119 changes: 119 additions & 0 deletions httpreq/req.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package httpreq

import (
"context"
"net/http"
)

type ContentType string

const (
ContentTypeNone ContentType = ""
ContentTypeFormEncode ContentType = "application/x-www-form-urlencoded"
ContentTypeFormData ContentType = "multipart/form-data"
ContentTypeJson ContentType = "application/json"
ContentTypePlain ContentType = "text/plain"
)

type fileHeader struct {
Filename string
// Header textproto.MIMEHeader
Size int64
content []byte
// tmpfile string
// tmpoff int64
// tmpshared bool
}

type request struct {
rawreq *http.Request
url string
files map[string]string // field -> path
fileHeaders map[string]fileHeader // field -> contents
datas map[string]string // key -> value
params map[string]string // key -> value
paramsList map[string][]string // key -> value list
}

func R() *request {
return &request{
rawreq: &http.Request{
Method: "GET",
Header: make(http.Header),
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
},
files: make(map[string]string),
fileHeaders: make(map[string]fileHeader),
datas: make(map[string]string),
params: make(map[string]string),
paramsList: make(map[string][]string),
}
}

func (r *request) SetAuth(key, value string) {
r.rawreq.SetBasicAuth(key, value)
}

func (r *request) SetHeader(key, value string) {
r.rawreq.Header.Set(key, value)
}
func (r *request) AddFile(fieldname, path string) *request {
r.files[fieldname] = path
return r
}

func (r *request) AddFileHeader(fieldname, filename string, content []byte) *request {
r.fileHeaders[fieldname] = fileHeader{
Filename: filename,
content: content,
Size: int64(len(content)),
}
return r
}

func (r *request) AddCookies(cookies []*http.Cookie) *request {
for _, cookie := range cookies {
r.rawreq.AddCookie(cookie)
}
return r
}
func (r *request) AddCookieKV(name, value string) *request {
cookie := &http.Cookie{
Name: name,
Value: value,
}
r.rawreq.AddCookie(cookie)
return r
}

func (r *request) SetUrl(url string) *request {
r.url = url
return r
}

func (r *request) SetMethod(method string) *request {
r.rawreq.Method = method
return r
}

func (r *request) SetParams(params map[string]string) *request {
r.params = params
return r
}

func (r *request) GetRawreq() *http.Request {
return r.rawreq
}

func (r *request) SetCtx(ctx context.Context) *request {
r.rawreq = r.rawreq.WithContext(ctx)
return r
}

func (r *request) EnableTrace(ctx context.Context) *request {
trace := clientTraceNew(r.rawreq.Context())
r.rawreq = r.rawreq.WithContext(trace.ctx)
return r
}
Loading

0 comments on commit cf99d9b

Please sign in to comment.