// `carlos geo `: what the platform's GeoIP database says about an // address (geoip spec §11.2). API mode only. Its strings await copy review. package main import ( "context" "encoding/json" "flag" "fmt" "io" "log" "net/http" "net/url" "os" ) const geoUsage = `usage: carlos geo --app [flags] What the platform's GeoIP database says about an address: the same answer the X-Carlos-Geo-* headers carry, for an address that is not the current request. --json prints the console's answer as it is. ` type apiGeoResponse struct { Found bool `json:"found"` Country string `json:"country"` Subdivision string `json:"subdivision"` Lat *float64 `json:"lat"` Lon *float64 `json:"lon"` Source string `json:"source"` Attribution string `json:"attribution"` AttributionURL string `json:"attribution_url"` } func runGeo(args []string) { fs := flag.NewFlagSet("geo", flag.ExitOnError) app := appFlag(fs, "app whose credential asks") asJSON := fs.Bool("json", false, "print the console's answer as JSON") consoleFlag := consoleOption(fs, "console to ask (default: the only logged-in one)") acctRaw := fs.String("account", "", "account (sqid or name) the app belongs to (default: your only account, or the configured/chosen one)") acctIDRaw := fs.String("account-id", "", "account sqid, matched as a sqid ONLY — supersedes --account") fs.Usage = flagUsage(fs, geoUsage) fs.Parse(ipLast(args)) if fs.NArg() != 1 { fs.Usage() os.Exit(2) } sess := requireAPISession("geo", *consoleFlag) acct := resolvedAccount(sess, acctRaw, acctIDRaw) app2 := resolvedApp(sess, acct, *app, geoUsage) path := apiPath(acct, app2, "/geo") + "?ip=" + url.QueryEscape(fs.Arg(0)) st, body, err := sess.getJSON(context.Background(), path) if err != nil { log.Fatalf("geo: %v", err) } if st != http.StatusOK { log.Fatalf("geo: %v", apiErrorFromBody(st, body)) } if *asJSON { stdout.Write(body) fmt.Fprintln(stdout) return } var res apiGeoResponse if err := json.Unmarshal(body, &res); err != nil { log.Fatalf("geo: unreadable response: %v", err) } printGeo(stdout, fs.Arg(0), res) } func printGeo(w io.Writer, ip string, res apiGeoResponse) { if !res.Found { fmt.Fprintf(w, "%s: no location. the database has no answer for this address\n", ip) return } fmt.Fprintf(w, "%s\n", ip) if res.Country != "" { fmt.Fprintf(w, " country %s\n", res.Country) } if res.Subdivision != "" { fmt.Fprintf(w, " subdivision %s\n", res.Subdivision) } if res.Lat != nil && res.Lon != nil { fmt.Fprintf(w, " point %.2f, %.2f (an area, not a position)\n", *res.Lat, *res.Lon) } fmt.Fprintf(w, " source %s\n", res.Source) if res.Attribution != "" { fmt.Fprintf(w, " credit %s (%s)\n", res.Attribution, res.AttributionURL) } } // ipLast moves a leading address after the flags, so both // `carlos geo 1.2.3.4 --app a` and `carlos geo --app a 1.2.3.4` work // (Go's flag package stops at the first non-flag). func ipLast(args []string) []string { if len(args) > 0 && len(args[0]) > 0 && args[0][0] != '-' { return append(append([]string(nil), args[1:]...), args[0]) } return args }