| 1 | package console |
| 2 | |
| 3 | // GET /api/cli/apps/{account}/{app}/geo?ip=<addr> (geoip spec §11.1): the |
| 4 | // platform's GeoIP answer for an arbitrary address, for work that is not |
| 5 | // the current request (batch jobs, logs, webhooks). The same fields as the |
| 6 | // X-Carlos-Geo-* headers, plus the attribution the answering database's |
| 7 | // licence requires. The queried address is never logged. |
| 8 | |
| 9 | import ( |
| 10 | "net" |
| 11 | "net/http" |
| 12 | |
| 13 | "github.com/carlosframework/platform/internal/geoip" |
| 14 | ) |
| 15 | |
| 16 | type geoResponse struct { |
| 17 | Found bool `json:"found"` |
| 18 | Country string `json:"country,omitempty"` |
| 19 | Subdivision string `json:"subdivision,omitempty"` |
| 20 | Lat *float64 `json:"lat,omitempty"` |
| 21 | Lon *float64 `json:"lon,omitempty"` |
| 22 | Source string `json:"source,omitempty"` |
| 23 | Attribution string `json:"attribution,omitempty"` |
| 24 | AttributionURL string `json:"attribution_url,omitempty"` |
| 25 | } |
| 26 | |
| 27 | func (s *Server) handleAPIGeo(w http.ResponseWriter, r *http.Request) { |
| 28 | // The instance family's gate: the app's member, app and instance |
| 29 | // tokens, and its per-caller budget of 60 a minute. |
| 30 | if _, _, _, ok := s.instanceGateFor(w, r, "geo"); !ok { |
| 31 | return |
| 32 | } |
| 33 | ip := net.ParseIP(r.URL.Query().Get("ip")) |
| 34 | if ip == nil { |
| 35 | jsonError(w, http.StatusBadRequest, "ip: want an IPv4 or IPv6 address") |
| 36 | return |
| 37 | } |
| 38 | if s.cfg.GeoIPReader == nil { |
| 39 | jsonError(w, http.StatusServiceUnavailable, "geo unavailable: this deployment has no GeoIP database") |
| 40 | return |
| 41 | } |
| 42 | // One snapshot: the answer, its source and its attribution all come |
| 43 | // from the same reader (spec §5.4). |
| 44 | a := geoip.FromSnapshot(s.cfg.GeoIPReader.Lookup(ip)) |
| 45 | switch a.State { |
| 46 | case geoip.StateOff: |
| 47 | jsonError(w, http.StatusServiceUnavailable, "geo unavailable: this deployment has no GeoIP database") |
| 48 | return |
| 49 | case geoip.StateMiss: |
| 50 | writeJSON(w, http.StatusOK, geoResponse{Found: false}) |
| 51 | return |
| 52 | } |
| 53 | out := geoResponse{Found: true, Country: a.Country, Subdivision: a.Subdivision, Source: a.Source} |
| 54 | if a.HasPoint { |
| 55 | lat, lon := a.Lat, a.Lon |
| 56 | out.Lat, out.Lon = &lat, &lon |
| 57 | } |
| 58 | if at, ok := geoip.AttributionFor(a.Source); ok { |
| 59 | out.Attribution, out.AttributionURL = at.Text, at.URL |
| 60 | } |
| 61 | writeJSON(w, http.StatusOK, out) |
| 62 | } |
| 63 | |