package globals import ( "bytes" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "platform/web/core" "strings" ) var ErrGostNotFound = errors.New("gost resource not found") func IsGostNotFound(err error) bool { return errors.Is(err, ErrGostNotFound) } type GostClient interface { ListChains() ([]*GostChainConfig, error) GetChain(name string) (*GostChainConfig, error) CreateChain(chain *GostChainConfig) error DeleteChain(name string) error SaveConfig() error CreateService(service *GostServiceConfig) error DeleteService(name string) error CreateAuther(auther *GostAutherConfig) error DeleteAuther(name string) error CreateAdmission(admission *GostAdmissionConfig) error DeleteAdmission(name string) error } type gostClient struct { baseURL string pathPrefix string username string password string } var GostInitializer = func(host string, port int, pathPrefix, username, password string) GostClient { baseURL := strings.TrimSpace(host) if !strings.Contains(baseURL, "://") { baseURL = fmt.Sprintf("http://%s:%d", baseURL, port) } return &gostClient{ baseURL: strings.TrimRight(baseURL, "/"), pathPrefix: normalizeGostPathPrefix(pathPrefix), username: username, password: password, } } func NewGost(host string, port int, pathPrefix, username, password string) GostClient { return GostInitializer(host, port, pathPrefix, username, password) } type GostChainConfig struct { Name string `json:"name"` Hops []GostHopConfig `json:"hops,omitempty"` } type GostHopConfig struct { Name string `json:"name,omitempty"` Nodes []GostNodeConfig `json:"nodes,omitempty"` } type GostNodeConfig struct { Name string `json:"name,omitempty"` Addr string `json:"addr"` Connector GostConnectorConfig `json:"connector"` Dialer GostDialerConfig `json:"dialer"` } type GostConnectorConfig struct { Type string `json:"type"` } type GostDialerConfig struct { Type string `json:"type"` } type GostServiceConfig struct { Name string `json:"name"` Addr string `json:"addr"` Admission string `json:"admission,omitempty"` Handler GostHandlerConfig `json:"handler"` Listener GostListenerConfig `json:"listener"` Recorders []GostRecorderConfig `json:"recorders,omitempty"` } type GostHandlerConfig struct { Type string `json:"type"` Chain string `json:"chain,omitempty"` Auther string `json:"auther,omitempty"` } type GostListenerConfig struct { Type string `json:"type"` } type GostRecorderConfig struct { Name string `json:"name"` Record string `json:"record"` } type GostAutherConfig struct { Name string `json:"name"` Auths []GostAuthConfig `json:"auths"` } type GostAuthConfig struct { Username string `json:"username"` Password string `json:"password"` } type GostAdmissionConfig struct { Name string `json:"name"` Whitelist bool `json:"whitelist"` Matchers []string `json:"matchers"` } func (c *gostClient) GetChain(name string) (*GostChainConfig, error) { body, err := c.get("/config/chains/" + url.PathEscape(name)) if err != nil { return nil, err } if len(body) == 0 { return &GostChainConfig{Name: name}, nil } var direct GostChainConfig if err := json.Unmarshal(body, &direct); err == nil && direct.Name != "" { return &direct, nil } var wrapper struct { Data *GostChainConfig `json:"data"` } if err := json.Unmarshal(body, &wrapper); err == nil && wrapper.Data != nil && wrapper.Data.Name != "" { return wrapper.Data, nil } return &GostChainConfig{Name: name}, nil } func (c *gostClient) ListChains() ([]*GostChainConfig, error) { body, err := c.get("/config/chains") if err != nil { return nil, err } if len(body) == 0 { return nil, nil } var resp struct { Data struct { Count int `json:"count"` List []*GostChainConfig `json:"list"` } `json:"data"` } if err := json.Unmarshal(body, &resp); err != nil { return nil, fmt.Errorf("parse gost chain list failed %s: %w", string(body), err) } return resp.Data.List, nil } func (c *gostClient) CreateChain(chain *GostChainConfig) error { return c.create("/config/chains", chain) } func (c *gostClient) DeleteChain(name string) error { return c.delete("/config/chains/" + url.PathEscape(name)) } func (c *gostClient) SaveConfig() error { return c.create("/config", nil) } func (c *gostClient) CreateService(service *GostServiceConfig) error { return c.create("/config/services", service) } func (c *gostClient) DeleteService(name string) error { return c.delete("/config/services/" + url.PathEscape(name)) } func (c *gostClient) CreateAuther(auther *GostAutherConfig) error { return c.create("/config/authers", auther) } func (c *gostClient) DeleteAuther(name string) error { return c.delete("/config/authers/" + url.PathEscape(name)) } func (c *gostClient) CreateAdmission(admission *GostAdmissionConfig) error { return c.create("/config/admissions", admission) } func (c *gostClient) DeleteAdmission(name string) error { return c.delete("/config/admissions/" + url.PathEscape(name)) } func (c *gostClient) create(path string, payload any) error { _, err := c.request(http.MethodPost, path, payload) return err } func (c *gostClient) get(path string) ([]byte, error) { body, err := c.request(http.MethodGet, path, nil) if err != nil { return nil, err } return body, nil } func (c *gostClient) delete(path string) error { _, err := c.request(http.MethodDelete, path, nil) return err } func (c *gostClient) request(method string, path string, payload any) ([]byte, error) { var bodyReader io.Reader if payload != nil { data, err := json.Marshal(payload) if err != nil { return nil, err } bodyReader = bytes.NewReader(data) } req, err := http.NewRequest(method, c.endpoint(path), bodyReader) if err != nil { return nil, err } req.SetBasicAuth(c.username, c.password) if payload != nil { req.Header.Set("Content-Type", "application/json") } resp, err := core.Fetch(req) if err != nil { return nil, err } defer func(Body io.ReadCloser) { _ = Body.Close() }(resp.Body) body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } if resp.StatusCode == http.StatusBadRequest { return nil, fmt.Errorf("%w: %s", ErrGostNotFound, string(body)) } if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { return nil, fmt.Errorf("gost api %s %s failed: %d %s", method, path, resp.StatusCode, string(body)) } return body, nil } func (c *gostClient) endpoint(path string) string { return c.baseURL + c.pathPrefix + path } func normalizeGostPathPrefix(prefix string) string { prefix = strings.TrimSpace(prefix) if prefix == "" { return "" } if !strings.HasPrefix(prefix, "/") { prefix = "/" + prefix } return strings.TrimRight(prefix, "/") }