location

package
v1.0.448 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 13, 2026 License: MIT Imports: 16 Imported by: 0

Documentation

Overview

Package location resolves the geographic location (city, region, country) of a client IP address, for example to show where a sign-in came from.

A Provider answers for one address. IPWhoIs looks it up with the ipwho.is JSON API; Cache wraps any Provider, keeps a configured number of answers and shares each one between the addresses of a network (an IPv4 /24 or an IPv6 /48 by default), so a busy network costs one lookup per TTL. Private, loopback, carrier-grade NAT and other special-purpose addresses have no location and are never sent to the service (IsPublic).

func newLocationHandler() (http.HandlerFunc, error) {
	whois, err := location.NewIPWhoIs(location.IPWhoIsConfig{})
	if err != nil {
		return nil, err
	}
	locations, err := location.NewCache(whois, location.CacheConfig{Size: 10000})
	if err != nil {
		return nil, err
	}
	return func(w http.ResponseWriter, r *http.Request) {
		// on error place is nil; the next call for the network retries
		place, _ := locations.Location(r.Context(), identity.ClientIPFromRequest(r))
		fmt.Fprintln(w, place.String()) // "City, Region, Country", or "" when unknown
	}, nil
}

IPLocation is a one-off lookup that logs and drops errors, for callers that carry on without a location.

Index

Examples

Constants

View Source
const (
	// DefaultCacheTTL is how long Cache keeps an answer when
	// CacheConfig.TTL is zero.
	DefaultCacheTTL = 24 * time.Hour
	// NoExpiry as CacheConfig.TTL keeps answers until they are evicted.
	NoExpiry time.Duration = -1
	// DefaultIPv4Bits is the IPv4 prefix length that shares one answer when
	// CacheConfig.IPv4Bits is zero: a /24 network.
	DefaultIPv4Bits = 24
	// DefaultIPv6Bits is the IPv6 prefix length that shares one answer when
	// CacheConfig.IPv6Bits is zero: a /48 site.
	DefaultIPv6Bits = 48
)
View Source
const (
	// DefaultIPWhoIsURL is the free ipwho.is endpoint. It allows a limited
	// number of requests per day per calling IP and answers HTTP 429 above
	// it; wrap the provider in a Cache.
	DefaultIPWhoIsURL = "https://ipwho.is"
	// DefaultTimeout bounds one lookup when IPWhoIsConfig.Timeout is zero.
	DefaultTimeout = 3 * time.Second
	// DefaultUserAgent is the User-Agent sent when IPWhoIsConfig.UserAgent
	// is empty.
	DefaultUserAgent = "porto"
)

Variables

View Source
var ErrQuotaExceeded = errors.New("location lookup quota exceeded")

ErrQuotaExceeded is wrapped by the error of an IPWhoIs lookup that the service refused with HTTP 429, and of every lookup skipped until its Retry-After has passed.

Functions

func IsPublic

func IsPublic(addr netip.Addr) bool

IsPublic reports whether addr can have a location: a global unicast address (in 2000::/3 for IPv6) that is not private (RFC 1918, RFC 4193), carrier-grade NAT, or another special-purpose range. An IPv4-mapped IPv6 address is checked as IPv4 and an IPv6 zone is ignored. Providers return no location for other addresses without a lookup.

func ParsePublicIP

func ParsePublicIP(raw string) (netip.Addr, bool)

ParsePublicIP parses raw, an IP address, optionally in brackets ([IPv6]) or with a port (IPv4:port, [IPv6]:port), and returns it in canonical form (IPv4-mapped IPv6 as IPv4, no zone) when it is public (see IsPublic). An IPv6 address with a port needs the brackets: without them the port reads as the last group of the address. It returns false for anything else, including unbalanced brackets, brackets around IPv4 and a malformed port, so that such input is not sent to a lookup service.

Types

type Cache

type Cache struct {
	// contains filtered or unexported fields
}

Cache is a Provider that remembers the answers of another Provider per network: every address of an IPv4 /24 or an IPv6 /48 (by default) gets the answer looked up for the first one. It keeps places and "no location" answers, but not errors, so a failed lookup is retried on the next call. Concurrent calls for the same network share one lookup. Cache is safe for concurrent use and starts no goroutines.

func NewCache

func NewCache(provider Provider, cfg CacheConfig) (*Cache, error)

NewCache returns a Cache over provider. It returns an error when provider is nil, cfg.Size is not positive, or a prefix length is out of range.

Example
package main

import (
	"context"
	"fmt"
	"net/http"
	"net/http/httptest"
	"net/netip"

	"github.com/effective-security/porto/pkg/location"
)

func main() {
	// a stand-in for https://ipwho.is
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		_, _ = w.Write([]byte(`{"success":true,"city":"Mountain View","region":"California","country":"United States","country_code":"US"}`))
	}))
	defer srv.Close()

	// leave BaseURL empty for location.DefaultIPWhoIsURL
	whois, err := location.NewIPWhoIs(location.IPWhoIsConfig{BaseURL: srv.URL})
	if err != nil {
		panic(err)
	}
	locations, err := location.NewCache(whois, location.CacheConfig{Size: 10000})
	if err != nil {
		panic(err)
	}

	ctx := context.Background()
	for _, ip := range []string{"8.8.8.8", "8.8.8.4", "10.0.0.1"} {
		// 8.8.8.4 is in the /24 of 8.8.8.8 and is answered from the cache;
		// the private 10.0.0.1 has no location and is not looked up
		place, err := locations.Location(ctx, netip.MustParseAddr(ip))
		if err != nil {
			panic(err)
		}
		fmt.Printf("%s: %q\n", ip, place.String())
	}
}
Output:
8.8.8.8: "Mountain View, California, United States"
8.8.8.4: "Mountain View, California, United States"
10.0.0.1: ""

