Skip to content

Commit

Permalink
Merge pull request #628 from getlantern/feat/adding-water-uris
Browse files Browse the repository at this point in the history
adding support to fetch WATER WASM files
  • Loading branch information
WendelHime authored Oct 22, 2024
2 parents 99f7b62 + 3b4cfb8 commit 9a68fb2
Show file tree
Hide file tree
Showing 9 changed files with 453 additions and 21 deletions.
8 changes: 5 additions & 3 deletions http-proxy/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,9 +186,10 @@ var (

algenevaAddr = flag.String("algeneva-addr", "", "Address at which to listen for algenAddr connections.")

waterAddr = flag.String("water-addr", "", "Address at which to listen for WATER connections.")
waterWASM = flag.String("water-wasm", "", "Base64 encoded WASM for WATER")
waterTransport = flag.String("water-transport", "", "WATER based transport name")
waterAddr = flag.String("water-addr", "", "Address at which to listen for WATER connections.")
waterWASM = flag.String("water-wasm", "", "Base64 encoded WASM for WATER")
waterWASMAvailableAt = flag.String("water-wasm-available-at", "", "URLs where the WATER WASM is available")
waterTransport = flag.String("water-transport", "", "WATER based transport name")

track = flag.String("track", "", "The track this proxy is running on")
)
Expand Down Expand Up @@ -473,6 +474,7 @@ func main() {
AlgenevaAddr: *algenevaAddr,
WaterAddr: *waterAddr,
WaterWASM: *waterWASM,
WaterWASMAvailableAt: *waterWASMAvailableAt,
WaterTransport: *waterTransport,
}
if *maxmindLicenseKey != "" {
Expand Down
38 changes: 34 additions & 4 deletions http_proxy.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package proxy

import (
"bytes"
"context"
"crypto/tls"
"encoding/base64"
"encoding/json"
"fmt"
"net"
Expand Down Expand Up @@ -185,9 +187,11 @@ type Proxy struct {

AlgenevaAddr string

WaterAddr string
WaterWASM string
WaterTransport string
// deprecated: use WaterWASMAvailableAt
WaterWASM string
WaterWASMAvailableAt string
WaterTransport string
WaterAddr string

throttleConfig throttle.Config
instrument instrument.Instrument
Expand Down Expand Up @@ -988,8 +992,34 @@ func (p *Proxy) listenAlgeneva(baseListen func(string) (net.Listener, error)) li
// Currently water doesn't support customized TCP connections and we need to listen and receive requests directly from the WATER listener
func (p *Proxy) listenWATER(addr string) (net.Listener, error) {
ctx := context.Background()
waterListener, err := water.NewWATERListener(ctx, p.WaterTransport, addr, p.WaterWASM)
var wasm []byte
if p.WaterWASM != "" {
var err error
wasm, err = base64.StdEncoding.DecodeString(p.WaterWASM)
if err != nil {
log.Errorf("failed to decode WASM base64: %v", err)
return nil, err
}
}

if p.WaterWASMAvailableAt != "" {
wasmBuffer := new(bytes.Buffer)
d, err := water.NewWASMDownloader(strings.Split(p.WaterWASMAvailableAt, ","), &http.Client{Timeout: 1 * time.Minute})
if err != nil {
return nil, log.Errorf("failed to create wasm downloader: %w", err)
}

err = d.DownloadWASM(ctx, wasmBuffer)
if err != nil {
return nil, log.Errorf("unable to download water wasm: %w", err)
}
wasm = wasmBuffer.Bytes()
}

// currently the WATER listener doesn't accept a multiplexed connections, so we need to listen and accept connections directly from the listener
waterListener, err := water.NewWATERListener(ctx, nil, p.WaterTransport, addr, wasm)
if err != nil {
log.Errorf("failed to starte WATER listener: %w", err)
return nil, err
}

Expand Down
75 changes: 75 additions & 0 deletions water/downloader.go
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)
}
}
143 changes: 143 additions & 0 deletions water/downloader_test.go
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)
})
}
}
44 changes: 44 additions & 0 deletions water/https_downloader.go
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
}
Loading

0 comments on commit 9a68fb2

Please sign in to comment.