package as213905 import ( "bytes" "context" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "strconv" "strings" "time" ) const defaultBaseURL = "https://as213905.com" type Options struct { BaseURL string HTTPClient *http.Client UserAgent string } type Client struct { apiKey string base *url.URL http *http.Client userAgent string } type APIError struct { Status int Code string `json:"error"` Message string `json:"message"` RequiredScope string `json:"required_scope"` RetryAfter int `json:"retry_after"` } func (e *APIError) Error() string { if e.Message != "" { return fmt.Sprintf("as213905: %s (%d): %s", e.Code, e.Status, e.Message) } return fmt.Sprintf("as213905: %s (%d)", e.Code, e.Status) } func New(apiKey string) *Client { c, err := NewWithOptions(apiKey, Options{}) if err != nil { panic(err) } return c } func NewWithOptions(apiKey string, opts Options) (*Client, error) { if strings.TrimSpace(apiKey) == "" { return nil, errors.New("as213905: api key is required") } base := opts.BaseURL if base == "" { base = defaultBaseURL } u, err := url.Parse(strings.TrimRight(base, "/")) if err != nil || u.Scheme == "" || u.Host == "" { return nil, errors.New("as213905: invalid base URL") } hc := opts.HTTPClient if hc == nil { hc = &http.Client{Timeout: 30 * time.Second} } ua := opts.UserAgent if ua == "" { ua = "as213905-go/1.1.0" } return &Client{apiKey: apiKey, base: u, http: hc, userAgent: ua}, nil } func (c *Client) do(ctx context.Context, method, path, organizationID string, body, out any) error { var reader io.Reader if body != nil { payload, err := json.Marshal(body) if err != nil { return err } reader = bytes.NewReader(payload) } rel, err := url.Parse(path) if err != nil { return err } u := c.base.ResolveReference(rel) req, err := http.NewRequestWithContext(ctx, method, u.String(), reader) if err != nil { return err } req.Header.Set("Authorization", "Bearer "+c.apiKey) req.Header.Set("Accept", "application/json") req.Header.Set("User-Agent", c.userAgent) if body != nil { req.Header.Set("Content-Type", "application/json") } if organizationID != "" { req.Header.Set("X-Organization-ID", organizationID) } res, err := c.http.Do(req) if err != nil { return err } defer res.Body.Close() data, err := io.ReadAll(io.LimitReader(res.Body, 4<<20)) if err != nil { return err } if res.StatusCode < 200 || res.StatusCode >= 300 { e := &APIError{Status: res.StatusCode, Code: "http_error"} _ = json.Unmarshal(data, e) if e.Code == "" { e.Code = "http_error" } if e.RetryAfter == 0 { e.RetryAfter, _ = strconv.Atoi(res.Header.Get("Retry-After")) } return e } if out == nil || len(data) == 0 { return nil } if err := json.Unmarshal(data, out); err != nil { return fmt.Errorf("as213905: decode response: %w", err) } return nil } func withTrafficQuery(path string, q *TrafficQuery) string { if q == nil { return path } v := url.Values{} if q.Days > 0 { v.Set("days", strconv.Itoa(q.Days)) } if q.From != "" { v.Set("from", q.From) } if q.To != "" { v.Set("to", q.To) } if len(v) > 0 { return path + "?" + v.Encode() } return path } func (c *Client) Organization(ctx context.Context) (Organization, error) { var r struct { Organization Organization `json:"organization"` } err := c.do(ctx, http.MethodGet, "/api/v1/organization", "", nil, &r) return r.Organization, err } func (c *Client) Invoices(ctx context.Context) ([]Invoice, error) { var r struct { Invoices []Invoice `json:"invoices"` } err := c.do(ctx, http.MethodGet, "/api/v1/invoices", "", nil, &r) return r.Invoices, err } func (c *Client) Services(ctx context.Context) ([]Service, error) { var r struct { Services []Service `json:"services"` } err := c.do(ctx, http.MethodGet, "/api/v1/services", "", nil, &r) return r.Services, err } func (c *Client) Service(ctx context.Context, id string) (Service, error) { var r struct { Service Service `json:"service"` } err := c.do(ctx, http.MethodGet, "/api/v1/services/"+url.PathEscape(id), "", nil, &r) return r.Service, err } func (c *Client) ServiceMetrics(ctx context.Context, id, target, metricRange string) (MetricSeries, error) { q := url.Values{} if target != "" { q.Set("target", target) } if metricRange != "" { q.Set("range", metricRange) } path := "/api/v1/services/" + url.PathEscape(id) + "/metrics" if len(q) > 0 { path += "?" + q.Encode() } var r MetricSeries err := c.do(ctx, http.MethodGet, path, "", nil, &r) return r, err } func (c *Client) PowerService(ctx context.Context, id, action string) error { return c.do(ctx, http.MethodPost, "/api/v1/services/"+url.PathEscape(id)+"/power", "", map[string]string{"action": action}, nil) } func (c *Client) Traffic(ctx context.Context, q *TrafficQuery) (Traffic, error) { var r Traffic err := c.do(ctx, http.MethodGet, withTrafficQuery("/api/v1/traffic", q), "", nil, &r) return r, err } func (c *Client) Tunnels(ctx context.Context) ([]Tunnel, error) { var r struct { Tunnels []Tunnel `json:"tunnels"` } err := c.do(ctx, http.MethodGet, "/api/v1/tunnels", "", nil, &r) return r.Tunnels, err } func (c *Client) CreateTunnel(ctx context.Context, input CreateTunnelRequest) (Tunnel, error) { var r struct { Tunnel Tunnel `json:"tunnel"` } err := c.do(ctx, http.MethodPost, "/api/v1/tunnels", "", input, &r) return r.Tunnel, err } func (c *Client) DeleteTunnel(ctx context.Context, id string) error { return c.do(ctx, http.MethodDelete, "/api/v1/tunnels/"+url.PathEscape(id), "", nil, nil) } func (c *Client) TunnelTraffic(ctx context.Context, id string, q *TrafficQuery) (Traffic, error) { var r Traffic err := c.do(ctx, http.MethodGet, withTrafficQuery("/api/v1/tunnels/"+url.PathEscape(id)+"/traffic", q), "", nil, &r) return r, err } func (c *Client) Geofeeds(ctx context.Context, org string) (GeofeedList, error) { var r GeofeedList err := c.do(ctx, http.MethodGet, "/api/v1/geofeeds", org, nil, &r) return r, err } func (c *Client) CreateGeofeed(ctx context.Context, org, name string) (GeofeedList, error) { var r GeofeedList err := c.do(ctx, http.MethodPost, "/api/v1/geofeeds", org, map[string]string{"name": name}, &r) return r, err } func (c *Client) Geofeed(ctx context.Context, org, id string) (Geofeed, error) { var r struct { Geofeed Geofeed `json:"geofeed"` } err := c.do(ctx, http.MethodGet, "/api/v1/geofeeds/"+url.PathEscape(id), org, nil, &r) return r.Geofeed, err } func (c *Client) RenameGeofeed(ctx context.Context, org, id, name string) error { return c.do(ctx, http.MethodPatch, "/api/v1/geofeeds/"+url.PathEscape(id), org, map[string]string{"name": name}, nil) } func (c *Client) DeleteGeofeed(ctx context.Context, org, id string) error { return c.do(ctx, http.MethodDelete, "/api/v1/geofeeds/"+url.PathEscape(id), org, nil, nil) } func (c *Client) ReplaceGeofeedRecords(ctx context.Context, org, id string, records []GeofeedRecord) error { return c.do(ctx, http.MethodPut, "/api/v1/geofeeds/"+url.PathEscape(id)+"/records", org, map[string]any{"records": records}, nil) } func (c *Client) RotateGeofeedURL(ctx context.Context, org, id string) (GeofeedList, error) { var r GeofeedList err := c.do(ctx, http.MethodPost, "/api/v1/geofeeds/"+url.PathEscape(id)+"/rotate", org, nil, &r) return r, err }