carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1// `carlos geo <ip>`: what the platform's GeoIP database says about an
2// address (geoip spec §11.2). API mode only. Its strings await copy review.
3package main
4
5import (
6 "context"
7 "encoding/json"
8 "flag"
9 "fmt"
10 "io"
11 "log"
12 "net/http"
13 "net/url"
14 "os"
15)
16
17const geoUsage = `usage: carlos geo <ip> --app <a> [flags]
18
19What the platform's GeoIP database says about an address: the same
20answer the X-Carlos-Geo-* headers carry, for an address that is not the
21current request. --json prints the console's answer as it is.
22`
23
24type apiGeoResponse struct {
25 Found bool `json:"found"`
26 Country string `json:"country"`
27 Subdivision string `json:"subdivision"`
28 Lat *float64 `json:"lat"`
29 Lon *float64 `json:"lon"`
30 Source string `json:"source"`
31 Attribution string `json:"attribution"`
32 AttributionURL string `json:"attribution_url"`
33}
34
35func runGeo(args []string) {
36 fs := flag.NewFlagSet("geo", flag.ExitOnError)
37 app := appFlag(fs, "app whose credential asks")
38 asJSON := fs.Bool("json", false, "print the console's answer as JSON")
39 consoleFlag := consoleOption(fs, "console to ask (default: the only logged-in one)")
40 acctRaw := fs.String("account", "", "account (sqid or name) the app belongs to (default: your only account, or the configured/chosen one)")
41 acctIDRaw := fs.String("account-id", "", "account sqid, matched as a sqid ONLY — supersedes --account")
42 fs.Usage = flagUsage(fs, geoUsage)
43 fs.Parse(ipLast(args))
44 if fs.NArg() != 1 {
45 fs.Usage()
46 os.Exit(2)
47 }
48 sess := requireAPISession("geo", *consoleFlag)
49 acct := resolvedAccount(sess, acctRaw, acctIDRaw)
50 app2 := resolvedApp(sess, acct, *app, geoUsage)
51 path := apiPath(acct, app2, "/geo") + "?ip=" + url.QueryEscape(fs.Arg(0))
52 st, body, err := sess.getJSON(context.Background(), path)
53 if err != nil {
54 log.Fatalf("geo: %v", err)
55 }
56 if st != http.StatusOK {
57 log.Fatalf("geo: %v", apiErrorFromBody(st, body))
58 }
59 if *asJSON {
60 stdout.Write(body)
61 fmt.Fprintln(stdout)
62 return
63 }
64 var res apiGeoResponse
65 if err := json.Unmarshal(body, &res); err != nil {
66 log.Fatalf("geo: unreadable response: %v", err)
67 }
68 printGeo(stdout, fs.Arg(0), res)
69}
70
71func printGeo(w io.Writer, ip string, res apiGeoResponse) {
72 if !res.Found {
73 fmt.Fprintf(w, "%s: no location. the database has no answer for this address\n", ip)
74 return
75 }
76 fmt.Fprintf(w, "%s\n", ip)
77 if res.Country != "" {
78 fmt.Fprintf(w, " country %s\n", res.Country)
79 }
80 if res.Subdivision != "" {
81 fmt.Fprintf(w, " subdivision %s\n", res.Subdivision)
82 }
83 if res.Lat != nil && res.Lon != nil {
84 fmt.Fprintf(w, " point %.2f, %.2f (an area, not a position)\n", *res.Lat, *res.Lon)
85 }
86 fmt.Fprintf(w, " source %s\n", res.Source)
87 if res.Attribution != "" {
88 fmt.Fprintf(w, " credit %s (%s)\n", res.Attribution, res.AttributionURL)
89 }
90}
91
92// ipLast moves a leading address after the flags, so both
93// `carlos geo 1.2.3.4 --app a` and `carlos geo --app a 1.2.3.4` work
94// (Go's flag package stops at the first non-flag).
95func ipLast(args []string) []string {
96 if len(args) > 0 && len(args[0]) > 0 && args[0][0] != '-' {
97 return append(append([]string(nil), args[1:]...), args[0])
98 }
99 return args
100}
101