commit 8fe49c35936d9a6f0b4a992c778d9461bf97b929 Author: arzumify Date: Mon Oct 28 09:47:23 2024 +0000 Initial commit diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..167993b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +tests/index.html -linguist-detectable +tests/index.html linguist-vendored \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..18e6a6a --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.idea +tests/client/main.wasm +tests/server/server +tests/server/server.crt +tests/server/server.key +tests/server/server.csr +tests/server/ca.crt +tests/server/ca.key +tests/server/ca.srl \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..969dabb --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,157 @@ +# GNU LESSER GENERAL PUBLIC LICENSE + +Version 3, 29 June 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. + + +Everyone is permitted to copy and distribute verbatim copies of this +license document, but changing it is not allowed. + +This version of the GNU Lesser General Public License incorporates the +terms and conditions of version 3 of the GNU General Public License, +supplemented by the additional permissions listed below. + +## 0. Additional Definitions. + +As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the +GNU General Public License. + +"The Library" refers to a covered work governed by this License, other +than an Application or a Combined Work as defined below. + +An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + +A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + +The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + +The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + +## 1. Exception to Section 3 of the GNU GPL. + +You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + +## 2. Conveying Modified Versions. + +If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + +- a) under this License, provided that you make a good faith effort + to ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or +- b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + +## 3. Object Code Incorporating Material from Library Header Files. + +The object code form of an Application may incorporate material from a +header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + +- a) Give prominent notice with each copy of the object code that + the Library is used in it and that the Library and its use are + covered by this License. +- b) Accompany the object code with a copy of the GNU GPL and this + license document. + +## 4. Combined Works. + +You may convey a Combined Work under terms of your choice that, taken +together, effectively do not restrict modification of the portions of +the Library contained in the Combined Work and reverse engineering for +debugging such modifications, if you also do each of the following: + +- a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. +- b) Accompany the Combined Work with a copy of the GNU GPL and this + license document. +- c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. +- d) Do one of the following: + - 0) Convey the Minimal Corresponding Source under the terms of + this License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + - 1) Use a suitable shared library mechanism for linking with + the Library. A suitable mechanism is one that (a) uses at run + time a copy of the Library already present on the user's + computer system, and (b) will operate properly with a modified + version of the Library that is interface-compatible with the + Linked Version. +- e) Provide Installation Information, but only if you would + otherwise be required to provide such information under section 6 + of the GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the Application + with a modified version of the Linked Version. (If you use option + 4d0, the Installation Information must accompany the Minimal + Corresponding Source and Corresponding Application Code. If you + use option 4d1, you must provide the Installation Information in + the manner specified by section 6 of the GNU GPL for conveying + Corresponding Source.) + +## 5. Combined Libraries. + +You may place library facilities that are a work based on the Library +side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + +- a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities, conveyed under the terms of this License. +- b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + +## 6. Revised Versions of the GNU Lesser General Public License. + +The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +as you received it specifies that a certain numbered version of the +GNU Lesser General Public License "or any later version" applies to +it, you have the option of following the terms and conditions either +of that published version or of any later version published by the +Free Software Foundation. If the Library as you received it does not +specify a version number of the GNU Lesser General Public License, you +may choose any version of the GNU Lesser General Public License ever +published by the Free Software Foundation. + +If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..323ec31 --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +# jsFetch + +Go library to bridge net/http and the JS Fetch API, without actually importing net/http. +Made using the [jsStreams](https://git.ailur.dev/Ailur/jsStreams) library. + +[![Go Report Card](https://goreportcard.com/badge/git.ailur.dev/ailur/jsFetch)](https://goreportcard.com/report/git.ailur.dev/ailur/jsFetch) [![Go Reference](https://pkg.go.dev/badge/git.ailur.dev/ailur/jsFetch.svg)](https://pkg.go.dev/git.ailur.dev/ailur/jsFetch) + +The API is exactly the same as net/http. \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..d6818b3 --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module git.ailur.dev/ailur/jsFetch + +go 1.22 + +require ( + git.ailur.dev/ailur/jsStreams v1.2.0 + github.com/go-chi/chi/v5 v5.1.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..3a15a92 --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +git.ailur.dev/ailur/jsStreams v1.2.0 h1:BRtLEyjkUoPKPu0Y6odUbSMlKCYNyR792TYRtujKfPw= +git.ailur.dev/ailur/jsStreams v1.2.0/go.mod h1:/ZCvbUcWkZRuKIkO7jH6b5vIjzdxIOP8ET8X0src5Go= +github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= +github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= diff --git a/main.go b/main.go new file mode 100644 index 0000000..f8c82c9 --- /dev/null +++ b/main.go @@ -0,0 +1,696 @@ +package jsFetch + +import ( + "context" + "errors" + "io" + "maps" + "net" + "runtime" + "strconv" + "strings" + "sync" + "time" + "unicode" + + "crypto/tls" + "mime/multipart" + "net/url" + "syscall/js" + + "git.ailur.dev/ailur/jsStreams" +) + +// Transport is here only for compatibility with the Go standard library. +// All fields are ignored in fetch. +type Transport struct { + // Proxy specifies a function to return a proxy for a given Request. + // It is ignored in fetch. + Proxy func(*Request) (*url.URL, error) + + // OnProxyError specifies a function to handle errors that occur while fetching a proxy. + // It is ignored in fetch. + OnProxyError func(*Request, *url.URL, error) + + // DialContext specifies the dial function for creating unencrypted TCP connections. + // It is ignored in fetch. + DialContext func(ctx context.Context, network, addr string) (net.Conn, error) + + // Dial specifies the dial function for creating unencrypted TCP connections. + // It is ignored in fetch. + Dial func(network, addr string) (net.Conn, error) + + // DialTLSContext specifies the dial function for creating TLS connections. + // It is ignored in fetch. + DialTLSContext func(ctx context.Context, network, addr string) (net.Conn, error) + + // DialTLS specifies the dial function for creating TLS connections. + // It is ignored in fetch. + DialTLS func(network, addr string) (net.Conn, error) + + // TLSClientConfig specifies the TLS configuration to use with tls.Client. + // It is ignored in fetch. + TLSClientConfig *tls.Config + + // TLSHandshakeTimeout specifies the maximum amount of time waiting to wait for a TLS handshake. + // It is ignored in fetch. + TLSHandshakeTimeout time.Duration + + // DisableKeepAlives specifies whether to disable keep-alive connections. + // It is ignored in fetch. + DisableKeepAlives bool + + // DisableCompression specifies whether to disable compression. + // It is ignored in fetch. + DisableCompression bool + + // MaxIdleConns specifies the maximum number of idle (keep-alive) connections to keep. + // It is ignored in fetch. + MaxIdleConns int + + // MaxIdleConnsPerHost specifies the maximum number of idle (keep-alive) connections to keep per host. + // It is ignored in fetch. + MaxIdleConnsPerHost int + + // IdleConnTimeout specifies the maximum amount of time an idle (keep-alive) connection will remain idle before closing itself. + // It is ignored in fetch. + IdleConnTimeout time.Duration + + // ResponseHeaderTimeout specifies the maximum amount of time to wait for a response header. + // It is ignored in fetch. + ResponseHeaderTimeout time.Duration + + // ExpectContinueTimeout specifies the maximum amount of time to wait for a 100-continue response. + // It is ignored in fetch. + ExpectContinueTimeout time.Duration + + // TLSNextProto specifies a function to upgrade the connection to a different protocol. + // It is ignored in fetch. + TLSNextProto map[string]func(authority string, c net.Conn) RoundTripper + + // ProxyConnectHeader specifies the headers to send to proxies during CONNECT requests. + // It is ignored in fetch. + ProxyConnectHeader Header + + // MaxResponseHeaderBytes specifies the maximum number of bytes to read from the server's response headers. + // It is ignored in fetch. + MaxResponseHeaderBytes int + + // WriteBufferSize specifies the size of the write buffer. + // It is ignored in fetch. + WriteBufferSize int + + // ReadBufferSize specifies the size of the read buffer. + // It is ignored in fetch. + ReadBufferSize int + + // ForceAttemptHTTP2 specifies whether to force an attempt to use HTTP/2. + // It is ignored in fetch. + ForceAttemptHTTP2 bool +} + +// RoundTrip executes a single HTTP transaction, returning a Response for the provided Request. +// In the context of fetch, it will automatically follow redirects. +// This implementation is a wrapper around the fetch API. +func (t *Transport) RoundTrip(req *Request) (resp *Response, err error) { + defer func() { + recovery := recover() + if recovery != nil { + runtimeErr, ok := recovery.(runtime.Error) + if ok { + err = runtimeErr + } else { + err = errors.New(recovery.(string)) + } + } + }() + + if req.GetBody != nil { + req.Body, err = req.GetBody() + if err != nil { + return nil, err + } + } + headersMapStringInterface := make(map[string]interface{}) + for key, value := range req.Header { + headersMapStringInterface[key] = value + } + if req.Body != nil { + headersMapStringInterface["Content-Length"] = req.ContentLength + } + fetchArgs := map[string]interface{}{ + "method": req.Method, + "headers": headersMapStringInterface, + } + // Since all supported browsers are chromium based, lets just detect for chrome. + // If we are, we can use a streamed client body + if req.Method != "GET" && req.Method != "HEAD" && req.Body != nil { + if !req.DisableStreamedClient { + if !js.Global().Get("chrome").IsUndefined() { + req.DisableStreamedClientChecks = true + } + } + var jsBody js.Value + if req.DisableStreamedClientChecks && !req.DisableStreamedClient { + jsBody = jsStreams.ReaderToReadableStream(req.Body) + fetchArgs["duplex"] = "half" + } else { + // Not today firefox, not today + // Mozilla, please add support for streamed client bodies + body, err := io.ReadAll(req.Body) + if err != nil { + return nil, err + } + jsBody = js.Global().Get("Uint8Array").New(len(body)) + js.CopyBytesToJS(jsBody, body) + } + fetchArgs["body"] = jsBody + } + promise := js.Global().Call("fetch", req.URL.String(), js.ValueOf(fetchArgs)) + + var waitGroup sync.WaitGroup + waitGroup.Add(1) + + promise.Call("then", js.FuncOf(func(this js.Value, args []js.Value) interface{} { + resp = new(Response) + resp.Request = req + resp.Header = make(Header) + args[0].Get("headers").Call("forEach", js.FuncOf(func(this js.Value, args []js.Value) interface{} { + resp.Header.Set(args[0].String(), args[1].String()) + return nil + })) + resp.Status = args[0].Get("statusText").String() + resp.StatusCode = args[0].Get("status").Int() + if resp.Header.Has("Content-Length") { + resp.ContentLength, err = strconv.ParseInt(resp.Header.Get("Content-Length"), 10, 64) + if err != nil { + resp.ContentLength = -1 + } + } + resp.Body = jsStreams.NewReadableStream(args[0].Get("body")) + + // Standard-library compatibility fields + resp.Proto = "HTTP/1.1" + resp.ProtoMajor = 1 + resp.ProtoMinor = 1 + resp.TransferEncoding = []string{"chunked"} + resp.Trailer = make(Header) + resp.TLS = nil + resp.Close = false + resp.Uncompressed = true + + waitGroup.Done() + return nil + })) + + promise.Call("catch", js.FuncOf(func(this js.Value, args []js.Value) interface{} { + err = errors.New(args[0].Get("message").String()) + waitGroup.Done() + return nil + })) + + waitGroup.Wait() + return +} + +// Clone returns a copy of the Transport. +func (t *Transport) Clone() (newTransport *Transport) { + newTransport = new(Transport) + newTransport.Proxy = t.Proxy + newTransport.OnProxyError = t.OnProxyError + newTransport.DialContext = t.DialContext + newTransport.Dial = t.Dial + newTransport.DialTLSContext = t.DialTLSContext + newTransport.DialTLS = t.DialTLS + newTransport.TLSClientConfig = t.TLSClientConfig + newTransport.TLSHandshakeTimeout = t.TLSHandshakeTimeout + newTransport.DisableKeepAlives = t.DisableKeepAlives + newTransport.DisableCompression = t.DisableCompression + newTransport.MaxIdleConns = t.MaxIdleConns + newTransport.MaxIdleConnsPerHost = t.MaxIdleConnsPerHost + newTransport.IdleConnTimeout = t.IdleConnTimeout + newTransport.ResponseHeaderTimeout = t.ResponseHeaderTimeout + newTransport.ExpectContinueTimeout = t.ExpectContinueTimeout + newTransport.TLSNextProto = t.TLSNextProto + newTransport.ProxyConnectHeader = t.ProxyConnectHeader.Clone() + newTransport.MaxResponseHeaderBytes = t.MaxResponseHeaderBytes + newTransport.WriteBufferSize = t.WriteBufferSize + newTransport.ReadBufferSize = t.ReadBufferSize + newTransport.ForceAttemptHTTP2 = t.ForceAttemptHTTP2 + return +} + +// CloseIdleConnections closes any idle connections. +// This does nothing in fetch. +func (t *Transport) CloseIdleConnections() {} + +// RegisterProtocol registers a new protocol with a custom RoundTripper. +// This does nothing in fetch. +func (t *Transport) RegisterProtocol(protocol string, rt RoundTripper) {} + +// FetchRoundTripper is a wrapper around the fetch API. It is used to make requests. +// It is the default RoundTripper used for this subset of net/http. +var FetchRoundTripper RoundTripper = &Transport{} + +// RoundTripper is an interface representing the ability to execute a single HTTP transaction. +type RoundTripper interface { + // RoundTrip executes a single HTTP transaction, returning a Response for the provided Request. + // In the context of fetch, it will automatically follow redirects. + RoundTrip(*Request) (*Response, error) +} + +// Client is a fetch client. It is used to make requests. +type Client struct { + // Transport specifies the mechanism by which individual requests are made. + // If nil, FetchRoundTripper is used. + Transport RoundTripper + + // Jar does nothing, but it is required for compatibility with the Go standard library. + Jar CookieJar + + // Timeout specifies a time limit for requests made by this Client. + Timeout time.Duration +} + +// Do sends an HTTP request and returns an HTTP response, following policy (such as redirects, cookies, auth) as configured on the client. +func (c *Client) Do(req *Request) (resp *Response, err error) { + if c.Transport == nil { + c.Transport = FetchRoundTripper + } + return c.Transport.RoundTrip(req) +} + +// CookieJar does nothing, but it is required for compatibility with the Go standard library. +type CookieJar interface { + SetCookies(u *url.URL, cookies []*Cookie) + Cookies(u *url.URL) []*Cookie +} + +// SameSite does nothing, but it is required for compatibility with the Go standard library. +type SameSite int + +// Cookie does nothing, but it is required for compatibility with the Go standard library. +type Cookie struct { + Name string + Value string + Quoted bool + Path string + Domain string + Expires time.Time + RawExpires string + MaxAge int + Secure bool + HttpOnly bool + SameSite SameSite + Partitioned bool + Raw string + Unparsed []string +} + +// Fetch is a fetch client. It is used to make requests. +// It wraps around JS fetch. +var Fetch = &Client{ + Transport: FetchRoundTripper, + Timeout: 20 * time.Second, +} + +func CanonicalHeaderKey(key string) string { + const allowedCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!#$%&'*+-.^_`|~" + for _, char := range []rune(key) { + if !strings.Contains(allowedCharacters, string(char)) { + return key + } + } + splitStrings := strings.Split(key, "-") + for splitString, _ := range splitStrings { + var stringBuilder strings.Builder + stringBuilder.WriteRune(unicode.ToUpper([]rune(splitStrings[splitString])[0])) + stringBuilder.WriteString(strings.ToLower(splitStrings[splitString][1:])) + splitStrings[splitString] = stringBuilder.String() + } + return strings.Join(splitStrings, "-") +} + +type Header map[string]string + +func (h Header) Add(key, value string) { + h[CanonicalHeaderKey(key)] = value +} + +func (h Header) Clone() (newHeader Header) { + maps.Copy(h, newHeader) + return +} + +func (h Header) Del(key string) { + delete(h, CanonicalHeaderKey(key)) +} + +func (h Header) Get(key string) (value string) { + value, _ = h[CanonicalHeaderKey(key)] + return +} + +func (h Header) Set(key, value string) { + h[CanonicalHeaderKey(key)] = value +} + +func (h Header) Has(key string) (has bool) { + _, has = h[CanonicalHeaderKey(key)] + return +} + +func (h Header) Values() (values []string) { + for _, value := range h { + values = append(values, value) + } + return +} + +func (h Header) Write(w io.Writer) error { + for key, value := range h { + _, err := w.Write([]byte(key + ": " + value + "\r\n")) + if err != nil { + return err + } + } + return nil +} + +func (h Header) WriteSubset(w io.Writer, exclude map[string]bool) error { + for key, value := range h { + excludeKey, _ := exclude[key] + if excludeKey { + continue + } + _, err := w.Write([]byte(key + ": " + value + "\r\n")) + if err != nil { + return err + } + } + return nil +} + +type Request struct { + // DisableStreamedClient specifies whether to use a streamed client body. + // This is set to false by default, but has checks to make sure it's running on a supported browser. + // HTTP/2 or QUIC to be enabled when using V8, which is not always the case, particularly on + // older servers or test servers. + // + // If DisableStreamedClientChecks is set to true, the client will first attempt to detect if + // the server supports HTTP/2 or QUIC and if you are running a supported JavaScript engine. + // Supported browser engines include: + // - V8 (Chrome, Edge, Opera) + // Unsupported browser engines include: + // - SpiderMonkey (Firefox) + // - JavaScriptCore (Safari) + // - Chakra (Internet Explorer) + // - KJS (Konqueror) + // - Presto (Opera Mini and ancient versions of Opera) + // Data from https://caniuse.com/mdn-api_request_request_request_body_readablestream + // TL;DR If it's chromium it'll work. + DisableStreamedClient bool + + // DisableStreamedClientChecks specifies whether to disable checks for streamed clients. + // It does nothing if UseStreamedClient is set to false. + // Having it set to false may add another HEAD request to the server, which may be undesirable. + // Also, forcing it on may be useful if SpiderMonkey one day supports streamed clients bodies. + DisableStreamedClientChecks bool + + // Method specifies the HTTP method (GET, POST, PUT, etc.). + Method string + + // URL specifies either the URL to fetch as a string, or a URL object. + URL *url.URL + + // Headers is a Headers object, allowing you to set request headers. + Header Header + + // Body is an optional body to be sent with the request. + Body io.ReadCloser + + // GetBody is an optional function that returns a ReadCloser for the body. + GetBody func() (io.ReadCloser, error) + + // ContentLength is the length of the body. It is mostly superseded by the Content-Length header. + ContentLength int64 + + // The following fields are not used by fetch and exist mostly for compatibility with the Go standard library. + // They will do nothing. Do not use them. + + // Proto, ProtoMajor, ProtoMinor specify the HTTP protocol version. + // This is useless, as fetch does not allow you to specify the protocol version. + Proto string + ProtoMajor int + ProtoMinor int + + // Close indicates whether to close the connection after the request. + // This is useless, as fetch does not allow you to specify whether to close the connection and instead forces you to use a WebSocket. + Close bool + + // Host specifies the host to perform the request to. + // This is useless, as it only makes sense in the context of the Host or :authority headers, both of which are filled-in automatically by fetch. + Host string + + // TransferEncoding specifies the transfer encodings to be used. + // This is useless, as since we always use a stream, the transfer encoding is always chunked. + TransferEncoding []string + + // Form, PostForm, and MultipartForm specify the parsed form data. + // This is useless, because even the Go standard library does not use it. + // Use Body instead. + Form url.Values + PostForm url.Values + MultipartForm *url.Values + + // Trailer specifies additional headers that are sent after the request body. + // This is useless, as fetch does not allow you to specify trailers. + // Not to mention, nothing supports trailers. + Trailer Header + + // RemoteAddr and RequestURI specify the remote address and Request-URI for the request. + // This is useless, as fetch does not allow you to specify the remote address, and you should use URL instead of RequestURI. + RemoteAddr string + RequestURI string + + // TLS allows you to specify the TLS connection state. + // This is useless, as fetch does not allow you to specify the TLS connection state. + TLS *tls.ConnectionState + + // Cancel is an optional channel that can be used to cancel the request. + // This isn't even in the Go standard library anymore. + Cancel <-chan struct{} + + // Response is the response that caused this request to be created, usually in a redirect. + // This is useless, as fetch follows redirects automatically. + Response *Response + + // Pattern is the pattern that was matched for this request. + // This is useless, as fetch does not support pattern matching. + Pattern string +} + +// AddCookie does nothing, but it is required for compatibility with the Go standard library. +func (r *Request) AddCookie(c *Cookie) {} + +// BasicAuth does nothing, but it is required for compatibility with the Go standard library. +func (r *Request) BasicAuth() (username, password string, ok bool) { return } + +func (r *Request) Clone() (newRequest *Request) { + newRequest = new(Request) + newRequest.Method = r.Method + newRequest.URL = r.URL + newRequest.Header = r.Header.Clone() + newRequest.Body = r.Body + newRequest.GetBody = r.GetBody + newRequest.ContentLength = r.ContentLength + newRequest.TransferEncoding = r.TransferEncoding + newRequest.Proto = r.Proto + newRequest.ProtoMajor = r.ProtoMajor + newRequest.ProtoMinor = r.ProtoMinor + newRequest.Close = r.Close + newRequest.Host = r.Host + newRequest.Form = r.Form + newRequest.PostForm = r.PostForm + newRequest.MultipartForm = r.MultipartForm + newRequest.Trailer = r.Trailer.Clone() + newRequest.RemoteAddr = r.RemoteAddr + newRequest.RequestURI = r.RequestURI + newRequest.TLS = r.TLS + newRequest.Cancel = r.Cancel + newRequest.Response = r.Response + newRequest.Pattern = r.Pattern + return +} + +// Context does nothing, but it is required for compatibility with the Go standard library. +func (r *Request) Context() context.Context { return context.Background() } + +// Cookie does nothing, but it is required for compatibility with the Go standard library. +func (r *Request) Cookie(name string) (*Cookie, error) { return nil, nil } + +// Cookies does nothing, but it is required for compatibility with the Go standard library. +func (r *Request) Cookies() []*Cookie { return nil } + +// CookiesNamed does nothing, but it is required for compatibility with the Go standard library. +func (r *Request) CookiesNamed(name string) []*Cookie { return nil } + +// FormFile does nothing, but it is required for compatibility with the Go standard library. +func (r *Request) FormFile(key string) (multipart.File, *multipart.FileHeader, error) { + return nil, nil, nil +} + +// FormValue does nothing, but it is required for compatibility with the Go standard library. +func (r *Request) FormValue(key string) string { return "" } + +// MultipartReader does nothing, but it is required for compatibility with the Go standard library. +func (r *Request) MultipartReader() (*multipart.Reader, error) { return nil, nil } + +// ParseForm does nothing, but it is required for compatibility with the Go standard library. +func (r *Request) ParseForm() error { return nil } + +// ParseMultipartForm does nothing, but it is required for compatibility with the Go standard library. +func (r *Request) ParseMultipartForm(maxMemory int64) error { return nil } + +// PostFormValue does nothing, but it is required for compatibility with the Go standard library. +func (r *Request) PostFormValue(key string) string { return "" } + +// ProtoAtLeast does nothing, but it is required for compatibility with the Go standard library. +func (r *Request) ProtoAtLeast(major, minor int) bool { return false } + +// Finally, something that does something! + +// Referer returns the value of the Referer header. +func (r *Request) Referer() string { + return r.Header.Get("Referer") +} + +// SetBasicAuth does nothing, but it is required for compatibility with the Go standard library. +func (r *Request) SetBasicAuth(username, password string) {} + +// UserAgent returns the value of the User-Agent header. +func (r *Request) UserAgent() string { + return r.Header.Get("User-Agent") +} + +// WithContext does nothing, but it is required for compatibility with the Go standard library. +func (r *Request) WithContext(ctx context.Context) *Request { return r } + +// Write writes an HTTP/1.1 request, which is the header and body, in wire format. +// It is not yet implemented (and probably never will be, because after all, why would you need to write an HTTP request in wire format?). +func (r *Request) Write(w io.Writer) error { return errors.New("not implemented") } + +// WriteProxy does nothing, but it is required for compatibility with the Go standard library. +func (r *Request) WriteProxy(w io.Writer) error { return nil } + +func NewRequest(method, uri string, body io.Reader) (request *Request, err error) { + request = new(Request) + request.Method = method + request.URL, err = url.Parse(uri) + if err != nil { + return + } + request.Header = make(Header) + request.Body = io.NopCloser(body) + return +} + +type Response struct { + // Status specifies the HTTP status. + // It will be an empty string if using HTTP/2. + Status string + + // StatusCode specifies the HTTP status code. + StatusCode int + + // Header specifies the response headers. + Header Header + + // Body is the response body. + Body io.ReadCloser + + // TransferEncoding specifies the transfer encodings that have been applied to the response. + TransferEncoding []string + + // ContentLength specifies the length of the body. + // It is mostly superseded by the Content-Length header. + // A value of -1 indicates that the length is unknown. + ContentLength int64 + + // Request is the request that was made to get this response. + Request *Request + + // The following fields are not used by fetch and exist mostly for compatibility with the Go standard library. + // They will do nothing. Do not use them. + + // Proto, ProtoMajor, ProtoMinor specify the HTTP protocol version. + // This is useless, as fetch does not allow you to specify the protocol version. + Proto string + ProtoMajor int + ProtoMinor int + + // Close indicates whether to close the connection after the request. + // This is useless, as fetch does not allow you to specify whether to close the connection and instead forces you to use a WebSocket. + Close bool + + // Uncompressed specifies whether the response is uncompressed. + // This is useless, as fetch does not allow you to specify whether the response is uncompressed. + Uncompressed bool + + // Trailer specifies additional headers that are sent after the request body. + // This is useless, as fetch does not allow you to specify trailers. + // Not to mention, nothing supports trailers. + Trailer Header + + // TLS allows you to specify the TLS connection state. + // This is useless, as fetch does not allow you to specify the TLS connection state. + TLS *tls.ConnectionState +} + +func Get(url string) (response *Response, err error) { + request, err := NewRequest("GET", url, nil) + if err != nil { + return + } + response, err = Fetch.Do(request) + return +} + +func Post(url string, body io.Reader) (response *Response, err error) { + request, err := NewRequest("POST", url, body) + if err != nil { + return + } + response, err = Fetch.Do(request) + return +} + +func PostForm(url string, data url.Values) (response *Response, err error) { + body := io.NopCloser(strings.NewReader(data.Encode())) + request, err := NewRequest("POST", url, body) + if err != nil { + return + } + request.Header.Add("Content-Type", "application/x-www-form-urlencoded") + response, err = Fetch.Do(request) + return +} + +// Cookies does nothing, but it is required for compatibility with the Go standard library. +func (r *Response) Cookies() []*Cookie { return nil } + +// Location returns the location header (if present). +func (r *Response) Location() (string, error) { + if r.Header.Has("Location") { + return r.Header.Get("Location"), nil + } else { + return "", errors.New("http: no Location header in response") + } +} + +// ProtoAtLeast does nothing, but it is required for compatibility with the Go standard library. +func (r *Response) ProtoAtLeast(major, minor int) bool { return true } + +// Write writes an HTTP/1.1 response, which is the header and body, in wire format. +// It is not yet implemented (and probably never will be, because after all, why would you need to write an HTTP response in wire format?). +func (r *Response) Write(w io.Writer) error { return nil } diff --git a/tests/client/index.html b/tests/client/index.html new file mode 100644 index 0000000..9843cbd --- /dev/null +++ b/tests/client/index.html @@ -0,0 +1,651 @@ + + + + + Wasm-Tester + + + + + + \ No newline at end of file diff --git a/tests/client/main.go b/tests/client/main.go new file mode 100644 index 0000000..0ad2def --- /dev/null +++ b/tests/client/main.go @@ -0,0 +1,120 @@ +package main + +import ( + "fmt" + "git.ailur.dev/ailur/jsFetch" + "io" + "net/http" + "strings" + "syscall/js" +) + +func tryGet(url string) error { + response, err := jsFetch.Get(url) + if err != nil { + return err + } + fmt.Println(response.StatusCode) + read, err := io.ReadAll(response.Body) + if err != nil { + return err + } + fmt.Println(string(read)) + return nil +} + +func tryHead(url string) error { + request, err := jsFetch.NewRequest("HEAD", url, nil) + if err != nil { + return err + } + response, err := jsFetch.Fetch.Do(request) + if err != nil { + return err + } + fmt.Println(response.StatusCode) + fmt.Println(response.Header) + return nil +} + +func tryPost(url string, message string) error { + response, err := jsFetch.Post(url, strings.NewReader(message)) + if err != nil { + return err + } + fmt.Println(response.StatusCode) + read := make([]byte, len(message)) + _, err = response.Body.Read(read) + if err != nil { + return err + } + fmt.Println(string(read)) + return nil +} + +func reportTest(fail bool, err error) { + request, _ := http.NewRequest("GET", "https://localhost:8080/reportTestResults", nil) + request.Header.Set("Success", fmt.Sprint(!fail)) + if err != nil { + request.Header.Set("Error", err.Error()) + } +} + +func main() { + js.Global().Set("tryGet", js.FuncOf(func(this js.Value, p []js.Value) interface{} { + go func() { + err := tryGet(p[0].String()) + if err != nil { + panic(err) + return + } + }() + return nil + })) + + js.Global().Set("tryHead", js.FuncOf(func(this js.Value, p []js.Value) interface{} { + go func() { + err := tryHead(p[0].String()) + if err != nil { + panic(err) + return + } + }() + return nil + })) + + js.Global().Set("tryPost", js.FuncOf(func(this js.Value, p []js.Value) interface{} { + go func() { + err := tryPost(p[0].String(), p[1].String()) + if err != nil { + panic(err) + return + } + }() + return nil + })) + + js.Global().Set("tryAll", js.FuncOf(func(this js.Value, p []js.Value) interface{} { + go func() { + err := tryGet("https://localhost:8080/hello") + if err != nil { + reportTest(true, err) + return + } + err = tryHead("https://localhost:8080/hello") + if err != nil { + reportTest(true, err) + return + } + err = tryPost("https://localhost:8080/hello", "Hello, world!") + if err != nil { + reportTest(true, err) + return + } + reportTest(false, nil) + }() + return nil + })) + + select {} +} diff --git a/tests/server/main.go b/tests/server/main.go new file mode 100644 index 0000000..db9c11f --- /dev/null +++ b/tests/server/main.go @@ -0,0 +1,89 @@ +package main + +import ( + "fmt" + "github.com/go-chi/chi/v5" + "io" + "net/http" + "os" +) + +func main() { + r := chi.NewRouter() + r.Use(func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, HEAD") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Success, Error") + next.ServeHTTP(w, r) + }) + }) + r.Get("/", func(w http.ResponseWriter, r *http.Request) { + file, err := os.ReadFile("../client/index.html") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + _, err = w.Write(file) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + }) + r.Get("/main.wasm", func(w http.ResponseWriter, r *http.Request) { + file, err := os.ReadFile("../client/main.wasm") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + _, err = w.Write(file) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + }) + r.Options("/hello", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, HEAD") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Success, Error") + }) + r.Options("/reportTestResults", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, HEAD") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Success, Error") + }) + r.Head("/hello", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain") + w.Header().Set("Success", "true") + }) + r.Get("/hello", func(w http.ResponseWriter, r *http.Request) { + _, err := w.Write([]byte("hello")) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + }) + r.Post("/hello", func(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + _, err = w.Write(body) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + }) + r.Get("/reportTestResults", func(w http.ResponseWriter, r *http.Request) { + success := r.Header.Get("Success") + if success == "true" { + fmt.Println("Test passed") + os.Exit(0) + } else { + fmt.Println("Test failed... " + r.Header.Get("Error")) + os.Exit(1) + } + }) + err := http.ListenAndServeTLS(":8080", "server.crt", "server.key", r) + if err != nil { + fmt.Println(err) + return + } +} diff --git a/tests/test.sh b/tests/test.sh new file mode 100755 index 0000000..7dff626 --- /dev/null +++ b/tests/test.sh @@ -0,0 +1,75 @@ +#!/bin/sh + +CA_FILE=$(realpath "./server/ca.crt") +CA_KEY=$(realpath "./server/ca.key") +SRL_FILE=$(realpath "./server/ca.srl") +CSR_FILE=$(realpath "./server/server.csr") +SSL_FILE=$(realpath "./server/server.crt") +SSL_KEY=$(realpath "./server/server.key") + +superuserCommand="pkexec" +if [ -z "$(command -v pkexec)" ]; then + superuserCommand="sudo" +fi + +if [ "$1" = "-u" ] || [ "$1" = "--uninstall" ]; then + echo "Uninstalling the certificate..." + if [ -z "$(command -v p11-kit)" ]; then + $superuserCommand sh -c "rm /usr/local/share/ca-certificates/$CA_FILE && update-ca-certificates" + else + $superuserCommand sh -c "trust anchor --remove $CA_FILE" + fi + rm "$CA_FILE" "$CA_KEY" "$CSR_FILE" "$SSL_FILE" "$SSL_KEY" "$SRL_FILE" + echo "Good, you've uninstalled the certificate." + + exit 0 +fi + +if ! [ -f "$CA_FILE" ] || ! [ -f "$CA_KEY" ] || ! [ -f "$CSR_FILE" ] || ! [ -f "$SSL_FILE" ] || ! [ -f "$SSL_KEY" ]; then + echo "Warning! This will add a certificate to your system's trust store." + echo "If this self-signed certificate is ever leaked, attackers can use it to cause damage." + echo "Please only run this script if you understand the risks and trust the source of the certificate." + echo "We take no responsibility for any damage caused by the use of this certificate... though that's said in the LICENSE." + echo "Do you want to continue? (yes/no)" + read -r answer + if [ "$answer" != "yes" ]; then + echo "Aborting." + exit 1 + fi + echo "Well, you said it, not me." + COUNTRY="GB" + STATE="London" + LOCALITY="London" + ORGANIZATION="Totally Real Company Inc." + ORGANIZATIONAL_UNIT="Testing Department" + COMMON_NAME="localhost" + openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ + -keyout "$CA_KEY" -out "$CA_FILE" \ + -subj "/C=$COUNTRY/ST=$STATE/L=$LOCALITY/O=$ORGANIZATION/OU=$ORGANIZATIONAL_UNIT/CN=$COMMON_NAME" + openssl req -nodes -newkey rsa:2048 \ + -keyout "$SSL_KEY" -out "$CSR_FILE" \ + -subj "/C=$COUNTRY/ST=$STATE/L=$LOCALITY/O=$ORGANIZATION/OU=$ORGANIZATIONAL_UNIT/CN=$COMMON_NAME" + printf "subjectAltName = DNS:%s\nauthorityKeyIdentifier = keyid,issuer\nbasicConstraints = CA:FALSE\nkeyUsage = digitalSignature, keyEncipherment\nextendedKeyUsage=serverAuth" $COMMON_NAME > /tmp/extfile.cnf + openssl x509 -req -in "$CSR_FILE" -CA "$CA_FILE" -CAkey "$CA_KEY" -CAcreateserial -out "$SSL_FILE" -days 365 \ + -extfile /tmp/extfile.cnf + echo "Self-signed certificate and key have been generated:" + echo "Trusting the certificate... (you may be prompted for your password)". + if [ -z "$(command -v p11-kit)" ]; then + $superuserCommand sh -c "cp $CA_FILE /usr/local/share/ca-certificates/$CA_FILE && update-ca-certificates" + else + $superuserCommand sh -c "trust anchor $CA_FILE" + fi + echo "Deleting temporary files..." + rm /tmp/extfile.cnf +fi + +echo "Building the server and client..." +go build -o server/server server/main.go +GOOS=js GOARCH=wasm go build -o client/main.wasm client/main.go +echo "Launching the client in your default browser..." +xdg-open "https://localhost:8080" +echo "Launching the server..." +cd server || exit 1 +echo "Server started. Press Ctrl+C to stop." +./server +echo "Alright, the server has stopped. If you want to remove the self-signed certificate, run ./test.sh --uninstall." \ No newline at end of file