| 1 | package console |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "net" |
| 6 | "net/http" |
| 7 | "sort" |
| 8 | "strings" |
| 9 | |
| 10 | "github.com/carlosframework/platform/internal/geo" |
| 11 | "github.com/carlosframework/platform/internal/provision" |
| 12 | ) |
| 13 | |
| 14 | // regionReq is what a create door hands claimApp: never a resolved region. |
| 15 | // The guess is resolved INSIDE claimApp, after it knows whether this is an |
| 16 | // adoption (spec §5.5). ip is request-local and only set for a person. The |
| 17 | // timezone and the address go into the guess and nowhere else: never |
| 18 | // logged, stored, audited or echoed (spec §6.5). |
| 19 | type regionReq struct { |
| 20 | explicit string // a person's choice, record vocabulary |
| 21 | hasExplicit bool |
| 22 | tz string |
| 23 | ip net.IP // request-local, person requests only |
| 24 | } |
| 25 | |
| 26 | // validateExplicitRegion checks a person's explicit region as the instance |
| 27 | // door checks one (spec §8.1): ValidRegion; armed and hosting unless the |
| 28 | // gate is dormant; not provably without the secrets key (R9). The home's |
| 29 | // name normalizes to "". A refusal lists exactly the codes this same check |
| 30 | // would accept now (codex plan-R4 2). |
| 31 | func (s *Server) validateExplicitRegion(ctx context.Context, raw string) (string, *doorRefusal) { |
| 32 | code := s.homeRegionIn(strings.TrimSpace(raw)) |
| 33 | if code == "" { |
| 34 | return "", nil |
| 35 | } |
| 36 | gate := s.readExplicitGate(ctx) |
| 37 | switch gate.refusal(code) { |
| 38 | case "": |
| 39 | return code, nil |
| 40 | case "unknown": |
| 41 | return "", &doorRefusal{http.StatusBadRequest, msgRegionUnknown(code, s.explicitChoices(gate))} |
| 42 | case "not-armed": |
| 43 | return "", &doorRefusal{http.StatusBadRequest, msgRegionNotArmed(code, s.explicitChoices(gate))} |
| 44 | } |
| 45 | return "", &doorRefusal{http.StatusBadRequest, msgNoSecretsKey(code)} |
| 46 | } |
| 47 | |
| 48 | // explicitGate is one read of what an explicit app region must pass. |
| 49 | type explicitGate struct { |
| 50 | caps map[string]placementCapability |
| 51 | published bool |
| 52 | cands []regionCandidate // nil when the edges read failed: R9 cannot refuse |
| 53 | } |
| 54 | |
| 55 | func (s *Server) readExplicitGate(ctx context.Context) explicitGate { |
| 56 | g := explicitGate{} |
| 57 | g.caps, g.published = placements(ctx, s.cfg.Releases) |
| 58 | if snap, err := s.liveEdges(ctx); err == nil { |
| 59 | g.cands = s.regionCands(snap) |
| 60 | } |
| 61 | return g |
| 62 | } |
| 63 | |
| 64 | // refusal is "" when an explicit, record-vocabulary code passes, else |
| 65 | // which bar it fails: "unknown", "not-armed" or "no-key". |
| 66 | func (g explicitGate) refusal(code string) string { |
| 67 | switch { |
| 68 | case code == "": |
| 69 | return "" |
| 70 | case !provision.ValidRegion(code): |
| 71 | return "unknown" |
| 72 | case g.published && !(g.caps[code].Reachable && g.caps[code].Hosting): |
| 73 | return "not-armed" |
| 74 | } |
| 75 | if c, ok := findCandidate(g.cands, code); ok && c.NoKeyProven { |
| 76 | return "no-key" |
| 77 | } |
| 78 | return "" |
| 79 | } |
| 80 | |
| 81 | // explicitChoices is the home's name plus every published code that |
| 82 | // refusal passes, sorted: never a code this door would refuse. |
| 83 | func (s *Server) explicitChoices(g explicitGate) []string { |
| 84 | set := map[string]bool{} |
| 85 | if s.cfg.HomeRegion != "" { |
| 86 | set[s.cfg.HomeRegion] = true |
| 87 | } |
| 88 | for code := range g.caps { |
| 89 | if c := s.homeRegionIn(code); c != "" && g.refusal(c) == "" { |
| 90 | set[c] = true |
| 91 | } |
| 92 | } |
| 93 | out := make([]string, 0, len(set)) |
| 94 | for c := range set { |
| 95 | out = append(out, c) |
| 96 | } |
| 97 | sort.Strings(out) |
| 98 | return out |
| 99 | } |
| 100 | |
| 101 | // resolveNewAppRegion is spec §5.5 and §5.6 for a NEW row. A guess must |
| 102 | // pass the same predicate that builds the C18 and C19 lists |
| 103 | // (explicitGate.refusal), so no door ever stores a region another door |
| 104 | // would refuse to list. |
| 105 | func (s *Server) resolveNewAppRegion(ctx context.Context, rq regionReq, adopting bool) (region, source string) { |
| 106 | if !s.appRegionsOn() { |
| 107 | return "", "default" |
| 108 | } |
| 109 | if rq.hasExplicit { |
| 110 | // Defence in depth for a future door that forgets to validate: a |
| 111 | // code that is not a region never reaches the row. "" is the home. |
| 112 | if rq.explicit != "" && !provision.ValidRegion(rq.explicit) { |
| 113 | return "", "default" |
| 114 | } |
| 115 | return rq.explicit, "chosen" |
| 116 | } |
| 117 | if adopting { |
| 118 | return "", "default" |
| 119 | } |
| 120 | g := s.readExplicitGate(ctx) |
| 121 | if g.cands == nil { |
| 122 | return "", "default" |
| 123 | } |
| 124 | guess, ok := guessRegion(g.cands, func(code string) bool { return g.refusal(code) == "" }, |
| 125 | guessInput{TZ: rq.tz, IP: rq.ip}, s.cfg.GeoIP) |
| 126 | if !ok { |
| 127 | return "", "default" |
| 128 | } |
| 129 | return guess.Code, guess.Source |
| 130 | } |
| 131 | |
| 132 | // placeOf names a record-vocabulary code: its AWS region's place, else the |
| 133 | // code under its name. |
| 134 | func (s *Server) placeOf(code string, cands []regionCandidate) string { |
| 135 | c, _ := findCandidate(cands, code) |
| 136 | return geo.PlaceName(c.AWSRegion, s.homeRegionOut(code)) |
| 137 | } |
| 138 | |
| 139 | // personIP is the request's own address, for a person only (spec §6.2). |
| 140 | // A service or app principal is a server; its address says nothing about |
| 141 | // where a person is. |
| 142 | func (s *Server) personIP(r *http.Request) net.IP { |
| 143 | if p, ok := principalFrom(r.Context()); ok && p.Kind != PrincipalUser { |
| 144 | return nil |
| 145 | } |
| 146 | return net.ParseIP(s.clientIP(r)) |
| 147 | } |
| 148 | |