ooni-probe-cli/internal/cmd/oohelper/internal/client.go
Simone Basso bb6563f363
refactor: move TH structs and definitions to model (#894)
This commit moves the TH structs and definitions to model. We don't want
oohelperd to depend on web_connectivity@v0.4.

Part of https://github.com/ooni/probe/issues/2240
2022-08-28 20:20:12 +02:00

142 lines
4.0 KiB
Go

package internal
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net"
"net/http"
"net/url"
"github.com/apex/log"
"github.com/ooni/probe-cli/v3/internal/model"
"github.com/ooni/probe-cli/v3/internal/netxlite"
"github.com/ooni/probe-cli/v3/internal/runtimex"
"github.com/ooni/probe-cli/v3/internal/version"
)
type (
// CtrlResponse is the type of response returned by the test helper.
CtrlResponse = model.THResponse
// ctrlRequest is the type of the request sent to the test helper.
ctrlRequest = model.THRequest
)
// The following errors may be returned by this implementation.
var (
ErrHTTPStatusCode = errors.New("oohelper: http status code indicates failure")
ErrUnsupportedURLScheme = errors.New("oohelper: unsupported URL scheme")
ErrUnsupportedExplicitPort = errors.New("oohelper: unsupported explicit port")
ErrEmptyURL = errors.New("oohelper: empty server and/or target URL")
ErrInvalidURL = errors.New("oohelper: cannot parse URL")
ErrCannotCreateRequest = errors.New("oohelper: cannot create HTTP request")
ErrCannotParseJSONReply = errors.New("oohelper: cannot parse JSON reply")
)
// Resolver resolves domain names.
type Resolver interface {
LookupHost(ctx context.Context, hostname string) ([]string, error)
}
// OOClient is a client for the OONI Web Connectivity test helper.
type OOClient struct {
// HTTPClient is the HTTP client to use.
HTTPClient model.HTTPClient
// Resolver is the resolver to user.
Resolver Resolver
}
// OOConfig contains configuration for the client.
type OOConfig struct {
// ServerURL is the URL of the test helper server.
ServerURL string
// TargetURL is the URL that we want to measure.
TargetURL string
}
// MakeTCPEndpoints constructs the list of TCP endpoints to send
// to the Web Connectivity test helper.
func MakeTCPEndpoints(URL *url.URL, addrs []string) ([]string, error) {
var (
port string
out []string
)
if URL.Host != URL.Hostname() {
return nil, ErrUnsupportedExplicitPort
}
switch URL.Scheme {
case "https":
port = "443"
case "http":
port = "80"
default:
return nil, ErrUnsupportedURLScheme
}
for _, addr := range addrs {
out = append(out, net.JoinHostPort(addr, port))
}
return out, nil
}
// Do sends a measurement request to the Web Connectivity test
// helper and receives the corresponding response.
func (oo OOClient) Do(ctx context.Context, config OOConfig) (*CtrlResponse, error) {
if config.TargetURL == "" || config.ServerURL == "" {
return nil, ErrEmptyURL
}
targetURL, err := url.Parse(config.TargetURL)
if err != nil {
return nil, fmt.Errorf("%w: %s", ErrInvalidURL, err.Error())
}
addrs, err := oo.Resolver.LookupHost(ctx, targetURL.Hostname())
endpoints := []string{}
if err == nil {
endpoints, err = MakeTCPEndpoints(targetURL, addrs)
if err != nil {
return nil, err
}
}
creq := ctrlRequest{
HTTPRequest: config.TargetURL,
HTTPRequestHeaders: map[string][]string{
"Accept": {model.HTTPHeaderAccept},
"Accept-Language": {model.HTTPHeaderAcceptLanguage},
"User-Agent": {model.HTTPHeaderUserAgent},
},
TCPConnect: endpoints,
}
data, err := json.Marshal(creq)
runtimex.PanicOnError(err, "oohelper: cannot marshal control request")
log.Debugf("out: %s", string(data))
req, err := http.NewRequestWithContext(ctx, "POST", config.ServerURL, bytes.NewReader(data))
if err != nil {
return nil, fmt.Errorf("%w: %s", ErrCannotCreateRequest, err.Error())
}
req.Header.Add("user-agent", fmt.Sprintf(
"oohelper/%s ooniprobe-engine/%s", version.Version, version.Version,
))
req.Header.Add("content-type", "application/json")
resp, err := oo.HTTPClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, ErrHTTPStatusCode
}
data, err = netxlite.ReadAllContext(ctx, resp.Body)
if err != nil {
return nil, err
}
var cresp CtrlResponse
if err := json.Unmarshal(data, &cresp); err != nil {
return nil, fmt.Errorf("%w: %s", ErrCannotParseJSONReply, err.Error())
}
return &cresp, nil
}