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:
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
+}