refactor: move more commands to internal/cmd (#207)
* refactor: move more commands to internal/cmd Part of https://github.com/ooni/probe/issues/1335. We would like all commands to be at the same level of engine rather than inside engine (now that we can do it). * fix: update .gitignore * refactor: also move jafar outside engine * We should be good now?
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
# oohelperd
|
||||
|
||||
This directory contains the source code of the Web
|
||||
Connectivity test helper written in Go.
|
||||
@@ -0,0 +1,32 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/webconnectivity"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/netx"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/netx/archival"
|
||||
)
|
||||
|
||||
// newfailure is a convenience shortcut to save typing
|
||||
var newfailure = archival.NewFailure
|
||||
|
||||
// CtrlDNSResult is the result of the DNS check performed by
|
||||
// the Web Connectivity test helper.
|
||||
type CtrlDNSResult = webconnectivity.ControlDNSResult
|
||||
|
||||
// DNSConfig configures the DNS check.
|
||||
type DNSConfig struct {
|
||||
Domain string
|
||||
Out chan CtrlDNSResult
|
||||
Resolver netx.Resolver
|
||||
Wg *sync.WaitGroup
|
||||
}
|
||||
|
||||
// DNSDo performs the DNS check.
|
||||
func DNSDo(ctx context.Context, config *DNSConfig) {
|
||||
defer config.Wg.Done()
|
||||
addrs, err := config.Resolver.LookupHost(ctx, config.Domain)
|
||||
config.Out <- CtrlDNSResult{Failure: newfailure(err), Addrs: addrs}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/atomicx"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/netx"
|
||||
)
|
||||
|
||||
type FakeResolver struct {
|
||||
NumFailures *atomicx.Int64
|
||||
Err error
|
||||
Result []string
|
||||
}
|
||||
|
||||
func NewFakeResolverThatFails() FakeResolver {
|
||||
return FakeResolver{NumFailures: atomicx.NewInt64(), Err: ErrNotFound}
|
||||
}
|
||||
|
||||
func NewFakeResolverWithResult(r []string) FakeResolver {
|
||||
return FakeResolver{NumFailures: atomicx.NewInt64(), Result: r}
|
||||
}
|
||||
|
||||
var ErrNotFound = &net.DNSError{
|
||||
Err: "no such host",
|
||||
}
|
||||
|
||||
func (c FakeResolver) LookupHost(ctx context.Context, hostname string) ([]string, error) {
|
||||
time.Sleep(10 * time.Microsecond)
|
||||
if c.Err != nil {
|
||||
if c.NumFailures != nil {
|
||||
c.NumFailures.Add(1)
|
||||
}
|
||||
return nil, c.Err
|
||||
}
|
||||
return c.Result, nil
|
||||
}
|
||||
|
||||
func (c FakeResolver) Network() string {
|
||||
return "fake"
|
||||
}
|
||||
|
||||
func (c FakeResolver) Address() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
var _ netx.Resolver = FakeResolver{}
|
||||
|
||||
type FakeTransport struct {
|
||||
Err error
|
||||
Func func(*http.Request) (*http.Response, error)
|
||||
Resp *http.Response
|
||||
}
|
||||
|
||||
func (txp FakeTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
time.Sleep(10 * time.Microsecond)
|
||||
if txp.Func != nil {
|
||||
return txp.Func(req)
|
||||
}
|
||||
if req.Body != nil {
|
||||
ioutil.ReadAll(req.Body)
|
||||
req.Body.Close()
|
||||
}
|
||||
if txp.Err != nil {
|
||||
return nil, txp.Err
|
||||
}
|
||||
txp.Resp.Request = req // non thread safe but it doesn't matter
|
||||
return txp.Resp, nil
|
||||
}
|
||||
|
||||
func (txp FakeTransport) CloseIdleConnections() {}
|
||||
|
||||
var _ netx.HTTPRoundTripper = FakeTransport{}
|
||||
|
||||
type FakeBody struct {
|
||||
Data []byte
|
||||
Err error
|
||||
}
|
||||
|
||||
func (fb *FakeBody) Read(p []byte) (int, error) {
|
||||
time.Sleep(10 * time.Microsecond)
|
||||
if fb.Err != nil {
|
||||
return 0, fb.Err
|
||||
}
|
||||
if len(fb.Data) <= 0 {
|
||||
return 0, io.EOF
|
||||
}
|
||||
n := copy(p, fb.Data)
|
||||
fb.Data = fb.Data[n:]
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (fb *FakeBody) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ io.ReadCloser = &FakeBody{}
|
||||
|
||||
type FakeResponseWriter struct {
|
||||
Body [][]byte
|
||||
HeaderMap http.Header
|
||||
StatusCode int
|
||||
}
|
||||
|
||||
func NewFakeResponseWriter() *FakeResponseWriter {
|
||||
return &FakeResponseWriter{HeaderMap: make(http.Header)}
|
||||
}
|
||||
|
||||
func (frw *FakeResponseWriter) Header() http.Header {
|
||||
return frw.HeaderMap
|
||||
}
|
||||
|
||||
func (frw *FakeResponseWriter) Write(b []byte) (int, error) {
|
||||
frw.Body = append(frw.Body, b)
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
func (frw *FakeResponseWriter) WriteHeader(statusCode int) {
|
||||
frw.StatusCode = statusCode
|
||||
}
|
||||
|
||||
var _ http.ResponseWriter = &FakeResponseWriter{}
|
||||
@@ -0,0 +1,67 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/webconnectivity"
|
||||
)
|
||||
|
||||
// CtrlHTTPResponse is the result of the HTTP check performed by
|
||||
// the Web Connectivity test helper.
|
||||
type CtrlHTTPResponse = webconnectivity.ControlHTTPRequestResult
|
||||
|
||||
// HTTPConfig configures the HTTP check.
|
||||
type HTTPConfig struct {
|
||||
Client *http.Client
|
||||
Headers map[string][]string
|
||||
MaxAcceptableBody int64
|
||||
Out chan CtrlHTTPResponse
|
||||
URL string
|
||||
Wg *sync.WaitGroup
|
||||
}
|
||||
|
||||
// HTTPDo performs the HTTP check.
|
||||
func HTTPDo(ctx context.Context, config *HTTPConfig) {
|
||||
defer config.Wg.Done()
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", config.URL, nil)
|
||||
if err != nil {
|
||||
config.Out <- CtrlHTTPResponse{Failure: newfailure(err)}
|
||||
return
|
||||
}
|
||||
// The original test helper failed with extra headers while here
|
||||
// we're implementing (for now?) a more liberal approach.
|
||||
for k, vs := range config.Headers {
|
||||
switch strings.ToLower(k) {
|
||||
case "user-agent":
|
||||
case "accept":
|
||||
case "accept-language":
|
||||
for _, v := range vs {
|
||||
req.Header.Add(k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
resp, err := config.Client.Do(req)
|
||||
if err != nil {
|
||||
config.Out <- CtrlHTTPResponse{Failure: newfailure(err)}
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
headers := make(map[string]string)
|
||||
for k := range resp.Header {
|
||||
headers[k] = resp.Header.Get(k)
|
||||
}
|
||||
reader := &io.LimitedReader{R: resp.Body, N: config.MaxAcceptableBody}
|
||||
data, err := ioutil.ReadAll(reader)
|
||||
config.Out <- CtrlHTTPResponse{
|
||||
BodyLength: int64(len(data)),
|
||||
Failure: newfailure(err),
|
||||
StatusCode: int64(resp.StatusCode),
|
||||
Headers: headers,
|
||||
Title: webconnectivity.GetTitle(string(data)),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package internal_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/ooni/probe-cli/v3/internal/cmd/oohelperd/internal"
|
||||
)
|
||||
|
||||
func TestHTTPDoWithInvalidURL(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
wg := new(sync.WaitGroup)
|
||||
httpch := make(chan internal.CtrlHTTPResponse, 1)
|
||||
wg.Add(1)
|
||||
go internal.HTTPDo(ctx, &internal.HTTPConfig{
|
||||
Client: http.DefaultClient,
|
||||
Headers: nil,
|
||||
MaxAcceptableBody: 1 << 24,
|
||||
Out: httpch,
|
||||
URL: "http://[::1]aaaa",
|
||||
Wg: wg,
|
||||
})
|
||||
// wait for measurement steps to complete
|
||||
wg.Wait()
|
||||
resp := <-httpch
|
||||
if resp.Failure == nil || !strings.HasSuffix(*resp.Failure, `invalid port "aaaa" after host`) {
|
||||
t.Fatal("not the failure we expected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPDoWithHTTPTransportFailure(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
ctx := context.Background()
|
||||
wg := new(sync.WaitGroup)
|
||||
httpch := make(chan internal.CtrlHTTPResponse, 1)
|
||||
wg.Add(1)
|
||||
go internal.HTTPDo(ctx, &internal.HTTPConfig{
|
||||
Client: &http.Client{
|
||||
Transport: internal.FakeTransport{
|
||||
Err: expected,
|
||||
},
|
||||
},
|
||||
Headers: nil,
|
||||
MaxAcceptableBody: 1 << 24,
|
||||
Out: httpch,
|
||||
URL: "http://www.x.org",
|
||||
Wg: wg,
|
||||
})
|
||||
// wait for measurement steps to complete
|
||||
wg.Wait()
|
||||
resp := <-httpch
|
||||
if resp.Failure == nil || !strings.HasSuffix(*resp.Failure, "mocked error") {
|
||||
t.Fatal("not the error we expected")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/netx"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/version"
|
||||
)
|
||||
|
||||
// Handler implements the Web Connectivity test helper HTTP API.
|
||||
type Handler struct {
|
||||
Client *http.Client
|
||||
Dialer netx.Dialer
|
||||
MaxAcceptableBody int64
|
||||
Resolver netx.Resolver
|
||||
}
|
||||
|
||||
func (h Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
w.Header().Add("Server", fmt.Sprintf(
|
||||
"oohelperd/%s ooniprobe-engine/%s", version.Version, version.Version,
|
||||
))
|
||||
if req.Method != "POST" {
|
||||
w.WriteHeader(400)
|
||||
return
|
||||
}
|
||||
if req.Header.Get("content-type") != "application/json" {
|
||||
w.WriteHeader(400)
|
||||
return
|
||||
}
|
||||
reader := &io.LimitedReader{R: req.Body, N: h.MaxAcceptableBody}
|
||||
data, err := ioutil.ReadAll(reader)
|
||||
if err != nil {
|
||||
w.WriteHeader(400)
|
||||
return
|
||||
}
|
||||
var creq CtrlRequest
|
||||
if err := json.Unmarshal(data, &creq); err != nil {
|
||||
w.WriteHeader(400)
|
||||
return
|
||||
}
|
||||
measureConfig := MeasureConfig{
|
||||
Client: h.Client,
|
||||
Dialer: h.Dialer,
|
||||
MaxAcceptableBody: h.MaxAcceptableBody,
|
||||
Resolver: h.Resolver,
|
||||
}
|
||||
cresp, err := Measure(req.Context(), measureConfig, &creq)
|
||||
if err != nil {
|
||||
w.WriteHeader(400)
|
||||
return
|
||||
}
|
||||
// We assume that the following call cannot fail because it's a
|
||||
// clearly serializable data structure.
|
||||
data, _ = json.Marshal(cresp)
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
w.Write(data)
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package internal_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/ooni/probe-cli/v3/internal/cmd/oohelperd/internal"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/netx/resolver"
|
||||
)
|
||||
|
||||
const simplerequest = `{
|
||||
"http_request": "https://dns.google",
|
||||
"http_request_headers": {
|
||||
"Accept": [
|
||||
"*/*"
|
||||
],
|
||||
"Accept-Language": [
|
||||
"en-US;q=0.8,en;q=0.5"
|
||||
],
|
||||
"User-Agent": [
|
||||
"Mozilla/5.0"
|
||||
]
|
||||
},
|
||||
"tcp_connect": [
|
||||
"8.8.8.8:443"
|
||||
]
|
||||
}`
|
||||
|
||||
const requestWithoutDomainName = `{
|
||||
"http_request": "https://8.8.8.8",
|
||||
"http_request_headers": {
|
||||
"Accept": [
|
||||
"*/*"
|
||||
],
|
||||
"Accept-Language": [
|
||||
"en-US;q=0.8,en;q=0.5"
|
||||
],
|
||||
"User-Agent": [
|
||||
"Mozilla/5.0"
|
||||
]
|
||||
},
|
||||
"tcp_connect": [
|
||||
"8.8.8.8:443"
|
||||
]
|
||||
}`
|
||||
|
||||
func TestWorkingAsIntended(t *testing.T) {
|
||||
handler := internal.Handler{
|
||||
Client: http.DefaultClient,
|
||||
Dialer: new(net.Dialer),
|
||||
MaxAcceptableBody: 1 << 24,
|
||||
Resolver: resolver.SystemResolver{},
|
||||
}
|
||||
srv := httptest.NewServer(handler)
|
||||
defer srv.Close()
|
||||
type expectationSpec struct {
|
||||
name string
|
||||
reqMethod string
|
||||
reqContentType string
|
||||
reqBody string
|
||||
respStatusCode int
|
||||
respContentType string
|
||||
parseBody bool
|
||||
}
|
||||
expectations := []expectationSpec{{
|
||||
name: "check for invalid method",
|
||||
reqMethod: "GET",
|
||||
respStatusCode: 400,
|
||||
}, {
|
||||
name: "check for invalid content-type",
|
||||
reqMethod: "POST",
|
||||
respStatusCode: 400,
|
||||
}, {
|
||||
name: "check for invalid request body",
|
||||
reqMethod: "POST",
|
||||
reqContentType: "application/json",
|
||||
reqBody: "{",
|
||||
respStatusCode: 400,
|
||||
}, {
|
||||
name: "with measurement failure",
|
||||
reqMethod: "POST",
|
||||
reqContentType: "application/json",
|
||||
reqBody: `{"http_request": "http://[::1]aaaa"}`,
|
||||
respStatusCode: 400,
|
||||
}, {
|
||||
name: "with reasonably good request",
|
||||
reqMethod: "POST",
|
||||
reqContentType: "application/json",
|
||||
reqBody: simplerequest,
|
||||
respStatusCode: 200,
|
||||
respContentType: "application/json",
|
||||
parseBody: true,
|
||||
}, {
|
||||
name: "when there's no domain name in the request",
|
||||
reqMethod: "POST",
|
||||
reqContentType: "application/json",
|
||||
reqBody: requestWithoutDomainName,
|
||||
respStatusCode: 200,
|
||||
respContentType: "application/json",
|
||||
parseBody: true,
|
||||
}}
|
||||
for _, expect := range expectations {
|
||||
t.Run(expect.name, func(t *testing.T) {
|
||||
body := strings.NewReader(expect.reqBody)
|
||||
req, err := http.NewRequest(expect.reqMethod, srv.URL, body)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %+v", expect.name, err)
|
||||
}
|
||||
if expect.reqContentType != "" {
|
||||
req.Header.Add("content-type", expect.reqContentType)
|
||||
}
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %+v", expect.name, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != expect.respStatusCode {
|
||||
t.Fatalf("unexpected status code: %+v", resp.StatusCode)
|
||||
}
|
||||
if v := resp.Header.Get("content-type"); v != expect.respContentType {
|
||||
t.Fatalf("unexpected content-type: %s", v)
|
||||
}
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !expect.parseBody {
|
||||
return
|
||||
}
|
||||
var v interface{}
|
||||
if err := json.Unmarshal(data, &v); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandlerWithRequestBodyReadingError(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
handler := internal.Handler{MaxAcceptableBody: 1 << 24}
|
||||
rw := internal.NewFakeResponseWriter()
|
||||
req := &http.Request{
|
||||
Method: "POST",
|
||||
Header: map[string][]string{
|
||||
"Content-Type": {"application/json"},
|
||||
"Content-Length": {"2048"},
|
||||
},
|
||||
Body: &internal.FakeBody{Err: expected},
|
||||
}
|
||||
handler.ServeHTTP(rw, req)
|
||||
if rw.StatusCode != 400 {
|
||||
t.Fatal("unexpected status code")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/webconnectivity"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/netx"
|
||||
)
|
||||
|
||||
type (
|
||||
// CtrlRequest is the request sent to the test helper
|
||||
CtrlRequest = webconnectivity.ControlRequest
|
||||
|
||||
// CtrlResponse is the response from the test helper
|
||||
CtrlResponse = webconnectivity.ControlResponse
|
||||
)
|
||||
|
||||
// MeasureConfig contains configuration for Measure.
|
||||
type MeasureConfig struct {
|
||||
Client *http.Client
|
||||
Dialer netx.Dialer
|
||||
MaxAcceptableBody int64
|
||||
Resolver netx.Resolver
|
||||
}
|
||||
|
||||
// Measure performs the measurement described by the request and
|
||||
// returns the corresponding response or an error.
|
||||
func Measure(ctx context.Context, config MeasureConfig, creq *CtrlRequest) (*CtrlResponse, error) {
|
||||
// parse input for correctness
|
||||
URL, err := url.Parse(creq.HTTPRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// dns: start
|
||||
wg := new(sync.WaitGroup)
|
||||
dnsch := make(chan CtrlDNSResult, 1)
|
||||
if net.ParseIP(URL.Hostname()) == nil {
|
||||
wg.Add(1)
|
||||
go DNSDo(ctx, &DNSConfig{
|
||||
Domain: URL.Hostname(),
|
||||
Out: dnsch,
|
||||
Resolver: config.Resolver,
|
||||
Wg: wg,
|
||||
})
|
||||
}
|
||||
// tcpconnect: start
|
||||
tcpconnch := make(chan TCPResultPair, len(creq.TCPConnect))
|
||||
for _, endpoint := range creq.TCPConnect {
|
||||
wg.Add(1)
|
||||
go TCPDo(ctx, &TCPConfig{
|
||||
Dialer: config.Dialer,
|
||||
Endpoint: endpoint,
|
||||
Out: tcpconnch,
|
||||
Wg: wg,
|
||||
})
|
||||
}
|
||||
// http: start
|
||||
httpch := make(chan CtrlHTTPResponse, 1)
|
||||
wg.Add(1)
|
||||
go HTTPDo(ctx, &HTTPConfig{
|
||||
Client: config.Client,
|
||||
Headers: creq.HTTPRequestHeaders,
|
||||
MaxAcceptableBody: config.MaxAcceptableBody,
|
||||
Out: httpch,
|
||||
URL: creq.HTTPRequest,
|
||||
Wg: wg,
|
||||
})
|
||||
// wait for measurement steps to complete
|
||||
wg.Wait()
|
||||
// assemble response
|
||||
cresp := new(CtrlResponse)
|
||||
select {
|
||||
case cresp.DNS = <-dnsch:
|
||||
default:
|
||||
// we land here when there's no domain name
|
||||
}
|
||||
cresp.HTTPRequest = <-httpch
|
||||
cresp.TCPConnect = make(map[string]CtrlTCPResult)
|
||||
for len(cresp.TCPConnect) < len(creq.TCPConnect) {
|
||||
tcpconn := <-tcpconnch
|
||||
cresp.TCPConnect[tcpconn.Endpoint] = tcpconn.Result
|
||||
}
|
||||
return cresp, nil
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/webconnectivity"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/netx"
|
||||
)
|
||||
|
||||
// CtrlTCPResult is the result of the TCP check performed by the test helper.
|
||||
type CtrlTCPResult = webconnectivity.ControlTCPConnectResult
|
||||
|
||||
// TCPResultPair contains the endpoint and the corresponding result.
|
||||
type TCPResultPair struct {
|
||||
Endpoint string
|
||||
Result CtrlTCPResult
|
||||
}
|
||||
|
||||
// TCPConfig configures the TCP connect check.
|
||||
type TCPConfig struct {
|
||||
Dialer netx.Dialer
|
||||
Endpoint string
|
||||
Out chan TCPResultPair
|
||||
Wg *sync.WaitGroup
|
||||
}
|
||||
|
||||
// TCPDo performs the TCP check.
|
||||
func TCPDo(ctx context.Context, config *TCPConfig) {
|
||||
defer config.Wg.Done()
|
||||
conn, err := config.Dialer.DialContext(ctx, "tcp", config.Endpoint)
|
||||
if conn != nil {
|
||||
conn.Close()
|
||||
}
|
||||
config.Out <- TCPResultPair{
|
||||
Endpoint: config.Endpoint,
|
||||
Result: CtrlTCPResult{
|
||||
Failure: newfailure(err),
|
||||
Status: err == nil,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
// Command oohelperd contains the Web Connectivity test helper.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/apex/log"
|
||||
"github.com/ooni/probe-cli/v3/internal/cmd/oohelperd/internal"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/netx"
|
||||
)
|
||||
|
||||
const maxAcceptableBody = 1 << 24
|
||||
|
||||
var (
|
||||
dialer netx.Dialer
|
||||
endpoint = flag.String("endpoint", ":8080", "Endpoint where to listen")
|
||||
httpx *http.Client
|
||||
resolver netx.Resolver
|
||||
srvcancel context.CancelFunc
|
||||
srvctx context.Context
|
||||
srvwg = new(sync.WaitGroup)
|
||||
)
|
||||
|
||||
func init() {
|
||||
srvctx, srvcancel = context.WithCancel(context.Background())
|
||||
dialer = netx.NewDialer(netx.Config{Logger: log.Log})
|
||||
txp := netx.NewHTTPTransport(netx.Config{Logger: log.Log})
|
||||
httpx = &http.Client{Transport: txp}
|
||||
resolver = netx.NewResolver(netx.Config{Logger: log.Log})
|
||||
}
|
||||
|
||||
func shutdown(srv *http.Server) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
srv.Shutdown(ctx)
|
||||
}
|
||||
|
||||
func main() {
|
||||
logmap := map[bool]log.Level{
|
||||
true: log.DebugLevel,
|
||||
false: log.InfoLevel,
|
||||
}
|
||||
debug := flag.Bool("debug", false, "Toggle debug mode")
|
||||
flag.Parse()
|
||||
log.SetLevel(logmap[*debug])
|
||||
testableMain()
|
||||
}
|
||||
|
||||
func testableMain() {
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/", internal.Handler{
|
||||
Client: httpx,
|
||||
Dialer: dialer,
|
||||
MaxAcceptableBody: maxAcceptableBody,
|
||||
Resolver: resolver,
|
||||
})
|
||||
srv := &http.Server{Addr: *endpoint, Handler: mux}
|
||||
srvwg.Add(1)
|
||||
go srv.ListenAndServe()
|
||||
<-srvctx.Done()
|
||||
shutdown(srv)
|
||||
srvwg.Done()
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSmoke(t *testing.T) {
|
||||
// Just check whether we can start and then tear down the server, so
|
||||
// we have coverage of this code and when we see that some lines aren't
|
||||
// covered we know these are genuine places where we're not testing
|
||||
// the code rather than just places like this simple main.
|
||||
go testableMain()
|
||||
srvcancel() // kills the listener
|
||||
srvwg.Wait() // joined
|
||||
}
|
||||
Reference in New Issue
Block a user