1// +build go1.7
2
3package management
4
5// Copyright 2017 Microsoft Corporation
6//
7//    Licensed under the Apache License, Version 2.0 (the "License");
8//    you may not use this file except in compliance with the License.
9//    You may obtain a copy of the License at
10//
11//        http://www.apache.org/licenses/LICENSE-2.0
12//
13//    Unless required by applicable law or agreed to in writing, software
14//    distributed under the License is distributed on an "AS IS" BASIS,
15//    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16//    See the License for the specific language governing permissions and
17//    limitations under the License.
18
19import (
20	"bytes"
21	"crypto/tls"
22	"fmt"
23	"net/http"
24	"time"
25)
26
27const (
28	msVersionHeader           = "x-ms-version"
29	requestIDHeader           = "x-ms-request-id"
30	uaHeader                  = "User-Agent"
31	contentHeader             = "Content-Type"
32	defaultContentHeaderValue = "application/xml"
33)
34
35func (client client) SendAzureGetRequest(url string) ([]byte, error) {
36	resp, err := client.sendAzureRequest("GET", url, "", nil)
37	if err != nil {
38		return nil, err
39	}
40	return getResponseBody(resp)
41}
42
43func (client client) SendAzurePostRequest(url string, data []byte) (OperationID, error) {
44	return client.doAzureOperation("POST", url, "", data)
45}
46
47func (client client) SendAzurePostRequestWithReturnedResponse(url string, data []byte) ([]byte, error) {
48	resp, err := client.sendAzureRequest("POST", url, "", data)
49	if err != nil {
50		return nil, err
51	}
52
53	return getResponseBody(resp)
54}
55
56func (client client) SendAzurePutRequest(url, contentType string, data []byte) (OperationID, error) {
57	return client.doAzureOperation("PUT", url, contentType, data)
58}
59
60func (client client) SendAzureDeleteRequest(url string) (OperationID, error) {
61	return client.doAzureOperation("DELETE", url, "", nil)
62}
63
64func (client client) doAzureOperation(method, url, contentType string, data []byte) (OperationID, error) {
65	response, err := client.sendAzureRequest(method, url, contentType, data)
66	if err != nil {
67		return "", err
68	}
69	return getOperationID(response)
70}
71
72func getOperationID(response *http.Response) (OperationID, error) {
73	requestID := response.Header.Get(requestIDHeader)
74	if requestID == "" {
75		return "", fmt.Errorf("Could not retrieve operation id from %q header", requestIDHeader)
76	}
77	return OperationID(requestID), nil
78}
79
80// sendAzureRequest constructs an HTTP client for the request, sends it to the
81// management API and returns the response or an error.
82func (client client) sendAzureRequest(method, url, contentType string, data []byte) (*http.Response, error) {
83	if method == "" {
84		return nil, fmt.Errorf(errParamNotSpecified, "method")
85	}
86	if url == "" {
87		return nil, fmt.Errorf(errParamNotSpecified, "url")
88	}
89
90	response, err := client.sendRequest(client.httpClient, url, method, contentType, data, 5)
91	if err != nil {
92		return nil, err
93	}
94
95	return response, nil
96}
97
98// createHTTPClient creates an HTTP Client configured with the key pair for
99// the subscription for this client.
100func (client client) createHTTPClient() (*http.Client, error) {
101	cert, err := tls.X509KeyPair(client.publishSettings.SubscriptionCert, client.publishSettings.SubscriptionKey)
102	if err != nil {
103		return nil, err
104	}
105
106	return &http.Client{
107		Transport: &http.Transport{
108			Proxy: http.ProxyFromEnvironment,
109			TLSClientConfig: &tls.Config{
110				Renegotiation: tls.RenegotiateOnceAsClient,
111				Certificates:  []tls.Certificate{cert},
112			},
113		},
114	}, nil
115}
116
117// sendRequest sends a request to the Azure management API using the given
118// HTTP client and parameters. It returns the response from the call or an
119// error.
120func (client client) sendRequest(httpClient *http.Client, url, requestType, contentType string, data []byte, numberOfRetries int) (*http.Response, error) {
121
122	absURI := client.createAzureRequestURI(url)
123
124	for {
125		request, reqErr := client.createAzureRequest(absURI, requestType, contentType, data)
126		if reqErr != nil {
127			return nil, reqErr
128		}
129
130		response, err := httpClient.Do(request)
131		if err != nil {
132			if numberOfRetries == 0 {
133				return nil, err
134			}
135
136			return client.sendRequest(httpClient, url, requestType, contentType, data, numberOfRetries-1)
137		}
138		if response.StatusCode == http.StatusTemporaryRedirect {
139			// ASM's way of moving traffic around, see https://msdn.microsoft.com/en-us/library/azure/ee460801.aspx
140			// Only handled automatically for GET/HEAD requests. This is for the rest of the http verbs.
141			u, err := response.Location()
142			if err != nil {
143				return response, fmt.Errorf("Redirect requested but location header could not be retrieved: %v", err)
144			}
145			absURI = u.String()
146			continue // re-issue request
147		}
148
149		if response.StatusCode >= http.StatusBadRequest {
150			body, err := getResponseBody(response)
151			if err != nil {
152				// Failed to read the response body
153				return nil, err
154			}
155			azureErr := getAzureError(body)
156			if azureErr != nil {
157				if numberOfRetries == 0 {
158					return nil, azureErr
159				}
160				if response.StatusCode == http.StatusServiceUnavailable || response.StatusCode == http.StatusTooManyRequests {
161					// Wait before retrying the operation
162					time.Sleep(client.config.OperationPollInterval)
163				}
164
165				return client.sendRequest(httpClient, url, requestType, contentType, data, numberOfRetries-1)
166			}
167		}
168
169		return response, nil
170	}
171}
172
173// createAzureRequestURI constructs the request uri using the management API endpoint and
174// subscription ID associated with the client.
175func (client client) createAzureRequestURI(url string) string {
176	return fmt.Sprintf("%s/%s/%s", client.config.ManagementURL, client.publishSettings.SubscriptionID, url)
177}
178
179// createAzureRequest packages up the request with the correct set of headers and returns
180// the request object or an error.
181func (client client) createAzureRequest(url string, requestType string, contentType string, data []byte) (*http.Request, error) {
182	var request *http.Request
183	var err error
184
185	if data != nil {
186		body := bytes.NewBuffer(data)
187		request, err = http.NewRequest(requestType, url, body)
188	} else {
189		request, err = http.NewRequest(requestType, url, nil)
190	}
191
192	if err != nil {
193		return nil, err
194	}
195
196	request.Header.Set(msVersionHeader, client.config.APIVersion)
197	request.Header.Set(uaHeader, client.config.UserAgent)
198
199	if contentType != "" {
200		request.Header.Set(contentHeader, contentType)
201	} else {
202		request.Header.Set(contentHeader, defaultContentHeaderValue)
203	}
204
205	return request, nil
206}
207