2d721baa91
These two small packages could easily be merged into the model package, since they're clearly model-like packages. Part of https://github.com/ooni/probe/issues/2115
129 lines
3.7 KiB
Go
129 lines
3.7 KiB
Go
package urlgetter
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/cookiejar"
|
|
"net/url"
|
|
|
|
"github.com/ooni/probe-cli/v3/internal/engine/netx"
|
|
"github.com/ooni/probe-cli/v3/internal/model"
|
|
"github.com/ooni/probe-cli/v3/internal/netxlite"
|
|
"github.com/ooni/probe-cli/v3/internal/runtimex"
|
|
)
|
|
|
|
const httpRequestFailed = "http_request_failed"
|
|
|
|
// ErrHTTPRequestFailed indicates that the HTTP request failed.
|
|
var ErrHTTPRequestFailed = &netxlite.ErrWrapper{
|
|
Failure: httpRequestFailed,
|
|
Operation: netxlite.TopLevelOperation,
|
|
WrappedErr: errors.New(httpRequestFailed),
|
|
}
|
|
|
|
// The Runner job is to run a single measurement
|
|
type Runner struct {
|
|
Config Config
|
|
HTTPConfig netx.Config
|
|
Target string
|
|
}
|
|
|
|
// Run runs a measurement and returns the measurement result
|
|
func (r Runner) Run(ctx context.Context) error {
|
|
targetURL, err := url.Parse(r.Target)
|
|
if err != nil {
|
|
return fmt.Errorf("urlgetter: invalid target URL: %w", err)
|
|
}
|
|
switch targetURL.Scheme {
|
|
case "http", "https":
|
|
return r.httpGet(ctx, r.Target)
|
|
case "dnslookup":
|
|
return r.dnsLookup(ctx, targetURL.Hostname())
|
|
case "tlshandshake":
|
|
return r.tlsHandshake(ctx, targetURL.Host)
|
|
case "tcpconnect":
|
|
return r.tcpConnect(ctx, targetURL.Host)
|
|
default:
|
|
return errors.New("unknown targetURL scheme")
|
|
}
|
|
}
|
|
|
|
// MaybeUserAgent returns ua if ua is not empty. Otherwise it
|
|
// returns httpheader.RandomUserAgent().
|
|
func MaybeUserAgent(ua string) string {
|
|
if ua == "" {
|
|
ua = model.HTTPHeaderUserAgent
|
|
}
|
|
return ua
|
|
}
|
|
|
|
func (r Runner) httpGet(ctx context.Context, url string) error {
|
|
// Implementation note: empty Method implies using the GET method
|
|
req, err := http.NewRequest(r.Config.Method, url, nil)
|
|
runtimex.PanicOnError(err, "http.NewRequest failed")
|
|
req = req.WithContext(ctx)
|
|
req.Header.Set("Accept", model.HTTPHeaderAccept)
|
|
req.Header.Set("Accept-Language", model.HTTPHeaderAcceptLanguage)
|
|
req.Header.Set("User-Agent", MaybeUserAgent(r.Config.UserAgent))
|
|
if r.Config.HTTPHost != "" {
|
|
req.Host = r.Config.HTTPHost
|
|
}
|
|
// Implementation note: the following cookiejar accepts all cookies
|
|
// from all domains. As such, would not be safe for usage where cookies
|
|
// matter, but it's totally fine for performing measurements.
|
|
jar, err := cookiejar.New(nil)
|
|
runtimex.PanicOnError(err, "cookiejar.New failed")
|
|
httpClient := &http.Client{
|
|
Jar: jar,
|
|
Transport: netx.NewHTTPTransport(r.HTTPConfig),
|
|
}
|
|
if r.Config.NoFollowRedirects {
|
|
httpClient.CheckRedirect = func(*http.Request, []*http.Request) error {
|
|
return http.ErrUseLastResponse
|
|
}
|
|
}
|
|
defer httpClient.CloseIdleConnections()
|
|
resp, err := httpClient.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
if _, err = netxlite.CopyContext(ctx, io.Discard, resp.Body); err != nil {
|
|
return err
|
|
}
|
|
// Implementation note: we shall check for this error once we have read the
|
|
// whole body. Even though we discard the body, we want to know whether we
|
|
// see any error when reading the body before inspecting the HTTP status code.
|
|
if resp.StatusCode >= 400 && r.Config.FailOnHTTPError {
|
|
return ErrHTTPRequestFailed
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r Runner) dnsLookup(ctx context.Context, hostname string) error {
|
|
resolver := netx.NewResolver(r.HTTPConfig)
|
|
_, err := resolver.LookupHost(ctx, hostname)
|
|
return err
|
|
}
|
|
|
|
func (r Runner) tlsHandshake(ctx context.Context, address string) error {
|
|
tlsDialer := netx.NewTLSDialer(r.HTTPConfig)
|
|
conn, err := tlsDialer.DialTLSContext(ctx, "tcp", address)
|
|
if conn != nil {
|
|
conn.Close()
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (r Runner) tcpConnect(ctx context.Context, address string) error {
|
|
dialer := netx.NewDialer(r.HTTPConfig)
|
|
conn, err := dialer.DialContext(ctx, "tcp", address)
|
|
if conn != nil {
|
|
conn.Close()
|
|
}
|
|
return err
|
|
}
|