func (*Cache) Location

func (c *Cache) Location(ctx context.Context, addr netip.Addr) (*Place, error)

Location implements Provider. Addresses that are not public return nil without a lookup and are not cached. A miss asks the wrapped Provider with ctx and addr, so ctx must allow for the provider's latency: a lookup cut short by ctx is an error and is not cached. Callers that arrive while that lookup runs wait for it, or for their own ctx, and share its result. When that lookup failed because its caller's ctx was done, a waiter whose ctx is not done looks the address up again instead. Each call returns its own copy of the Place.

type CacheConfig

type CacheConfig struct {
	// Size is the number of networks whose answers are kept; the least
	// recently used one is evicted first. Required.
	Size int `json:"size,omitempty" yaml:"size,omitempty"`
	// TTL is how long an answer is kept. Zero means DefaultCacheTTL and
	// NoExpiry (any negative value) keeps answers until they are evicted.
	TTL time.Duration `json:"ttl,omitempty" yaml:"ttl,omitempty"`
	// IPv4Bits is the prefix length of the IPv4 networks whose addresses
	// share one answer, 1 to 32. Default DefaultIPv4Bits.
	IPv4Bits int `json:"ipv4_bits,omitempty" yaml:"ipv4_bits,omitempty"`
	// IPv6Bits is the prefix length of the IPv6 networks whose addresses
	// share one answer, 1 to 128. Default DefaultIPv6Bits.
	IPv6Bits int `json:"ipv6_bits,omitempty" yaml:"ipv6_bits,omitempty"`
}

CacheConfig configures NewCache.

type IPWhoIs

type IPWhoIs struct {
	// contains filtered or unexported fields
}

IPWhoIs is a Provider backed by the ipwho.is JSON API. It sends one request per call; wrap it in a Cache to share answers. After an HTTP 429 it fails every lookup with ErrQuotaExceeded, without a request, until the Retry-After the service sent has passed. It is safe for concurrent use.

func NewIPWhoIs

func NewIPWhoIs(cfg IPWhoIsConfig) (*IPWhoIs, error)

NewIPWhoIs returns an ipwho.is Provider. It returns an error when cfg.BaseURL is not an absolute http or https URL, or cfg.Timeout is negative.

func (*IPWhoIs) Location

func (p *IPWhoIs) Location(ctx context.Context, addr netip.Addr) (*Place, error)

Location implements Provider. It looks up public addresses only, in canonical form (see ParsePublicIP). A non-200 status is an error, one that wraps ErrQuotaExceeded for 429; an answer with "success": false for an invalid address or a reserved range is no location, any other one an error.

type IPWhoIsConfig

type IPWhoIsConfig struct {
	// BaseURL is the http or https URL of the lookup service; the address
	// is appended as a path segment and the fields parameter is added to
	// its query. Other query parameters, such as an API key, are kept and
	// are not included in errors. Default DefaultIPWhoIsURL.
	BaseURL string `json:"base_url,omitempty" yaml:"base_url,omitempty"`
	// Timeout bounds one lookup, including reading the answer. Default
	// DefaultTimeout.
	Timeout time.Duration `json:"timeout,omitempty" yaml:"timeout,omitempty"`
	// UserAgent is sent with each lookup. Default DefaultUserAgent.
	UserAgent string `json:"user_agent,omitempty" yaml:"user_agent,omitempty"`
}

IPWhoIsConfig configures NewIPWhoIs. The zero value uses the free ipwho.is endpoint.

type Place

type Place struct {
	City        string `json:"city,omitempty" yaml:"city,omitempty"`
	Region      string `json:"region,omitempty" yaml:"region,omitempty"`
	Country     string `json:"country,omitempty" yaml:"country,omitempty"`
	CountryCode string `json:"country_code,omitempty" yaml:"country_code,omitempty"`
}

Place is the location a Provider reports for an IP address. Any field may be empty.

func FetchIPLocation

func FetchIPLocation(ctx context.Context, baseURL string, addr netip.Addr) *Place

FetchIPLocation looks up addr once with an ipwho.is provider at baseURL (DefaultIPWhoIsURL when empty) and default settings. It is for callers that show a location when one is available and carry on without it: errors, including an invalid baseURL, are logged at WARNING and return nil, as does an address without a location, which is not looked up. Each call builds a new provider, so the 429 backoff of IPWhoIs does not apply; services that look up many addresses should keep one Provider wrapped in a Cache instead.

func Location

func Location(ctx context.Context, addr netip.Addr) (*Place, error)

Location returns the place of addr from the default provider.

func (*Place) String

func (p *Place) String() string

String returns "City, Region, Country" for display, skipping blank names and a name repeated next to itself (a city-state such as Singapore). A nil Place returns "".

type Provider

type Provider interface {
	// Location returns the place of addr. It returns nil and a nil error
	// when addr has no location: addr is not public (see IsPublic), or the
	// service does not know it. An error means the lookup failed (network,
	// timeout, quota) and may succeed later. Implementations must be safe
	// for concurrent use.
	Location(ctx context.Context, addr netip.Addr) (*Place, error)
}

Provider resolves the geographic location of an IP address. IPWhoIs looks it up over HTTP; Cache wraps any Provider and shares answers between addresses of the same network.

func GetDefaultProvider

func GetDefaultProvider() (Provider, error)

GetDefaultProvider returns the default provider. It is initialized on the first call and cached for subsequent calls.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL