-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #628 from getlantern/feat/adding-water-uris
adding support to fetch WATER WASM files
- Loading branch information
Showing
9 changed files
with
453 additions
and
21 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
package water | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"errors" | ||
"io" | ||
"net/http" | ||
"strings" | ||
) | ||
|
||
//go:generate mockgen -package=water -destination=mocks_test.go . WASMDownloader | ||
|
||
type WASMDownloader interface { | ||
DownloadWASM(context.Context, io.Writer) error | ||
} | ||
|
||
type downloader struct { | ||
urls []string | ||
httpClient *http.Client | ||
httpDownloader WASMDownloader | ||
} | ||
|
||
// NewWASMDownloader creates a new WASMDownloader instance. | ||
func NewWASMDownloader(urls []string, client *http.Client) (WASMDownloader, error) { | ||
if len(urls) == 0 { | ||
return nil, log.Error("WASM downloader requires URLs to download but received empty list") | ||
} | ||
return &downloader{ | ||
urls: urls, | ||
httpClient: client, | ||
}, nil | ||
} | ||
|
||
// DownloadWASM downloads the WASM file from the given URLs, verifies the hash | ||
// sum and writes the file to the given writer. | ||
func (d *downloader) DownloadWASM(ctx context.Context, w io.Writer) error { | ||
joinedErrs := errors.New("failed to download WASM from all URLs") | ||
for _, url := range d.urls { | ||
if strings.HasPrefix(url, "magnet:?") { | ||
// Skip magnet links for now | ||
joinedErrs = errors.Join(joinedErrs, errors.New("magnet links are not supported")) | ||
continue | ||
} | ||
tempBuffer := &bytes.Buffer{} | ||
err := d.downloadWASM(ctx, tempBuffer, url) | ||
if err != nil { | ||
joinedErrs = errors.Join(joinedErrs, err) | ||
continue | ||
} | ||
|
||
_, err = tempBuffer.WriteTo(w) | ||
if err != nil { | ||
joinedErrs = errors.Join(joinedErrs, err) | ||
continue | ||
} | ||
|
||
return nil | ||
} | ||
return joinedErrs | ||
} | ||
|
||
// downloadWASM checks what kind of URL was given and downloads the WASM file | ||
// from the URL. It can be a HTTPS URL or a magnet link. | ||
func (d *downloader) downloadWASM(ctx context.Context, w io.Writer, url string) error { | ||
switch { | ||
case strings.HasPrefix(url, "http://"), strings.HasPrefix(url, "https://"): | ||
if d.httpDownloader == nil { | ||
d.httpDownloader = NewHTTPSDownloader(d.httpClient, url) | ||
} | ||
return d.httpDownloader.DownloadWASM(ctx, w) | ||
default: | ||
return log.Errorf("unsupported protocol: %s", url) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
package water | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"io" | ||
"net/http" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
gomock "go.uber.org/mock/gomock" | ||
) | ||
|
||
func TestNewWASMDownloader(t *testing.T) { | ||
var tests = []struct { | ||
name string | ||
givenURLs []string | ||
givenHTTPClient *http.Client | ||
assert func(*testing.T, WASMDownloader, error) | ||
}{ | ||
{ | ||
name: "it should return an error when providing an empty list of URLs", | ||
assert: func(t *testing.T, d WASMDownloader, err error) { | ||
assert.Error(t, err) | ||
assert.Nil(t, d) | ||
}, | ||
}, | ||
{ | ||
name: "it should successfully return a wasm downloader", | ||
givenURLs: []string{"http://example.com"}, | ||
givenHTTPClient: http.DefaultClient, | ||
assert: func(t *testing.T, wDownloader WASMDownloader, err error) { | ||
assert.NoError(t, err) | ||
d := wDownloader.(*downloader) | ||
assert.Equal(t, []string{"http://example.com"}, d.urls) | ||
assert.Equal(t, http.DefaultClient, d.httpClient) | ||
}, | ||
}, | ||
} | ||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
d, err := NewWASMDownloader(tt.givenURLs, tt.givenHTTPClient) | ||
tt.assert(t, d, err) | ||
}) | ||
} | ||
} | ||
|
||
func TestDownloadWASM(t *testing.T) { | ||
ctx := context.Background() | ||
|
||
contentMessage := "content" | ||
var tests = []struct { | ||
name string | ||
givenHTTPClient *http.Client | ||
givenURLs []string | ||
givenWriter io.Writer | ||
setupHTTPDownloader func(ctrl *gomock.Controller) WASMDownloader | ||
assert func(*testing.T, io.Reader, error) | ||
}{ | ||
{ | ||
name: "it should return an error telling magnet links are not supported", | ||
givenURLs: []string{"magnet:?"}, | ||
assert: func(t *testing.T, r io.Reader, err error) { | ||
b, berr := io.ReadAll(r) | ||
require.NoError(t, berr) | ||
assert.Empty(t, b) | ||
assert.Error(t, err) | ||
assert.ErrorContains(t, err, "magnet links are not supported") | ||
}, | ||
}, | ||
{ | ||
name: "it should return an unupported protocol error when we provide an URL with not implemented downloader", | ||
givenURLs: []string{ | ||
"udp://example.com", | ||
}, | ||
assert: func(t *testing.T, r io.Reader, err error) { | ||
b, berr := io.ReadAll(r) | ||
require.NoError(t, berr) | ||
assert.Empty(t, b) | ||
assert.Error(t, err) | ||
assert.ErrorContains(t, err, "unsupported protocol") | ||
}, | ||
}, | ||
{ | ||
name: "it should return an error with the HTTP error", | ||
givenURLs: []string{ | ||
"http://example.com", | ||
}, | ||
setupHTTPDownloader: func(ctrl *gomock.Controller) WASMDownloader { | ||
httpDownloader := NewMockWASMDownloader(ctrl) | ||
httpDownloader.EXPECT().DownloadWASM(ctx, gomock.Any()).Return(assert.AnError) | ||
return httpDownloader | ||
}, | ||
assert: func(t *testing.T, r io.Reader, err error) { | ||
b, berr := io.ReadAll(r) | ||
require.NoError(t, berr) | ||
assert.Empty(t, b) | ||
assert.Error(t, err) | ||
assert.ErrorContains(t, err, assert.AnError.Error()) | ||
assert.ErrorContains(t, err, "failed to download WASM from all URLs") | ||
}, | ||
}, | ||
{ | ||
name: "it should return an io.Reader with the expected content", | ||
givenURLs: []string{ | ||
"http://example.com", | ||
}, | ||
setupHTTPDownloader: func(ctrl *gomock.Controller) WASMDownloader { | ||
httpDownloader := NewMockWASMDownloader(ctrl) | ||
httpDownloader.EXPECT().DownloadWASM(ctx, gomock.Any()).DoAndReturn( | ||
func(ctx context.Context, w io.Writer) error { | ||
_, err := w.Write([]byte(contentMessage)) | ||
return err | ||
}) | ||
return httpDownloader | ||
}, | ||
assert: func(t *testing.T, r io.Reader, err error) { | ||
b, berr := io.ReadAll(r) | ||
require.NoError(t, berr) | ||
assert.NoError(t, err) | ||
assert.Equal(t, contentMessage, string(b)) | ||
}, | ||
}, | ||
} | ||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
var httpDownloader WASMDownloader | ||
if tt.setupHTTPDownloader != nil { | ||
ctrl := gomock.NewController(t) | ||
defer ctrl.Finish() | ||
httpDownloader = tt.setupHTTPDownloader(ctrl) | ||
} | ||
|
||
b := &bytes.Buffer{} | ||
wDownloader, err := NewWASMDownloader(tt.givenURLs, tt.givenHTTPClient) | ||
require.NoError(t, err) | ||
wDownloader.(*downloader).httpDownloader = httpDownloader | ||
err = wDownloader.DownloadWASM(ctx, b) | ||
tt.assert(t, b, err) | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
package water | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
) | ||
|
||
type httpsDownloader struct { | ||
cli *http.Client | ||
url string | ||
} | ||
|
||
func NewHTTPSDownloader(client *http.Client, url string) WASMDownloader { | ||
return &httpsDownloader{cli: client, url: url} | ||
} | ||
|
||
func (d *httpsDownloader) DownloadWASM(ctx context.Context, w io.Writer) error { | ||
if d.cli == nil { | ||
d.cli = http.DefaultClient | ||
} | ||
|
||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, d.url, http.NoBody) | ||
if err != nil { | ||
return fmt.Errorf("failed to create a new HTTP request: %w", err) | ||
} | ||
resp, err := d.cli.Do(req) | ||
if err != nil { | ||
return fmt.Errorf("failed to send a HTTP request: %w", err) | ||
} | ||
defer resp.Body.Close() | ||
|
||
if resp.StatusCode != http.StatusOK { | ||
return fmt.Errorf("failed to download WASM file: %s", resp.Status) | ||
} | ||
|
||
_, err = io.Copy(w, resp.Body) | ||
if err != nil { | ||
return fmt.Errorf("failed to write the WASM file: %w", err) | ||
} | ||
|
||
return nil | ||
} |
Oops, something went wrong.