chore: merge probe-engine into probe-cli (#201)
This is how I did it: 1. `git clone https://github.com/ooni/probe-engine internal/engine` 2. ``` (cd internal/engine && git describe --tags) v0.23.0 ``` 3. `nvim go.mod` (merging `go.mod` with `internal/engine/go.mod` 4. `rm -rf internal/.git internal/engine/go.{mod,sum}` 5. `git add internal/engine` 6. `find . -type f -name \*.go -exec sed -i 's@/ooni/probe-engine@/ooni/probe-cli/v3/internal/engine@g' {} \;` 7. `go build ./...` (passes) 8. `go test -race ./...` (temporary failure on RiseupVPN) 9. `go mod tidy` 10. this commit message Once this piece of work is done, we can build a new version of `ooniprobe` that is using `internal/engine` directly. We need to do more work to ensure all the other functionality in `probe-engine` (e.g. making mobile packages) are still WAI. Part of https://github.com/ooni/probe/issues/1335
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
# Package github.com/ooni/probe-engine/geolocate
|
||||
|
||||
Package geolocate implements IP lookup, resolver lookup, and geolocation.
|
||||
@@ -0,0 +1,31 @@
|
||||
package geolocate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/internal/httpx"
|
||||
)
|
||||
|
||||
type avastResponse struct {
|
||||
IP string `json:"ip"`
|
||||
}
|
||||
|
||||
func avastIPLookup(
|
||||
ctx context.Context,
|
||||
httpClient *http.Client,
|
||||
logger Logger,
|
||||
userAgent string,
|
||||
) (string, error) {
|
||||
var v avastResponse
|
||||
err := (httpx.Client{
|
||||
BaseURL: "https://ip-info.ff.avast.com",
|
||||
HTTPClient: httpClient,
|
||||
Logger: logger,
|
||||
UserAgent: userAgent,
|
||||
}).GetJSON(ctx, "/v1/info", &v)
|
||||
if err != nil {
|
||||
return DefaultProbeIP, err
|
||||
}
|
||||
return v.IP, nil
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package geolocate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/apex/log"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/internal/httpheader"
|
||||
)
|
||||
|
||||
func TestIPLookupWorksUsingAvast(t *testing.T) {
|
||||
ip, err := avastIPLookup(
|
||||
context.Background(),
|
||||
http.DefaultClient,
|
||||
log.Log,
|
||||
httpheader.UserAgent(),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if net.ParseIP(ip) == nil {
|
||||
t.Fatalf("not an IP address: '%s'", ip)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package geolocate
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type FakeTransport struct {
|
||||
Err error
|
||||
Resp *http.Response
|
||||
}
|
||||
|
||||
func (txp FakeTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
time.Sleep(10 * time.Microsecond)
|
||||
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() {}
|
||||
@@ -0,0 +1,242 @@
|
||||
// Package geolocate implements IP lookup, resolver lookup, and geolocation.
|
||||
package geolocate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/internal/runtimex"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/model"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/version"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultProbeASN is the default probe ASN as number.
|
||||
DefaultProbeASN uint = 0
|
||||
|
||||
// DefaultProbeCC is the default probe CC.
|
||||
DefaultProbeCC = "ZZ"
|
||||
|
||||
// DefaultProbeIP is the default probe IP.
|
||||
DefaultProbeIP = model.DefaultProbeIP
|
||||
|
||||
// DefaultProbeNetworkName is the default probe network name.
|
||||
DefaultProbeNetworkName = ""
|
||||
|
||||
// DefaultResolverASN is the default resolver ASN.
|
||||
DefaultResolverASN uint = 0
|
||||
|
||||
// DefaultResolverIP is the default resolver IP.
|
||||
DefaultResolverIP = "127.0.0.2"
|
||||
|
||||
// DefaultResolverNetworkName is the default resolver network name.
|
||||
DefaultResolverNetworkName = ""
|
||||
)
|
||||
|
||||
var (
|
||||
// DefaultProbeASNString is the default probe ASN as a string.
|
||||
DefaultProbeASNString = fmt.Sprintf("AS%d", DefaultProbeASN)
|
||||
|
||||
// DefaultResolverASNString is the default resolver ASN as a string.
|
||||
DefaultResolverASNString = fmt.Sprintf("AS%d", DefaultResolverASN)
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrMissingResourcesManager indicates that no resources
|
||||
// manager has been configured inside of Config.
|
||||
ErrMissingResourcesManager = errors.New("geolocate: ResourcesManager is nil")
|
||||
)
|
||||
|
||||
// Logger is the definition of Logger used by this package.
|
||||
type Logger interface {
|
||||
Debugf(format string, v ...interface{})
|
||||
}
|
||||
|
||||
// Results contains geolocate results
|
||||
type Results struct {
|
||||
// ASN is the autonomous system number
|
||||
ASN uint
|
||||
|
||||
// CountryCode is the country code
|
||||
CountryCode string
|
||||
|
||||
// DidResolverLookup indicates whether we did a resolver lookup.
|
||||
DidResolverLookup bool
|
||||
|
||||
// NetworkName is the network name
|
||||
NetworkName string
|
||||
|
||||
// IP is the probe IP
|
||||
ProbeIP string
|
||||
|
||||
// ResolverASN is the resolver ASN
|
||||
ResolverASN uint
|
||||
|
||||
// ResolverIP is the resolver IP
|
||||
ResolverIP string
|
||||
|
||||
// ResolverNetworkName is the resolver network name
|
||||
ResolverNetworkName string
|
||||
}
|
||||
|
||||
// ASNString returns the ASN as a string
|
||||
func (r *Results) ASNString() string {
|
||||
return fmt.Sprintf("AS%d", r.ASN)
|
||||
}
|
||||
|
||||
type probeIPLookupper interface {
|
||||
LookupProbeIP(ctx context.Context) (addr string, err error)
|
||||
}
|
||||
|
||||
type asnLookupper interface {
|
||||
LookupASN(path string, ip string) (asn uint, network string, err error)
|
||||
}
|
||||
|
||||
type countryLookupper interface {
|
||||
LookupCC(path string, ip string) (cc string, err error)
|
||||
}
|
||||
|
||||
type resolverIPLookupper interface {
|
||||
LookupResolverIP(ctx context.Context) (addr string, err error)
|
||||
}
|
||||
|
||||
// ResourcesManager manages the required resources.
|
||||
type ResourcesManager interface {
|
||||
// ASNDatabasePath returns the path of the ASN database.
|
||||
ASNDatabasePath() string
|
||||
|
||||
// CountryDatabasePath returns the path of the country database.
|
||||
CountryDatabasePath() string
|
||||
|
||||
// MaybeUpdateResources ensures that the required resources
|
||||
// have been downloaded and are current.
|
||||
MaybeUpdateResources(ctx context.Context) error
|
||||
}
|
||||
|
||||
// Config contains configuration for a geolocate Task.
|
||||
type Config struct {
|
||||
// EnableResolverLookup indicates whether we want to
|
||||
// perform the optional resolver lookup.
|
||||
EnableResolverLookup bool
|
||||
|
||||
// HTTPClient is the HTTP client to use. If not set, then
|
||||
// we will use the http.DefaultClient.
|
||||
HTTPClient *http.Client
|
||||
|
||||
// Logger is the logger to use. If not set, then we will
|
||||
// use a logger that discards all messages.
|
||||
Logger Logger
|
||||
|
||||
// ResourcesManager is the mandatory resources manager. If not
|
||||
// set, we will not be able to perform any lookup.
|
||||
ResourcesManager ResourcesManager
|
||||
|
||||
// UserAgent is the user agent to use. If not set, then
|
||||
// we will use a default user agent.
|
||||
UserAgent string
|
||||
}
|
||||
|
||||
// Must ensures that NewTask is successful.
|
||||
func Must(task *Task, err error) *Task {
|
||||
runtimex.PanicOnError(err, "NewTask failed")
|
||||
return task
|
||||
}
|
||||
|
||||
// NewTask creates a new instance of Task from config.
|
||||
func NewTask(config Config) (*Task, error) {
|
||||
if config.HTTPClient == nil {
|
||||
config.HTTPClient = http.DefaultClient
|
||||
}
|
||||
if config.Logger == nil {
|
||||
config.Logger = model.DiscardLogger
|
||||
}
|
||||
if config.ResourcesManager == nil {
|
||||
return nil, ErrMissingResourcesManager
|
||||
}
|
||||
if config.UserAgent == "" {
|
||||
config.UserAgent = fmt.Sprintf("ooniprobe-engine/%s", version.Version)
|
||||
}
|
||||
return &Task{
|
||||
countryLookupper: mmdbLookupper{},
|
||||
enableResolverLookup: config.EnableResolverLookup,
|
||||
probeIPLookupper: ipLookupClient{
|
||||
HTTPClient: config.HTTPClient,
|
||||
Logger: config.Logger,
|
||||
UserAgent: config.UserAgent,
|
||||
},
|
||||
probeASNLookupper: mmdbLookupper{},
|
||||
resolverASNLookupper: mmdbLookupper{},
|
||||
resolverIPLookupper: resolverLookupClient{},
|
||||
resourcesManager: config.ResourcesManager,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Task performs a geolocation. You must create a new
|
||||
// instance of Task using the NewTask factory.
|
||||
type Task struct {
|
||||
countryLookupper countryLookupper
|
||||
enableResolverLookup bool
|
||||
probeIPLookupper probeIPLookupper
|
||||
probeASNLookupper asnLookupper
|
||||
resolverASNLookupper asnLookupper
|
||||
resolverIPLookupper resolverIPLookupper
|
||||
resourcesManager ResourcesManager
|
||||
}
|
||||
|
||||
// Run runs the task.
|
||||
func (op Task) Run(ctx context.Context) (*Results, error) {
|
||||
var err error
|
||||
out := &Results{
|
||||
ASN: DefaultProbeASN,
|
||||
CountryCode: DefaultProbeCC,
|
||||
NetworkName: DefaultProbeNetworkName,
|
||||
ProbeIP: DefaultProbeIP,
|
||||
ResolverASN: DefaultResolverASN,
|
||||
ResolverIP: DefaultResolverIP,
|
||||
ResolverNetworkName: DefaultResolverNetworkName,
|
||||
}
|
||||
if err := op.resourcesManager.MaybeUpdateResources(ctx); err != nil {
|
||||
return out, fmt.Errorf("MaybeUpdateResource failed: %w", err)
|
||||
}
|
||||
ip, err := op.probeIPLookupper.LookupProbeIP(ctx)
|
||||
if err != nil {
|
||||
return out, fmt.Errorf("lookupProbeIP failed: %w", err)
|
||||
}
|
||||
out.ProbeIP = ip
|
||||
asn, networkName, err := op.probeASNLookupper.LookupASN(
|
||||
op.resourcesManager.ASNDatabasePath(), out.ProbeIP)
|
||||
if err != nil {
|
||||
return out, fmt.Errorf("lookupASN failed: %w", err)
|
||||
}
|
||||
out.ASN = asn
|
||||
out.NetworkName = networkName
|
||||
cc, err := op.countryLookupper.LookupCC(
|
||||
op.resourcesManager.CountryDatabasePath(), out.ProbeIP)
|
||||
if err != nil {
|
||||
return out, fmt.Errorf("lookupProbeCC failed: %w", err)
|
||||
}
|
||||
out.CountryCode = cc
|
||||
if op.enableResolverLookup {
|
||||
out.DidResolverLookup = true
|
||||
// Note: ignoring the result of lookupResolverIP and lookupASN
|
||||
// here is intentional. We don't want this (~minor) failure
|
||||
// to influence the result of the overall lookup. Another design
|
||||
// here could be that of retrying the operation N times?
|
||||
resolverIP, err := op.resolverIPLookupper.LookupResolverIP(ctx)
|
||||
if err != nil {
|
||||
return out, nil
|
||||
}
|
||||
out.ResolverIP = resolverIP
|
||||
resolverASN, resolverNetworkName, err := op.resolverASNLookupper.LookupASN(
|
||||
op.resourcesManager.ASNDatabasePath(), out.ResolverIP,
|
||||
)
|
||||
if err != nil {
|
||||
return out, nil
|
||||
}
|
||||
out.ResolverASN = resolverASN
|
||||
out.ResolverNetworkName = resolverNetworkName
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
@@ -0,0 +1,395 @@
|
||||
package geolocate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type taskResourcesManager struct {
|
||||
asnDatabasePath string
|
||||
countryDatabasePath string
|
||||
err error
|
||||
}
|
||||
|
||||
func (c taskResourcesManager) ASNDatabasePath() string {
|
||||
return c.asnDatabasePath
|
||||
}
|
||||
|
||||
func (c taskResourcesManager) CountryDatabasePath() string {
|
||||
return c.countryDatabasePath
|
||||
}
|
||||
|
||||
func (c taskResourcesManager) MaybeUpdateResources(ctx context.Context) error {
|
||||
return c.err
|
||||
}
|
||||
|
||||
func TestLocationLookupCannotUpdateResources(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
op := Task{
|
||||
resourcesManager: taskResourcesManager{err: expected},
|
||||
}
|
||||
ctx := context.Background()
|
||||
out, err := op.Run(ctx)
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatalf("not the error we expected: %+v", err)
|
||||
}
|
||||
if out.ASN != DefaultProbeASN {
|
||||
t.Fatal("invalid ASN value")
|
||||
}
|
||||
if out.CountryCode != DefaultProbeCC {
|
||||
t.Fatal("invalid CountryCode value")
|
||||
}
|
||||
if out.NetworkName != DefaultProbeNetworkName {
|
||||
t.Fatal("invalid NetworkName value")
|
||||
}
|
||||
if out.ProbeIP != DefaultProbeIP {
|
||||
t.Fatal("invalid ProbeIP value")
|
||||
}
|
||||
if out.ResolverASN != DefaultResolverASN {
|
||||
t.Fatal("invalid ResolverASN value")
|
||||
}
|
||||
if out.ResolverIP != DefaultResolverIP {
|
||||
t.Fatal("invalid ResolverIP value")
|
||||
}
|
||||
if out.ResolverNetworkName != DefaultResolverNetworkName {
|
||||
t.Fatal("invalid ResolverNetworkName value")
|
||||
}
|
||||
}
|
||||
|
||||
type taskProbeIPLookupper struct {
|
||||
ip string
|
||||
err error
|
||||
}
|
||||
|
||||
func (c taskProbeIPLookupper) LookupProbeIP(ctx context.Context) (string, error) {
|
||||
return c.ip, c.err
|
||||
}
|
||||
|
||||
func TestLocationLookupCannotLookupProbeIP(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
op := Task{
|
||||
resourcesManager: taskResourcesManager{},
|
||||
probeIPLookupper: taskProbeIPLookupper{err: expected},
|
||||
}
|
||||
ctx := context.Background()
|
||||
out, err := op.Run(ctx)
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatalf("not the error we expected: %+v", err)
|
||||
}
|
||||
if out.ASN != DefaultProbeASN {
|
||||
t.Fatal("invalid ASN value")
|
||||
}
|
||||
if out.CountryCode != DefaultProbeCC {
|
||||
t.Fatal("invalid CountryCode value")
|
||||
}
|
||||
if out.NetworkName != DefaultProbeNetworkName {
|
||||
t.Fatal("invalid NetworkName value")
|
||||
}
|
||||
if out.ProbeIP != DefaultProbeIP {
|
||||
t.Fatal("invalid ProbeIP value")
|
||||
}
|
||||
if out.ResolverASN != DefaultResolverASN {
|
||||
t.Fatal("invalid ResolverASN value")
|
||||
}
|
||||
if out.ResolverIP != DefaultResolverIP {
|
||||
t.Fatal("invalid ResolverIP value")
|
||||
}
|
||||
if out.ResolverNetworkName != DefaultResolverNetworkName {
|
||||
t.Fatal("invalid ResolverNetworkName value")
|
||||
}
|
||||
}
|
||||
|
||||
type taskASNLookupper struct {
|
||||
err error
|
||||
asn uint
|
||||
name string
|
||||
}
|
||||
|
||||
func (c taskASNLookupper) LookupASN(path string, ip string) (uint, string, error) {
|
||||
return c.asn, c.name, c.err
|
||||
}
|
||||
|
||||
func TestLocationLookupCannotLookupProbeASN(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
op := Task{
|
||||
resourcesManager: taskResourcesManager{},
|
||||
probeIPLookupper: taskProbeIPLookupper{ip: "1.2.3.4"},
|
||||
probeASNLookupper: taskASNLookupper{err: expected},
|
||||
}
|
||||
ctx := context.Background()
|
||||
out, err := op.Run(ctx)
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatalf("not the error we expected: %+v", err)
|
||||
}
|
||||
if out.ASN != DefaultProbeASN {
|
||||
t.Fatal("invalid ASN value")
|
||||
}
|
||||
if out.CountryCode != DefaultProbeCC {
|
||||
t.Fatal("invalid CountryCode value")
|
||||
}
|
||||
if out.NetworkName != DefaultProbeNetworkName {
|
||||
t.Fatal("invalid NetworkName value")
|
||||
}
|
||||
if out.ProbeIP != "1.2.3.4" {
|
||||
t.Fatal("invalid ProbeIP value")
|
||||
}
|
||||
if out.ResolverASN != DefaultResolverASN {
|
||||
t.Fatal("invalid ResolverASN value")
|
||||
}
|
||||
if out.ResolverIP != DefaultResolverIP {
|
||||
t.Fatal("invalid ResolverIP value")
|
||||
}
|
||||
if out.ResolverNetworkName != DefaultResolverNetworkName {
|
||||
t.Fatal("invalid ResolverNetworkName value")
|
||||
}
|
||||
}
|
||||
|
||||
type taskCCLookupper struct {
|
||||
err error
|
||||
cc string
|
||||
}
|
||||
|
||||
func (c taskCCLookupper) LookupCC(path string, ip string) (string, error) {
|
||||
return c.cc, c.err
|
||||
}
|
||||
|
||||
func TestLocationLookupCannotLookupProbeCC(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
op := Task{
|
||||
resourcesManager: taskResourcesManager{},
|
||||
probeIPLookupper: taskProbeIPLookupper{ip: "1.2.3.4"},
|
||||
probeASNLookupper: taskASNLookupper{asn: 1234, name: "1234.com"},
|
||||
countryLookupper: taskCCLookupper{cc: "US", err: expected},
|
||||
}
|
||||
ctx := context.Background()
|
||||
out, err := op.Run(ctx)
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatalf("not the error we expected: %+v", err)
|
||||
}
|
||||
if out.ASN != 1234 {
|
||||
t.Fatal("invalid ASN value")
|
||||
}
|
||||
if out.CountryCode != DefaultProbeCC {
|
||||
t.Fatal("invalid CountryCode value")
|
||||
}
|
||||
if out.NetworkName != "1234.com" {
|
||||
t.Fatal("invalid NetworkName value")
|
||||
}
|
||||
if out.ProbeIP != "1.2.3.4" {
|
||||
t.Fatal("invalid ProbeIP value")
|
||||
}
|
||||
if out.ResolverASN != DefaultResolverASN {
|
||||
t.Fatal("invalid ResolverASN value")
|
||||
}
|
||||
if out.ResolverIP != DefaultResolverIP {
|
||||
t.Fatal("invalid ResolverIP value")
|
||||
}
|
||||
if out.ResolverNetworkName != DefaultResolverNetworkName {
|
||||
t.Fatal("invalid ResolverNetworkName value")
|
||||
}
|
||||
}
|
||||
|
||||
type taskResolverIPLookupper struct {
|
||||
ip string
|
||||
err error
|
||||
}
|
||||
|
||||
func (c taskResolverIPLookupper) LookupResolverIP(ctx context.Context) (string, error) {
|
||||
return c.ip, c.err
|
||||
}
|
||||
|
||||
func TestLocationLookupCannotLookupResolverIP(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
op := Task{
|
||||
resourcesManager: taskResourcesManager{},
|
||||
probeIPLookupper: taskProbeIPLookupper{ip: "1.2.3.4"},
|
||||
probeASNLookupper: taskASNLookupper{asn: 1234, name: "1234.com"},
|
||||
countryLookupper: taskCCLookupper{cc: "IT"},
|
||||
resolverIPLookupper: taskResolverIPLookupper{err: expected},
|
||||
enableResolverLookup: true,
|
||||
}
|
||||
ctx := context.Background()
|
||||
out, err := op.Run(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("not the error we expected: %+v", err)
|
||||
}
|
||||
if out.ASN != 1234 {
|
||||
t.Fatal("invalid ASN value")
|
||||
}
|
||||
if out.CountryCode != "IT" {
|
||||
t.Fatal("invalid CountryCode value")
|
||||
}
|
||||
if out.NetworkName != "1234.com" {
|
||||
t.Fatal("invalid NetworkName value")
|
||||
}
|
||||
if out.ProbeIP != "1.2.3.4" {
|
||||
t.Fatal("invalid ProbeIP value")
|
||||
}
|
||||
if out.DidResolverLookup != true {
|
||||
t.Fatal("invalid DidResolverLookup value")
|
||||
}
|
||||
if out.ResolverASN != DefaultResolverASN {
|
||||
t.Fatal("invalid ResolverASN value")
|
||||
}
|
||||
if out.ResolverIP != DefaultResolverIP {
|
||||
t.Fatal("invalid ResolverIP value")
|
||||
}
|
||||
if out.ResolverNetworkName != DefaultResolverNetworkName {
|
||||
t.Fatal("invalid ResolverNetworkName value")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocationLookupCannotLookupResolverNetworkName(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
op := Task{
|
||||
resourcesManager: taskResourcesManager{},
|
||||
probeIPLookupper: taskProbeIPLookupper{ip: "1.2.3.4"},
|
||||
probeASNLookupper: taskASNLookupper{asn: 1234, name: "1234.com"},
|
||||
countryLookupper: taskCCLookupper{cc: "IT"},
|
||||
resolverIPLookupper: taskResolverIPLookupper{ip: "4.3.2.1"},
|
||||
resolverASNLookupper: taskASNLookupper{err: expected},
|
||||
enableResolverLookup: true,
|
||||
}
|
||||
ctx := context.Background()
|
||||
out, err := op.Run(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("not the error we expected: %+v", err)
|
||||
}
|
||||
if out.ASN != 1234 {
|
||||
t.Fatal("invalid ASN value")
|
||||
}
|
||||
if out.CountryCode != "IT" {
|
||||
t.Fatal("invalid CountryCode value")
|
||||
}
|
||||
if out.NetworkName != "1234.com" {
|
||||
t.Fatal("invalid NetworkName value")
|
||||
}
|
||||
if out.ProbeIP != "1.2.3.4" {
|
||||
t.Fatal("invalid ProbeIP value")
|
||||
}
|
||||
if out.DidResolverLookup != true {
|
||||
t.Fatal("invalid DidResolverLookup value")
|
||||
}
|
||||
if out.ResolverASN != DefaultResolverASN {
|
||||
t.Fatalf("invalid ResolverASN value: %+v", out.ResolverASN)
|
||||
}
|
||||
if out.ResolverIP != "4.3.2.1" {
|
||||
t.Fatalf("invalid ResolverIP value: %+v", out.ResolverIP)
|
||||
}
|
||||
if out.ResolverNetworkName != DefaultResolverNetworkName {
|
||||
t.Fatal("invalid ResolverNetworkName value")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocationLookupSuccessWithResolverLookup(t *testing.T) {
|
||||
op := Task{
|
||||
resourcesManager: taskResourcesManager{},
|
||||
probeIPLookupper: taskProbeIPLookupper{ip: "1.2.3.4"},
|
||||
probeASNLookupper: taskASNLookupper{asn: 1234, name: "1234.com"},
|
||||
countryLookupper: taskCCLookupper{cc: "IT"},
|
||||
resolverIPLookupper: taskResolverIPLookupper{ip: "4.3.2.1"},
|
||||
resolverASNLookupper: taskASNLookupper{asn: 4321, name: "4321.com"},
|
||||
enableResolverLookup: true,
|
||||
}
|
||||
ctx := context.Background()
|
||||
out, err := op.Run(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("not the error we expected: %+v", err)
|
||||
}
|
||||
if out.ASN != 1234 {
|
||||
t.Fatal("invalid ASN value")
|
||||
}
|
||||
if out.CountryCode != "IT" {
|
||||
t.Fatal("invalid CountryCode value")
|
||||
}
|
||||
if out.NetworkName != "1234.com" {
|
||||
t.Fatal("invalid NetworkName value")
|
||||
}
|
||||
if out.ProbeIP != "1.2.3.4" {
|
||||
t.Fatal("invalid ProbeIP value")
|
||||
}
|
||||
if out.DidResolverLookup != true {
|
||||
t.Fatal("invalid DidResolverLookup value")
|
||||
}
|
||||
if out.ResolverASN != 4321 {
|
||||
t.Fatalf("invalid ResolverASN value: %+v", out.ResolverASN)
|
||||
}
|
||||
if out.ResolverIP != "4.3.2.1" {
|
||||
t.Fatalf("invalid ResolverIP value: %+v", out.ResolverIP)
|
||||
}
|
||||
if out.ResolverNetworkName != "4321.com" {
|
||||
t.Fatal("invalid ResolverNetworkName value")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocationLookupSuccessWithoutResolverLookup(t *testing.T) {
|
||||
op := Task{
|
||||
resourcesManager: taskResourcesManager{},
|
||||
probeIPLookupper: taskProbeIPLookupper{ip: "1.2.3.4"},
|
||||
probeASNLookupper: taskASNLookupper{asn: 1234, name: "1234.com"},
|
||||
countryLookupper: taskCCLookupper{cc: "IT"},
|
||||
resolverIPLookupper: taskResolverIPLookupper{ip: "4.3.2.1"},
|
||||
resolverASNLookupper: taskASNLookupper{asn: 4321, name: "4321.com"},
|
||||
}
|
||||
ctx := context.Background()
|
||||
out, err := op.Run(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("not the error we expected: %+v", err)
|
||||
}
|
||||
if out.ASN != 1234 {
|
||||
t.Fatal("invalid ASN value")
|
||||
}
|
||||
if out.CountryCode != "IT" {
|
||||
t.Fatal("invalid CountryCode value")
|
||||
}
|
||||
if out.NetworkName != "1234.com" {
|
||||
t.Fatal("invalid NetworkName value")
|
||||
}
|
||||
if out.ProbeIP != "1.2.3.4" {
|
||||
t.Fatal("invalid ProbeIP value")
|
||||
}
|
||||
if out.DidResolverLookup != false {
|
||||
t.Fatal("invalid DidResolverLookup value")
|
||||
}
|
||||
if out.ResolverASN != DefaultResolverASN {
|
||||
t.Fatalf("invalid ResolverASN value: %+v", out.ResolverASN)
|
||||
}
|
||||
if out.ResolverIP != DefaultResolverIP {
|
||||
t.Fatalf("invalid ResolverIP value: %+v", out.ResolverIP)
|
||||
}
|
||||
if out.ResolverNetworkName != DefaultResolverNetworkName {
|
||||
t.Fatal("invalid ResolverNetworkName value")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSmoke(t *testing.T) {
|
||||
maybeFetchResources(t)
|
||||
config := Config{
|
||||
EnableResolverLookup: true,
|
||||
ResourcesManager: taskResourcesManager{
|
||||
asnDatabasePath: asnDBPath,
|
||||
countryDatabasePath: countryDBPath,
|
||||
},
|
||||
}
|
||||
task := Must(NewTask(config))
|
||||
result, err := task.Run(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result == nil {
|
||||
t.Fatal("expected non nil result")
|
||||
}
|
||||
// we already checked above that the returned
|
||||
// value is okay for all codepaths.
|
||||
}
|
||||
|
||||
func TestNewTaskWithNoResourcesManager(t *testing.T) {
|
||||
task, err := NewTask(Config{})
|
||||
if !errors.Is(err, ErrMissingResourcesManager) {
|
||||
t.Fatal("not the error we expected")
|
||||
}
|
||||
if task != nil {
|
||||
t.Fatal("expected nil task here")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package geolocate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func invalidIPLookup(
|
||||
ctx context.Context,
|
||||
httpClient *http.Client,
|
||||
logger Logger,
|
||||
userAgent string,
|
||||
) (string, error) {
|
||||
return "invalid IP", nil
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package geolocate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/internal/httpheader"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/internal/httpx"
|
||||
)
|
||||
|
||||
func ipConfigIPLookup(
|
||||
ctx context.Context,
|
||||
httpClient *http.Client,
|
||||
logger Logger,
|
||||
userAgent string,
|
||||
) (string, error) {
|
||||
data, err := (httpx.Client{
|
||||
BaseURL: "https://ipconfig.io",
|
||||
HTTPClient: httpClient,
|
||||
Logger: logger,
|
||||
UserAgent: httpheader.CLIUserAgent(),
|
||||
}).FetchResource(ctx, "/")
|
||||
if err != nil {
|
||||
return DefaultProbeIP, err
|
||||
}
|
||||
ip := strings.Trim(string(data), "\r\n\t ")
|
||||
logger.Debugf("ipconfig: body: %s", ip)
|
||||
return ip, nil
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package geolocate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/apex/log"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/internal/httpheader"
|
||||
)
|
||||
|
||||
func TestIPLookupWorksUsingIPConfig(t *testing.T) {
|
||||
ip, err := ipConfigIPLookup(
|
||||
context.Background(),
|
||||
http.DefaultClient,
|
||||
log.Log,
|
||||
httpheader.UserAgent(),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if net.ParseIP(ip) == nil {
|
||||
t.Fatalf("not an IP address: '%s'", ip)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package geolocate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/internal/httpheader"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/internal/httpx"
|
||||
)
|
||||
|
||||
type ipInfoResponse struct {
|
||||
IP string `json:"ip"`
|
||||
}
|
||||
|
||||
func ipInfoIPLookup(
|
||||
ctx context.Context,
|
||||
httpClient *http.Client,
|
||||
logger Logger,
|
||||
userAgent string,
|
||||
) (string, error) {
|
||||
var v ipInfoResponse
|
||||
err := (httpx.Client{
|
||||
Accept: "application/json",
|
||||
BaseURL: "https://ipinfo.io",
|
||||
HTTPClient: httpClient,
|
||||
Logger: logger,
|
||||
UserAgent: httpheader.CLIUserAgent(), // we must be a CLI client
|
||||
}).GetJSON(ctx, "/", &v)
|
||||
if err != nil {
|
||||
return DefaultProbeIP, err
|
||||
}
|
||||
return v.IP, nil
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package geolocate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/apex/log"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/internal/httpheader"
|
||||
)
|
||||
|
||||
func TestIPLookupWorksUsingIPInfo(t *testing.T) {
|
||||
ip, err := ipInfoIPLookup(
|
||||
context.Background(),
|
||||
http.DefaultClient,
|
||||
log.Log,
|
||||
httpheader.UserAgent(),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if net.ParseIP(ip) == nil {
|
||||
t.Fatalf("not an IP address: '%s'", ip)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package geolocate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/internal/multierror"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrAllIPLookuppersFailed indicates that we failed with looking
|
||||
// up the probe IP for with all the lookuppers that we tried.
|
||||
ErrAllIPLookuppersFailed = errors.New("all IP lookuppers failed")
|
||||
|
||||
// ErrInvalidIPAddress indicates that the code returned to us a
|
||||
// string that actually isn't a valid IP address.
|
||||
ErrInvalidIPAddress = errors.New("lookupper did not return a valid IP")
|
||||
)
|
||||
|
||||
type lookupFunc func(
|
||||
ctx context.Context, client *http.Client,
|
||||
logger Logger, userAgent string,
|
||||
) (string, error)
|
||||
|
||||
type method struct {
|
||||
name string
|
||||
fn lookupFunc
|
||||
}
|
||||
|
||||
var (
|
||||
methods = []method{
|
||||
{
|
||||
name: "avast",
|
||||
fn: avastIPLookup,
|
||||
},
|
||||
{
|
||||
name: "ipconfig",
|
||||
fn: ipConfigIPLookup,
|
||||
},
|
||||
{
|
||||
name: "ipinfo",
|
||||
fn: ipInfoIPLookup,
|
||||
},
|
||||
{
|
||||
name: "stun_ekiga",
|
||||
fn: stunEkigaIPLookup,
|
||||
},
|
||||
{
|
||||
name: "stun_google",
|
||||
fn: stunGoogleIPLookup,
|
||||
},
|
||||
{
|
||||
name: "ubuntu",
|
||||
fn: ubuntuIPLookup,
|
||||
},
|
||||
}
|
||||
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
type ipLookupClient struct {
|
||||
// HTTPClient is the HTTP client to use
|
||||
HTTPClient *http.Client
|
||||
|
||||
// Logger is the logger to use
|
||||
Logger Logger
|
||||
|
||||
// UserAgent is the user agent to use
|
||||
UserAgent string
|
||||
}
|
||||
|
||||
func makeSlice() []method {
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
ret := make([]method, len(methods))
|
||||
perm := r.Perm(len(methods))
|
||||
for idx, randIdx := range perm {
|
||||
ret[idx] = methods[randIdx]
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (c ipLookupClient) doWithCustomFunc(
|
||||
ctx context.Context, fn lookupFunc,
|
||||
) (string, error) {
|
||||
ip, err := fn(ctx, c.HTTPClient, c.Logger, c.UserAgent)
|
||||
if err != nil {
|
||||
return DefaultProbeIP, err
|
||||
}
|
||||
if net.ParseIP(ip) == nil {
|
||||
return DefaultProbeIP, fmt.Errorf("%w: %s", ErrInvalidIPAddress, ip)
|
||||
}
|
||||
c.Logger.Debugf("iplookup: IP: %s", ip)
|
||||
return ip, nil
|
||||
}
|
||||
|
||||
func (c ipLookupClient) LookupProbeIP(ctx context.Context) (string, error) {
|
||||
union := multierror.New(ErrAllIPLookuppersFailed)
|
||||
for _, method := range makeSlice() {
|
||||
c.Logger.Debugf("iplookup: using %s", method.name)
|
||||
ip, err := c.doWithCustomFunc(ctx, method.fn)
|
||||
if err == nil {
|
||||
return ip, nil
|
||||
}
|
||||
union.Add(err)
|
||||
}
|
||||
return DefaultProbeIP, union
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package geolocate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/apex/log"
|
||||
)
|
||||
|
||||
func TestIPLookupGood(t *testing.T) {
|
||||
ip, err := (ipLookupClient{
|
||||
HTTPClient: http.DefaultClient,
|
||||
Logger: log.Log,
|
||||
UserAgent: "ooniprobe-engine/0.1.0",
|
||||
}).LookupProbeIP(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if net.ParseIP(ip) == nil {
|
||||
t.Fatal("not an IP address")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIPLookupAllFailed(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel() // immediately cancel to cause Do() to fail
|
||||
ip, err := (ipLookupClient{
|
||||
HTTPClient: http.DefaultClient,
|
||||
Logger: log.Log,
|
||||
UserAgent: "ooniprobe-engine/0.1.0",
|
||||
}).LookupProbeIP(ctx)
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
t.Fatal("expected an error here")
|
||||
}
|
||||
if ip != DefaultProbeIP {
|
||||
t.Fatal("expected the default IP here")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIPLookupInvalidIP(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ip, err := (ipLookupClient{
|
||||
HTTPClient: http.DefaultClient,
|
||||
Logger: log.Log,
|
||||
UserAgent: "ooniprobe-engine/0.1.0",
|
||||
}).doWithCustomFunc(ctx, invalidIPLookup)
|
||||
if !errors.Is(err, ErrInvalidIPAddress) {
|
||||
t.Fatal("expected an error here")
|
||||
}
|
||||
if ip != DefaultProbeIP {
|
||||
t.Fatal("expected the default IP here")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package geolocate
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/oschwald/geoip2-golang"
|
||||
)
|
||||
|
||||
type mmdbLookupper struct{}
|
||||
|
||||
func (mmdbLookupper) LookupASN(path, ip string) (asn uint, org string, err error) {
|
||||
asn, org = DefaultProbeASN, DefaultProbeNetworkName
|
||||
db, err := geoip2.Open(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer db.Close()
|
||||
record, err := db.ASN(net.ParseIP(ip))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
asn = record.AutonomousSystemNumber
|
||||
if record.AutonomousSystemOrganization != "" {
|
||||
org = record.AutonomousSystemOrganization
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// LookupASN returns the ASN and the organization associated with the
|
||||
// given ip using the ASN database at path.
|
||||
func LookupASN(path, ip string) (asn uint, org string, err error) {
|
||||
return (mmdbLookupper{}).LookupASN(path, ip)
|
||||
}
|
||||
|
||||
func (mmdbLookupper) LookupCC(path, ip string) (cc string, err error) {
|
||||
cc = DefaultProbeCC
|
||||
db, err := geoip2.Open(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer db.Close()
|
||||
record, err := db.Country(net.ParseIP(ip))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// With MaxMind DB we used record.RegisteredCountry.IsoCode but that does
|
||||
// not seem to work with the db-ip.com database. The record is empty, at
|
||||
// least for my own IP address in Italy. --Simone (2020-02-25)
|
||||
if record.Country.IsoCode != "" {
|
||||
cc = record.Country.IsoCode
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package geolocate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/apex/log"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/resources"
|
||||
)
|
||||
|
||||
const (
|
||||
asnDBPath = "../testdata/asn.mmdb"
|
||||
countryDBPath = "../testdata/country.mmdb"
|
||||
ipAddr = "35.204.49.125"
|
||||
)
|
||||
|
||||
func maybeFetchResources(t *testing.T) {
|
||||
c := &resources.Client{
|
||||
HTTPClient: http.DefaultClient,
|
||||
Logger: log.Log,
|
||||
UserAgent: "ooniprobe-engine/0.1.0",
|
||||
WorkDir: "../testdata/",
|
||||
}
|
||||
if err := c.Ensure(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLookupASN(t *testing.T) {
|
||||
maybeFetchResources(t)
|
||||
asn, org, err := LookupASN(asnDBPath, ipAddr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if asn <= 0 {
|
||||
t.Fatal("unexpected ASN value")
|
||||
}
|
||||
if len(org) <= 0 {
|
||||
t.Fatal("unexpected org value")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLookupASNInvalidFile(t *testing.T) {
|
||||
maybeFetchResources(t)
|
||||
asn, org, err := LookupASN("/nonexistent", ipAddr)
|
||||
if err == nil {
|
||||
t.Fatal("expected an error here")
|
||||
}
|
||||
if asn != DefaultProbeASN {
|
||||
t.Fatal("expected a zero ASN")
|
||||
}
|
||||
if org != DefaultProbeNetworkName {
|
||||
t.Fatal("expected an empty org")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLookupASNInvalidIP(t *testing.T) {
|
||||
maybeFetchResources(t)
|
||||
asn, org, err := LookupASN(asnDBPath, "xxx")
|
||||
if err == nil {
|
||||
t.Fatal("expected an error here")
|
||||
}
|
||||
if asn != DefaultProbeASN {
|
||||
t.Fatal("expected a zero ASN")
|
||||
}
|
||||
if org != DefaultProbeNetworkName {
|
||||
t.Fatal("expected an empty org")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLookupCC(t *testing.T) {
|
||||
maybeFetchResources(t)
|
||||
cc, err := (mmdbLookupper{}).LookupCC(countryDBPath, ipAddr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(cc) != 2 {
|
||||
t.Fatal("does not seem a country code")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLookupCCInvalidFile(t *testing.T) {
|
||||
maybeFetchResources(t)
|
||||
cc, err := (mmdbLookupper{}).LookupCC("/nonexistent", ipAddr)
|
||||
if err == nil {
|
||||
t.Fatal("expected an error here")
|
||||
}
|
||||
if cc != DefaultProbeCC {
|
||||
t.Fatal("expected an empty cc")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLookupCCInvalidIP(t *testing.T) {
|
||||
maybeFetchResources(t)
|
||||
cc, err := (mmdbLookupper{}).LookupCC(asnDBPath, "xxx")
|
||||
if err == nil {
|
||||
t.Fatal("expected an error here")
|
||||
}
|
||||
if cc != DefaultProbeCC {
|
||||
t.Fatal("expected an empty cc")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package geolocate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrNoIPAddressReturned indicates that no IP address was
|
||||
// returned by a specific DNS resolver.
|
||||
ErrNoIPAddressReturned = errors.New("geolocate: no IP address returned")
|
||||
)
|
||||
|
||||
type dnsResolver interface {
|
||||
LookupHost(ctx context.Context, host string) (addrs []string, err error)
|
||||
}
|
||||
|
||||
type resolverLookupClient struct{}
|
||||
|
||||
func (rlc resolverLookupClient) do(ctx context.Context, r dnsResolver) (string, error) {
|
||||
var ips []string
|
||||
ips, err := r.LookupHost(ctx, "whoami.akamai.net")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(ips) < 1 {
|
||||
return "", ErrNoIPAddressReturned
|
||||
}
|
||||
return ips[0], nil
|
||||
}
|
||||
|
||||
func (rlc resolverLookupClient) LookupResolverIP(ctx context.Context) (ip string, err error) {
|
||||
return rlc.do(ctx, &net.Resolver{})
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package geolocate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLookupResolverIP(t *testing.T) {
|
||||
addr, err := (resolverLookupClient{}).LookupResolverIP(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if addr == "" {
|
||||
t.Fatal("expected a non-empty string")
|
||||
}
|
||||
}
|
||||
|
||||
type brokenHostLookupper struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (bhl brokenHostLookupper) LookupHost(ctx context.Context, host string) ([]string, error) {
|
||||
return nil, bhl.err
|
||||
}
|
||||
|
||||
func TestLookupResolverIPFailure(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
rlc := resolverLookupClient{}
|
||||
addr, err := rlc.do(context.Background(), brokenHostLookupper{
|
||||
err: expected,
|
||||
})
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatalf("not the error we expected: %+v", err)
|
||||
}
|
||||
if len(addr) != 0 {
|
||||
t.Fatal("expected an empty address")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLookupResolverIPNoAddressReturned(t *testing.T) {
|
||||
rlc := resolverLookupClient{}
|
||||
addr, err := rlc.do(context.Background(), brokenHostLookupper{})
|
||||
if !errors.Is(err, ErrNoIPAddressReturned) {
|
||||
t.Fatalf("not the error we expected: %+v", err)
|
||||
}
|
||||
if len(addr) != 0 {
|
||||
t.Fatal("expected an empty address")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package geolocate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/pion/stun"
|
||||
)
|
||||
|
||||
type stunClient interface {
|
||||
Close() error
|
||||
Start(m *stun.Message, h stun.Handler) error
|
||||
}
|
||||
|
||||
type stunConfig struct {
|
||||
Dial func(network string, address string) (stunClient, error)
|
||||
Endpoint string
|
||||
Logger Logger
|
||||
}
|
||||
|
||||
func stunDialer(network string, address string) (stunClient, error) {
|
||||
return stun.Dial(network, address)
|
||||
}
|
||||
|
||||
func stunIPLookup(ctx context.Context, config stunConfig) (string, error) {
|
||||
config.Logger.Debugf("STUNIPLookup: start using %s", config.Endpoint)
|
||||
ip, err := func() (string, error) {
|
||||
dial := config.Dial
|
||||
if dial == nil {
|
||||
dial = stunDialer
|
||||
}
|
||||
clnt, err := dial("udp", config.Endpoint)
|
||||
if err != nil {
|
||||
return DefaultProbeIP, err
|
||||
}
|
||||
defer clnt.Close()
|
||||
message := stun.MustBuild(stun.TransactionID, stun.BindingRequest)
|
||||
errch, ipch := make(chan error, 1), make(chan string, 1)
|
||||
err = clnt.Start(message, func(ev stun.Event) {
|
||||
if ev.Error != nil {
|
||||
errch <- ev.Error
|
||||
return
|
||||
}
|
||||
var xorAddr stun.XORMappedAddress
|
||||
if err := xorAddr.GetFrom(ev.Message); err != nil {
|
||||
errch <- err
|
||||
return
|
||||
}
|
||||
ipch <- xorAddr.IP.String()
|
||||
})
|
||||
if err != nil {
|
||||
return DefaultProbeIP, err
|
||||
}
|
||||
select {
|
||||
case err := <-errch:
|
||||
return DefaultProbeIP, err
|
||||
case ip := <-ipch:
|
||||
return ip, nil
|
||||
case <-ctx.Done():
|
||||
return DefaultProbeIP, ctx.Err()
|
||||
}
|
||||
}()
|
||||
if err != nil {
|
||||
config.Logger.Debugf("STUNIPLookup: failure using %s: %+v", config.Endpoint, err)
|
||||
return DefaultProbeIP, err
|
||||
}
|
||||
return ip, nil
|
||||
}
|
||||
|
||||
func stunEkigaIPLookup(
|
||||
ctx context.Context,
|
||||
httpClient *http.Client,
|
||||
logger Logger,
|
||||
userAgent string,
|
||||
) (string, error) {
|
||||
return stunIPLookup(ctx, stunConfig{
|
||||
Endpoint: "stun.ekiga.net:3478",
|
||||
Logger: logger,
|
||||
})
|
||||
}
|
||||
|
||||
func stunGoogleIPLookup(
|
||||
ctx context.Context,
|
||||
httpClient *http.Client,
|
||||
logger Logger,
|
||||
userAgent string,
|
||||
) (string, error) {
|
||||
return stunIPLookup(ctx, stunConfig{
|
||||
Endpoint: "stun.l.google.com:19302",
|
||||
Logger: logger,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package geolocate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/apex/log"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/internal/httpheader"
|
||||
"github.com/pion/stun"
|
||||
)
|
||||
|
||||
func TestSTUNIPLookupCanceledContext(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel() // stop immediately
|
||||
ip, err := stunIPLookup(ctx, stunConfig{
|
||||
Endpoint: "stun.ekiga.net:3478",
|
||||
Logger: log.Log,
|
||||
})
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("not the error we expected: %+v", err)
|
||||
}
|
||||
if ip != DefaultProbeIP {
|
||||
t.Fatalf("not the IP address we expected: %+v", ip)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSTUNIPLookupDialFailure(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
ctx := context.Background()
|
||||
ip, err := stunIPLookup(ctx, stunConfig{
|
||||
Dial: func(network, address string) (stunClient, error) {
|
||||
return nil, expected
|
||||
},
|
||||
Endpoint: "stun.ekiga.net:3478",
|
||||
Logger: log.Log,
|
||||
})
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatalf("not the error we expected: %+v", err)
|
||||
}
|
||||
if ip != DefaultProbeIP {
|
||||
t.Fatalf("not the IP address we expected: %+v", ip)
|
||||
}
|
||||
}
|
||||
|
||||
type MockableSTUNClient struct {
|
||||
StartErr error
|
||||
Event stun.Event
|
||||
}
|
||||
|
||||
func (c MockableSTUNClient) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c MockableSTUNClient) Start(m *stun.Message, h stun.Handler) error {
|
||||
if c.StartErr != nil {
|
||||
return c.StartErr
|
||||
}
|
||||
go func() {
|
||||
<-time.After(100 * time.Millisecond)
|
||||
h(c.Event)
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestSTUNIPLookupStartReturnsError(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
ctx := context.Background()
|
||||
ip, err := stunIPLookup(ctx, stunConfig{
|
||||
Dial: func(network, address string) (stunClient, error) {
|
||||
return MockableSTUNClient{StartErr: expected}, nil
|
||||
},
|
||||
Endpoint: "stun.ekiga.net:3478",
|
||||
Logger: log.Log,
|
||||
})
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatalf("not the error we expected: %+v", err)
|
||||
}
|
||||
if ip != DefaultProbeIP {
|
||||
t.Fatalf("not the IP address we expected: %+v", ip)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSTUNIPLookupStunEventContainsError(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
ctx := context.Background()
|
||||
ip, err := stunIPLookup(ctx, stunConfig{
|
||||
Dial: func(network, address string) (stunClient, error) {
|
||||
return MockableSTUNClient{Event: stun.Event{
|
||||
Error: expected,
|
||||
}}, nil
|
||||
},
|
||||
Endpoint: "stun.ekiga.net:3478",
|
||||
Logger: log.Log,
|
||||
})
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatalf("not the error we expected: %+v", err)
|
||||
}
|
||||
if ip != DefaultProbeIP {
|
||||
t.Fatalf("not the IP address we expected: %+v", ip)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSTUNIPLookupCannotDecodeMessage(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ip, err := stunIPLookup(ctx, stunConfig{
|
||||
Dial: func(network, address string) (stunClient, error) {
|
||||
return MockableSTUNClient{Event: stun.Event{
|
||||
Message: &stun.Message{},
|
||||
}}, nil
|
||||
},
|
||||
Endpoint: "stun.ekiga.net:3478",
|
||||
Logger: log.Log,
|
||||
})
|
||||
if !errors.Is(err, stun.ErrAttributeNotFound) {
|
||||
t.Fatalf("not the error we expected: %+v", err)
|
||||
}
|
||||
if ip != DefaultProbeIP {
|
||||
t.Fatalf("not the IP address we expected: %+v", ip)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIPLookupWorksUsingSTUNEkiga(t *testing.T) {
|
||||
ip, err := stunEkigaIPLookup(
|
||||
context.Background(),
|
||||
http.DefaultClient,
|
||||
log.Log,
|
||||
httpheader.UserAgent(),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if net.ParseIP(ip) == nil {
|
||||
t.Fatalf("not an IP address: '%s'", ip)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIPLookupWorksUsingSTUNGoogle(t *testing.T) {
|
||||
ip, err := stunGoogleIPLookup(
|
||||
context.Background(),
|
||||
http.DefaultClient,
|
||||
log.Log,
|
||||
httpheader.UserAgent(),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if net.ParseIP(ip) == nil {
|
||||
t.Fatalf("not an IP address: '%s'", ip)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package geolocate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"net/http"
|
||||
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/internal/httpx"
|
||||
)
|
||||
|
||||
type ubuntuResponse struct {
|
||||
XMLName xml.Name `xml:"Response"`
|
||||
IP string `xml:"Ip"`
|
||||
}
|
||||
|
||||
func ubuntuIPLookup(
|
||||
ctx context.Context,
|
||||
httpClient *http.Client,
|
||||
logger Logger,
|
||||
userAgent string,
|
||||
) (string, error) {
|
||||
data, err := (httpx.Client{
|
||||
BaseURL: "https://geoip.ubuntu.com/",
|
||||
HTTPClient: httpClient,
|
||||
Logger: logger,
|
||||
UserAgent: userAgent,
|
||||
}).FetchResource(ctx, "/lookup")
|
||||
if err != nil {
|
||||
return DefaultProbeIP, err
|
||||
}
|
||||
logger.Debugf("ubuntu: body: %s", string(data))
|
||||
var v ubuntuResponse
|
||||
err = xml.Unmarshal(data, &v)
|
||||
if err != nil {
|
||||
return DefaultProbeIP, err
|
||||
}
|
||||
return v.IP, nil
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package geolocate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/apex/log"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/internal/httpheader"
|
||||
)
|
||||
|
||||
func TestUbuntuParseError(t *testing.T) {
|
||||
ip, err := ubuntuIPLookup(
|
||||
context.Background(),
|
||||
&http.Client{Transport: FakeTransport{
|
||||
Resp: &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: ioutil.NopCloser(strings.NewReader("<")),
|
||||
},
|
||||
}},
|
||||
log.Log,
|
||||
httpheader.UserAgent(),
|
||||
)
|
||||
if err == nil || !strings.HasPrefix(err.Error(), "XML syntax error") {
|
||||
t.Fatalf("not the error we expected: %+v", err)
|
||||
}
|
||||
if ip != DefaultProbeIP {
|
||||
t.Fatalf("not the expected IP address: %s", ip)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIPLookupWorksUsingUbuntu(t *testing.T) {
|
||||
ip, err := ubuntuIPLookup(
|
||||
context.Background(),
|
||||
http.DefaultClient,
|
||||
log.Log,
|
||||
httpheader.UserAgent(),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if net.ParseIP(ip) == nil {
|
||||
t.Fatalf("not an IP address: '%s'", ip)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user