diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..ddd33e7ddd6e10d1443dbeac3d311ae4cf54e61c --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/.idea/ +/vendor/ +*.iml diff --git a/api/api_backup.go b/api/api_backup.go new file mode 100644 index 0000000000000000000000000000000000000000..8ad86b0aa88a4a01bd42d4532f75cd9479a2cc36 --- /dev/null +++ b/api/api_backup.go @@ -0,0 +1,55 @@ +package api + +import ( + "fmt" + "net/http" + "net/url" + "strconv" + "strings" +) + +func (c Client) GetBackup(id ServerId) (FTPBackup, error) { + var result FTPBackup + + path, err := FTPBackupPath(id) + if err != nil { + return result, fmt.Errorf("api.backup: could not parse url\n %w", err) + } + request, err := http.NewRequest("GET", c.baseUrl.ResolveReference(path).String(), nil) + if err != nil { + return result, fmt.Errorf("api.backup: could not build request\n %w", err) + } + err = c.doRequest(request, &result) + if err != nil { + return result, fmt.Errorf("api.backup: could not get backup\n %w", err) + } + return result, nil +} + +type EditBackupParams struct { + Password string + AutoLogin bool + AclEnabled bool +} + +func (c Client) EditBackup(id ServerId, params EditBackupParams) error { + body := strings.NewReader(url.Values{ + "password": []string{params.Password}, + "autologin": []string{strconv.FormatBool(params.AutoLogin)}, + "acl_enabled": []string{strconv.FormatBool(params.AclEnabled)}, + }.Encode()) + + path, err := FTPBackupPath(id) + if err != nil { + return fmt.Errorf("api.backup: could not parse url\n %w", err) + } + request, err := http.NewRequest("PUT", c.baseUrl.ResolveReference(path).String(), body) + if err != nil { + return fmt.Errorf("api.backup: could not build request\n %w", err) + } + err = c.doRequest(request, nil) + if err != nil { + return fmt.Errorf("api.backup: could not edit backup\n %w", err) + } + return nil +} diff --git a/api/api_bmcsession.go b/api/api_bmcsession.go new file mode 100644 index 0000000000000000000000000000000000000000..4cf8a7889868435329d0083824983564e93a4a8a --- /dev/null +++ b/api/api_bmcsession.go @@ -0,0 +1,63 @@ +package api + +import ( + "fmt" + "net/http" + "net/url" + "strings" +) + +func (c Client) GetBMCSession(id ServerId) (BMCSession, error) { + var result BMCSession + + path, err := BMCSessionPath(id) + if err != nil { + return result, fmt.Errorf("api.bmcsession: could not parse url\n %w", err) + } + request, err := http.NewRequest("GET", c.baseUrl.ResolveReference(path).String(), nil) + if err != nil { + return result, fmt.Errorf("api.bmcsession: could not build request\n %w", err) + } + err = c.doRequest(request, &result) + if err != nil { + return result, fmt.Errorf("api.bmcsession: could not get bmc session\n %w", err) + } + return result, nil +} + +func (c Client) CreateBMCSession(id ServerId, authorizedIp string) error { + body := strings.NewReader(url.Values{ + "ip": []string{authorizedIp}, + }.Encode()) + + path, err := BMCSessionPath(id) + if err != nil { + return fmt.Errorf("api.bmcsession: could not parse url\n %w", err) + } + request, err := http.NewRequest("POST", c.baseUrl.ResolveReference(path).String(), body) + request.Header.Set("Content-Type", "application/x-www-form-urlencoded") + if err != nil { + return fmt.Errorf("api.bmcsession: could not build request\n %w", err) + } + err = c.doRequest(request, nil) + if err != nil { + return fmt.Errorf("api.bmcsession: could not create bmc session\n %w", err) + } + return nil +} + +func (c Client) CloseBMCSession(id ServerId) error { + path, err := BMCSessionPath(id) + if err != nil { + return fmt.Errorf("api.bmcsession: could not parse url\n %w", err) + } + request, err := http.NewRequest("DELETE", c.baseUrl.ResolveReference(path).String(), nil) + if err != nil { + return fmt.Errorf("api.bmcsession: could not build request\n %w", err) + } + err = c.doRequest(request, nil) + if err != nil { + return fmt.Errorf("api.bmcsession: could not close bmc session\n %w", err) + } + return nil +} diff --git a/api/api_disk.go b/api/api_disk.go new file mode 100644 index 0000000000000000000000000000000000000000..ecb125a223257ad3b047c96cd147f577cd896ab3 --- /dev/null +++ b/api/api_disk.go @@ -0,0 +1,25 @@ +package api + +import ( + "fmt" + "net/http" + "net/url" +) + +func (c Client) GetDisk(ref DiskRef) (Disk, error) { + var result Disk + + path, err := url.Parse(string(ref)) + if err != nil { + return result, fmt.Errorf("api.disk: could not parse url\n %w", err) + } + request, err := http.NewRequest("GET", c.baseUrl.ResolveReference(path).String(), nil) + if err != nil { + return result, fmt.Errorf("api.disk: could not build request\n %w", err) + } + err = c.doRequest(request, &result) + if err != nil { + return result, fmt.Errorf("api.disk: could not get disk\n %w", err) + } + return result, nil +} diff --git a/api/api_operatingsystem.go b/api/api_operatingsystem.go new file mode 100644 index 0000000000000000000000000000000000000000..87696c2c5820e5aa348916428500dc3f6d6cf436 --- /dev/null +++ b/api/api_operatingsystem.go @@ -0,0 +1,24 @@ +package api + +import ( + "fmt" + "net/http" +) + +func (c Client) GetOperatingSystems(id ServerId) ([]OperatingSystem, error) { + var result []OperatingSystem + + path, err := OperatingSystemPath(id) + if err != nil { + return result, fmt.Errorf("api.operatingsystem: could not parse url\n %w", err) + } + request, err := http.NewRequest("GET", c.baseUrl.ResolveReference(path).String(), nil) + if err != nil { + return result, fmt.Errorf("api.operatingsystem: could not build request\n %w", err) + } + err = c.doRequest(request, &result) + if err != nil { + return result, fmt.Errorf("api.operatingsystem: could not list operatingsystems\n %w", err) + } + return result, nil +} diff --git a/api/api_productinfo.go b/api/api_productinfo.go new file mode 100644 index 0000000000000000000000000000000000000000..77e7ded627983bc7921773b7b4d2c85168511e9a --- /dev/null +++ b/api/api_productinfo.go @@ -0,0 +1,24 @@ +package api + +import ( + "fmt" + "net/http" +) + +func (c Client) GetProductInfo(id ServerId) (ProductInfo, error) { + var result ProductInfo + + path, err := ProductInfoPath(id) + if err != nil { + return result, fmt.Errorf("api.productinfo: could not parse url\n %w", err) + } + request, err := http.NewRequest("GET", c.baseUrl.ResolveReference(path).String(), nil) + if err != nil { + return result, fmt.Errorf("api.productinfo: could not build request\n %w", err) + } + err = c.doRequest(request, &result) + if err != nil { + return result, fmt.Errorf("api.productinfo: could not get productinfo\n %w", err) + } + return result, nil +} diff --git a/api/api_raidcontroller.go b/api/api_raidcontroller.go new file mode 100644 index 0000000000000000000000000000000000000000..162a1f187ac52b5076125bb9a5672a27183dc0ba --- /dev/null +++ b/api/api_raidcontroller.go @@ -0,0 +1,25 @@ +package api + +import ( + "fmt" + "net/http" + "net/url" +) + +func (c Client) GetRaidController(ref RaidControllerRef) (RaidController, error) { + var result RaidController + + path, err := url.Parse(string(ref)) + if err != nil { + return result, fmt.Errorf("api.raidcontroller: could not parse url\n %w", err) + } + request, err := http.NewRequest("GET", c.baseUrl.ResolveReference(path).String(), nil) + if err != nil { + return result, fmt.Errorf("api.raidcontroller: could not build request\n %w", err) + } + err = c.doRequest(request, &result) + if err != nil { + return result, fmt.Errorf("api.raidcontroller: could not get raidcontroller\n %w", err) + } + return result, nil +} diff --git a/api/api_rescueimage.go b/api/api_rescueimage.go new file mode 100644 index 0000000000000000000000000000000000000000..2b43d0bb647b2485b54ae9c8b7f1a7bbfb13db2c --- /dev/null +++ b/api/api_rescueimage.go @@ -0,0 +1,24 @@ +package api + +import ( + "fmt" + "net/http" +) + +func (c Client) GetRescueImages(id ServerId) ([]RescueImageId, error) { + var result []RescueImageId + + path, err := RescueImagePath(id) + if err != nil { + return result, fmt.Errorf("api.rescueimage: could not parse url\n %w", err) + } + request, err := http.NewRequest("GET", c.baseUrl.ResolveReference(path).String(), nil) + if err != nil { + return result, fmt.Errorf("api.rescueimage: could not build request\n %w", err) + } + err = c.doRequest(request, &result) + if err != nil { + return result, fmt.Errorf("api.rescueimage: could not list rescueimages\n %w", err) + } + return result, nil +} diff --git a/api/api_server.go b/api/api_server.go new file mode 100644 index 0000000000000000000000000000000000000000..d2eacd630d59f57f262af600709b9fac855cb1b3 --- /dev/null +++ b/api/api_server.go @@ -0,0 +1,152 @@ +package api + +import ( + "fmt" + "net/http" + "net/url" + "strings" +) + +func (c Client) GetServers() ([]ServerRef, error) { + var result []ServerRef + + path, err := url.Parse(serverPath) + if err != nil { + return result, fmt.Errorf("api.server: could not parse url\n %w", err) + } + request, err := http.NewRequest("GET", c.baseUrl.ResolveReference(path).String(), nil) + if err != nil { + return result, fmt.Errorf("api.server: could not build request\n %w", err) + } + err = c.doRequest(request, &result) + if err != nil { + return result, fmt.Errorf("api.server: could not list servers\n %w", err) + } + return result, nil +} + +func (c Client) GetServer(ref ServerRef) (Server, error) { + var result Server + + path, err := url.Parse(string(ref)) + if err != nil { + return result, fmt.Errorf("api.server: could not parse url\n %w", err) + } + request, err := http.NewRequest("GET", c.baseUrl.ResolveReference(path).String(), nil) + if err != nil { + return result, fmt.Errorf("api.server: could not build request\n %w", err) + } + err = c.doRequest(request, &result) + if err != nil { + return result, fmt.Errorf("api.server: could not get server\n %w", err) + } + return result, nil +} + +func (c Client) UpdateServer(ref ServerRef, hostname string) error { + body := strings.NewReader(url.Values{ + "hostname": []string{hostname}, + }.Encode()) + + path, err := url.Parse(string(ref)) + if err != nil { + return fmt.Errorf("api.server: could not parse url\n %w", err) + } + request, err := http.NewRequest("PUT", c.baseUrl.ResolveReference(path).String(), body) + request.Header.Set("Content-Type", "application/x-www-form-urlencoded") + if err != nil { + return fmt.Errorf("api.server: could not build request\n %w", err) + } + err = c.doRequest(request, nil) + if err != nil { + return fmt.Errorf("api.server: could not update server\n %w", err) + } + return nil +} + +func (c Client) BootServerNormal(id ServerId) error { + path, err := ServerBootPath(id, BootModeNormal) + if err != nil { + return fmt.Errorf("api.server: could not parse url\n %w", err) + } + request, err := http.NewRequest("POST", c.baseUrl.ResolveReference(path).String(), nil) + if err != nil { + return fmt.Errorf("api.server: could not build request\n %w", err) + } + err = c.doRequest(request, nil) + if err != nil { + return fmt.Errorf("api.server: could not boot server in normal mode\n %w", err) + } + return nil +} + +func (c Client) BootServerRescue(id ServerId, image RescueImageId) (RescueCredentials, error) { + var result RescueCredentials + body := strings.NewReader(url.Values{ + "image": []string{string(image)}, + }.Encode()) + + path, err := ServerBootPath(id, BootModeRescue) + if err != nil { + return result, fmt.Errorf("api.server: could not parse url\n %w", err) + } + request, err := http.NewRequest("POST", c.baseUrl.ResolveReference(path).String(), body) + request.Header.Set("Content-Type", "application/x-www-form-urlencoded") + if err != nil { + return result, fmt.Errorf("api.server: could not build request\n %w", err) + } + err = c.doRequest(request, nil) + if err != nil { + return result, fmt.Errorf("api.server: could not boot server in rescue mode\n %w", err) + } + return result, nil +} + +type ServerNotificationParams struct { + Reason string + Email string +} + +func (c Client) ShutdownServer(id ServerId, params ServerNotificationParams) error { + body := strings.NewReader(url.Values{ + "reason": []string{params.Reason}, + "email": []string{params.Email}, + }.Encode()) + + path, err := ServerShutdownPath(id) + if err != nil { + return fmt.Errorf("api.server: could not parse url\n %w", err) + } + request, err := http.NewRequest("POST", c.baseUrl.ResolveReference(path).String(), body) + request.Header.Set("Content-Type", "application/x-www-form-urlencoded") + if err != nil { + return fmt.Errorf("api.server: could not build request\n %w", err) + } + err = c.doRequest(request, nil) + if err != nil { + return fmt.Errorf("api.server: could not shutdown server\n %w", err) + } + return nil +} + +func (c Client) RebootServer(id ServerId, params ServerNotificationParams) error { + body := strings.NewReader(url.Values{ + "reason": []string{params.Reason}, + "email": []string{params.Email}, + }.Encode()) + + path, err := ServerRebootPath(id) + if err != nil { + return fmt.Errorf("api.server: could not parse url\n %w", err) + } + request, err := http.NewRequest("POST", c.baseUrl.ResolveReference(path).String(), body) + request.Header.Set("Content-Type", "application/x-www-form-urlencoded") + if err != nil { + return fmt.Errorf("api.server: could not build request\n %w", err) + } + err = c.doRequest(request, nil) + if err != nil { + return fmt.Errorf("api.server: could not reboot server\n %w", err) + } + return nil +} diff --git a/api/api_serverip.go b/api/api_serverip.go new file mode 100644 index 0000000000000000000000000000000000000000..20afa553662ba346d499d20d39644926816bb2f9 --- /dev/null +++ b/api/api_serverip.go @@ -0,0 +1,25 @@ +package api + +import ( + "fmt" + "net/http" + "net/url" +) + +func (c Client) GetIPAddress(ref ServerIPRef) (ServerIP, error) { + var result ServerIP + + path, err := url.Parse(string(ref)) + if err != nil { + return result, fmt.Errorf("api.serverip: could not parse url\n %w", err) + } + request, err := http.NewRequest("GET", c.baseUrl.ResolveReference(path).String(), nil) + if err != nil { + return result, fmt.Errorf("api.serverip: could not build request\n %w", err) + } + err = c.doRequest(request, &result) + if err != nil { + return result, fmt.Errorf("api.serverip: could not get serverip\n %w", err) + } + return result, nil +} diff --git a/api/client.go b/api/client.go new file mode 100644 index 0000000000000000000000000000000000000000..6304c29eb9ad0c1572a515ec6eb795baa288305f --- /dev/null +++ b/api/client.go @@ -0,0 +1,57 @@ +package api + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "scaleway-dedibox-api/httputil" +) + +type Client struct { + http *httputil.Client + baseUrl *url.URL +} + +func NewClient(client *httputil.Client, baseUrl *url.URL) Client { + if client == nil { + client = httputil.NewClient(nil, nil) + } + return Client{ + http: client, + baseUrl: baseUrl, + } +} + +type parsedError struct { + Code int `json:"code"` + Error string `json:"error"` +} + +func parseError(body io.ReadCloser) (parsedError, error) { + var result parsedError + return result, json.NewDecoder(body).Decode(&result) +} + +func (c Client) doRequest(request *http.Request, target any) error { + response, err := c.http.Do(request) + if err != nil { + return fmt.Errorf("api: could not execute request\n %w", err) + } + if !httputil.IsHttpStatusSuccess(response.StatusCode) { + parsedError, err := parseError(response.Body) + if err != nil { + return fmt.Errorf("api: got non-success status: %s", response.Status) + } else { + return fmt.Errorf("api: got non-success status: %s, code: %d, reason: %s", + response.Status, parsedError.Code, parsedError.Error) + } + } + if target != nil { + if err := json.NewDecoder(response.Body).Decode(&target); err != nil { + return fmt.Errorf("api: could not deserialize response body\n %w", err) + } + } + return nil +} diff --git a/api/model_bmcsession.go b/api/model_bmcsession.go new file mode 100644 index 0000000000000000000000000000000000000000..f8fe8a8541bfa058374030dccdc93eb2ed27fbf7 --- /dev/null +++ b/api/model_bmcsession.go @@ -0,0 +1,20 @@ +package api + +import ( + "net/url" + "path" + "strconv" +) + +const bmcSessionPath = "/api/v1/server/bmc/session/" + +func BMCSessionPath(id ServerId) (*url.URL, error) { + return url.Parse(path.Join(bmcSessionPath, strconv.Itoa(int(id)))) +} + +type BMCSession struct { + Url string `json:"url,omitempty"` + Login string `json:"login,omitempty"` + Password string `json:"password,omitempty"` + Expiration *TimeDto `json:"expiration,omitempty"` +} diff --git a/api/model_disk.go b/api/model_disk.go new file mode 100644 index 0000000000000000000000000000000000000000..6b71b4312ac782ed5ba160de4103a6ec0cf5343d --- /dev/null +++ b/api/model_disk.go @@ -0,0 +1,45 @@ +package api + +import ( + "fmt" + "path" + "strconv" +) + +type DiskId int +type DiskRef string + +const diskPath = "/api/v1/server/hardware/disk/" + +func ToDiskRef(id DiskId) DiskRef { + return DiskRef(path.Join(diskPath, strconv.Itoa(int(id)))) +} + +func ToDiskId(ref DiskRef) (DiskId, error) { + prefix, id := path.Split(string(ref)) + if prefix != diskPath { + return 0, fmt.Errorf("api.model_disk: invalid disk ref %s", ref) + } + parsed, err := strconv.Atoi(id) + if err != nil { + return 0, fmt.Errorf("api.model_disk: invalid disk ref %s\n %w", ref, err) + } + return DiskId(parsed), nil +} + +type DiskType = string + +const ( + DiskTypeSATA DiskType = "SATA" + DiskTypeSAS DiskType = "SAS" + DiskTypeSSHD DiskType = "SSHD" + DiskTypeNVME DiskType = "NVME" +) + +type Disk struct { + Id DiskId `json:"id,omitempty"` + Connector string `json:"connector,omitempty"` + Type DiskType `json:"type,omitempty"` + Capacity int `json:"capacity,omitempty"` + RaidController WrappedRef[RaidControllerRef] `json:"raid_controller,omitempty"` +} diff --git a/api/model_ftpbackup.go b/api/model_ftpbackup.go new file mode 100644 index 0000000000000000000000000000000000000000..db4de5dc1b094dcf3633502cda54bd8ec915672f --- /dev/null +++ b/api/model_ftpbackup.go @@ -0,0 +1,25 @@ +package api + +import ( + "net/url" + "path" + "strconv" +) + +const ftpBackupPath = "/api/v1/server/backup/" + +func FTPBackupPath(id ServerId) (*url.URL, error) { + return url.Parse(path.Join(ftpBackupPath, strconv.Itoa(int(id)))) +} + +type FTPBackup struct { + Login string `json:"login,omitempty"` + Server string `json:"server,omitempty"` + Active bool `json:"active,omitempty"` + AclEnabled bool `json:"acl_enabled,omitempty"` + AutoLogin bool `json:"auto_login,omitempty"` + QuotaSpace int `json:"quota_space,omitempty"` + QuotaSpaceUsed int `json:"quota_space_used,omitempty"` + QuotaFiles int `json:"quota_files,omitempty"` + QuotaFilesUsed int `json:"quota_files_used,omitempty"` +} diff --git a/api/model_operatingsystem.go b/api/model_operatingsystem.go new file mode 100644 index 0000000000000000000000000000000000000000..cc4dd36eb1a5f9583a435aad3fc2028fad0298eb --- /dev/null +++ b/api/model_operatingsystem.go @@ -0,0 +1,44 @@ +package api + +import ( + "net/url" + "path" + "strconv" + "time" +) + +type OperatingSystemId = int + +const operatingSystemPath = "/api/v1/server/operatingSystems/" + +func OperatingSystemPath(id ServerId) (*url.URL, error) { + return url.Parse(path.Join(operatingSystemPath, strconv.Itoa(int(id)))) +} + +type OperatingSystemType = string + +const ( + OperatingSystemTypeServer OperatingSystemType = "server" + OperatingSystemTypePanel OperatingSystemType = "panel" + OperatingSystemTypeDesktop OperatingSystemType = "desktop" + OperatingSystemTypeLive OperatingSystemType = "live" + OperatingSystemTypeCustom OperatingSystemType = "custom" + OperatingSystemTypeVirtualization OperatingSystemType = "virtualization" +) + +type OperatingSystemArch = string + +const ( + OperatingSystemArch32bit OperatingSystemArch = "32 bits" + OperatingSystemArch64bit OperatingSystemArch = "64 bits" +) + +type OperatingSystem struct { + Id OperatingSystemId `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Version string `json:"version,omitempty"` + Type OperatingSystemType `json:"type,omitempty"` + Arch OperatingSystemArch `json:"arch,omitempty"` + Release *time.Time `json:"release,omitempty"` + EndOfLife *time.Time `json:"end_of_life,omitempty"` +} diff --git a/api/model_productinfo.go b/api/model_productinfo.go new file mode 100644 index 0000000000000000000000000000000000000000..5eceae6f0861e20ca209e4917d862ed24e92eec9 --- /dev/null +++ b/api/model_productinfo.go @@ -0,0 +1,19 @@ +package api + +import ( + "net/url" + "path" + "strconv" +) + +const productInfoPath = "/api/v1/server/product/" + +func ProductInfoPath(id ServerId) (*url.URL, error) { + return url.Parse(path.Join(productInfoPath, strconv.Itoa(int(id)))) +} + +type ProductInfo struct { + ProductId int `json:"product_id"` + Name string `json:"name,omitempty"` + Price int `json:"price,omitempty"` +} diff --git a/api/model_raidcontroller.go b/api/model_raidcontroller.go new file mode 100644 index 0000000000000000000000000000000000000000..c2dfab280de8b19a41988e3b8ed13243b8dcb490 --- /dev/null +++ b/api/model_raidcontroller.go @@ -0,0 +1,51 @@ +package api + +import ( + "fmt" + "path" + "strconv" +) + +type RaidControllerId int +type RaidControllerRef string + +const raidControllerPath = "/api/v1/server/hardware/raidController/" + +func ToRaidControllerRef(id RaidControllerId) RaidControllerRef { + return RaidControllerRef(path.Join(raidControllerPath, strconv.Itoa(int(id)))) +} + +func ToRaidControllerId(ref RaidControllerRef) (RaidControllerId, error) { + prefix, id := path.Split(string(ref)) + if prefix != diskPath { + return 0, fmt.Errorf("api.model_raidcontroller: invalid raid controller ref %s", ref) + } + parsed, err := strconv.Atoi(id) + if err != nil { + return 0, fmt.Errorf("api.model_raidcontroller: invalid raid controller ref %s\n %w", ref, err) + } + return RaidControllerId(parsed), nil +} + +type RaidLevel string + +const ( + RaidLevelNone RaidLevel = "NORAID" + RaidLevel0 RaidLevel = "RAID0" + RaidLevel1 RaidLevel = "RAID1" + RaidLevel5 RaidLevel = "RAID5" + RaidLevel6 RaidLevel = "RAID6" +) + +type SupportedRaidLevel struct { + RaidLevel RaidLevel `json:"raid_level,omitempty"` + Min int `json:"min,omitempty"` + Max int `json:"max,omitempty"` +} + +type RaidController struct { + Id RaidControllerId `json:"id,omitempty"` + Model string `json:"model,omitempty"` + Disks []Disk `json:"disks,omitempty"` + SupportedRaidLevels []SupportedRaidLevel `json:"supported_raid_levels,omitempty"` +} diff --git a/api/model_rescueimage.go b/api/model_rescueimage.go new file mode 100644 index 0000000000000000000000000000000000000000..49c753611783f033f858d9e3cc1440664fdda23c --- /dev/null +++ b/api/model_rescueimage.go @@ -0,0 +1,15 @@ +package api + +import ( + "net/url" + "path" + "strconv" +) + +const rescueImagePath = "/api/v1/server/rescue_images/" + +type RescueImageId string + +func RescueImagePath(id ServerId) (*url.URL, error) { + return url.Parse(path.Join(rescueImagePath, strconv.Itoa(int(id)))) +} diff --git a/api/model_server.go b/api/model_server.go new file mode 100644 index 0000000000000000000000000000000000000000..46fb1022b70f20f6d298e1515e54a45cdeae9f12 --- /dev/null +++ b/api/model_server.go @@ -0,0 +1,140 @@ +package api + +import ( + "fmt" + "net" + "net/url" + "path" + "strconv" + "time" +) + +type ServerId int +type ServerRef string + +const serverPath = "/api/v1/server/" +const serverBootPath = "/api/v1/server/boot/" +const serverShutdownPath = "/api/v1/server/shutdown/" +const serverRebootPath = "/api/v1/server/reboot/" + +func ToServerRef(id ServerId) ServerRef { + return ServerRef(path.Join(serverPath, strconv.Itoa(int(id)))) +} + +func ToServerId(ref ServerRef) (ServerId, error) { + prefix, id := path.Split(string(ref)) + if prefix != diskPath { + return 0, fmt.Errorf("api.model_server: invalid server ref %s", ref) + } + parsed, err := strconv.Atoi(id) + if err != nil { + return 0, fmt.Errorf("api.model_server: invalid server ref %s\n %w", ref, err) + } + return ServerId(parsed), nil +} + +type BootMode string + +const ( + BootModeNormal BootMode = "normal" + BootModeTest BootMode = "test" + BootModeRescue BootMode = "rescue" +) + +func ServerBootPath(id ServerId, mode BootMode) (*url.URL, error) { + return url.Parse(path.Join(serverBootPath, string(mode), strconv.Itoa(int(id)))) +} + +func ServerShutdownPath(id ServerId) (*url.URL, error) { + return url.Parse(path.Join(serverShutdownPath, strconv.Itoa(int(id)))) +} + +func ServerRebootPath(id ServerId) (*url.URL, error) { + return url.Parse(path.Join(serverRebootPath, strconv.Itoa(int(id)))) +} + +type Location struct { + Datacenter string `json:"datacenter,omitempty"` + Room string `json:"room,omitempty"` + Zone string `json:"zone,omitempty"` + Line string `json:"line,omitempty"` + Rack string `json:"rack,omitempty"` + Block string `json:"block,omitempty"` + Position int `json:"position,omitempty"` +} + +type Contacts struct { + Owner string `json:"owner,omitempty"` + Tech string `json:"tech,omitempty"` +} + +type RescueProtocol string + +const ( + RescueProtocolSsh RescueProtocol = "ssh" + RescueProtocolVnc RescueProtocol = "vnc" +) + +type RescueCredentials struct { + Login string `json:"login,omitempty"` + Password string `json:"password,omitempty"` + Protocol RescueProtocol `json:"protocol,omitempty"` + IP net.IP `json:"ip,omitempty"` +} + +type ServerBMC struct { + SessionKey string `json:"session_key,omitempty"` +} + +type DriveArray struct { + RaidLevel RaidLevel `json:"raid_level,omitempty"` +} + +type InstallStatus string + +const ( + InstallStatusBlank InstallStatus = "blank" + InstallStatusInstalling InstallStatus = "installing" + InstallStatusInstalled InstallStatus = "installed" + InstallStatusErrored InstallStatus = "errored" +) + +type InstallStage string + +const ( + ServerStageBoot InstallStage = "BOOT" + ServerStageWget InstallStage = "WGET" + ServerStageOs InstallStage = "OS" + ServerStageConfig InstallStage = "CONFIG" + ServerStageStart InstallStage = "START" + ServerStageCleanRaid InstallStage = "CLEAN_RAID" + ServerStageFdisk InstallStage = "FDISK" + ServerStageFormat InstallStage = "FORMAT" + ServerStageBootloader InstallStage = "BOOTLOADER" + ServerStageRebooting InstallStage = "REBOOTING" +) + +type Server struct { + Id ServerId `json:"id,omitempty"` + Offer string `json:"offer,omitempty"` + Hostname string `json:"hostname,omitempty"` + OperatingSystem OperatingSystem `json:"os,omitempty"` + Power string `json:"power,omitempty"` + BootMode BootMode `json:"boot_mode,omitempty"` + LastReboot *time.Time `json:"last_reboot,omitempty"` + AntiDdos bool `json:"anti_ddos"` + HardwareWatch bool `json:"hardware_watch"` + ProactiveMonitoring bool `json:"proactive_monitoring"` + Support string `json:"support,omitempty"` + Abuse string `json:"abuse,omitempty"` + Location Location `json:"location,omitempty"` + IP []ServerIP `json:"ip,omitempty"` + Contacts Contacts `json:"contacts,omitempty"` + RescueCredentials RescueCredentials `json:"rescue_credentials,omitempty"` + Bmc ServerBMC `json:"bmc,omitempty"` + Disks []WrappedRef[DiskRef] `json:"disks,omitempty"` + DriveArrays []DriveArray `json:"drive_arrays,omitempty"` + RaidControllers []WrappedRef[RaidControllerRef] `json:"raid_controllers,omitempty"` + InstallStatus InstallStatus `json:"install_status,omitempty"` + Stage InstallStage `json:"stage,omitempty"` +} diff --git a/api/model_serverip.go b/api/model_serverip.go new file mode 100644 index 0000000000000000000000000000000000000000..cfa679dc75106ccb0c236ab1bc9723fe6043c09a --- /dev/null +++ b/api/model_serverip.go @@ -0,0 +1,40 @@ +package api + +import ( + "net" + "path" +) + +const serverIPPath = "/api/v1/server/ip/" + +type ServerIPRef string + +func ToServerIPRef(ip net.IP) ServerIPRef { + return ServerIPRef(path.Join(serverIPPath, ip.String())) +} + +type ServerIPType string + +const ( + ServerIPTypePublic ServerIPType = "public" + ServerIPTypePrivate ServerIPType = "private" + ServerIPTypeFailover ServerIPType = "failover" +) + +type ServerIPStatus string + +const ( + ServerIPStatusActive ServerIPStatus = "active" + ServerIPStatusUpdating ServerIPStatus = "updating" +) + +type ServerIP struct { + Address net.IP `json:"address,omitempty"` + Type ServerIPType `json:"type,omitempty"` + Reverse string `json:"reverse,omitempty"` + MacAddress string `json:"mac,omitempty"` + Destination net.IP `json:"destination,omitempty"` + Status ServerIPStatus `json:"status,omitempty"` + SwitchPortState string `json:"switch_port_state,omitempty"` + Server WrappedRef[ServerRef] `json:"server,omitempty"` +} diff --git a/api/model_time.go b/api/model_time.go new file mode 100644 index 0000000000000000000000000000000000000000..f9fe6eb143097301d2e0de49b8bda65503b6d4ab --- /dev/null +++ b/api/model_time.go @@ -0,0 +1,37 @@ +package api + +import ( + "fmt" + "time" +) + +type TimeDto time.Time + +func (dto TimeDto) MarshalJSON() ([]byte, error) { + if y := time.Time(dto).Year(); y < 0 || y >= 10000 { + // RFC 3339 is clear that years are 4 digits exactly. + // See golang.org/issue/4556#c15 for more discussion. + return nil, fmt.Errorf("api.time: year outside of range [0,9999]") + } + + b := make([]byte, 0, len(time.RFC3339Nano)+2) + b = append(b, '"') + b = time.Time(dto).AppendFormat(b, time.RFC3339Nano) + b = append(b, '"') + return b, nil +} + +func (dto *TimeDto) UnmarshalJSON(data []byte) error { + // Ignore null, like in the main JSON package. + if string(data) == "null" { + return nil + } + // Fractional seconds are handled implicitly by Parse. + var err error + result, err := time.Parse(`"`+time.RFC1123+`"`, string(data)) + if err != nil { + return err + } + *dto = TimeDto(result) + return nil +} diff --git a/api/model_wrappedref.go b/api/model_wrappedref.go new file mode 100644 index 0000000000000000000000000000000000000000..3d52a18b978a13a245a5b6e595edf6155a67cb21 --- /dev/null +++ b/api/model_wrappedref.go @@ -0,0 +1,5 @@ +package api + +type WrappedRef[T any] struct { + Ref T `json:"$ref,omitempty"` +} diff --git a/deviceauth/config.go b/deviceauth/config.go new file mode 100644 index 0000000000000000000000000000000000000000..e941f80de24e019e31abc6cad2143188f023e5a5 --- /dev/null +++ b/deviceauth/config.go @@ -0,0 +1,118 @@ +package deviceauth + +import ( + "fmt" + "golang.org/x/oauth2" + "net/url" + "scaleway-dedibox-api/httputil" + "strings" + "time" +) + +const ( + errAuthorizationPending = "authorization_pending" + errSlowDown = "slow_down" + errAccessDenied = "access_denied" + errExpiredToken = "expired_token" +) + +type Endpoint struct { + AuthURL string + TokenURL string + DeviceCodeURL string + // AuthStyle optionally specifies how the endpoint wants the + // client ID & client secret sent. The zero value means to + // auto-detect. + AuthStyle AuthStyle +} + +type DeviceAuthConfig struct { + Client *httputil.Client + + // ClientID is the application's ID. + ClientID string + + // ClientSecret is the application's secret. + ClientSecret string + + // Endpoint contains the resource server's token endpoint URLs. + Endpoint Endpoint + + // Scope specifies optional requested permissions. + Scopes []string +} + +const maximumBodySize = 1 << 2 + +// AuthDevice returns a device auth struct which contains a device code +// and authorization information provided for users to enter on another device. +func (config *DeviceAuthConfig) AuthDevice(opts ...AuthCodeOption) (*DeviceAuth, error) { + v := url.Values{ + "client_id": {config.ClientID}, + } + if len(config.Scopes) > 0 { + v.Set("scope", strings.Join(config.Scopes, " ")) + } + for _, opt := range opts { + opt.setValue(v) + } + v.Set("new_credentials", "yes") + now := time.Now() + da, err := config.retrieveDeviceAuth(v) + if err != nil { + return nil, fmt.Errorf("deviceauth: unable to authenticate device\n %w", err) + } + verificationUri := da.VerificationURI + if verificationUri == "" { + verificationUri = da.VerificationURL + } + return &DeviceAuth{ + DeviceCode: da.DeviceCode, + UserCode: da.UserCode, + VerificationURI: verificationUri, + VerificationURIComplete: da.VerificationURIComplete, + Expires: da.ExpiresIn.expiry(now), + Interval: time.Duration(da.Interval) * time.Second, + }, nil +} + +// Poll polls to exchange a device code for a token. +func (config *DeviceAuthConfig) Poll(devideAuth *DeviceAuth, options ...AuthCodeOption) (*oauth2.Token, error) { + params := url.Values{ + "client_id": {config.ClientID}, + "grant_type": {"http://oauth.net/grant_type/device/1.0"}, + "device_code": {devideAuth.DeviceCode}, + "code": {devideAuth.DeviceCode}, + } + if len(config.Scopes) > 0 { + params.Set("scope", strings.Join(config.Scopes, " ")) + } + for _, option := range options { + option.setValue(params) + } + + // If no interval was provided, the client MUST use a reasonable default polling interval. + // See https://tools.ietf.org/html/draft-ietf-oauth-device-flow-07#section-3.5 + interval := devideAuth.Interval + if interval == 0 { + interval = 5 * time.Second + } + + for { + tok, err := config.retrieveToken(params, config.Endpoint.AuthStyle) + if err == nil { + return tok, nil + } + + errTyp := parseError(err) + switch errTyp { + case errAccessDenied, errExpiredToken: + return tok, fmt.Errorf("deviceauth: unable to poll token: %s\n %w", errTyp, err) + case errSlowDown: + interval += 5 * time.Second + fallthrough + case errAuthorizationPending: + time.Sleep(interval) + } + } +} diff --git a/deviceauth/model_authcodeoption.go b/deviceauth/model_authcodeoption.go new file mode 100644 index 0000000000000000000000000000000000000000..8837d713af2c45e20fc53afab30daa9379db09bf --- /dev/null +++ b/deviceauth/model_authcodeoption.go @@ -0,0 +1,39 @@ +package deviceauth + +import "net/url" + +var ( + // AccessTypeOnline and AccessTypeOffline are options passed + // to the Options.AuthCodeURL method. They modify the + // "access_type" field that gets sent in the URL returned by + // AuthCodeURL. + // + // Online is the default if neither is specified. If your + // application needs to refresh access tokens when the user + // is not present at the browser, then use offline. This will + // result in your application obtaining a refresh token the + // first time your application exchanges an authorization + // code for a user. + AccessTypeOnline AuthCodeOption = SetAuthURLParam("access_type", "online") + AccessTypeOffline AuthCodeOption = SetAuthURLParam("access_type", "offline") + + // ApprovalForce forces the users to view the consent dialog + // and confirm the permissions request at the URL returned + // from AuthCodeURL, even if they've already done so. + ApprovalForce AuthCodeOption = SetAuthURLParam("prompt", "consent") +) + +// An AuthCodeOption is passed to Config.AuthCodeURL. +type AuthCodeOption interface { + setValue(url.Values) +} + +type setParam struct{ k, v string } + +func (p setParam) setValue(m url.Values) { m.Set(p.k, p.v) } + +// SetAuthURLParam builds an AuthCodeOption which passes key/value parameters +// to a provider's authorization endpoint. +func SetAuthURLParam(key, value string) AuthCodeOption { + return setParam{key, value} +} diff --git a/deviceauth/model_authstyle.go b/deviceauth/model_authstyle.go new file mode 100644 index 0000000000000000000000000000000000000000..171e3d45c425c49a1db40e18cdaaf8dab0c3c274 --- /dev/null +++ b/deviceauth/model_authstyle.go @@ -0,0 +1,9 @@ +package deviceauth + +// AuthStyle is a copy of the golang.org/x/oauth2 package's AuthStyle type. +type AuthStyle int + +const ( + AuthStyleInParams AuthStyle = 1 + AuthStyleInHeader AuthStyle = 2 +) diff --git a/deviceauth/model_deviceauth.go b/deviceauth/model_deviceauth.go new file mode 100644 index 0000000000000000000000000000000000000000..136d7e82b0f410033ca3a39a8a9caea4a467016b --- /dev/null +++ b/deviceauth/model_deviceauth.go @@ -0,0 +1,12 @@ +package deviceauth + +import "time" + +type DeviceAuth struct { + DeviceCode string `json:"device-code"` + UserCode string `json:"user-code"` + VerificationURI string `json:"verification-uri"` + VerificationURIComplete string `json:"verification-uri-complete"` + Expires time.Time `json:"expires"` + Interval time.Duration `json:"interval"` +} diff --git a/deviceauth/model_duration.go b/deviceauth/model_duration.go new file mode 100644 index 0000000000000000000000000000000000000000..65a7cb6527d010e1cc8672f10dae31738b45923f --- /dev/null +++ b/deviceauth/model_duration.go @@ -0,0 +1,36 @@ +package deviceauth + +import ( + "encoding/json" + "math" + "time" +) + +type durationDto int32 + +func (d durationDto) expiry(now time.Time) (t time.Time) { + if d != 0 { + return now.Add(time.Duration(d) * time.Second) + } + return +} + +func (d *durationDto) UnmarshalJSON(b []byte) error { + if len(b) == 0 || string(b) == "null" { + return nil + } + var n json.Number + err := json.Unmarshal(b, &n) + if err != nil { + return err + } + i, err := n.Int64() + if err != nil { + return err + } + if i > math.MaxInt32 { + i = math.MaxInt32 + } + *d = durationDto(i) + return nil +} diff --git a/deviceauth/request_deviceauth.go b/deviceauth/request_deviceauth.go new file mode 100644 index 0000000000000000000000000000000000000000..c81d0d5cc1b459baa9613fdd2d56678c217f058b --- /dev/null +++ b/deviceauth/request_deviceauth.go @@ -0,0 +1,65 @@ +package deviceauth + +import ( + "encoding/json" + "fmt" + "golang.org/x/oauth2" + "io" + "io/ioutil" + "net/http" + "net/url" + "scaleway-dedibox-api/httputil" + "strings" +) + +type jsonDeviceAuth struct { + DeviceCode string `json:"device_code"` + UserCode string `json:"user_code"` + VerificationURI string `json:"verification_uri"` + VerificationURL string `json:"verification_url"` + VerificationURIComplete string `json:"verification_uri_complete,omitempty"` + ExpiresIn durationDto `json:"expires_in"` + Interval durationDto `json:"interval,omitempty"` +} + +func (config *DeviceAuthConfig) retrieveDeviceAuth(params url.Values) (*jsonDeviceAuth, error) { + request, err := http.NewRequest("POST", config.Endpoint.DeviceCodeURL, strings.NewReader(params.Encode())) + if err != nil { + return nil, fmt.Errorf("deviceauth.request_deviceauth: unable to build device auth request\n %w", err) + } + request.Header.Set("Content-Type", "application/x-www-form-urlencoded") + reponse, err := config.Client.Do(request) + if err != nil { + return nil, fmt.Errorf("deviceauth.request_deviceauth: unable to authenticate device\n %w", err) + } + + body := io.LimitReader(reponse.Body, maximumBodySize) + if !httputil.IsHttpStatusSuccess(reponse.StatusCode) { + fullBody, err := ioutil.ReadAll(body) + if err != nil { + return nil, fmt.Errorf("deviceauth.request_deviceauth: unable to authenticate device\n %w", err) + } + return nil, &oauth2.RetrieveError{ + Response: reponse, + Body: fullBody, + } + } + + var deviceAuth = &jsonDeviceAuth{} + err = json.NewDecoder(body).Decode(&deviceAuth) + if err != nil { + return nil, fmt.Errorf("deviceauth.request_deviceauth: unable to deserialize device auth response\n %w", err) + } + + return deviceAuth, nil +} + +func parseError(err error) string { + e, ok := err.(*oauth2.RetrieveError) + if ok { + eResp := make(map[string]string) + _ = json.Unmarshal(e.Body, &eResp) + return eResp["error"] + } + return "" +} diff --git a/deviceauth/request_token.go b/deviceauth/request_token.go new file mode 100644 index 0000000000000000000000000000000000000000..85303669b736656e6692de59f0fc0153dc3ede2c --- /dev/null +++ b/deviceauth/request_token.go @@ -0,0 +1,131 @@ +package deviceauth + +import ( + "encoding/json" + "fmt" + "golang.org/x/oauth2" + "io" + "io/ioutil" + "mime" + "net/http" + "net/url" + "scaleway-dedibox-api/httputil" + "strconv" + "strings" + "time" +) + +func cloneURLValues(v url.Values) url.Values { + v2 := make(url.Values, len(v)) + for k, vv := range v { + v2[k] = append([]string(nil), vv...) + } + return v2 +} + +// newTokenRequest returns a fallback *http.Request to retrieve a fallback token +// from tokenURL using the provided clientID, clientSecret, and POST +// body parameters. +func newTokenRequest(tokenURL, clientID, clientSecret string, v url.Values, authStyle AuthStyle) (*http.Request, error) { + if authStyle == AuthStyleInParams { + v = cloneURLValues(v) + if clientID != "" { + v.Set("client_id", clientID) + } + if clientSecret != "" { + v.Set("client_secret", clientSecret) + } + } + req, err := http.NewRequest("POST", tokenURL, strings.NewReader(v.Encode())) + if err != nil { + return nil, fmt.Errorf("deviceauth.request_token: could not build token request\n %w", err) + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + if authStyle == AuthStyleInHeader { + req.SetBasicAuth(url.QueryEscape(clientID), url.QueryEscape(clientSecret)) + } + return req, nil +} + +// tokenJSON is the struct representing the HTTP response from OAuth2 +// providers returning a token in JSON form. +type tokenJSON struct { + AccessToken string `json:"access_token"` + TokenType string `json:"token_type"` + RefreshToken string `json:"refresh_token"` + ExpiresIn durationDto `json:"expires_in"` // at least PayPal returns string, while most return number +} + +func (config *DeviceAuthConfig) doTokenRoundTrip(request *http.Request) (*oauth2.Token, error) { + now := time.Now() + response, err := config.Client.Do(request) + if err != nil { + return nil, fmt.Errorf("deviceauth.request_token: unable to fetch token\n %w", err) + } + body := io.LimitReader(response.Body, maximumBodySize) + if !httputil.IsHttpStatusSuccess(response.StatusCode) { + fullBody, err := ioutil.ReadAll(body) + if err != nil { + return nil, fmt.Errorf("deviceauth.request_token: unable to fetch token\n %w", err) + } + return nil, &oauth2.RetrieveError{ + Response: response, + Body: fullBody, + } + } + + var token *oauth2.Token + content, _, _ := mime.ParseMediaType(response.Header.Get("Content-Type")) + switch content { + case "application/x-www-form-urlencoded", "text/plain": + fullBody, err := ioutil.ReadAll(body) + if err != nil { + return nil, fmt.Errorf("deviceauth.request_token: could not read token response\n %w", err) + } + vals, err := url.ParseQuery(string(fullBody)) + if err != nil { + return nil, fmt.Errorf("deviceauth.request_token: could not parse token response\n %w", err) + } + token = &oauth2.Token{ + AccessToken: vals.Get("access_token"), + TokenType: vals.Get("token_type"), + RefreshToken: vals.Get("refresh_token"), + } + e := vals.Get("expires_in") + expires, _ := strconv.Atoi(e) + if expires != 0 { + token.Expiry = now.Add(time.Duration(expires) * time.Second) + } + default: + var tj tokenJSON + if err = json.NewDecoder(body).Decode(&tj); err != nil { + return nil, fmt.Errorf("deviceauth.request_token: could not deserialize token response\n %w", err) + } + token = &oauth2.Token{ + AccessToken: tj.AccessToken, + TokenType: tj.TokenType, + RefreshToken: tj.RefreshToken, + Expiry: tj.ExpiresIn.expiry(now), + } + } + if token.AccessToken == "" { + return nil, fmt.Errorf("deviceauth.request_token: server response missing access_token") + } + return token, nil +} + +func (config *DeviceAuthConfig) retrieveToken(params url.Values, authStyle AuthStyle) (*oauth2.Token, error) { + req, err := newTokenRequest(config.Endpoint.TokenURL, config.ClientID, config.ClientSecret, params, authStyle) + if err != nil { + return nil, fmt.Errorf("deviceauth.request_token: could not retrieve token\n %w", err) + } + token, err := config.doTokenRoundTrip(req) + // Don’t overwrite `RefreshToken` with an empty value if this was a token refreshing request. + if token != nil && token.RefreshToken == "" { + token.RefreshToken = params.Get("refresh_token") + } + if err != nil { + return nil, fmt.Errorf("deviceauth.request_token: could not retrieve token\n %w", err) + } + return token, nil +} diff --git a/deviceauth/tokensource_deviceauth.go b/deviceauth/tokensource_deviceauth.go new file mode 100644 index 0000000000000000000000000000000000000000..541c08c84770f9b72522479217e3c910aba88022 --- /dev/null +++ b/deviceauth/tokensource_deviceauth.go @@ -0,0 +1,33 @@ +package deviceauth + +import ( + "fmt" + "golang.org/x/oauth2" +) + +type deviceAuthTokenSource struct { + deviceAuthConfig *DeviceAuthConfig +} + +func (s *deviceAuthTokenSource) Token() (*oauth2.Token, error) { + device, err := s.deviceAuthConfig.AuthDevice(AccessTypeOffline) + if err != nil { + return nil, fmt.Errorf("deviceauth.tokensource_deviceauth: could not authenticate device\n %w", err) + } + fmt.Printf( + "\n Please go to the following URL and enter this code '%s' to verify this oauth app:\n\n %s\n\n\n", + device.UserCode, + device.VerificationURI, + ) + token, err := s.deviceAuthConfig.Poll(device, AccessTypeOffline) + if err != nil { + return nil, fmt.Errorf("deviceauth.tokensource_deviceauth: did not receive a device token\n %w", err) + } + return token, nil +} + +func DeviceAuthTokenSource(config *DeviceAuthConfig) oauth2.TokenSource { + return &deviceAuthTokenSource{ + deviceAuthConfig: config, + } +} diff --git a/deviceauth/tokensource_persisting.go b/deviceauth/tokensource_persisting.go new file mode 100644 index 0000000000000000000000000000000000000000..4451913c5a3ea32494749521acad03c7203b56e3 --- /dev/null +++ b/deviceauth/tokensource_persisting.go @@ -0,0 +1,62 @@ +package deviceauth + +import ( + "fmt" + "github.com/sirupsen/logrus" + "golang.org/x/oauth2" + "scaleway-dedibox-api/util" + "sync" +) + +var persistingTokenSourceLog = logrus.WithField("file", "deviceauth.tokensource_persisting") + +// reuseTokenSource is a TokenSource that holds a single token in storage +// and validates its expiry before each call to retrieve it with +// Token. If it's expired, it will be auto-refreshed using the +// fallback TokenSource. +type persistingTokenSource struct { + fallback oauth2.TokenSource // called when token is expired. + mutex sync.Mutex // guards token + token *oauth2.Token + persister util.Persister[oauth2.Token] +} + +// Token returns the current token if it's still valid, else will +// refresh the current token and return the new one. +func (source *persistingTokenSource) Token() (*oauth2.Token, error) { + source.mutex.Lock() + defer source.mutex.Unlock() + if source.token.Valid() { + return source.token, nil + } + token, err := source.persister.Restore() + if err != nil { + persistingTokenSourceLog.Debugln("requesting new token") + token, err = source.fallback.Token() + } else { + persistingTokenSourceLog.Debugln("token restored") + } + if err != nil { + return nil, fmt.Errorf("deviceauth.tokensource_persisting: could not request new token\n %w", err) + } + source.token = token + persistingTokenSourceLog.Debugln("persisting token") + err = source.persister.Persist(token) + if err != nil { + return nil, fmt.Errorf("deviceauth.tokensource_persisting: could not persist new token\n %w", err) + } + return token, nil +} + +func PersistingTokenSource(fallback oauth2.TokenSource, persister util.Persister[oauth2.Token]) (oauth2.TokenSource, error) { + // Don't wrap a reuseTokenSource in itself. That would work, + // but cause an unnecessary number of mutex operations. + // Just build the equivalent one. + if wrapped, ok := fallback.(*persistingTokenSource); ok { + fallback = wrapped.fallback + } + return &persistingTokenSource{ + fallback: fallback, + persister: persister, + }, nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000000000000000000000000000000000000..92621893fe169f3ce0e70dbef094acdc2d93399f --- /dev/null +++ b/go.mod @@ -0,0 +1,13 @@ +module scaleway-dedibox-api + +go 1.18 + +require ( + github.com/sirupsen/logrus v1.8.1 + golang.org/x/oauth2 v0.0.0-20220524215830-622c5d57e401 + github.com/golang/protobuf v1.4.2 // indirect + golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect + golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect + google.golang.org/appengine v1.6.6 // indirect + google.golang.org/protobuf v1.25.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000000000000000000000000000000000000..8f803bec39429ebd1fd962981ed2006103d3f9b4 --- /dev/null +++ b/go.sum @@ -0,0 +1,377 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20220524215830-622c5d57e401 h1:zwrSfklXn0gxyLRX/aR+q6cgHbV/ItVyzbPlbA+dkAw= +golang.org/x/oauth2 v0.0.0-20220524215830-622c5d57e401/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/httputil/client.go b/httputil/client.go new file mode 100644 index 0000000000000000000000000000000000000000..49bc1a4858b27be5b20136dcd98c108c7765894b --- /dev/null +++ b/httputil/client.go @@ -0,0 +1,79 @@ +package httputil + +import ( + "context" + "fmt" + "golang.org/x/net/context/ctxhttp" + "net/http" +) + +type Client struct { + client http.Client + ctx context.Context + requestMiddleware *RequestMiddleware + responseMiddleware *ResponseMiddleware +} + +func NewClient(ctx context.Context, client *http.Client) *Client { + if client == nil { + client = &http.Client{} + } + return &Client{ + ctx: ctx, + client: *client, + } +} + +func (c *Client) WithRequestMiddleware(middleware RequestMiddleware) *Client { + if c.requestMiddleware != nil { + middleware = RequestMiddlewares(*c.requestMiddleware, middleware) + } + return &Client{ + client: c.client, + requestMiddleware: &middleware, + responseMiddleware: c.responseMiddleware, + } +} + +func (c *Client) WithResponseMiddleware(middleware ResponseMiddleware) *Client { + if c.responseMiddleware != nil { + middleware = ResponseMiddlewares(*c.responseMiddleware, middleware) + } + return &Client{ + client: c.client, + requestMiddleware: c.requestMiddleware, + responseMiddleware: &middleware, + } +} + +func (c *Client) Do(req *http.Request) (*http.Response, error) { + if c.requestMiddleware != nil { + if err := (*c.requestMiddleware)(req); err != nil { + return nil, fmt.Errorf("httputil.client: could not execute request middleware\n %w", err) + } + } + var resp *http.Response + var err error + if c.ctx == nil { + resp, err = c.client.Do(req) + } else { + resp, err = ctxhttp.Do(c.ctx, &c.client, req) + } + if err != nil { + return nil, fmt.Errorf("httputil.client: could not execute request\n %w", err) + } + if resp == nil { + return nil, fmt.Errorf("httputil.client: got a nil response during an http request") + } + if c.responseMiddleware != nil { + if err := (*c.responseMiddleware)(req, resp); err != nil { + return nil, fmt.Errorf("httputil.client: could not execute response middleware\n %w", err) + } + } + + return resp, nil +} + +func IsHttpStatusSuccess(status int) bool { + return status >= 200 && status < 300 +} diff --git a/httputil/middleware.go b/httputil/middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..829f08dbd35e2fc0d160612718414f022be1d7ae --- /dev/null +++ b/httputil/middleware.go @@ -0,0 +1,30 @@ +package httputil + +import ( + "net/http" +) + +type RequestMiddleware func(req *http.Request) error +type ResponseMiddleware func(req *http.Request, resp *http.Response) error + +func RequestMiddlewares(middlewares ...RequestMiddleware) RequestMiddleware { + return func(req *http.Request) error { + for _, middleware := range middlewares { + if err := middleware(req); err != nil { + return err + } + } + return nil + } +} + +func ResponseMiddlewares(middlewares ...ResponseMiddleware) ResponseMiddleware { + return func(req *http.Request, resp *http.Response) error { + for _, middleware := range middlewares { + if err := middleware(req, resp); err != nil { + return err + } + } + return nil + } +} diff --git a/httputil/middleware_tokensource.go b/httputil/middleware_tokensource.go new file mode 100644 index 0000000000000000000000000000000000000000..c77a040b68668778921369bab82eaa829bb00e31 --- /dev/null +++ b/httputil/middleware_tokensource.go @@ -0,0 +1,29 @@ +package httputil + +import ( + "fmt" + "github.com/sirupsen/logrus" + "golang.org/x/oauth2" + "net/http" + "time" +) + +var tokenSourceMiddlewareLog = logrus.WithField("file", "httputil.middleware_tokensource") + +func TokenSourceMiddleware(tokenSource oauth2.TokenSource) RequestMiddleware { + return func(req *http.Request) error { + token, err := tokenSource.Token() + if err != nil { + return err + } + if token.Type() != "Bearer" { + return fmt.Errorf("httputil.middleware_tokensource: cannot use token of type %s for authorization", token.Type()) + } + tokenSourceMiddlewareLog.Debugln("authorizing with token having expiry of", token.Expiry.Format(time.RFC3339)) + req.Header.Set( + "Authorization", + fmt.Sprintf("Bearer %s", token.AccessToken), + ) + return nil + } +} diff --git a/httputil/middleware_tracing.go b/httputil/middleware_tracing.go new file mode 100644 index 0000000000000000000000000000000000000000..9a73bb3cdd3dfe9a19e7e45f7728d5972c849a0d --- /dev/null +++ b/httputil/middleware_tracing.go @@ -0,0 +1,44 @@ +package httputil + +import ( + "fmt" + "github.com/sirupsen/logrus" + "net/http" + "net/http/httputil" + "strings" +) + +func TracingMiddleware(traceAll bool) ResponseMiddleware { + return func(req *http.Request, resp *http.Response) error { + if traceAll || !IsHttpStatusSuccess(resp.StatusCode) { + originalAuth := req.Header.Get("Authorization") + if originalAuth != "" { + if strings.HasPrefix(originalAuth, "Bearer ") { + req.Header.Set("Authorization", "Bearer [redacted]") + } else { + req.Header.Set("Authorization", "[redacted]") + } + } + reqTrace, err := httputil.DumpRequestOut(req, false) + if err != nil { + return fmt.Errorf("httputil.middleware_tracing: could not trace http request %w", err) + } + // only dump body for errors + respTrace, err := httputil.DumpResponse(resp, traceAll || !IsHttpStatusSuccess(resp.StatusCode)) + if err != nil { + return fmt.Errorf("httputil.middleware_tracing: could not trace http response %w", err) + } + logrus.Infoln( + strings.Join( + []string{ + "--- HTTP TRACE ---", + string(reqTrace), + strings.TrimSpace(string(respTrace)), + }, + "\n", + ), + ) + } + return nil + } +} diff --git a/util/filepersister.go b/util/filepersister.go new file mode 100644 index 0000000000000000000000000000000000000000..04eb542a3b31a203d072de90ec73bc80adf5a69e --- /dev/null +++ b/util/filepersister.go @@ -0,0 +1,44 @@ +package util + +import ( + "encoding/json" + "fmt" + "os" +) + +type FilePersister[T any] struct { + path string +} + +func NewFilePersister[T any](path string) Persister[T] { + return Persister[T](FilePersister[T]{path}) +} + +func (persister FilePersister[T]) Restore() (*T, error) { + file, err := os.OpenFile(persister.path, os.O_RDONLY, 0600) + if err != nil { + return nil, fmt.Errorf("filepersister: could not restore file %s\n %w", persister.path, + fmt.Errorf("could not open file\n %w", err)) + } + var data T + err = json.NewDecoder(file).Decode(&data) + if err != nil { + return nil, fmt.Errorf("filepersister: could not restore file %s\n %w", persister.path, + fmt.Errorf("could not deserialize data\n %w", err)) + } + return &data, nil +} + +func (persister FilePersister[T]) Persist(data *T) error { + file, err := os.OpenFile(persister.path, os.O_RDWR|os.O_CREATE, 0600) + if err != nil { + return fmt.Errorf("filepersister: could not persist file %s\n %w", persister.path, + fmt.Errorf("could not open file\n %w", err)) + } + err = json.NewEncoder(file).Encode(data) + if err != nil { + return fmt.Errorf("filepersister: could not persist file %s\n %w", persister.path, + fmt.Errorf("could not serialize data\n %w", err)) + } + return nil +} diff --git a/util/persister.go b/util/persister.go new file mode 100644 index 0000000000000000000000000000000000000000..d2ad115d726110ba28481edf32c7ffbef21d5ff3 --- /dev/null +++ b/util/persister.go @@ -0,0 +1,6 @@ +package util + +type Persister[T any] interface { + Restore() (*T, error) + Persist(data *T) error +}