forked from TrueCloudLab/restic
61cb1cc6f8
This includes github.com/kurin/blazer 0.2.0, which resolves #1291
206 lines
6.2 KiB
Go
206 lines
6.2 KiB
Go
// +build go1.7
|
|
|
|
package management
|
|
|
|
// Copyright 2017 Microsoft Corporation
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/tls"
|
|
"fmt"
|
|
"net/http"
|
|
)
|
|
|
|
const (
|
|
msVersionHeader = "x-ms-version"
|
|
requestIDHeader = "x-ms-request-id"
|
|
uaHeader = "User-Agent"
|
|
contentHeader = "Content-Type"
|
|
defaultContentHeaderValue = "application/xml"
|
|
)
|
|
|
|
func (client client) SendAzureGetRequest(url string) ([]byte, error) {
|
|
resp, err := client.sendAzureRequest("GET", url, "", nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return getResponseBody(resp)
|
|
}
|
|
|
|
func (client client) SendAzurePostRequest(url string, data []byte) (OperationID, error) {
|
|
return client.doAzureOperation("POST", url, "", data)
|
|
}
|
|
|
|
func (client client) SendAzurePostRequestWithReturnedResponse(url string, data []byte) ([]byte, error) {
|
|
resp, err := client.sendAzureRequest("POST", url, "", data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return getResponseBody(resp)
|
|
}
|
|
|
|
func (client client) SendAzurePutRequest(url, contentType string, data []byte) (OperationID, error) {
|
|
return client.doAzureOperation("PUT", url, contentType, data)
|
|
}
|
|
|
|
func (client client) SendAzureDeleteRequest(url string) (OperationID, error) {
|
|
return client.doAzureOperation("DELETE", url, "", nil)
|
|
}
|
|
|
|
func (client client) doAzureOperation(method, url, contentType string, data []byte) (OperationID, error) {
|
|
response, err := client.sendAzureRequest(method, url, contentType, data)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return getOperationID(response)
|
|
}
|
|
|
|
func getOperationID(response *http.Response) (OperationID, error) {
|
|
requestID := response.Header.Get(requestIDHeader)
|
|
if requestID == "" {
|
|
return "", fmt.Errorf("Could not retrieve operation id from %q header", requestIDHeader)
|
|
}
|
|
return OperationID(requestID), nil
|
|
}
|
|
|
|
// sendAzureRequest constructs an HTTP client for the request, sends it to the
|
|
// management API and returns the response or an error.
|
|
func (client client) sendAzureRequest(method, url, contentType string, data []byte) (*http.Response, error) {
|
|
if method == "" {
|
|
return nil, fmt.Errorf(errParamNotSpecified, "method")
|
|
}
|
|
if url == "" {
|
|
return nil, fmt.Errorf(errParamNotSpecified, "url")
|
|
}
|
|
|
|
httpClient, err := client.createHTTPClient()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
response, err := client.sendRequest(httpClient, url, method, contentType, data, 5)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return response, nil
|
|
}
|
|
|
|
// createHTTPClient creates an HTTP Client configured with the key pair for
|
|
// the subscription for this client.
|
|
func (client client) createHTTPClient() (*http.Client, error) {
|
|
cert, err := tls.X509KeyPair(client.publishSettings.SubscriptionCert, client.publishSettings.SubscriptionKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &http.Client{
|
|
Transport: &http.Transport{
|
|
Proxy: http.ProxyFromEnvironment,
|
|
TLSClientConfig: &tls.Config{
|
|
Renegotiation: tls.RenegotiateOnceAsClient,
|
|
Certificates: []tls.Certificate{cert},
|
|
},
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// sendRequest sends a request to the Azure management API using the given
|
|
// HTTP client and parameters. It returns the response from the call or an
|
|
// error.
|
|
func (client client) sendRequest(httpClient *http.Client, url, requestType, contentType string, data []byte, numberOfRetries int) (*http.Response, error) {
|
|
|
|
absURI := client.createAzureRequestURI(url)
|
|
|
|
for {
|
|
request, reqErr := client.createAzureRequest(absURI, requestType, contentType, data)
|
|
if reqErr != nil {
|
|
return nil, reqErr
|
|
}
|
|
|
|
response, err := httpClient.Do(request)
|
|
if err != nil {
|
|
if numberOfRetries == 0 {
|
|
return nil, err
|
|
}
|
|
|
|
return client.sendRequest(httpClient, url, requestType, contentType, data, numberOfRetries-1)
|
|
}
|
|
if response.StatusCode == http.StatusTemporaryRedirect {
|
|
// ASM's way of moving traffic around, see https://msdn.microsoft.com/en-us/library/azure/ee460801.aspx
|
|
// Only handled automatically for GET/HEAD requests. This is for the rest of the http verbs.
|
|
u, err := response.Location()
|
|
if err != nil {
|
|
return response, fmt.Errorf("Redirect requested but location header could not be retrieved: %v", err)
|
|
}
|
|
absURI = u.String()
|
|
continue // re-issue request
|
|
}
|
|
|
|
if response.StatusCode >= http.StatusBadRequest {
|
|
body, err := getResponseBody(response)
|
|
if err != nil {
|
|
// Failed to read the response body
|
|
return nil, err
|
|
}
|
|
azureErr := getAzureError(body)
|
|
if azureErr != nil {
|
|
if numberOfRetries == 0 {
|
|
return nil, azureErr
|
|
}
|
|
|
|
return client.sendRequest(httpClient, url, requestType, contentType, data, numberOfRetries-1)
|
|
}
|
|
}
|
|
|
|
return response, nil
|
|
}
|
|
}
|
|
|
|
// createAzureRequestURI constructs the request uri using the management API endpoint and
|
|
// subscription ID associated with the client.
|
|
func (client client) createAzureRequestURI(url string) string {
|
|
return fmt.Sprintf("%s/%s/%s", client.config.ManagementURL, client.publishSettings.SubscriptionID, url)
|
|
}
|
|
|
|
// createAzureRequest packages up the request with the correct set of headers and returns
|
|
// the request object or an error.
|
|
func (client client) createAzureRequest(url string, requestType string, contentType string, data []byte) (*http.Request, error) {
|
|
var request *http.Request
|
|
var err error
|
|
|
|
if data != nil {
|
|
body := bytes.NewBuffer(data)
|
|
request, err = http.NewRequest(requestType, url, body)
|
|
} else {
|
|
request, err = http.NewRequest(requestType, url, nil)
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
request.Header.Set(msVersionHeader, client.config.APIVersion)
|
|
request.Header.Set(uaHeader, client.config.UserAgent)
|
|
|
|
if contentType != "" {
|
|
request.Header.Set(contentHeader, contentType)
|
|
} else {
|
|
request.Header.Set(contentHeader, defaultContentHeaderValue)
|
|
}
|
|
|
|
return request, nil
|
|
}
|