nativehttp

Simple interface to the native http client
Log | Files | Refs | README | LICENSE

commit 8528e2bfb0445b02a42aa96ea23415459306517f
parent 4df43343e61de8334567a92788524a6c4c54a6d6
Author: Jack Mordaunt <jackmordaunt.dev@gmail.com>
Date:   Mon, 12 Feb 2024 18:59:55 +0800

nativehttp: add Get implementation

Signed-off-by: Jack Mordaunt <jackmordaunt.dev@gmail.com>

Diffstat:
Mgo.mod | 10+++++++++-
Ago.sum | 10++++++++++
Ahttp.go | 8++++++++
Ahttp_test.go | 34++++++++++++++++++++++++++++++++++
Ahttp_windows.go | 330+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 391 insertions(+), 1 deletion(-)

diff --git a/go.mod b/go.mod @@ -1,3 +1,11 @@ module git.sr.ht/~jackmordaunt/nativehttp -go 1.22.0 +go 1.21.0 + +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 @@ -0,0 +1,10 @@ +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 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +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.go b/http.go @@ -0,0 +1,8 @@ +package nativehttp + +import "io" + +// Get a resource, returning an [io.ReaderCloser] containing the response body. +func Get(uri string) (io.ReadCloser, error) { + return get(uri) +} diff --git a/http_test.go b/http_test.go @@ -0,0 +1,34 @@ +package nativehttp + +import ( + "io" + "net/http" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGet(t *testing.T) { + svr := http.Server{ + Addr: "localhost:1337", + Handler: http.HandlerFunc(func(wr http.ResponseWriter, r *http.Request) { + t.Log("request") + if _, err := io.Copy(wr, strings.NewReader("hellope!")); err != nil { + t.Fatalf("failed copying data to response writer") + } + }), + } + + defer svr.Close() + + go svr.ListenAndServe() + + r, err := Get("http://127.0.0.1:1337") + assert.NoError(t, err) + assert.NotNil(t, r) + + by, err := io.ReadAll(r) + assert.NoError(t, err) + assert.Equal(t, string(by), "hellope!") +} diff --git a/http_windows.go b/http_windows.go @@ -0,0 +1,330 @@ +package nativehttp + +import ( + "io" + "net/url" + "runtime" + "syscall" + "unsafe" + + "git.sr.ht/~jackmordaunt/nativehttp/winhttp" +) + +func get(uri string) (io.ReadCloser, error) { + openRequestFlags := winhttp.OpenRequestFlagNone + port := uint32(winhttp.DEFAULT_PORT) + + if HasPrefix(uri, "https://") { + openRequestFlags = winhttp.OpenRequestFlagSecure + port = winhttp.DEFAULT_HTTPS_PORT + } + + u, err := url.Parse(uri) + if err != nil { + return nil, err + } + + domain := u.Hostname() + path := TrimPrefix(u.Path, "/") + + if customPort := u.Port(); customPort != "" { + n, err := parseUint(customPort) + if err != nil { + return nil, err + } + port = uint32(n) + } + + agentStr, err := UTF16PtrFromString("HTTP Plato Desktop Spark/1.0") + if err != nil { + return nil, err + } + + session := winhttp.Open( + agentStr, + winhttp.AcessTypeDefaultProxy, + nil, + nil, + winhttp.OpenFlagAsync, + ) + if session == 0 { + return nil, GetLastError() + } + + defer cleanup(&err, func() error { + if ok := winhttp.CloseHandle(session); ok == 0 { + return GetLastError() + } + return nil + }) + + domainStr, err := UTF16PtrFromString(domain) + if err != nil { + return nil, err + } + + connect := winhttp.Connect(session, domainStr, port, 0) + if connect == 0 { + return nil, GetLastError() + } + + defer cleanup(&err, func() error { + if ok := winhttp.CloseHandle(connect); ok == 0 { + return GetLastError() + } + return nil + }) + + pathStr, err := UTF16PtrFromString(path) + if err != nil { + return nil, err + } + + getStr, err := UTF16PtrFromString("GET") + if err != nil { + return nil, err + } + + request := winhttp.OpenRequest( + connect, + getStr, + pathStr, + nil, + nil, + nil, + openRequestFlags|winhttp.OpenRequestFlagNullCodepage, + ) + if request == 0 { + return nil, GetLastError() + } + + defer cleanup(&err, func() error { + if ok := winhttp.CloseHandle(request); ok == 0 { + return GetLastError() + } + return nil + }) + + if ok := winhttp.SendRequest(request, nil, 0, nil, 0, 0, 0); ok == 0 { + return nil, GetLastError() + } + + if ok := winhttp.ReceiveResponse(request, nil); ok == 0 { + return nil, GetLastError() + } + + runtime.KeepAlive(agentStr) + runtime.KeepAlive(domainStr) + runtime.KeepAlive(pathStr) + runtime.KeepAlive(getStr) + + return &winhttpRequest{ + session: session, + connect: connect, + request: request, + }, nil +} + +// winhttpRequest adapts a request into an [io.ReadCloser]. +type winhttpRequest struct { + session winhttp.HINTERNET + connect winhttp.HINTERNET + request winhttp.HINTERNET + buf []byte +} + +var _ io.ReadCloser = (*winhttpRequest)(nil) + +func newRequest(h winhttp.HINTERNET) *winhttpRequest { + return &winhttpRequest{request: h} +} + +func (r *winhttpRequest) Read(p []byte) (int, error) { + var size winhttp.DWORD + var n winhttp.DWORD + + if ok := winhttp.QueryDataAvailable(r.request, &size); ok == 0 { + return 0, GetLastError() + } + + if size <= 0 { + return 0, io.EOF + } + + if len(r.buf) < int(size+1) { + r.buf = make([]byte, size+1) + } + + data := unsafe.SliceData(r.buf) + + if ok := winhttp.ReadData(r.request, unsafe.Pointer(data), size, &n); ok == 0 { + return 0, GetLastError() + } + + nn := copy(p, r.buf[:n]) + + runtime.KeepAlive(data) + + return nn, nil +} + +func (r *winhttpRequest) Close() error { + if ok := winhttp.CloseHandle(r.request); ok == 0 { + return GetLastError() + } + return nil +} + +// cleanup executes a fallible function if the captured error is not nil. +func cleanup(err *error, fn func() error) { + if err != nil && *err != nil { + if e := fn(); e != nil { + *err = ErrorJoin(*err, e) + } + } +} + +/* + package strconv +*/ + +// parseUint is a specialized helper that parses a sequence of digits into an unsigned integer. +func parseUint(s string) (n uint, _ error) { + const asciiDigitOffset = 48 + + width := uint(len(s)) + + for ii := width; ii > 0; ii-- { + c := s[ii] + switch c { + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': + d := uint(c) - asciiDigitOffset + n += d * pow(10, width-ii) + default: + return 0, parseErr{literal: c} + } + } + + return n, nil +} + +type parseErr struct { + literal byte +} + +func (p parseErr) Error() string { + return "parse: invalid literal (not a digit) " + string(p.literal) +} + +/* + package math +*/ + +func pow(base, exponent uint) (n uint) { + n = 1 + for ii := uint(0); ii < exponent; ii++ { + n *= base + } + return n +} + +/* + package strings +*/ + +// TrimPrefix returns s without the provided leading prefix string. +// If s doesn't start with prefix, s is returned unchanged. +func TrimPrefix(s, prefix string) string { + if HasPrefix(s, prefix) { + return s[len(prefix):] + } + return s +} + +// HasPrefix tests whether the string s begins with prefix. +func HasPrefix(s, prefix string) bool { + return len(s) >= len(prefix) && s[0:len(prefix)] == prefix +} + +/* + package errors +*/ + +// ErrorJoin returns an error that wraps the given errors. +// Any nil error values are discarded. +// ErrorJoin returns nil if every value in errs is nil. +// The error formats as the concatenation of the strings obtained +// by calling the Error method of each element of errs, with a newline +// between each string. +// +// A non-nil error returned by ErrorJoin implements the Unwrap() []error method. +func ErrorJoin(errs ...error) error { + n := 0 + for _, err := range errs { + if err != nil { + n++ + } + } + if n == 0 { + return nil + } + e := &joinError{ + errs: make([]error, 0, n), + } + for _, err := range errs { + if err != nil { + e.errs = append(e.errs, err) + } + } + return e +} + +type joinError struct { + errs []error +} + +func (e *joinError) Error() string { + var b []byte + for i, err := range e.errs { + if i > 0 { + b = append(b, '\n') + } + b = append(b, err.Error()...) + } + return string(b) +} + +func (e *joinError) Unwrap() []error { + return e.errs +} + +// UTF16PtrFromString returns pointer to the UTF-16 encoding of +// the UTF-8 string s, with a terminating NUL added. If s +// contains a NUL byte at any location, it returns (nil, syscall.EINVAL). +func UTF16PtrFromString(s string) (*uint16, error) { + a, err := UTF16FromString(s) + if err != nil { + return nil, err + } + return &a[0], nil +} + +// UTF16FromString returns the UTF-16 encoding of the UTF-8 string +// s, with a terminating NUL added. If s contains a NUL byte at any +// location, it returns (nil, syscall.EINVAL). +func UTF16FromString(s string) ([]uint16, error) { + return syscall.UTF16FromString(s) +} + +var ( + modkernel32 = syscall.NewLazyDLL("kernel32") + procGetLastError = modkernel32.NewProc("GetLastError") +) + +func GetLastError() (lasterr error) { + r0, _, _ := procGetLastError.Call() + if r0 != 0 { + lasterr = syscall.Errno(r0) + } + return +}