github.com/m-lab/locate@v0.17.6/clientgeo/appengine.go (about)

     1  package clientgeo
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"net/http"
     7  	"strings"
     8  
     9  	"github.com/apex/log"
    10  	"github.com/m-lab/locate/metrics"
    11  	"github.com/m-lab/locate/static"
    12  )
    13  
    14  var (
    15  	// ErrBadLatLonFormat is returned with a lat,lon header is missing or corrupt.
    16  	ErrBadLatLonFormat = errors.New("lat,lon format was missing or corrupt")
    17  
    18  	// ErrNullLatLon is returned with a 0,0 lat/lon value is provided.
    19  	ErrNullLatLon = errors.New("lat,lon value was null: " + nullLatLon)
    20  
    21  	latlonMethod  = "appengine-latlong"
    22  	regionMethod  = "appengine-region"
    23  	countryMethod = "appengine-country"
    24  	noneMethod    = "appengine-none"
    25  	nullLatLon    = "0.000000,0.000000"
    26  )
    27  
    28  // NewAppEngineLocator creates a new AppEngineLocator.
    29  func NewAppEngineLocator() *AppEngineLocator {
    30  	return &AppEngineLocator{}
    31  }
    32  
    33  // AppEngineLocator finds a client location using AppEngine headers for lat/lon,
    34  // region, or country.
    35  type AppEngineLocator struct{}
    36  
    37  // Locate finds a location for the given client request using AppEngine headers.
    38  // If no location is found, an error is returned.
    39  func (sl *AppEngineLocator) Locate(req *http.Request) (*Location, error) {
    40  	headers := req.Header
    41  	fields := log.Fields{
    42  		"CityLatLong": headers.Get("X-AppEngine-CityLatLong"),
    43  		"Country":     headers.Get("X-AppEngine-Country"),
    44  		"Region":      headers.Get("X-AppEngine-Region"),
    45  		"Proto":       headers.Get("X-Forwarded-Proto"),
    46  		"Path":        req.URL.Path,
    47  	}
    48  
    49  	country := headers.Get("X-AppEngine-Country")
    50  	metrics.AppEngineTotal.WithLabelValues(country).Inc()
    51  
    52  	// First, try the given lat/lon. Avoid invalid values like 0,0.
    53  	latlon := headers.Get("X-AppEngine-CityLatLong")
    54  	loc, err := splitLatLon(latlon)
    55  	if err == nil {
    56  		log.WithFields(fields).Info(latlonMethod)
    57  		loc.Headers.Set(hLocateClientlatlon, latlon)
    58  		loc.Headers.Set(hLocateClientlatlonMethod, latlonMethod)
    59  		return loc, nil
    60  	}
    61  	// The next two fallback methods require the country, so check this next.
    62  	if country == "" || static.Countries[country] == "" {
    63  		// Without a valid country value, we can neither lookup the
    64  		// region nor country.
    65  		log.WithFields(fields).Info(noneMethod)
    66  		loc.Headers.Set(hLocateClientlatlonMethod, noneMethod)
    67  		return loc, errors.New(hLocateClientlatlonMethod + ": " + noneMethod)
    68  	}
    69  	// Second, country is valid, so try to lookup region.
    70  	region := strings.ToUpper(headers.Get("X-AppEngine-Region"))
    71  	if region != "" && static.Regions[country+"-"+region] != "" {
    72  		latlon = static.Regions[country+"-"+region]
    73  		log.WithFields(fields).Info(regionMethod)
    74  		loc, err := splitLatLon(latlon)
    75  		loc.Headers.Set(hLocateClientlatlon, latlon)
    76  		loc.Headers.Set(hLocateClientlatlonMethod, regionMethod)
    77  		return loc, err
    78  	}
    79  	// Third, region was not found, fallback to using the country.
    80  	latlon = static.Countries[country]
    81  	log.WithFields(fields).Info(countryMethod)
    82  	loc, err = splitLatLon(latlon)
    83  	loc.Headers.Set(hLocateClientlatlon, latlon)
    84  	loc.Headers.Set(hLocateClientlatlonMethod, countryMethod)
    85  	return loc, err
    86  }
    87  
    88  // Reload does nothing.
    89  func (sl *AppEngineLocator) Reload(ctx context.Context) {}
    90  
    91  // splitLatLon attempts to split the "<lat>,<lon>" string provided by AppEngine
    92  // into two fields. The return values preserve the original lat,lon order.
    93  func splitLatLon(latlon string) (*Location, error) {
    94  	loc := &Location{
    95  		// The empty header type is nil, so we set it.
    96  		Headers: http.Header{},
    97  	}
    98  	if latlon == nullLatLon {
    99  		return loc, ErrNullLatLon
   100  	}
   101  	fields := strings.Split(latlon, ",")
   102  	if len(fields) != 2 {
   103  		return loc, ErrBadLatLonFormat
   104  	}
   105  	loc.Latitude = fields[0]
   106  	loc.Longitude = fields[1]
   107  	return loc, nil
   108  }