Files
aaisp-chaos/chaos.go
James O'Gorman 89bacd6ee3 Set User-Agent header
It's nice to third parties if you set User-Agent so the caller can be
identified. By default Go will set a User-Agent header to a string like
"Go-http-client/1.1".

The library now sets it to a string that identifies the source of the
program (github.com/jamesog/aaisp-chaos) and the compiled Go OS, arch
and version, to aid any potential debugging:

    chaos-go (darwin; amd64; go1.15.5) github.com/jamesog/aaisp-chaos
2020-11-28 15:56:16 +00:00

177 lines
4.4 KiB
Go

// Package chaos provides access to Andrews and Arnold's CHAOS v2 API.
package chaos
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"runtime"
"strings"
"time"
)
const defaultEndpoint = "https://chaos2.aa.net.uk"
// API provides the accessors for querying the CHAOS service.
type API struct {
Endpoint string
login url.Values
}
// New takes an Auth with API credentials and returns an API object.
func New(auth Auth) *API {
return &API{Endpoint: defaultEndpoint, login: auth.form()}
}
// Auth is the authentication credentials for the API.
//
// The API requires either account authentication (AccountNumber and AccountPassword) or control authentication (ControlLogin and ControlPassword.)
//
// ControlLogin may also be passed when using account authentication.
type Auth struct {
AccountNumber string
AccountPassword string
ControlLogin string
ControlPassword string
}
// Construct form values for sending as authentication data.
func (a Auth) form() url.Values {
f := url.Values{}
if a.AccountNumber != "" {
f.Set("account_number", a.AccountNumber)
}
if a.AccountPassword != "" {
f.Set("account_password", a.AccountPassword)
}
if a.ControlLogin != "" {
f.Set("control_login", a.ControlLogin)
}
if a.ControlPassword != "" {
f.Set("control_password", a.ControlPassword)
}
return f
}
func userAgent() string {
return fmt.Sprintf("chaos-go (%s; %s; %s) github.com/jamesog/aaisp-chaos",
runtime.GOOS,
runtime.GOARCH,
runtime.Version(),
)
}
func (api API) makeRequest(url string) ([]byte, error) {
client := &http.Client{
Timeout: 10 * time.Second,
}
req, err := http.NewRequest("POST", api.Endpoint+url, strings.NewReader(api.login.Encode()))
if err != nil {
return nil, err
}
req.Header.Set("User-Agent", userAgent())
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := client.Do(req)
if err != nil {
return nil, err
}
body, err := ioutil.ReadAll(resp.Body)
defer resp.Body.Close()
if err != nil {
return nil, fmt.Errorf("error reading response body: %w", err)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("bad response code: %d", resp.StatusCode)
}
return body, nil
}
// The API returns timestamps in the format "YYYY-mm-dd HH:mm:ss" rather than RFC3389.
//
// Use a custom type to unmarshal JSON to this format.
type chaosTime struct {
time.Time
}
func (t *chaosTime) UnmarshalJSON(b []byte) error {
s := strings.Trim(string(b), `"`)
// The API returns times in UK local rather than UTC
loc, err := time.LoadLocation("Europe/London")
if err != nil {
loc = time.Local
}
nt, err := time.ParseInLocation("2006-01-02 15:04:05", s, loc)
if err != nil {
return err
}
t.Time = nt
return nil
}
// BroadbandInfo represents information about a broadband line.
type BroadbandInfo struct {
ID int `json:"id,string"`
Login string `json:"login"`
Postcode string `json:"postcode"`
TXRate int `json:"tx_rate,string"`
RXRate int `json:"rx_rate,string"`
TXRateAdjusted int `json:"tx_rate_adjusted,string"`
QuotaMonthly int `json:"quota_monthly,string"`
QuotaRemaining int `json:"quota_remaining,string"`
QuotaTimestamp chaosTime `json:"quota_timestamp"`
}
// BroadbandInfo fetches broadband info.
func (api API) BroadbandInfo() ([]BroadbandInfo, error) {
resp, err := api.makeRequest("/broadband/info")
if err != nil {
return nil, err
}
r := struct {
Info []BroadbandInfo `json:"info"`
Error string `json:"error"`
}{}
err = json.Unmarshal(resp, &r)
if err != nil {
return nil, fmt.Errorf("BroadbandInfo JSON decode: %w", err)
}
if r.Error != "" {
return nil, errors.New(r.Error)
}
return r.Info, nil
}
// BroadbandQuota is quota.
type BroadbandQuota struct {
ID int `json:"id,string"`
QuotaMonthly int `json:"quota_monthly"`
QuotaRemaining int `json:"quota_remaining,string"`
QuotaTimestamp chaosTime `json:"quota_timestamp,string"`
}
// BroadbandQuota fetches the broadband quota.
func (api API) BroadbandQuota() ([]BroadbandQuota, error) {
resp, err := api.makeRequest("/broadband/quota")
if err != nil {
return nil, err
}
r := struct {
Quota []BroadbandQuota `json:"quota"`
Error string `json:"error"`
}{}
err = json.Unmarshal(resp, &r)
if err != nil {
return nil, fmt.Errorf("BroadbandQuota JSON decode: %w", err)
}
if r.Error != "" {
return nil, errors.New(r.Error)
}
return r.Quota, nil
}