package hosting import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "net/url" "time" ) // Client is the Synor Hosting SDK client type Client struct { config Config httpClient *http.Client closed bool } // New creates a new Hosting client func New(config Config) *Client { if config.Endpoint == "" { config.Endpoint = "https://hosting.synor.io/v1" } if config.Timeout == 0 { config.Timeout = 60 * time.Second } if config.Retries == 0 { config.Retries = 3 } return &Client{ config: config, httpClient: &http.Client{ Timeout: config.Timeout, }, } } // ==================== Domain Operations ==================== // CheckAvailability checks domain availability func (c *Client) CheckAvailability(ctx context.Context, name string) (*DomainAvailability, error) { var result DomainAvailability err := c.request(ctx, "GET", "/domains/check/"+url.PathEscape(name), nil, &result) if err != nil { return nil, err } return &result, nil } // RegisterDomain registers a new domain func (c *Client) RegisterDomain(ctx context.Context, name string, opts *RegisterDomainOptions) (*Domain, error) { body := map[string]interface{}{ "name": name, } if opts != nil { if opts.Years > 0 { body["years"] = opts.Years } body["auto_renew"] = opts.AutoRenew if opts.Records != nil { body["records"] = opts.Records } } var result Domain err := c.request(ctx, "POST", "/domains", body, &result) if err != nil { return nil, err } return &result, nil } // GetDomain gets domain information func (c *Client) GetDomain(ctx context.Context, name string) (*Domain, error) { var result Domain err := c.request(ctx, "GET", "/domains/"+url.PathEscape(name), nil, &result) if err != nil { return nil, err } return &result, nil } // ListDomains lists all domains func (c *Client) ListDomains(ctx context.Context) ([]Domain, error) { var result struct { Domains []Domain `json:"domains"` } err := c.request(ctx, "GET", "/domains", nil, &result) if err != nil { return nil, err } return result.Domains, nil } // UpdateDomainRecord updates domain record func (c *Client) UpdateDomainRecord(ctx context.Context, name string, record *DomainRecord) (*Domain, error) { var result Domain err := c.request(ctx, "PUT", "/domains/"+url.PathEscape(name)+"/record", record, &result) if err != nil { return nil, err } return &result, nil } // ResolveDomain resolves a domain func (c *Client) ResolveDomain(ctx context.Context, name string) (*DomainRecord, error) { var result DomainRecord err := c.request(ctx, "GET", "/domains/"+url.PathEscape(name)+"/resolve", nil, &result) if err != nil { return nil, err } return &result, nil } // RenewDomain renews a domain func (c *Client) RenewDomain(ctx context.Context, name string, years int) (*Domain, error) { var result Domain err := c.request(ctx, "POST", "/domains/"+url.PathEscape(name)+"/renew", map[string]interface{}{"years": years}, &result) if err != nil { return nil, err } return &result, nil } // ==================== DNS Operations ==================== // GetDnsZone gets DNS zone for a domain func (c *Client) GetDnsZone(ctx context.Context, domain string) (*DnsZone, error) { var result DnsZone err := c.request(ctx, "GET", "/dns/"+url.PathEscape(domain), nil, &result) if err != nil { return nil, err } return &result, nil } // SetDnsRecords sets DNS records for a domain func (c *Client) SetDnsRecords(ctx context.Context, domain string, records []DnsRecord) (*DnsZone, error) { var result DnsZone err := c.request(ctx, "PUT", "/dns/"+url.PathEscape(domain), map[string]interface{}{"records": records}, &result) if err != nil { return nil, err } return &result, nil } // AddDnsRecord adds a DNS record func (c *Client) AddDnsRecord(ctx context.Context, domain string, record DnsRecord) (*DnsZone, error) { var result DnsZone err := c.request(ctx, "POST", "/dns/"+url.PathEscape(domain)+"/records", record, &result) if err != nil { return nil, err } return &result, nil } // DeleteDnsRecord deletes a DNS record func (c *Client) DeleteDnsRecord(ctx context.Context, domain, recordType, name string) (*DnsZone, error) { var result DnsZone path := fmt.Sprintf("/dns/%s/records/%s/%s", url.PathEscape(domain), recordType, url.PathEscape(name)) err := c.request(ctx, "DELETE", path, nil, &result) if err != nil { return nil, err } return &result, nil } // ==================== Deployment Operations ==================== // Deploy deploys a site from CID func (c *Client) Deploy(ctx context.Context, cid string, opts *DeployOptions) (*Deployment, error) { body := map[string]interface{}{ "cid": cid, } if opts != nil { if opts.Domain != "" { body["domain"] = opts.Domain } if opts.Subdomain != "" { body["subdomain"] = opts.Subdomain } if opts.Headers != nil { body["headers"] = opts.Headers } if opts.Redirects != nil { body["redirects"] = opts.Redirects } body["spa"] = opts.SPA body["clean_urls"] = opts.CleanURLs body["trailing_slash"] = opts.TrailingSlash } var result Deployment err := c.request(ctx, "POST", "/deployments", body, &result) if err != nil { return nil, err } return &result, nil } // GetDeployment gets deployment by ID func (c *Client) GetDeployment(ctx context.Context, id string) (*Deployment, error) { var result Deployment err := c.request(ctx, "GET", "/deployments/"+url.PathEscape(id), nil, &result) if err != nil { return nil, err } return &result, nil } // ListDeployments lists deployments func (c *Client) ListDeployments(ctx context.Context, domain string) ([]Deployment, error) { path := "/deployments" if domain != "" { path += "?domain=" + url.QueryEscape(domain) } var result struct { Deployments []Deployment `json:"deployments"` } err := c.request(ctx, "GET", path, nil, &result) if err != nil { return nil, err } return result.Deployments, nil } // Rollback rolls back to a previous deployment func (c *Client) Rollback(ctx context.Context, domain, deploymentID string) (*Deployment, error) { var result Deployment err := c.request(ctx, "POST", "/deployments/"+url.PathEscape(deploymentID)+"/rollback", map[string]interface{}{"domain": domain}, &result) if err != nil { return nil, err } return &result, nil } // DeleteDeployment deletes a deployment func (c *Client) DeleteDeployment(ctx context.Context, id string) error { return c.request(ctx, "DELETE", "/deployments/"+url.PathEscape(id), nil, nil) } // GetDeploymentStats gets deployment stats func (c *Client) GetDeploymentStats(ctx context.Context, id, period string) (*DeploymentStats, error) { path := fmt.Sprintf("/deployments/%s/stats?period=%s", url.PathEscape(id), url.QueryEscape(period)) var result DeploymentStats err := c.request(ctx, "GET", path, nil, &result) if err != nil { return nil, err } return &result, nil } // ==================== SSL Operations ==================== // ProvisionSSL provisions SSL certificate func (c *Client) ProvisionSSL(ctx context.Context, domain string, opts *ProvisionSSLOptions) (*Certificate, error) { body := make(map[string]interface{}) if opts != nil { body["include_www"] = opts.IncludeWWW body["auto_renew"] = opts.AutoRenew } var result Certificate err := c.request(ctx, "POST", "/ssl/"+url.PathEscape(domain), body, &result) if err != nil { return nil, err } return &result, nil } // GetCertificate gets certificate status func (c *Client) GetCertificate(ctx context.Context, domain string) (*Certificate, error) { var result Certificate err := c.request(ctx, "GET", "/ssl/"+url.PathEscape(domain), nil, &result) if err != nil { return nil, err } return &result, nil } // RenewCertificate renews SSL certificate func (c *Client) RenewCertificate(ctx context.Context, domain string) (*Certificate, error) { var result Certificate err := c.request(ctx, "POST", "/ssl/"+url.PathEscape(domain)+"/renew", nil, &result) if err != nil { return nil, err } return &result, nil } // DeleteCertificate deletes/revokes SSL certificate func (c *Client) DeleteCertificate(ctx context.Context, domain string) error { return c.request(ctx, "DELETE", "/ssl/"+url.PathEscape(domain), nil, nil) } // ==================== Site Configuration ==================== // GetSiteConfig gets site configuration func (c *Client) GetSiteConfig(ctx context.Context, domain string) (*SiteConfig, error) { var result SiteConfig err := c.request(ctx, "GET", "/sites/"+url.PathEscape(domain)+"/config", nil, &result) if err != nil { return nil, err } return &result, nil } // UpdateSiteConfig updates site configuration func (c *Client) UpdateSiteConfig(ctx context.Context, domain string, config map[string]interface{}) (*SiteConfig, error) { var result SiteConfig err := c.request(ctx, "PATCH", "/sites/"+url.PathEscape(domain)+"/config", config, &result) if err != nil { return nil, err } return &result, nil } // PurgeCache purges CDN cache func (c *Client) PurgeCache(ctx context.Context, domain string, paths []string) (int, error) { body := make(map[string]interface{}) if paths != nil { body["paths"] = paths } var result struct { Purged int `json:"purged"` } err := c.request(ctx, "DELETE", "/sites/"+url.PathEscape(domain)+"/cache", body, &result) if err != nil { return 0, err } return result.Purged, nil } // ==================== Analytics ==================== // GetAnalytics gets site analytics func (c *Client) GetAnalytics(ctx context.Context, domain string, opts *AnalyticsOptions) (*AnalyticsData, error) { params := url.Values{} if opts != nil { if opts.Period != "" { params.Set("period", opts.Period) } if opts.StartDate != "" { params.Set("start", opts.StartDate) } if opts.EndDate != "" { params.Set("end", opts.EndDate) } } path := "/sites/" + url.PathEscape(domain) + "/analytics" if len(params) > 0 { path += "?" + params.Encode() } var result AnalyticsData err := c.request(ctx, "GET", path, nil, &result) if err != nil { return nil, err } return &result, nil } // ==================== Lifecycle ==================== // Close closes the client func (c *Client) Close() { c.closed = true } // IsClosed returns whether the client is closed func (c *Client) IsClosed() bool { return c.closed } // HealthCheck performs a health check func (c *Client) HealthCheck(ctx context.Context) bool { var result struct { Status string `json:"status"` } err := c.request(ctx, "GET", "/health", nil, &result) return err == nil && result.Status == "healthy" } func (c *Client) request(ctx context.Context, method, path string, body interface{}, result interface{}) error { if c.closed { return NewHostingError("Client has been closed", "CLIENT_CLOSED", 0) } var lastErr error for attempt := 0; attempt < c.config.Retries; attempt++ { err := c.doRequest(ctx, method, path, body, result) if err == nil { return nil } lastErr = err if c.config.Debug { fmt.Printf("Attempt %d failed: %v\n", attempt+1, err) } if attempt < c.config.Retries-1 { time.Sleep(time.Duration(attempt+1) * time.Second) } } return lastErr } func (c *Client) doRequest(ctx context.Context, method, path string, body interface{}, result interface{}) error { var bodyReader io.Reader if body != nil { jsonBody, err := json.Marshal(body) if err != nil { return err } bodyReader = bytes.NewReader(jsonBody) } req, err := http.NewRequestWithContext(ctx, method, c.config.Endpoint+path, bodyReader) if err != nil { return err } req.Header.Set("Authorization", "Bearer "+c.config.APIKey) req.Header.Set("Content-Type", "application/json") req.Header.Set("X-SDK-Version", "go/0.1.0") resp, err := c.httpClient.Do(req) if err != nil { return err } defer resp.Body.Close() respBody, err := io.ReadAll(resp.Body) if err != nil { return err } if resp.StatusCode >= 400 { var errResp struct { Message string `json:"message"` Error string `json:"error"` Code string `json:"code"` } json.Unmarshal(respBody, &errResp) msg := errResp.Message if msg == "" { msg = errResp.Error } if msg == "" { msg = fmt.Sprintf("HTTP %d", resp.StatusCode) } return NewHostingError(msg, errResp.Code, resp.StatusCode) } if result != nil && len(respBody) > 0 { return json.Unmarshal(respBody, result) } return nil }