package console import ( "context" "crypto/subtle" "fmt" "github.com/carlosframework/platform/internal/geo" "github.com/carlosframework/platform/internal/geoip" "net" "net/http" "strings" ) // autoRegion is the picker's "let the server guess" value. "@" is outside // the placement-code grammar, so no real region collides with it (codex // R2-3). const autoRegion = "@auto" type regionOption struct{ Value, Label string } type regionPickerView struct { Auto string // the @auto option's label (C3) Options []regionOption Attribution bool // C24, when the IP step is configured // AttributionText and AttributionURL are the credit the loaded // database requires (geoip spec §13): DB-IP's is C24, GeoLite2's is // MaxMind's. AttributionText, AttributionURL string } // newAppPicker builds the picker, or nil: switch off, an edges read // failure, or fewer than two eligible regions (§10). The server's own // guess is IP, then home; the page script relabels @auto with the // browser's timezone guess. func (s *Server) newAppPicker(ctx context.Context, ip net.IP) *regionPickerView { if !s.appRegionsOn() { return nil } files, err := s.liveEdges(ctx) if err != nil { return nil } cands := s.regionCands(files) if eligibleCount(cands) < 2 { return nil } var loc geo.Locator = s.cfg.GeoIP var rec *recordingLocator if s.cfg.GeoIPReader != nil { rec = &recordingLocator{sw: s.cfg.GeoIPReader} loc = rec } g, _ := guessRegion(cands, func(string) bool { return true }, guessInput{IP: ip}, loc) v := ®ionPickerView{Auto: fmt.Sprintf(regionCopy.AutoOption, s.placeOf(g.Code, cands))} switch { case rec != nil: // The credit names the database that made THIS guess, read under // the same lock as the lookup (code review R4-4); a guess that used // no GeoIP data owes no credit. if a, ok := geoip.AttributionFor(rec.source); ok && rec.used { v.Attribution, v.AttributionText, v.AttributionURL = true, a.Text, a.URL } case s.cfg.GeoIP != nil: // A bare Locator (tests, older wiring): the IP step is DB-IP's. v.Attribution, v.AttributionText, v.AttributionURL = true, regionCopy.Attribution, regionCopy.AttributionURL } for _, c := range cands { if c.Eligible { v.Options = append(v.Options, regionOption{Value: s.homeRegionOut(c.Code), Label: s.placeOf(c.Code, cands)}) } } return v } // pickedRegion turns the form's region field into a regionReq. ok=false // means the submitted code is not an acceptable region NOW (C5). It is the // API door's check (validateExplicitRegion), because claimApp does not // re-validate an explicit region; with the switch off an explicit region is // neither validated nor refused (spec §9.6, as apicli.go does). func (s *Server) pickedRegion(ctx context.Context, r *http.Request) (regionReq, bool) { rq := regionReq{tz: r.FormValue("tz"), ip: s.personIP(r)} v := strings.TrimSpace(r.FormValue("region")) if v == "" || v == autoRegion || !s.appRegionsOn() { return rq, true } code, ref := s.validateExplicitRegion(ctx, v) if ref != nil { return rq, false } rq.explicit, rq.hasExplicit = code, true return rq, true } // handleNewAppRegion is the page script's guess (spec §9.5): POST with csrf // and tz, session-gated, writing nothing. The hints are read from the body, // used for this one guess and neither logged nor echoed (spec §6.5). func (s *Server) handleNewAppRegion(w http.ResponseWriter, r *http.Request) { _, raw, _, _, ok := s.account(w, r) if !ok { return } if subtle.ConstantTimeCompare([]byte(r.FormValue("csrf")), []byte(raw)) != 1 { jsonError(w, http.StatusForbidden, "Cross-site request blocked.") return } files, err := s.liveEdges(r.Context()) cands := homeOnlyCandidates() if err == nil { cands = s.regionCands(files) } g, ok := guessRegion(cands, func(string) bool { return true }, guessInput{TZ: r.FormValue("tz"), IP: s.personIP(r)}, s.cfg.GeoIP) if !ok { g = guessResult{"", "default"} } writeJSON(w, http.StatusOK, map[string]string{ "code": s.homeRegionOut(g.Code), "name": s.placeOf(g.Code, cands), "source": g.Source, }) } // recordingLocator remembers which database answered the guess's lookup. type recordingLocator struct { sw *geo.Swappable source string used bool } func (r *recordingLocator) Locate(ip net.IP) (geo.Point, bool) { p, ok, src := r.sw.LocateFrom(ip) if ok { r.source, r.used = src, true } return p, ok }