carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package console
2
3import (
4 "context"
5 "crypto/subtle"
6 "fmt"
7 "github.com/carlosframework/platform/internal/geo"
8 "github.com/carlosframework/platform/internal/geoip"
9 "net"
10 "net/http"
11 "strings"
12)
13
14// autoRegion is the picker's "let the server guess" value. "@" is outside
15// the placement-code grammar, so no real region collides with it (codex
16// R2-3).
17const autoRegion = "@auto"
18
19type regionOption struct{ Value, Label string }
20
21type regionPickerView struct {
22 Auto string // the @auto option's label (C3)
23 Options []regionOption
24 Attribution bool // C24, when the IP step is configured
25 // AttributionText and AttributionURL are the credit the loaded
26 // database requires (geoip spec §13): DB-IP's is C24, GeoLite2's is
27 // MaxMind's.
28 AttributionText, AttributionURL string
29}
30
31// newAppPicker builds the picker, or nil: switch off, an edges read
32// failure, or fewer than two eligible regions (§10). The server's own
33// guess is IP, then home; the page script relabels @auto with the
34// browser's timezone guess.
35func (s *Server) newAppPicker(ctx context.Context, ip net.IP) *regionPickerView {
36 if !s.appRegionsOn() {
37 return nil
38 }
39 files, err := s.liveEdges(ctx)
40 if err != nil {
41 return nil
42 }
43 cands := s.regionCands(files)
44 if eligibleCount(cands) < 2 {
45 return nil
46 }
47 var loc geo.Locator = s.cfg.GeoIP
48 var rec *recordingLocator
49 if s.cfg.GeoIPReader != nil {
50 rec = &recordingLocator{sw: s.cfg.GeoIPReader}
51 loc = rec
52 }
53 g, _ := guessRegion(cands, func(string) bool { return true }, guessInput{IP: ip}, loc)
54 v := &regionPickerView{Auto: fmt.Sprintf(regionCopy.AutoOption, s.placeOf(g.Code, cands))}
55 switch {
56 case rec != nil:
57 // The credit names the database that made THIS guess, read under
58 // the same lock as the lookup (code review R4-4); a guess that used
59 // no GeoIP data owes no credit.
60 if a, ok := geoip.AttributionFor(rec.source); ok && rec.used {
61 v.Attribution, v.AttributionText, v.AttributionURL = true, a.Text, a.URL
62 }
63 case s.cfg.GeoIP != nil:
64 // A bare Locator (tests, older wiring): the IP step is DB-IP's.
65 v.Attribution, v.AttributionText, v.AttributionURL = true, regionCopy.Attribution, regionCopy.AttributionURL
66 }
67 for _, c := range cands {
68 if c.Eligible {
69 v.Options = append(v.Options, regionOption{Value: s.homeRegionOut(c.Code), Label: s.placeOf(c.Code, cands)})
70 }
71 }
72 return v
73}
74
75// pickedRegion turns the form's region field into a regionReq. ok=false
76// means the submitted code is not an acceptable region NOW (C5). It is the
77// API door's check (validateExplicitRegion), because claimApp does not
78// re-validate an explicit region; with the switch off an explicit region is
79// neither validated nor refused (spec §9.6, as apicli.go does).
80func (s *Server) pickedRegion(ctx context.Context, r *http.Request) (regionReq, bool) {
81 rq := regionReq{tz: r.FormValue("tz"), ip: s.personIP(r)}
82 v := strings.TrimSpace(r.FormValue("region"))
83 if v == "" || v == autoRegion || !s.appRegionsOn() {
84 return rq, true
85 }
86 code, ref := s.validateExplicitRegion(ctx, v)
87 if ref != nil {
88 return rq, false
89 }
90 rq.explicit, rq.hasExplicit = code, true
91 return rq, true
92}
93
94// handleNewAppRegion is the page script's guess (spec §9.5): POST with csrf
95// and tz, session-gated, writing nothing. The hints are read from the body,
96// used for this one guess and neither logged nor echoed (spec §6.5).
97func (s *Server) handleNewAppRegion(w http.ResponseWriter, r *http.Request) {
98 _, raw, _, _, ok := s.account(w, r)
99 if !ok {
100 return
101 }
102 if subtle.ConstantTimeCompare([]byte(r.FormValue("csrf")), []byte(raw)) != 1 {
103 jsonError(w, http.StatusForbidden, "Cross-site request blocked.")
104 return
105 }
106 files, err := s.liveEdges(r.Context())
107 cands := homeOnlyCandidates()
108 if err == nil {
109 cands = s.regionCands(files)
110 }
111 g, ok := guessRegion(cands, func(string) bool { return true }, guessInput{TZ: r.FormValue("tz"), IP: s.personIP(r)}, s.cfg.GeoIP)
112 if !ok {
113 g = guessResult{"", "default"}
114 }
115 writeJSON(w, http.StatusOK, map[string]string{
116 "code": s.homeRegionOut(g.Code), "name": s.placeOf(g.Code, cands), "source": g.Source,
117 })
118}
119
120// recordingLocator remembers which database answered the guess's lookup.
121type recordingLocator struct {
122 sw *geo.Swappable
123 source string
124 used bool
125}
126
127func (r *recordingLocator) Locate(ip net.IP) (geo.Point, bool) {
128 p, ok, src := r.sw.LocateFrom(ip)
129 if ok {
130 r.source, r.used = src, true
131 }
132 return p, ok
133}
134