proxy_api.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563
  1. package server
  2. import (
  3. "context"
  4. "encoding/json"
  5. "errors"
  6. "net/http"
  7. "strings"
  8. "time"
  9. "vocat/internal/i18n"
  10. localproxy "vocat/internal/proxy"
  11. "vocat/internal/store"
  12. )
  13. func (s *Server) routeProxyAPI(w http.ResponseWriter, r *http.Request, cleanPath string) bool {
  14. switch cleanPath {
  15. case "upstream-proxies":
  16. s.handleUpstreamProxies(w, r)
  17. case "upstream-proxy-probe":
  18. s.handleUpstreamProbeConfig(w, r)
  19. case "upstream-proxy-countries":
  20. if !requireMethod(w, r, http.MethodGet) {
  21. return true
  22. }
  23. writeJSON(w, http.StatusOK, map[string]any{"data": proxyCountries})
  24. case "upstream-proxy-country-rules":
  25. s.handleCountryRules(w, r)
  26. case "upstream-proxy-device-bindings":
  27. s.handleDeviceProxyBindings(w, r)
  28. default:
  29. segments := splitAPIPath(cleanPath)
  30. switch {
  31. case len(segments) == 2 && segments[0] == "upstream-proxies":
  32. s.handleUpstreamProxy(w, r, segments[1])
  33. case len(segments) == 4 &&
  34. segments[0] == "upstream-proxies" &&
  35. segments[2] == "actions" &&
  36. segments[3] == "probe":
  37. s.handleUpstreamProbe(w, r, segments[1])
  38. case len(segments) == 2 && segments[0] == "upstream-proxy-country-rules":
  39. s.handleCountryRule(w, r, segments[1])
  40. case len(segments) == 2 && segments[0] == "upstream-proxy-device-bindings":
  41. s.handleDeviceProxyBinding(w, r, segments[1])
  42. default:
  43. return false
  44. }
  45. }
  46. return true
  47. }
  48. type upstreamProxyPayload struct {
  49. ID string `json:"id"`
  50. Name string `json:"name"`
  51. Addr string `json:"addr"`
  52. Username string `json:"username"`
  53. Password string `json:"password"`
  54. Enabled bool `json:"enabled"`
  55. }
  56. func (s *Server) handleUpstreamProxies(w http.ResponseWriter, r *http.Request) {
  57. switch r.Method {
  58. case http.MethodGet:
  59. values, err := s.store.ListUpstreamProxies(r.Context())
  60. if err != nil {
  61. s.writeStoreError(w, err)
  62. return
  63. }
  64. result := make([]map[string]any, 0, len(values))
  65. for _, value := range values {
  66. result = append(result, upstreamProxyResponse(value.Redacted()))
  67. }
  68. writeJSON(w, http.StatusOK, map[string]any{"data": result})
  69. case http.MethodPost:
  70. var payload upstreamProxyPayload
  71. if err := s.decodeJSON(w, r, &payload); err != nil {
  72. writeError(w, http.StatusBadRequest, "invalid_request", err.Error())
  73. return
  74. }
  75. if !validObjectID(payload.ID) {
  76. writeError(w, http.StatusBadRequest, "invalid_proxy_id", "proxy ID must use 1-64 safe characters")
  77. return
  78. }
  79. s.saveAndProbeUpstream(w, r, payload)
  80. default:
  81. w.Header().Set("Allow", "GET, POST")
  82. writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "method not allowed")
  83. }
  84. }
  85. func (s *Server) handleUpstreamProxy(w http.ResponseWriter, r *http.Request, id string) {
  86. switch r.Method {
  87. case http.MethodPut:
  88. var payload upstreamProxyPayload
  89. if err := s.decodeJSON(w, r, &payload); err != nil {
  90. writeError(w, http.StatusBadRequest, "invalid_request", err.Error())
  91. return
  92. }
  93. if payload.ID != "" && payload.ID != id {
  94. writeError(w, http.StatusConflict, "immutable_proxy_id", "upstream proxy ID cannot be changed")
  95. return
  96. }
  97. payload.ID = id
  98. s.saveAndProbeUpstream(w, r, payload)
  99. case http.MethodDelete:
  100. bindings, listErr := s.store.ListDeviceProxyBindings(r.Context())
  101. if listErr != nil {
  102. s.writeStoreError(w, listErr)
  103. return
  104. }
  105. if err := s.store.DeleteUpstreamProxy(r.Context(), id); err != nil {
  106. s.writeStoreError(w, err)
  107. return
  108. }
  109. for _, binding := range bindings {
  110. if binding.UpstreamProxyID == id {
  111. s.requestProxyRouteReconnect(binding.DeviceID)
  112. }
  113. }
  114. writeJSON(w, http.StatusOK, map[string]any{"data": map[string]any{"deleted": true}})
  115. default:
  116. w.Header().Set("Allow", "PUT, DELETE")
  117. writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "method not allowed")
  118. }
  119. }
  120. func (s *Server) handleDeviceProxyBindings(w http.ResponseWriter, r *http.Request) {
  121. if !requireMethod(w, r, http.MethodGet) {
  122. return
  123. }
  124. values, err := s.store.ListDeviceProxyBindings(r.Context())
  125. if err != nil {
  126. s.writeStoreError(w, err)
  127. return
  128. }
  129. result := make([]map[string]any, 0, len(values))
  130. for _, value := range values {
  131. result = append(result, deviceProxyBindingResponse(value))
  132. }
  133. writeJSON(w, http.StatusOK, map[string]any{"data": result})
  134. }
  135. func (s *Server) handleDeviceProxyBinding(w http.ResponseWriter, r *http.Request, deviceID string) {
  136. deviceID = strings.TrimSpace(deviceID)
  137. if !validDeviceID(deviceID) {
  138. writeError(w, http.StatusBadRequest, "invalid_device_id", "device ID must use 1-64 safe characters")
  139. return
  140. }
  141. if _, err := s.store.Device(r.Context(), deviceID); err != nil {
  142. s.writeStoreError(w, err)
  143. return
  144. }
  145. switch r.Method {
  146. case http.MethodPut:
  147. var request struct {
  148. UpstreamProxyID string `json:"upstream_proxy_id"`
  149. }
  150. if err := s.decodeJSON(w, r, &request); err != nil {
  151. writeError(w, http.StatusBadRequest, "invalid_request", err.Error())
  152. return
  153. }
  154. request.UpstreamProxyID = strings.TrimSpace(request.UpstreamProxyID)
  155. upstream, err := s.store.UpstreamProxy(r.Context(), request.UpstreamProxyID)
  156. if err != nil {
  157. s.writeStoreError(w, err)
  158. return
  159. }
  160. if !upstream.Enabled {
  161. writeError(w, http.StatusConflict, "upstream_proxy_disabled", "enable the upstream proxy before binding a device")
  162. return
  163. }
  164. // Once bound, a device may not be silently rebinded to a different
  165. // upstream proxy. Force the caller to DELETE first so the change is
  166. // intentional. Re-binding the same upstream stays idempotent.
  167. if existing, err := s.store.DeviceProxyBinding(r.Context(), deviceID); err == nil && existing.UpstreamProxyID != upstream.ID {
  168. writeError(w, http.StatusConflict, "device_already_bound", "device is already bound to another upstream proxy; delete the binding first")
  169. return
  170. } else if err != nil && !errors.Is(err, store.ErrNotFound) {
  171. s.writeStoreError(w, err)
  172. return
  173. }
  174. value := store.DeviceProxyBinding{DeviceID: deviceID, UpstreamProxyID: upstream.ID}
  175. if err := s.store.UpsertDeviceProxyBinding(r.Context(), value); err != nil {
  176. s.writeStoreError(w, err)
  177. return
  178. }
  179. reconnected, reconnectErr := s.requestProxyRouteReconnect(deviceID)
  180. response := deviceProxyBindingResponse(value)
  181. response["reconnect_requested"] = reconnected
  182. if reconnectErr != nil {
  183. response["reconnect_error"] = reconnectErr.Error()
  184. }
  185. writeJSON(w, http.StatusOK, map[string]any{"data": response})
  186. case http.MethodDelete:
  187. if err := s.store.DeleteDeviceProxyBinding(r.Context(), deviceID); err != nil {
  188. s.writeStoreError(w, err)
  189. return
  190. }
  191. reconnected, reconnectErr := s.requestProxyRouteReconnect(deviceID)
  192. response := map[string]any{"deleted": true, "reconnect_requested": reconnected}
  193. if reconnectErr != nil {
  194. response["reconnect_error"] = reconnectErr.Error()
  195. }
  196. writeJSON(w, http.StatusOK, map[string]any{"data": response})
  197. default:
  198. w.Header().Set("Allow", "PUT, DELETE")
  199. writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "method not allowed")
  200. }
  201. }
  202. // A binding is already durable before this is called. Reconnect failures are
  203. // returned as advisory information: the chosen route will still be used on
  204. // the next VoWiFi start/reconnect.
  205. func (s *Server) requestProxyRouteReconnect(deviceID string) (bool, error) {
  206. if s.vowifi == nil {
  207. return false, nil
  208. }
  209. config, err := s.store.Device(context.Background(), deviceID)
  210. if err != nil {
  211. return false, err
  212. }
  213. if !config.VoWiFiEnabled {
  214. return false, nil
  215. }
  216. if _, err := s.vowifi.RequestReconnect(deviceID); err != nil {
  217. s.logger.Warn("VoWiFi proxy route saved but immediate reconnect was not started", "device_id", deviceID, "error", err)
  218. return false, err
  219. }
  220. return true, nil
  221. }
  222. func (s *Server) saveAndProbeUpstream(
  223. w http.ResponseWriter,
  224. r *http.Request,
  225. payload upstreamProxyPayload,
  226. ) {
  227. value := store.UpstreamProxy{
  228. ID: payload.ID,
  229. Name: payload.Name,
  230. Addr: payload.Addr,
  231. Username: payload.Username,
  232. Password: payload.Password,
  233. Enabled: payload.Enabled,
  234. }
  235. if err := s.store.UpsertUpstreamProxy(r.Context(), value); err != nil {
  236. s.writeStoreError(w, err)
  237. return
  238. }
  239. saved, err := s.store.UpstreamProxy(r.Context(), value.ID)
  240. if err != nil {
  241. s.writeStoreError(w, err)
  242. return
  243. }
  244. bindings, err := s.store.ListDeviceProxyBindings(r.Context())
  245. if err != nil {
  246. s.writeStoreError(w, err)
  247. return
  248. }
  249. for _, binding := range bindings {
  250. if binding.UpstreamProxyID == saved.ID {
  251. s.requestProxyRouteReconnect(binding.DeviceID)
  252. }
  253. }
  254. probe, probeErr := localproxy.ProbeSOCKS5(
  255. r.Context(),
  256. saved.Addr,
  257. saved.Username,
  258. saved.Password,
  259. 8*time.Second,
  260. )
  261. probeResponse := probeMap(probe, probeErr)
  262. message := i18n.T("代理已保存;UDP ASSOCIATE 尚未通过。")
  263. if probeErr == nil && probe.UDPAssociateOK {
  264. message = i18n.T("代理已保存,SOCKS5 认证与 UDP ASSOCIATE 均通过。")
  265. }
  266. writeJSON(w, http.StatusOK, map[string]any{
  267. "data": map[string]any{
  268. "status": "saved",
  269. "proxy": upstreamProxyResponse(saved.Redacted()),
  270. "probe": probeResponse,
  271. "message": message,
  272. },
  273. })
  274. }
  275. func (s *Server) handleUpstreamProbe(w http.ResponseWriter, r *http.Request, id string) {
  276. if !requireMethod(w, r, http.MethodPost) {
  277. return
  278. }
  279. value, err := s.store.UpstreamProxy(r.Context(), id)
  280. if err != nil {
  281. s.writeStoreError(w, err)
  282. return
  283. }
  284. result, probeErr := localproxy.ProbeSOCKS5(
  285. r.Context(),
  286. value.Addr,
  287. value.Username,
  288. value.Password,
  289. 8*time.Second,
  290. )
  291. message := i18n.T("代理不能承载 VoWiFi 所需的 UDP。")
  292. if probeErr == nil && result.UDPAssociateOK {
  293. message = i18n.T("SOCKS5 认证与 UDP ASSOCIATE 探测通过。")
  294. }
  295. writeJSON(w, http.StatusOK, map[string]any{
  296. "data": map[string]any{
  297. "status": "probed",
  298. "probe": probeMap(result, probeErr),
  299. "message": message,
  300. },
  301. })
  302. }
  303. // handleUpstreamProbeConfig probes a front proxy straight from the editor's
  304. // form values, so connectivity (above all UDP ASSOCIATE, which VoWiFi depends
  305. // on) can be verified before the proxy is ever saved. When the form edits an
  306. // existing proxy and leaves the password blank (meaning "keep the stored
  307. // secret"), the stored record supplies the missing credentials.
  308. func (s *Server) handleUpstreamProbeConfig(w http.ResponseWriter, r *http.Request) {
  309. if !requireMethod(w, r, http.MethodPost) {
  310. return
  311. }
  312. var payload upstreamProxyPayload
  313. if err := s.decodeJSON(w, r, &payload); err != nil {
  314. writeError(w, http.StatusBadRequest, "invalid_request", err.Error())
  315. return
  316. }
  317. addr := strings.TrimSpace(payload.Addr)
  318. username := strings.TrimSpace(payload.Username)
  319. password := payload.Password
  320. if id := strings.TrimSpace(payload.ID); id != "" {
  321. if stored, err := s.store.UpstreamProxy(r.Context(), id); err == nil {
  322. if addr == "" {
  323. addr = stored.Addr
  324. }
  325. if username == "" {
  326. username = stored.Username
  327. }
  328. if password == "" || password == store.SecretMask {
  329. password = stored.Password
  330. }
  331. }
  332. }
  333. if addr == "" {
  334. writeError(w, http.StatusBadRequest, "invalid_proxy_addr", "Socks5 address is required")
  335. return
  336. }
  337. result, probeErr := localproxy.ProbeSOCKS5(
  338. r.Context(),
  339. addr,
  340. username,
  341. password,
  342. 8*time.Second,
  343. )
  344. message := i18n.T("代理不能承载 VoWiFi 所需的 UDP。")
  345. if probeErr == nil && result.UDPAssociateOK {
  346. message = i18n.T("SOCKS5 认证与 UDP ASSOCIATE 探测通过。")
  347. }
  348. writeJSON(w, http.StatusOK, map[string]any{
  349. "data": map[string]any{
  350. "status": "probed",
  351. "probe": probeMap(result, probeErr),
  352. "message": message,
  353. },
  354. })
  355. }
  356. func (s *Server) handleCountryRules(w http.ResponseWriter, r *http.Request) {
  357. if !requireMethod(w, r, http.MethodGet) {
  358. return
  359. }
  360. values, err := s.store.ListCountryRules(r.Context())
  361. if err != nil {
  362. s.writeStoreError(w, err)
  363. return
  364. }
  365. result := make([]map[string]any, 0, len(values))
  366. for _, value := range values {
  367. result = append(result, countryRuleResponse(value))
  368. }
  369. writeJSON(w, http.StatusOK, map[string]any{"data": result})
  370. }
  371. func (s *Server) handleCountryRule(w http.ResponseWriter, r *http.Request, countryCode string) {
  372. countryCode = strings.ToUpper(strings.TrimSpace(countryCode))
  373. switch r.Method {
  374. case http.MethodPut:
  375. var request struct {
  376. UpstreamProxyID string `json:"upstream_proxy_id"`
  377. Enabled bool `json:"enabled"`
  378. }
  379. if err := s.decodeJSON(w, r, &request); err != nil {
  380. writeError(w, http.StatusBadRequest, "invalid_request", err.Error())
  381. return
  382. }
  383. country := countryByCode(countryCode)
  384. if country == nil {
  385. writeError(w, http.StatusBadRequest, "invalid_country", "country code is not in the supported MCC table")
  386. return
  387. }
  388. if _, err := s.store.UpstreamProxy(r.Context(), request.UpstreamProxyID); err != nil {
  389. s.writeStoreError(w, err)
  390. return
  391. }
  392. value := store.CountryRule{
  393. CountryCode: countryCode,
  394. CountryName: country.Name,
  395. UpstreamProxyID: request.UpstreamProxyID,
  396. Enabled: request.Enabled,
  397. }
  398. if err := s.store.UpsertCountryRule(r.Context(), value); err != nil {
  399. s.writeStoreError(w, err)
  400. return
  401. }
  402. writeJSON(w, http.StatusOK, map[string]any{"data": countryRuleResponse(value)})
  403. case http.MethodDelete:
  404. if err := s.store.DeleteCountryRule(r.Context(), countryCode); err != nil {
  405. s.writeStoreError(w, err)
  406. return
  407. }
  408. writeJSON(w, http.StatusOK, map[string]any{"data": map[string]any{"deleted": true}})
  409. default:
  410. w.Header().Set("Allow", "PUT, DELETE")
  411. writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "method not allowed")
  412. }
  413. }
  414. func upstreamProxyResponse(value store.UpstreamProxy) map[string]any {
  415. return map[string]any{
  416. "id": value.ID,
  417. "name": value.Name,
  418. "addr": value.Addr,
  419. "username": value.Username,
  420. "password": value.Password,
  421. "enabled": value.Enabled,
  422. }
  423. }
  424. func countryRuleResponse(value store.CountryRule) map[string]any {
  425. return map[string]any{
  426. "country_code": value.CountryCode,
  427. "country_name": value.CountryName,
  428. "upstream_proxy_id": value.UpstreamProxyID,
  429. "enabled": value.Enabled,
  430. }
  431. }
  432. func deviceProxyBindingResponse(value store.DeviceProxyBinding) map[string]any {
  433. return map[string]any{
  434. "device_id": value.DeviceID,
  435. "upstream_proxy_id": value.UpstreamProxyID,
  436. }
  437. }
  438. func probeMap(result localproxy.ProbeResult, err error) map[string]any {
  439. encoded, _ := json.Marshal(result)
  440. var response map[string]any
  441. _ = json.Unmarshal(encoded, &response)
  442. if err != nil {
  443. response["error"] = err.Error()
  444. }
  445. return response
  446. }
  447. func validObjectID(value string) bool {
  448. return validDeviceID(value)
  449. }
  450. type proxyCountry struct {
  451. Code string
  452. Name string
  453. MCCs []string
  454. }
  455. func (country proxyCountry) MarshalJSON() ([]byte, error) {
  456. return json.Marshal(map[string]any{
  457. "country_code": country.Code,
  458. "country_name": i18n.T(country.Name),
  459. "mccs": country.MCCs,
  460. })
  461. }
  462. func countryByCode(code string) *proxyCountry {
  463. for index := range proxyCountries {
  464. if proxyCountries[index].Code == code {
  465. return &proxyCountries[index]
  466. }
  467. }
  468. return nil
  469. }
  470. // countryNameForMCC resolves a mobile country code to a display name using the
  471. // shared MCC table. It returns an empty string for an unknown or empty MCC.
  472. func countryNameForMCC(mcc string) string {
  473. if mcc == "" {
  474. return ""
  475. }
  476. for index := range proxyCountries {
  477. for _, candidate := range proxyCountries[index].MCCs {
  478. if candidate == mcc {
  479. return i18n.T(proxyCountries[index].Name)
  480. }
  481. }
  482. }
  483. return ""
  484. }
  485. var proxyCountries = []proxyCountry{
  486. {Code: "CN", Name: "中国", MCCs: []string{"460", "461"}},
  487. {Code: "HK", Name: "中国香港", MCCs: []string{"454"}},
  488. {Code: "MO", Name: "中国澳门", MCCs: []string{"455"}},
  489. {Code: "TW", Name: "中国台湾", MCCs: []string{"466"}},
  490. {Code: "US", Name: "美国", MCCs: []string{"310", "311", "312", "313", "314", "315", "316"}},
  491. {Code: "CA", Name: "加拿大", MCCs: []string{"302"}},
  492. {Code: "GB", Name: "英国", MCCs: []string{"234", "235"}},
  493. {Code: "DE", Name: "德国", MCCs: []string{"262"}},
  494. {Code: "FR", Name: "法国", MCCs: []string{"208"}},
  495. {Code: "IT", Name: "意大利", MCCs: []string{"222"}},
  496. {Code: "ES", Name: "西班牙", MCCs: []string{"214"}},
  497. {Code: "PT", Name: "葡萄牙", MCCs: []string{"268"}},
  498. {Code: "NL", Name: "荷兰", MCCs: []string{"204"}},
  499. {Code: "BE", Name: "比利时", MCCs: []string{"206"}},
  500. {Code: "CH", Name: "瑞士", MCCs: []string{"228"}},
  501. {Code: "AT", Name: "奥地利", MCCs: []string{"232"}},
  502. {Code: "IE", Name: "爱尔兰", MCCs: []string{"272"}},
  503. {Code: "DK", Name: "丹麦", MCCs: []string{"238"}},
  504. {Code: "SE", Name: "瑞典", MCCs: []string{"240"}},
  505. {Code: "NO", Name: "挪威", MCCs: []string{"242"}},
  506. {Code: "FI", Name: "芬兰", MCCs: []string{"244"}},
  507. {Code: "PL", Name: "波兰", MCCs: []string{"260"}},
  508. {Code: "CZ", Name: "捷克", MCCs: []string{"230"}},
  509. {Code: "GR", Name: "希腊", MCCs: []string{"202"}},
  510. {Code: "RO", Name: "罗马尼亚", MCCs: []string{"226"}},
  511. {Code: "HU", Name: "匈牙利", MCCs: []string{"216"}},
  512. {Code: "UA", Name: "乌克兰", MCCs: []string{"255"}},
  513. {Code: "RU", Name: "俄罗斯", MCCs: []string{"250"}},
  514. {Code: "TR", Name: "土耳其", MCCs: []string{"286"}},
  515. {Code: "JP", Name: "日本", MCCs: []string{"440", "441"}},
  516. {Code: "KR", Name: "韩国", MCCs: []string{"450"}},
  517. {Code: "SG", Name: "新加坡", MCCs: []string{"525"}},
  518. {Code: "MY", Name: "马来西亚", MCCs: []string{"502"}},
  519. {Code: "TH", Name: "泰国", MCCs: []string{"520"}},
  520. {Code: "VN", Name: "越南", MCCs: []string{"452"}},
  521. {Code: "PH", Name: "菲律宾", MCCs: []string{"515"}},
  522. {Code: "ID", Name: "印度尼西亚", MCCs: []string{"510"}},
  523. {Code: "IN", Name: "印度", MCCs: []string{"404", "405", "406"}},
  524. {Code: "PK", Name: "巴基斯坦", MCCs: []string{"410"}},
  525. {Code: "AE", Name: "阿联酋", MCCs: []string{"424", "430", "431"}},
  526. {Code: "SA", Name: "沙特阿拉伯", MCCs: []string{"420"}},
  527. {Code: "IL", Name: "以色列", MCCs: []string{"425"}},
  528. {Code: "AU", Name: "澳大利亚", MCCs: []string{"505"}},
  529. {Code: "NZ", Name: "新西兰", MCCs: []string{"530"}},
  530. {Code: "BR", Name: "巴西", MCCs: []string{"724"}},
  531. {Code: "MX", Name: "墨西哥", MCCs: []string{"334"}},
  532. {Code: "AR", Name: "阿根廷", MCCs: []string{"722"}},
  533. {Code: "CL", Name: "智利", MCCs: []string{"730"}},
  534. {Code: "CO", Name: "哥伦比亚", MCCs: []string{"732"}},
  535. {Code: "ZA", Name: "南非", MCCs: []string{"655"}},
  536. {Code: "EG", Name: "埃及", MCCs: []string{"602"}},
  537. {Code: "NG", Name: "尼日利亚", MCCs: []string{"621"}},
  538. {Code: "KE", Name: "肯尼亚", MCCs: []string{"639"}},
  539. }