rpRegistrationPolicy.ts 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. // Copyright (c) Microsoft Corporation. All rights reserved.
  2. // Licensed under the MIT License. See License.txt in the project root for license information.
  3. import { HttpOperationResponse } from "../httpOperationResponse";
  4. import * as utils from "../util/utils";
  5. import { WebResource } from "../webResource";
  6. import { BaseRequestPolicy, RequestPolicy, RequestPolicyFactory, RequestPolicyOptions } from "./requestPolicy";
  7. export function rpRegistrationPolicy(retryTimeout = 30): RequestPolicyFactory {
  8. return {
  9. create: (nextPolicy: RequestPolicy, options: RequestPolicyOptions) => {
  10. return new RPRegistrationPolicy(nextPolicy, options, retryTimeout);
  11. }
  12. };
  13. }
  14. export class RPRegistrationPolicy extends BaseRequestPolicy {
  15. constructor(nextPolicy: RequestPolicy, options: RequestPolicyOptions, readonly _retryTimeout = 30) {
  16. super(nextPolicy, options);
  17. }
  18. public sendRequest(request: WebResource): Promise<HttpOperationResponse> {
  19. return this._nextPolicy.sendRequest(request.clone())
  20. .then(response => registerIfNeeded(this, request, response));
  21. }
  22. }
  23. function registerIfNeeded(policy: RPRegistrationPolicy, request: WebResource, response: HttpOperationResponse): Promise<HttpOperationResponse> {
  24. if (response.status === 409) {
  25. const rpName = checkRPNotRegisteredError(response.bodyAsText as string);
  26. if (rpName) {
  27. const urlPrefix = extractSubscriptionUrl(request.url);
  28. return registerRP(policy, urlPrefix, rpName, request)
  29. // Autoregistration of ${provider} failed for some reason. We will not return this error
  30. // instead will return the initial response with 409 status code back to the user.
  31. // do nothing here as we are returning the original response at the end of this method.
  32. .catch(() => false)
  33. .then(registrationStatus => {
  34. if (registrationStatus) {
  35. // Retry the original request. We have to change the x-ms-client-request-id
  36. // otherwise Azure endpoint will return the initial 409 (cached) response.
  37. request.headers.set("x-ms-client-request-id", utils.generateUuid());
  38. return policy._nextPolicy.sendRequest(request.clone());
  39. }
  40. return response;
  41. });
  42. }
  43. }
  44. return Promise.resolve(response);
  45. }
  46. /**
  47. * Reuses the headers of the original request and url (if specified).
  48. * @param {WebResource} originalRequest The original request
  49. * @param {boolean} reuseUrlToo Should the url from the original request be reused as well. Default false.
  50. * @returns {object} A new request object with desired headers.
  51. */
  52. function getRequestEssentials(originalRequest: WebResource, reuseUrlToo = false): WebResource {
  53. const reqOptions: WebResource = originalRequest.clone();
  54. if (reuseUrlToo) {
  55. reqOptions.url = originalRequest.url;
  56. }
  57. // We have to change the x-ms-client-request-id otherwise Azure endpoint
  58. // will return the initial 409 (cached) response.
  59. reqOptions.headers.set("x-ms-client-request-id", utils.generateUuid());
  60. // Set content-type to application/json
  61. reqOptions.headers.set("Content-Type", "application/json; charset=utf-8");
  62. return reqOptions;
  63. }
  64. /**
  65. * Validates the error code and message associated with 409 response status code. If it matches to that of
  66. * RP not registered then it returns the name of the RP else returns undefined.
  67. * @param {string} body The response body received after making the original request.
  68. * @returns {string} The name of the RP if condition is satisfied else undefined.
  69. */
  70. function checkRPNotRegisteredError(body: string): string {
  71. let result, responseBody;
  72. if (body) {
  73. try {
  74. responseBody = JSON.parse(body);
  75. } catch (err) {
  76. // do nothing;
  77. }
  78. if (responseBody && responseBody.error && responseBody.error.message &&
  79. responseBody.error.code && responseBody.error.code === "MissingSubscriptionRegistration") {
  80. const matchRes = responseBody.error.message.match(/.*'(.*)'/i);
  81. if (matchRes) {
  82. result = matchRes.pop();
  83. }
  84. }
  85. }
  86. return result;
  87. }
  88. /**
  89. * Extracts the first part of the URL, just after subscription:
  90. * https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/
  91. * @param {string} url The original request url
  92. * @returns {string} The url prefix as explained above.
  93. */
  94. function extractSubscriptionUrl(url: string): string {
  95. let result;
  96. const matchRes = url.match(/.*\/subscriptions\/[a-f0-9-]+\//ig);
  97. if (matchRes && matchRes[0]) {
  98. result = matchRes[0];
  99. } else {
  100. throw new Error(`Unable to extract subscriptionId from the given url - ${url}.`);
  101. }
  102. return result;
  103. }
  104. /**
  105. * Registers the given provider.
  106. * @param {RPRegistrationPolicy} policy The RPRegistrationPolicy this function is being called against.
  107. * @param {string} urlPrefix https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/
  108. * @param {string} provider The provider name to be registered.
  109. * @param {WebResource} originalRequest The original request sent by the user that returned a 409 response
  110. * with a message that the provider is not registered.
  111. * @param {registrationCallback} callback The callback that handles the RP registration
  112. */
  113. function registerRP(policy: RPRegistrationPolicy, urlPrefix: string, provider: string, originalRequest: WebResource): Promise<boolean> {
  114. const postUrl = `${urlPrefix}providers/${provider}/register?api-version=2016-02-01`;
  115. const getUrl = `${urlPrefix}providers/${provider}?api-version=2016-02-01`;
  116. const reqOptions = getRequestEssentials(originalRequest);
  117. reqOptions.method = "POST";
  118. reqOptions.url = postUrl;
  119. return policy._nextPolicy.sendRequest(reqOptions)
  120. .then(response => {
  121. if (response.status !== 200) {
  122. throw new Error(`Autoregistration of ${provider} failed. Please try registering manually.`);
  123. }
  124. return getRegistrationStatus(policy, getUrl, originalRequest);
  125. });
  126. }
  127. /**
  128. * Polls the registration status of the provider that was registered. Polling happens at an interval of 30 seconds.
  129. * Polling will happen till the registrationState property of the response body is "Registered".
  130. * @param {RPRegistrationPolicy} policy The RPRegistrationPolicy this function is being called against.
  131. * @param {string} url The request url for polling
  132. * @param {WebResource} originalRequest The original request sent by the user that returned a 409 response
  133. * with a message that the provider is not registered.
  134. * @returns {Promise<boolean>} True if RP Registration is successful.
  135. */
  136. function getRegistrationStatus(policy: RPRegistrationPolicy, url: string, originalRequest: WebResource): Promise<boolean> {
  137. const reqOptions: any = getRequestEssentials(originalRequest);
  138. reqOptions.url = url;
  139. reqOptions.method = "GET";
  140. return policy._nextPolicy.sendRequest(reqOptions).then(res => {
  141. const obj = (res.parsedBody as any);
  142. if (res.parsedBody && obj.registrationState && obj.registrationState === "Registered") {
  143. return true;
  144. } else {
  145. return utils.delay(policy._retryTimeout * 1000).then(() => getRegistrationStatus(policy, url, originalRequest));
  146. }
  147. });
  148. }