Initial commit

This commit is contained in:
Tracker-Friendly 2024-10-28 09:47:23 +00:00
commit 8fe49c3593
11 changed files with 1819 additions and 0 deletions

2
.gitattributes vendored Normal file
View File

@ -0,0 +1,2 @@
tests/index.html -linguist-detectable
tests/index.html linguist-vendored

9
.gitignore vendored Normal file
View File

@ -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

157
LICENSE.md Normal file
View File

@ -0,0 +1,157 @@
# GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc.
<https://fsf.org/>
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.

8
README.md Normal file
View File

@ -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.

8
go.mod Normal file
View File

@ -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
)

4
go.sum Normal file
View File

@ -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=

696
main.go Normal file
View File

@ -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 }

651
tests/client/index.html Normal file
View File

@ -0,0 +1,651 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Wasm-Tester</title>
<script>
// @license magnet:?xt=urn:btih:c80d50af7d3db9be66a4d0a86db0286e4fd33292&dn=bsd-3-clause.txt BSD-3-Clause
/*
* wasm_exec (https://github.com/golang/go)
* (c) The Go Authors
* @license BSD-3-Clause
*/
"use strict";
(() => {
const enosys = () => {
const err = new Error("not implemented");
err.code = "ENOSYS";
return err;
};
if (!globalThis.fs) {
let outputBuf = "";
globalThis.fs = {
constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused
writeSync(fd, buf) {
outputBuf += decoder.decode(buf);
const nl = outputBuf.lastIndexOf("\n");
if (nl != -1) {
console.log(outputBuf.substring(0, nl));
outputBuf = outputBuf.substring(nl + 1);
}
return buf.length;
},
write(fd, buf, offset, length, position, callback) {
if (offset !== 0 || length !== buf.length || position !== null) {
callback(enosys());
return;
}
const n = this.writeSync(fd, buf);
callback(null, n);
},
chmod(path, mode, callback) { callback(enosys()); },
chown(path, uid, gid, callback) { callback(enosys()); },
close(fd, callback) { callback(enosys()); },
fchmod(fd, mode, callback) { callback(enosys()); },
fchown(fd, uid, gid, callback) { callback(enosys()); },
fstat(fd, callback) { callback(enosys()); },
fsync(fd, callback) { callback(null); },
ftruncate(fd, length, callback) { callback(enosys()); },
lchown(path, uid, gid, callback) { callback(enosys()); },
link(path, link, callback) { callback(enosys()); },
lstat(path, callback) { callback(enosys()); },
mkdir(path, perm, callback) { callback(enosys()); },
open(path, flags, mode, callback) { callback(enosys()); },
read(fd, buffer, offset, length, position, callback) { callback(enosys()); },
readdir(path, callback) { callback(enosys()); },
readlink(path, callback) { callback(enosys()); },
rename(from, to, callback) { callback(enosys()); },
rmdir(path, callback) { callback(enosys()); },
stat(path, callback) { callback(enosys()); },
symlink(path, link, callback) { callback(enosys()); },
truncate(path, length, callback) { callback(enosys()); },
unlink(path, callback) { callback(enosys()); },
utimes(path, atime, mtime, callback) { callback(enosys()); },
};
}
if (!globalThis.process) {
globalThis.process = {
getuid() { return -1; },
getgid() { return -1; },
geteuid() { return -1; },
getegid() { return -1; },
getgroups() { throw enosys(); },
pid: -1,
ppid: -1,
umask() { throw enosys(); },
cwd() { throw enosys(); },
chdir() { throw enosys(); },
}
}
if (!globalThis.crypto) {
throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)");
}
if (!globalThis.performance) {
throw new Error("globalThis.performance is not available, polyfill required (performance.now only)");
}
if (!globalThis.TextEncoder) {
throw new Error("globalThis.TextEncoder is not available, polyfill required");
}
if (!globalThis.TextDecoder) {
throw new Error("globalThis.TextDecoder is not available, polyfill required");
}
const encoder = new TextEncoder("utf-8");
const decoder = new TextDecoder("utf-8");
globalThis.Go = class {
constructor() {
this.argv = ["js"];
this.env = {};
this.exit = (code) => {
if (code !== 0) {
console.warn("exit code:", code);
}
};
this._exitPromise = new Promise((resolve) => {
this._resolveExitPromise = resolve;
});
this._pendingEvent = null;
this._scheduledTimeouts = new Map();
this._nextCallbackTimeoutID = 1;
const setInt64 = (addr, v) => {
this.mem.setUint32(addr + 0, v, true);
this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true);
}
const setInt32 = (addr, v) => {
this.mem.setUint32(addr + 0, v, true);
}
const getInt64 = (addr) => {
const low = this.mem.getUint32(addr + 0, true);
const high = this.mem.getInt32(addr + 4, true);
return low + high * 4294967296;
}
const loadValue = (addr) => {
const f = this.mem.getFloat64(addr, true);
if (f === 0) {
return undefined;
}
if (!isNaN(f)) {
return f;
}
const id = this.mem.getUint32(addr, true);
return this._values[id];
}
const storeValue = (addr, v) => {
const nanHead = 0x7FF80000;
if (typeof v === "number" && v !== 0) {
if (isNaN(v)) {
this.mem.setUint32(addr + 4, nanHead, true);
this.mem.setUint32(addr, 0, true);
return;
}
this.mem.setFloat64(addr, v, true);
return;
}
if (v === undefined) {
this.mem.setFloat64(addr, 0, true);
return;
}
let id = this._ids.get(v);
if (id === undefined) {
id = this._idPool.pop();
if (id === undefined) {
id = this._values.length;
}
this._values[id] = v;
this._goRefCounts[id] = 0;
this._ids.set(v, id);
}
this._goRefCounts[id]++;
let typeFlag = 0;
switch (typeof v) {
case "object":
if (v !== null) {
typeFlag = 1;
}
break;
case "string":
typeFlag = 2;
break;
case "symbol":
typeFlag = 3;
break;
case "function":
typeFlag = 4;
break;
}
this.mem.setUint32(addr + 4, nanHead | typeFlag, true);
this.mem.setUint32(addr, id, true);
}
const loadSlice = (addr) => {
const array = getInt64(addr + 0);
const len = getInt64(addr + 8);
return new Uint8Array(this._inst.exports.mem.buffer, array, len);
}
const loadSliceOfValues = (addr) => {
const array = getInt64(addr + 0);
const len = getInt64(addr + 8);
const a = new Array(len);
for (let i = 0; i < len; i++) {
a[i] = loadValue(array + i * 8);
}
return a;
}
const loadString = (addr) => {
const saddr = getInt64(addr + 0);
const len = getInt64(addr + 8);
return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len));
}
const timeOrigin = Date.now() - performance.now();
this.importObject = {
_gotest: {
add: (a, b) => a + b,
},
gojs: {
// Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters)
// may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported
// function. A goroutine can switch to a new stack if the current stack is too small (see morestack function).
// This changes the SP, thus we have to update the SP used by the imported function.
// func wasmExit(code int32)
"runtime.wasmExit": (sp) => {
sp >>>= 0;
const code = this.mem.getInt32(sp + 8, true);
this.exited = true;
delete this._inst;
delete this._values;
delete this._goRefCounts;
delete this._ids;
delete this._idPool;
this.exit(code);
},
// func wasmWrite(fd uintptr, p unsafe.Pointer, n int32)
"runtime.wasmWrite": (sp) => {
sp >>>= 0;
const fd = getInt64(sp + 8);
const p = getInt64(sp + 16);
const n = this.mem.getInt32(sp + 24, true);
fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n));
},
// func resetMemoryDataView()
"runtime.resetMemoryDataView": (sp) => {
sp >>>= 0;
this.mem = new DataView(this._inst.exports.mem.buffer);
},
// func nanotime1() int64
"runtime.nanotime1": (sp) => {
sp >>>= 0;
setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000);
},
// func walltime() (sec int64, nsec int32)
"runtime.walltime": (sp) => {
sp >>>= 0;
const msec = (new Date).getTime();
setInt64(sp + 8, msec / 1000);
this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true);
},
// func scheduleTimeoutEvent(delay int64) int32
"runtime.scheduleTimeoutEvent": (sp) => {
sp >>>= 0;
const id = this._nextCallbackTimeoutID;
this._nextCallbackTimeoutID++;
this._scheduledTimeouts.set(id, setTimeout(
() => {
this._resume();
while (this._scheduledTimeouts.has(id)) {
// for some reason Go failed to register the timeout event, log and try again
// (temporary workaround for https://github.com/golang/go/issues/28975)
console.warn("scheduleTimeoutEvent: missed timeout event");
this._resume();
}
},
getInt64(sp + 8),
));
this.mem.setInt32(sp + 16, id, true);
},
// func clearTimeoutEvent(id int32)
"runtime.clearTimeoutEvent": (sp) => {
sp >>>= 0;
const id = this.mem.getInt32(sp + 8, true);
clearTimeout(this._scheduledTimeouts.get(id));
this._scheduledTimeouts.delete(id);
},
// func getRandomData(r []byte)
"runtime.getRandomData": (sp) => {
sp >>>= 0;
crypto.getRandomValues(loadSlice(sp + 8));
},
// func finalizeRef(v ref)
"syscall/js.finalizeRef": (sp) => {
sp >>>= 0;
const id = this.mem.getUint32(sp + 8, true);
this._goRefCounts[id]--;
if (this._goRefCounts[id] === 0) {
const v = this._values[id];
this._values[id] = null;
this._ids.delete(v);
this._idPool.push(id);
}
},
// func stringVal(value string) ref
"syscall/js.stringVal": (sp) => {
sp >>>= 0;
storeValue(sp + 24, loadString(sp + 8));
},
// func valueGet(v ref, p string) ref
"syscall/js.valueGet": (sp) => {
sp >>>= 0;
const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16));
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 32, result);
},
// func valueSet(v ref, p string, x ref)
"syscall/js.valueSet": (sp) => {
sp >>>= 0;
Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32));
},
// func valueDelete(v ref, p string)
"syscall/js.valueDelete": (sp) => {
sp >>>= 0;
Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16));
},
// func valueIndex(v ref, i int) ref
"syscall/js.valueIndex": (sp) => {
sp >>>= 0;
storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16)));
},
// valueSetIndex(v ref, i int, x ref)
"syscall/js.valueSetIndex": (sp) => {
sp >>>= 0;
Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24));
},
// func valueCall(v ref, m string, args []ref) (ref, bool)
"syscall/js.valueCall": (sp) => {
sp >>>= 0;
try {
const v = loadValue(sp + 8);
const m = Reflect.get(v, loadString(sp + 16));
const args = loadSliceOfValues(sp + 32);
const result = Reflect.apply(m, v, args);
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 56, result);
this.mem.setUint8(sp + 64, 1);
} catch (err) {
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 56, err);
this.mem.setUint8(sp + 64, 0);
}
},
// func valueInvoke(v ref, args []ref) (ref, bool)
"syscall/js.valueInvoke": (sp) => {
sp >>>= 0;
try {
const v = loadValue(sp + 8);
const args = loadSliceOfValues(sp + 16);
const result = Reflect.apply(v, undefined, args);
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, result);
this.mem.setUint8(sp + 48, 1);
} catch (err) {
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, err);
this.mem.setUint8(sp + 48, 0);
}
},
// func valueNew(v ref, args []ref) (ref, bool)
"syscall/js.valueNew": (sp) => {
sp >>>= 0;
try {
const v = loadValue(sp + 8);
const args = loadSliceOfValues(sp + 16);
const result = Reflect.construct(v, args);
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, result);
this.mem.setUint8(sp + 48, 1);
} catch (err) {
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, err);
this.mem.setUint8(sp + 48, 0);
}
},
// func valueLength(v ref) int
"syscall/js.valueLength": (sp) => {
sp >>>= 0;
setInt64(sp + 16, parseInt(loadValue(sp + 8).length));
},
// valuePrepareString(v ref) (ref, int)
"syscall/js.valuePrepareString": (sp) => {
sp >>>= 0;
const str = encoder.encode(String(loadValue(sp + 8)));
storeValue(sp + 16, str);
setInt64(sp + 24, str.length);
},
// valueLoadString(v ref, b []byte)
"syscall/js.valueLoadString": (sp) => {
sp >>>= 0;
const str = loadValue(sp + 8);
loadSlice(sp + 16).set(str);
},
// func valueInstanceOf(v ref, t ref) bool
"syscall/js.valueInstanceOf": (sp) => {
sp >>>= 0;
this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0);
},
// func copyBytesToGo(dst []byte, src ref) (int, bool)
"syscall/js.copyBytesToGo": (sp) => {
sp >>>= 0;
const dst = loadSlice(sp + 8);
const src = loadValue(sp + 32);
if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) {
this.mem.setUint8(sp + 48, 0);
return;
}
const toCopy = src.subarray(0, dst.length);
dst.set(toCopy);
setInt64(sp + 40, toCopy.length);
this.mem.setUint8(sp + 48, 1);
},
// func copyBytesToJS(dst ref, src []byte) (int, bool)
"syscall/js.copyBytesToJS": (sp) => {
sp >>>= 0;
const dst = loadValue(sp + 8);
const src = loadSlice(sp + 16);
if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) {
this.mem.setUint8(sp + 48, 0);
return;
}
const toCopy = src.subarray(0, dst.length);
dst.set(toCopy);
setInt64(sp + 40, toCopy.length);
this.mem.setUint8(sp + 48, 1);
},
"debug": (value) => {
console.log(value);
},
}
};
}
async run(instance) {
if (!(instance instanceof WebAssembly.Instance)) {
throw new Error("Go.run: WebAssembly.Instance expected");
}
this._inst = instance;
this.mem = new DataView(this._inst.exports.mem.buffer);
this._values = [ // JS values that Go currently has references to, indexed by reference id
NaN,
0,
null,
true,
false,
globalThis,
this,
];
this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id
this._ids = new Map([ // mapping from JS values to reference ids
[0, 1],
[null, 2],
[true, 3],
[false, 4],
[globalThis, 5],
[this, 6],
]);
this._idPool = []; // unused ids that have been garbage collected
this.exited = false; // whether the Go program has exited
// Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory.
let offset = 4096;
const strPtr = (str) => {
const ptr = offset;
const bytes = encoder.encode(str + "\0");
new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes);
offset += bytes.length;
if (offset % 8 !== 0) {
offset += 8 - (offset % 8);
}
return ptr;
};
const argc = this.argv.length;
const argvPtrs = [];
this.argv.forEach((arg) => {
argvPtrs.push(strPtr(arg));
});
argvPtrs.push(0);
const keys = Object.keys(this.env).sort();
keys.forEach((key) => {
argvPtrs.push(strPtr(`${key}=${this.env[key]}`));
});
argvPtrs.push(0);
const argv = offset;
argvPtrs.forEach((ptr) => {
this.mem.setUint32(offset, ptr, true);
this.mem.setUint32(offset + 4, 0, true);
offset += 8;
});
// The linker guarantees global data starts from at least wasmMinDataAddr.
// Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr.
const wasmMinDataAddr = 4096 + 8192;
if (offset >= wasmMinDataAddr) {
throw new Error("total length of command line and environment variables exceeds limit");
}
this._inst.exports.run(argc, argv);
if (this.exited) {
this._resolveExitPromise();
}
await this._exitPromise;
}
_resume() {
if (this.exited) {
throw new Error("Go program has already exited");
}
this._inst.exports.resume();
if (this.exited) {
this._resolveExitPromise();
}
}
_makeFuncWrapper(id) {
const go = this;
return function () {
const event = { id: id, this: this, args: arguments };
go._pendingEvent = event;
go._resume();
return event.result;
};
}
}
})();
// @license-end
</script>
</head>
<body>
<script>
// @license magnet:?xt=urn:btih:0ef1b8170b3b615170ff270def6427c317705f85&dn=lgpl-3.0.txt LGPL-3.0
/*
* jsStreams_tester
* (c) Arzumify
* @license LGPL-3.0
*/
const go = new Go();
WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject).then((result) => {
go.run(result.instance);
})
document.addEventListener("DOMContentLoadd", async () => {
// Give some time for the WASM to load
setTimeout(async () => {
try {
// Start test one: GET request to the server
await tryGet("https://localhost:8080/hello")
} catch (e) {
// Test failed
fetch("/reportTestResults", {
method: "GET",
headers: {
"Success": "false",
"Error": e.message,
},
});
} finally {
// Test passed
// Start test two: HEAD request to the server
try {
await tryHead("https://localhost:8080/hello")
} catch (e) {
// Test failed
fetch("/reportTestResults", {
method: "GET",
headers: {
"Success": "false",
"Error": e.message,
},
});
} finally {
// Test passed
// Start test three: POST request to the server
try {
await tryPost("https://localhost:8080/hello", "Hello, World!")
} catch (e) {
// Test failed
fetch("/reportTestResults", {
method: "GET",
headers: {
"Success": "false",
"Error": e.message,
},
});
} finally {
// Test passed
fetch("/reportTestResults", {
method: "GET",
headers: {
"Success": "true",
},
});
}
}
}
}, 5000);
})
// @license-end
</script>
</body>
</html>

120
tests/client/main.go Normal file
View File

@ -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 {}
}

89
tests/server/main.go Normal file
View File

@ -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
}
}

75
tests/test.sh Executable file
View File

@ -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."