UNPKG

@azure/service-bus

Version:
1,434 lines (1,422 loc) • 486 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var coreAmqp = require('@azure/core-amqp'); var coreAuth = require('@azure/core-auth'); var coreClient = require('@azure/core-client'); var coreRestPipeline = require('@azure/core-rest-pipeline'); var logger$1 = require('@azure/logger'); var coreUtil = require('@azure/core-util'); var coreXml = require('@azure/core-xml'); var buffer = require('buffer'); var url = require('url'); var Long = require('long'); var rheaPromise = require('rhea-promise'); var isBuffer = require('is-buffer'); var abortController = require('@azure/abort-controller'); var crypto = require('crypto'); var coreTracing = require('@azure/core-tracing'); var os = require('os'); var corePaging = require('@azure/core-paging'); function _interopNamespaceDefault(e) { var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n.default = e; return Object.freeze(n); } var os__namespace = /*#__PURE__*/_interopNamespaceDefault(os); // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. /** * The `@azure/logger` configuration for this package. * This will output logs using the `azure:service-bus` namespace prefix. * @internal */ const logger = createServiceBusLogger("service-bus"); /** * Logging for ServiceBusReceivers of any type (session, non-session) * @internal */ const receiverLogger = createServiceBusLogger("service-bus:receiver"); /** * Logging for ServiceBusSenders * @internal */ const senderLogger = createServiceBusLogger("service-bus:sender"); /** * Logging for ServiceBusRuleManagers * @internal */ const ruleManagerLogger = createServiceBusLogger("service-bus:rulemanager"); /** * Logging for connection management * @internal */ const connectionLogger = createServiceBusLogger("service-bus:connection"); /** * Logging for the ServiceBusAdministrationClient * @internal */ const administrationLogger = createServiceBusLogger("service-bus:administration"); /** * Logging related to message encoding/decoding. * @internal */ const messageLogger = createServiceBusLogger("service-bus:messages"); /** * Logging related to message encoding/decoding. * @internal */ const managementClientLogger = createServiceBusLogger("service-bus:management"); /** * Logs the error's stack trace to "verbose" if a stack trace is available. * @param error - Error containing a stack trace. * @internal */ function logErrorStackTrace(_logger, error) { if (coreUtil.isObjectWithProperties(error, ["stack"]) && error.stack) { _logger.verbose(error.stack); } } /** * Creates an AzureLogger with any additional methods for standardized logging (for example, with errors) * @internal */ function createServiceBusLogger(namespace) { const _logger = logger$1.createClientLogger(namespace); _logger["logError"] = (err, ...args) => { let l; // abort errors are user initiated so we don't have to treat them as warnings, like we // would with other errors. if (isError(err) && err.name === "AbortError") { l = _logger.info; } else { l = _logger.warning; } // tack on the error object so it also gets logged. args.push(":", err); // let the normal formatting work and include the error at the end. l(...args); // optionally log the stack trace if it's available but this always goes to verbose if (err && err.stack) { _logger.verbose(err.stack); } }; return _logger; } /** * @internal */ function isError(err) { return err != null && err.name != null; } // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. /** * @internal */ const packageJsonInfo = { name: "@azure/service-bus", version: "7.9.5", }; /** * The amount of time in milliseconds that a receiver * will wait while draining credits before returning. * @internal */ const receiveDrainTimeoutInMs = 200; /** * @internal */ const max32BitNumber = Math.pow(2, 31) - 1; /** * Queue name identifier * @internal */ const QUEUE_NAME = "QueueName"; /** * Topic name identifier * @internal */ const TOPIC_NAME = "TopicName"; /** * Subscription name identifier * @internal */ const SUBSCRIPTION_NAME = "SubscriptionName"; /** * Accessed at field * @internal */ const ACCESSED_AT = "AccessedAt"; /** * Updated at field * @internal */ const UPDATED_AT = "UpdatedAt"; /** * Created at field * @internal */ const CREATED_AT = "CreatedAt"; /** * Authorization rules on the entity * @internal */ const AUTHORIZATION_RULES = "AuthorizationRules"; /** * Entity Availability Status field * @internal */ const ENTITY_AVAILABILITY_STATUS = "EntityAvailabilityStatus"; /** * Enable express option * @internal */ const ENABLE_EXPRESS = "EnableExpress"; /** * The entity's size in bytes. * * @internal */ const SIZE_IN_BYTES = "SizeInBytes"; /** * The entity's message count. * * @internal */ const MESSAGE_COUNT = "MessageCount"; /** * The topic's subscription count. * * @internal */ const SUBSCRIPTION_COUNT = "SubscriptionCount"; /** * The topic / subscription's count details. * * @internal */ const COUNT_DETAILS = "CountDetails"; /** * Max idle time before entity is deleted. * This is specified in ISO-8601 duration format such as "PT1M" for 1 minute, "PT5S" for 5 seconds. * @internal */ const AUTO_DELETE_ON_IDLE = "AutoDeleteOnIdle"; /** * The status information on response * * @internal */ const STATUS = "Status"; /** * The URL of Service Bus entity to forward messages to. * * @internal */ const FORWARD_TO = "ForwardTo"; /** * The user meta data information * * @internal */ const USER_METADATA = "UserMetadata"; /** * The maximum size in megabytes. * * @internal */ const MAX_SIZE_IN_MEGABYTES = "MaxSizeInMegabytes"; /** * The maximum size in kilobytes. * * @internal */ const MAX_MESSAGE_SIZE_IN_KILOBYTES = "MaxMessageSizeInKilobytes"; /** * The default message time to live. * This is specified in ISO-8601 duration format such as "PT1M" for 1 minute, "PT5S" for 5 seconds. * @internal */ const DEFAULT_MESSAGE_TIME_TO_LIVE = "DefaultMessageTimeToLive"; /** * The lock duration. * This is specified in ISO-8601 duration format such as "PT1M" for 1 minute, "PT5S" for 5 seconds. * @internal */ const LOCK_DURATION = "LockDuration"; /** * The indication if session is required or not. * * @internal */ const REQUIRES_SESSION = "RequiresSession"; /** * The indication if duplicate detection is required or not. * * @internal */ const REQUIRES_DUPLICATE_DETECTION = "RequiresDuplicateDetection"; /** * The indication if dead lettering on message expiration. If it is enabled and a message expires, * the Service Bus moves the message from the queue into the entity dead-letter sub-queue. * If disabled, message will be permanently deleted from the main entity. * Settable only at entity creation time. * * @internal */ const DEAD_LETTERING_ON_MESSAGE_EXPIRATION = "DeadLetteringOnMessageExpiration"; /** * The indication if dead lettering on filter evaluation exceptions. * * @internal */ const DEAD_LETTERING_ON_FILTER_EVALUATION_EXCEPTIONS = "DeadLetteringOnFilterEvaluationExceptions"; /** * The history time window for duplicate detection. * This is specified in ISO-8601 duration format such as "PT1M" for 1 minute, "PT5S" for 5 seconds. * @internal */ const DUPLICATE_DETECTION_HISTORY_TIME_WINDOW = "DuplicateDetectionHistoryTimeWindow"; /** * The maximum delivery count of messages after which if it is still not settled, gets moved to the dead-letter sub-queue. * * @internal */ const MAX_DELIVERY_COUNT = "MaxDeliveryCount"; /** * Indicates if the queue has enabled batch operations. * * @internal */ const ENABLE_BATCHED_OPERATIONS = "EnableBatchedOperations"; /** * Indicates whether the topic can be ordered * * @internal */ const SUPPORT_ORDERING = "SupportOrdering"; /** * Indicates whether the topic/queue should be split across multiple partitions * * @internal */ const ENABLE_PARTITIONING = "EnablePartitioning"; /** * The URL of Service Bus entity to forward deadlettered messages to. * * @internal */ const FORWARD_DEADLETTERED_MESSAGES_TO = "ForwardDeadLetteredMessagesTo"; /** * Query string parameter to set Service Bus API version * * @internal */ const API_VERSION_QUERY_KEY = "api-version"; /** * Current API version being sent to service bus * * @internal */ const CURRENT_API_VERSION = "2021-05"; /** * Marker for atom metadata. * * @internal */ const XML_METADATA_MARKER = "$"; /** * Marker for atom value. * * @internal */ const XML_VALUE_MARKER = "_"; /** * Constant representing the property where the atom default elements are stored. * * @internal */ const ATOM_METADATA_MARKER = "_"; /** * Known HTTP status codes as documented and referenced in ATOM based management API feature * https://docs.microsoft.com/dotnet/api/system.net.httpstatuscode?view=netframework-4.8 * @internal */ const HttpResponseCodes = { 100: "Continue", 101: "SwitchingProtocols", 200: "Ok", 201: "Created", 202: "Accepted", 203: "NonAuthoritativeInformation", 204: "NoContent", 205: "ResetContent", 206: "PartialContent", 300: "MultipleChoices", 301: "Moved", 302: "Redirect", 303: "RedirectMethod", 304: "NotModified", 305: "UseProxy", 306: "Unused", 400: "BadRequest", 401: "Unauthorized", 402: "PaymentRequired", 403: "Forbidden", 404: "NotFound", 405: "MethodNotAllowed", 406: "NotAcceptable", 407: "ProxyAuthenticationRequired", 409: "Conflict", 410: "Gone", 411: "LengthRequired", 412: "PreconditionFailed", 413: "RequestEntityTooLarge", 414: "RequestUriTooLong", 415: "UnsupportedMediaType", 416: "RequestRangeNotSatisfiable", 417: "ExpectationFailed", 426: "UpgradeRequired", 500: "InternalServerError", 501: "NotImplemented", 502: "BadGateway", 503: "ServiceUnavailable", 504: "GatewayTimeout", 505: "HttpVersionNotSupported", }; // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. /** * @internal */ /** * @internal */ const parseURL = (rawUrl) => { return new url.URL(rawUrl); }; // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. /** * A collection of HttpHeaders that can be sent with a HTTP request. */ function getHeaderKey(headerName) { return headerName.toLowerCase(); } /** * A collection of HTTP header key/value pairs. */ class HttpHeaders { constructor(rawHeaders) { this._headersMap = {}; if (rawHeaders) { for (const headerName in rawHeaders) { this.set(headerName, rawHeaders[headerName]); } } } /** * Set a header in this collection with the provided name and value. The name is * case-insensitive. * @param headerName - The name of the header to set. This value is case-insensitive. * @param headerValue - The value of the header to set. */ set(headerName, headerValue) { this._headersMap[getHeaderKey(headerName)] = { name: headerName, value: headerValue.toString(), }; } /** * Get the header value for the provided header name, or undefined if no header exists in this * collection with the provided name. * @param headerName - The name of the header. */ get(headerName) { const header = this._headersMap[getHeaderKey(headerName)]; return !header ? undefined : header.value; } /** * Get whether or not this header collection contains a header entry for the provided header name. */ contains(headerName) { return !!this._headersMap[getHeaderKey(headerName)]; } /** * Remove the header with the provided headerName. Return whether or not the header existed and * was removed. * @param headerName - The name of the header to remove. */ remove(headerName) { const result = this.contains(headerName); delete this._headersMap[getHeaderKey(headerName)]; return result; } /** * Get the headers that are contained this collection as an object. */ rawHeaders() { return this.toJson({ preserveCase: true }); } /** * Get the headers that are contained in this collection as an array. */ headersArray() { const headers = []; for (const headerKey in this._headersMap) { headers.push(this._headersMap[headerKey]); } return headers; } /** * Get the header names that are contained in this collection. */ headerNames() { const headerNames = []; const headers = this.headersArray(); for (let i = 0; i < headers.length; ++i) { headerNames.push(headers[i].name); } return headerNames; } /** * Get the header values that are contained in this collection. */ headerValues() { const headerValues = []; const headers = this.headersArray(); for (let i = 0; i < headers.length; ++i) { headerValues.push(headers[i].value); } return headerValues; } /** * Get the JSON object representation of this HTTP header collection. */ toJson(options = {}) { const result = {}; if (options.preserveCase) { for (const headerKey in this._headersMap) { const header = this._headersMap[headerKey]; result[header.name] = header.value; } } else { for (const headerKey in this._headersMap) { const header = this._headersMap[headerKey]; result[getHeaderKey(header.name)] = header.value; } } return result; } /** * Get the string representation of this HTTP header collection. */ toString() { return JSON.stringify(this.toJson({ preserveCase: true })); } /** * Create a deep clone/copy of this HttpHeaders collection. */ clone() { const resultPreservingCasing = {}; for (const headerKey in this._headersMap) { const header = this._headersMap[headerKey]; resultPreservingCasing[header.name] = header.value; } return new HttpHeaders(resultPreservingCasing); } } // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. function toHttpHeaderLike(headers) { return new HttpHeaders(headers.toJSON({ preserveCase: true })); } function toWebResourceLike(request) { return { url: request.url, method: request.method, headers: toHttpHeaderLike(request.headers), withCredentials: request.withCredentials, timeout: request.timeout, requestId: request.headers.get("x-ms-client-request-id") || "", }; } /** * Helper to transform PipelineResponse to slimmed-down HttpResponse used in Service Bus. */ function toHttpResponse(response) { return { request: toWebResourceLike(response.request), status: response.status, headers: toHttpHeaderLike(response.headers), }; } // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. /** * Translation between the MessagingErrorCodes into a ServiceBusCode * * @internal */ const wellKnownMessageCodesToServiceBusCodes = new Map([ ["MessagingEntityNotFoundError", "MessagingEntityNotFound"], ["MessageLockLostError", "MessageLockLost"], ["MessageNotFoundError", "MessageNotFound"], ["MessageTooLargeError", "MessageSizeExceeded"], ["MessagingEntityAlreadyExistsError", "MessagingEntityAlreadyExists"], ["MessagingEntityDisabledError", "MessagingEntityDisabled"], ["QuotaExceededError", "QuotaExceeded"], ["ServerBusyError", "ServiceBusy"], ["OperationTimeoutError", "ServiceTimeout"], ["ServiceUnavailableError", "ServiceTimeout"], ["ServiceCommunicationError", "ServiceCommunicationProblem"], ["SessionCannotBeLockedError", "SessionCannotBeLocked"], ["SessionLockLostError", "SessionLockLost"], ["UnauthorizedError", "UnauthorizedAccess"], ]); /** * Errors that occur within Service Bus. */ class ServiceBusError extends coreAmqp.MessagingError { constructor(messageOrError, code) { const message = typeof messageOrError === "string" ? messageOrError : messageOrError.message; super(message); if (typeof messageOrError === "string") { this.code = code ?? "GeneralError"; } else { for (const prop in messageOrError) { this[prop] = messageOrError[prop]; } this.code = ServiceBusError.normalizeMessagingCode(messageOrError.code); // For GeneralErrors, prefix the error message with the MessagingError code to provide // more context to the user. if (this.code === "GeneralError" && messageOrError.code) { this.message = `${messageOrError.code}: ${this.message}`; } } this.name = "ServiceBusError"; } static normalizeMessagingCode(oldCode) { if (oldCode == null || !wellKnownMessageCodesToServiceBusCodes.has(oldCode)) { return "GeneralError"; } return wellKnownMessageCodesToServiceBusCodes.get(oldCode); } } /** * Translates an error into either an Error or a ServiceBusError which provides a `reason` code that * can be used by clients to programmatically react to errors. * * If you are calling `@azure/core-amqp/translate` you should swap to using this function instead since it provides * Service Bus specific handling of the error (falling back to default translate behavior otherwise). * * @internal */ function translateServiceBusError(err) { if (isServiceBusError(err)) { return err; } const translatedError = coreAmqp.translate(err); if (coreAmqp.isMessagingError(translatedError)) { return new ServiceBusError(translatedError); } return translatedError; } /** * Determines if an error is of type `ServiceBusError` * * @param err - An error to check to see if it's of type ServiceBusError */ function isServiceBusError(err) { return coreUtil.isObjectWithProperties(err, ["name"]) && err.name === "ServiceBusError"; } // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. /** * @internal * Provides a uniue name by appending a string guid to the given string in the following format: * `{name}-{uuid}`. * @param name - The nme of the entity */ function getUniqueName(name) { return `${name}-${rheaPromise.generate_uuid()}`; } /** * @internal * Returns the passed identifier if it is not undefined or empty; * otherwise generate and returns a unique one in the following format; * `{prefix}-{uuid}`. * @param prefix - The prefix used to generate identifier * @param identifier - an identifier name */ function ensureValidIdentifier(prefix, identifier) { return identifier ? identifier : getUniqueName(prefix); } /** * @internal * If you try to turn a Guid into a Buffer in .NET, the bytes of the first three groups get * flipped within the group, but the last two groups don't get flipped, so we end up with a * different byte order. This is the order of bytes needed to make Service Bus recognize the token. * * @param lockToken - The lock token whose bytes need to be reorded. * @returns Buffer representing reordered bytes. */ function reorderLockToken(lockTokenBytes) { if (!lockTokenBytes || !buffer.Buffer.isBuffer(lockTokenBytes)) { return lockTokenBytes; } return buffer.Buffer.from([ lockTokenBytes[3], lockTokenBytes[2], lockTokenBytes[1], lockTokenBytes[0], lockTokenBytes[5], lockTokenBytes[4], lockTokenBytes[7], lockTokenBytes[6], lockTokenBytes[8], lockTokenBytes[9], lockTokenBytes[10], lockTokenBytes[11], lockTokenBytes[12], lockTokenBytes[13], lockTokenBytes[14], lockTokenBytes[15], ]); } /** * @internal * Provides the time in milliseconds after which the lock renewal should occur. * @param lockedUntilUtc - The time until which the message is locked. */ function calculateRenewAfterDuration(lockedUntilUtc) { const now = Date.now(); const lockedUntil = lockedUntilUtc.getTime(); const remainingTime = lockedUntil - now; receiverLogger.verbose("Locked until utc : %d", lockedUntil); receiverLogger.verbose("Current time is : %d", now); receiverLogger.verbose("Remaining time is : %d", remainingTime); if (remainingTime < 1000) { return 0; } const buffer = Math.min(remainingTime / 2, 10000); // 10 seconds const renewAfter = remainingTime - buffer; receiverLogger.verbose("Renew after : %d", renewAfter); return renewAfter; } /** * @internal * Converts the .net ticks to a JS Date object. * * - The epoch for the DateTimeOffset type is `0000-01-01`, while the epoch for JS Dates is * `1970-01-01`. * - The DateTimeOffset ticks value for the date `1970-01-01` is `621355968000000000`. * - Hence, to convert it to the JS epoch; we `subtract` the delta from the given value. * - Ticks in DateTimeOffset is `1/10000000` second, while ticks in JS Date is `1/1000` second. * - Thus, we `divide` the value by `10000` to convert it to JS Date ticks. * * @param buf - Input as a Buffer * @returns The JS Date object. */ function convertTicksToDate(buf) { const epochMicroDiff = 621355968000000000; const longValue = Long.fromBytesBE(buf); const timeInMS = longValue.sub(epochMicroDiff).div(10000).toNumber(); const result = new Date(timeInMS); logger.verbose("The converted date is: %s", result.toString()); return result; } /** * @internal * Converts any given input to a Buffer. * @param input - The input that needs to be converted to a Buffer. */ function toBuffer(input) { let result; messageLogger.verbose("[utils.toBuffer] The given message body that needs to be converted to buffer is: ", input); if (isBuffer(input)) { result = input; } else { // string, undefined, null, boolean, array, object, number should end up here // coercing undefined to null as that will ensure that null value will be given to the // customer on receive. if (input === undefined) input = null; try { const inputStr = JSON.stringify(input); result = buffer.Buffer.from(inputStr, "utf8"); } catch (err) { const msg = `An error occurred while executing JSON.stringify() on the given input ` + input + `${err instanceof Error ? err.stack : JSON.stringify(err)}`; messageLogger.warning("[utils.toBuffer] " + msg); throw err instanceof Error ? err : new Error(msg); } } messageLogger.verbose("[utils.toBuffer] The converted buffer is: %O.", result); return result; } /** * @internal * Helper utility to retrieve `string` value from given string, * or throws error if undefined. */ function getString(value, nameOfProperty) { const result = getStringOrUndefined(value); if (result === undefined) { throw new Error(`"${nameOfProperty}" received from service expected to be a string value and not undefined.`); } return result; } /** * @internal * Helper utility to retrieve `string` value from given input, * or undefined if not passed in. */ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types function getStringOrUndefined(value) { if (!coreUtil.isDefined(value)) { return undefined; } return value.toString(); } /** * @internal * Helper utility to retrieve `integer` value from given string, * or throws error if undefined. */ function getInteger(value, nameOfProperty) { const result = getIntegerOrUndefined(value); if (result === undefined) { throw new Error(`"${nameOfProperty}" received from service expected to be a number value and not undefined.`); } return result; } /** * @internal * Helper utility to retrieve `integer` value from given string, * or undefined if not passed in. */ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types function getIntegerOrUndefined(value) { if (!coreUtil.isDefined(value)) { return undefined; } const result = parseInt(value.toString()); return isNaN(result) ? undefined : result; } /** * @internal * Helper utility to convert ISO-8601 time into Date type. */ function getDate(value, nameOfProperty) { return new Date(getString(value, nameOfProperty)); } /** * @internal * Helper utility to retrieve `boolean` value from given string, * or throws error if undefined. */ function getBoolean(value, nameOfProperty) { const result = getBooleanOrUndefined(value); if (result === undefined) { throw new Error(`"${nameOfProperty}" received from service expected to be a boolean value and not undefined.`); } return result; } /** * @internal * Helper utility to retrieve `boolean` value from given string, * or undefined if not passed in. */ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types function getBooleanOrUndefined(value) { if (!coreUtil.isDefined(value)) { return undefined; } return value.toString().trim().toLowerCase() === "true"; } /** * @internal * Helps in differentiating JSON like objects from other kinds of objects. */ const EMPTY_JSON_OBJECT_CONSTRUCTOR = {}.constructor; /** * @internal * Returns `true` if given input is a JSON like object. */ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types function isJSONLikeObject(value) { // `value.constructor === {}.constructor` differentiates among the "object"s, // would filter the JSON objects and won't match any array or other kinds of objects // ------------------------------------------------------------------------------- // Few examples | typeof obj ==="object" | obj.constructor==={}.constructor // ------------------------------------------------------------------------------- // {abc:1} | true | true // ["a","b"] | true | false // [{"a":1},{"b":2}] | true | false // new Date() | true | false // 123 | false | false // ------------------------------------------------------------------------------- return typeof value === "object" && value.constructor === EMPTY_JSON_OBJECT_CONSTRUCTOR; } /** * @internal * Helper utility to retrieve message count details from given input, */ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types function getMessageCountDetails(value) { const xmlnsPrefix = getXMLNSPrefix(value); if (!coreUtil.isDefined(value)) { value = {}; } return { activeMessageCount: parseInt(value[`${xmlnsPrefix}:ActiveMessageCount`]) || 0, deadLetterMessageCount: parseInt(value[`${xmlnsPrefix}:DeadLetterMessageCount`]) || 0, scheduledMessageCount: parseInt(value[`${xmlnsPrefix}:ScheduledMessageCount`]) || 0, transferMessageCount: parseInt(value[`${xmlnsPrefix}:TransferMessageCount`]) || 0, transferDeadLetterMessageCount: parseInt(value[`${xmlnsPrefix}:TransferDeadLetterMessageCount`]) || 0, }; } /** * @internal * Gets the xmlns prefix from the root of the objects that are part of the parsed response body. */ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types function getXMLNSPrefix(value) { if (!value[XML_METADATA_MARKER]) { throw new Error(`Error occurred while parsing the response body - cannot find the XML_METADATA_MARKER "$" on the object ${JSON.stringify(value)}`); } const keys = Object.keys(value[XML_METADATA_MARKER]); if (keys.length !== 1) { throw new Error(`Error occurred while parsing the response body - unexpected number of "xmlns:\${prefix}" keys at ${JSON.stringify(value[XML_METADATA_MARKER])}`); } if (!keys[0].startsWith("xmlns:")) { throw new Error(`Error occurred while parsing the response body - unexpected key at ${JSON.stringify(value[XML_METADATA_MARKER])}`); } // Pick the substring that's after "xmlns:" const xmlnsPrefix = keys[0].substring(6); if (!xmlnsPrefix) { throw new Error(`Error occurred while parsing the response body - unexpected xmlns prefix at ${JSON.stringify(value[XML_METADATA_MARKER])}`); } return xmlnsPrefix; } /** * @internal * Helper utility to retrieve array of `AuthorizationRule` from given input, * or undefined if not passed in. */ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types function getAuthorizationRulesOrUndefined(value) { const authorizationRules = []; // Ignore special case as Service Bus treats "" as a valid value for authorization rules if (typeof value === "string" && value.trim() === "") { return undefined; } if (!coreUtil.isDefined(value)) { return undefined; } const rawAuthorizationRules = value.AuthorizationRule; if (Array.isArray(rawAuthorizationRules)) { for (let i = 0; i < rawAuthorizationRules.length; i++) { authorizationRules.push(buildAuthorizationRule(rawAuthorizationRules[i])); } } else { authorizationRules.push(buildAuthorizationRule(rawAuthorizationRules)); } return authorizationRules; } /** * @internal * Helper utility to build an instance of parsed authorization rule as `AuthorizationRule` from given input. */ function buildAuthorizationRule(value) { let accessRights; if (coreUtil.isDefined(value["Rights"])) { accessRights = value["Rights"]["AccessRights"]; } const authorizationRule = { claimType: value["ClaimType"], accessRights, keyName: value["KeyName"], primaryKey: value["PrimaryKey"], secondaryKey: value["SecondaryKey"], }; if (authorizationRule.accessRights && !Array.isArray(authorizationRule.accessRights)) { authorizationRule.accessRights = [authorizationRule.accessRights]; } return authorizationRule; } /** * @internal * Helper utility to extract output containing array of `RawAuthorizationRule` instances from given input, * or undefined if not passed in. */ function getRawAuthorizationRules(authorizationRules) { if (!coreUtil.isDefined(authorizationRules)) { return undefined; } if (!Array.isArray(authorizationRules)) { throw new TypeError(`authorizationRules must be an array of AuthorizationRule objects or undefined, but received ${JSON.stringify(authorizationRules, undefined, 2)}`); } const rawAuthorizationRules = []; for (let i = 0; i < authorizationRules.length; i++) { rawAuthorizationRules.push(buildRawAuthorizationRule(authorizationRules[i])); } return { AuthorizationRule: rawAuthorizationRules }; } /** * @internal * Helper utility to build an instance of raw authorization rule as RawAuthorizationRule from given `AuthorizationRule` input. * @param authorizationRule - parsed Authorization Rule instance */ function buildRawAuthorizationRule(authorizationRule) { if (!isJSONLikeObject(authorizationRule) || authorizationRule === null) { throw new TypeError(`Expected authorizationRule input to be a JS object value, but received ${JSON.stringify(authorizationRule, undefined, 2)}`); } const rawAuthorizationRule = { ClaimType: authorizationRule.claimType, // ClaimValue is not settable by the users, but service expects the value for PUT requests ClaimValue: "None", Rights: { AccessRights: authorizationRule.accessRights, }, KeyName: authorizationRule.keyName, PrimaryKey: authorizationRule.primaryKey, SecondaryKey: authorizationRule.secondaryKey, }; rawAuthorizationRule[XML_METADATA_MARKER] = { "p5:type": "SharedAccessAuthorizationRule", "xmlns:p5": "http://www.w3.org/2001/XMLSchema-instance", }; return rawAuthorizationRule; } /** * @internal * Helper utility to check if given string is an absolute URL */ function isAbsoluteUrl(url) { const _url = url.toLowerCase(); return _url.startsWith("sb://") || _url.startsWith("http://") || _url.startsWith("https://"); } /** * An executor for a function that returns a Promise that obeys both a timeout and an * optional AbortSignal. * @param timeoutMs - The number of milliseconds to allow before throwing an OperationTimeoutError. * @param timeoutMessage - The message to place in the .description field for the thrown exception for Timeout. * @param abortSignal - The abortSignal associated with containing operation. * @param abortErrorMsg - The abort error message associated with containing operation. * @param value - The value to be resolved with after a timeout of t milliseconds. * * @internal */ async function waitForTimeoutOrAbortOrResolve(args) { if (args.abortSignal && args.abortSignal.aborted) { throw new abortController.AbortError(coreAmqp.StandardAbortMessage); } let timer = undefined; let clearAbortSignal = undefined; const clearAbortSignalAndTimer = () => { (args.timeoutFunctions?.clearTimeoutFn ?? clearTimeout)(timer); if (clearAbortSignal) { clearAbortSignal(); } }; // eslint-disable-next-line promise/param-names const abortOrTimeoutPromise = new Promise((_resolve, reject) => { clearAbortSignal = checkAndRegisterWithAbortSignal(reject, args.abortSignal); timer = (args.timeoutFunctions?.setTimeoutFn ?? setTimeout)(() => { reject(new rheaPromise.OperationTimeoutError(args.timeoutMessage)); }, args.timeoutMs); }); try { return await Promise.race([abortOrTimeoutPromise, args.actionFn()]); } finally { clearAbortSignalAndTimer(); } } /** * Registers listener to the abort event on the abortSignal to call your abortFn and * returns a function that will clear the same listener. * * If abort signal is already aborted, then throws an AbortError and returns a function that does nothing * * @returns A function that removes any of our attached event listeners on the abort signal or an empty function if * the abortSignal was not defined. * * @internal */ function checkAndRegisterWithAbortSignal(onAbortFn, abortSignal) { if (abortSignal == null) { return () => { /** Nothing to do here, no abort signal */ }; } if (abortSignal.aborted) { throw new abortController.AbortError(coreAmqp.StandardAbortMessage); } const onAbort = () => { abortSignal.removeEventListener("abort", onAbort); onAbortFn(new abortController.AbortError(coreAmqp.StandardAbortMessage)); }; abortSignal.addEventListener("abort", onAbort); return () => abortSignal.removeEventListener("abort", onAbort); } /** * @internal * The user agent prefix string for the ServiceBus client. * See guideline at https://azure.github.io/azure-sdk/general_azurecore.html#telemetry-policy */ const libInfo = `azsdk-js-azureservicebus/${packageJsonInfo.version}`; /** * @internal * Returns the formatted prefix by removing the spaces, by appending the libInfo. */ function formatUserAgentPrefix(prefix) { let userAgentPrefix = `${(prefix || "").replace(" ", "")}`; userAgentPrefix = userAgentPrefix.length > 0 ? userAgentPrefix + " " : ""; return `${userAgentPrefix}${libInfo}`; } /** * @internal * Helper method which returns `HttpResponse` from an object of shape `PipelineResponse`. * TODO: remove this and use toHttpResponse() directly */ const getHttpResponseOnly = (pipelineResponse) => toHttpResponse(pipelineResponse); /** * @internal * Waits for one second if a sender is not sendable then check again. Throws * SenderBusyError if it is still not sendable. * Only waits when operation timeout is greater than one second. * @returns the actual waiting time. */ async function waitForSendable(sendLogger, logPrefix, name, timeout, sender, outgoingAvaiable) { let waitTimeForSendable = 1000; if (!sender?.sendable() && timeout > waitTimeForSendable) { sendLogger.verbose("%s Sender '%s', waiting for 1 second for sender to become sendable", logPrefix, name); await coreAmqp.delay(waitTimeForSendable); sendLogger.verbose("%s Sender '%s' after waiting for a second, credit: %d available: %d", logPrefix, name, sender?.credit, outgoingAvaiable); } else { waitTimeForSendable = 0; } if (!sender?.sendable()) { // let us retry to send the message after some time. const msg = `[${logPrefix}] Sender "${name}", ` + `cannot send the message right now. Please try later.`; sendLogger.warning(msg); const amqpError = { condition: coreAmqp.ErrorNameConditionMapper.SenderBusyError, description: msg, }; throw translateServiceBusError(amqpError); } return waitTimeForSendable; } // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. /** applies options to the pipeline request. */ function applyRequestOptions(request, options) { if (options.headers) { const headers = options.headers; for (const headerName of Object.keys(headers)) { request.headers.set(headerName, headers[headerName]); } } request.onDownloadProgress = options.onDownloadProgress; request.onUploadProgress = options.onUploadProgress; request.abortSignal = options.abortSignal; request.timeout = options.timeout; if (options.tracingOptions) { request.tracingOptions = options.tracingOptions; } } /** * @internal * Utility to execute Atom XML operations as HTTP requests */ async function executeAtomXmlOperation(serviceBusAtomManagementClient, request, serializer, operationOptions, requestObject) { if (requestObject) { request.body = coreXml.stringifyXML(serializer.serialize(requestObject), { rootName: "entry" }); if (request.method === "PUT") { request.headers.set("content-length", buffer.Buffer.byteLength(request.body)); } } administrationLogger.verbose(`Executing ATOM based HTTP request: ${request.body}`); const reqPrepareOptions = { headers: operationOptions.requestOptions?.customHeaders, onUploadProgress: operationOptions.requestOptions?.onUploadProgress, onDownloadProgress: operationOptions.requestOptions?.onDownloadProgress, abortSignal: operationOptions.abortSignal, tracingOptions: operationOptions.tracingOptions, disableJsonStringifyOnBody: true, timeout: operationOptions.requestOptions?.timeout || 0, }; applyRequestOptions(request, reqPrepareOptions); const response = await serviceBusAtomManagementClient.sendRequest(request); administrationLogger.verbose(`Received ATOM based HTTP response: ${response.bodyAsText}`); try { if (response.bodyAsText) { response.parsedBody = await coreXml.parseXML(response.bodyAsText, { includeRoot: true, }); } } catch (err) { const error = new coreRestPipeline.RestError(`Error occurred while parsing the response body - expected the service to return valid xml content.`, { code: coreRestPipeline.RestError.PARSE_ERROR, statusCode: response.status, request: response.request, response, }); administrationLogger.logError(err, "Error parsing response body from Service"); throw error; } return serializer.deserialize(response); } /** * @internal * The key-value pairs having undefined/null as the values would lead to the empty tags in the serialized XML request. * Empty tags in the request body is problematic because of the following reasons. * - ATOM based management operations throw a "Bad Request" error if empty tags are included in the XML request body at top level. * - At the inner levels, Service assigns the empty strings as values to the empty tags instead of throwing an error. * * This method recursively removes the key-value pairs with undefined/null as the values from the request object that is to be serialized. * */ function sanitizeSerializableObject(resource) { Object.keys(resource).forEach(function (property) { if (!coreUtil.isDefined(resource[property])) { delete resource[property]; } else if (isJSONLikeObject(resource[property])) { sanitizeSerializableObject(resource[property]); } }); } /** * @internal * Serializes input information to construct the Atom XML request * @param resourceName - Name of the resource to be serialized like `QueueDescription` * @param resource - The entity details * @param allowedProperties - The set of properties that are allowed by the service for the * associated operation(s); */ function serializeToAtomXmlRequest(resourceName, resource) { const content = {}; content[resourceName] = Object.assign({}, resource); sanitizeSerializableObject(content[resourceName]); content[resourceName][XML_METADATA_MARKER] = { xmlns: "http://schemas.microsoft.com/netservices/2010/10/servicebus/connect", "xmlns:i": "http://www.w3.org/2001/XMLSchema-instance", }; content[XML_METADATA_MARKER] = { type: "application/xml" }; const requestDetails = { updated: new Date().toISOString(), content: content, }; requestDetails[XML_METADATA_MARKER] = { xmlns: "http://www.w3.org/2005/Atom", }; return requestDetails; } /** * @internal * Transforms response to contain the parsed data. * @param nameProperties - The set of 'name' properties to be constructed on the * resultant object e.g., QueueName, TopicName, etc. */ async function deserializeAtomXmlResponse(nameProperties, response) { // If received data is a non-valid HTTP response, the body is expected to contain error information if (response.status < 200 || response.status >= 300) { throw buildError(response); } parseAtomResult(response, nameProperties); return response; } /** * @internal * Utility to deserialize the given JSON content in response body based on * if it's a single `entry` or `feed` and updates the `response.parsedBody` to hold the evaluated output. * @param response - Response containing the JSON value in `response.parsedBody` * @param nameProperties - The set of 'name' properties to be constructed on the * resultant object e.g., QueueName, TopicName, etc. * */ function parseAtomResult(response, nameProperties) { const atomResponseInJson = response.parsedBody; let result; if (!atomResponseInJson) { response.parsedBody = undefined; return; } if (atomResponseInJson.feed) { result = parseFeedResult(atomResponseInJson.feed); } else if (atomResponseInJson.entry) { result = parseEntryResult(atomResponseInJson.entry); } if (result) { if (Array.isArray(result)) { result.forEach((entry) => { setName(entry, nameProperties); }); } else { setName(result, nameProperties); } response.parsedBody = result; return; } administrationLogger.warning("Failure in parsing response body from service. Expected response to be in Atom XML format and have either feed or entry components, but received - %0", atomResponseInJson); throw new coreRestPipeline.RestError("Error occurred while parsing the response body - expected the service to return atom xml content with either feed or entry elements.", { code: coreRestPipeline.RestError.PARSE_ERROR, statusCode: response.status, request: response.request, response, }); } /** * @internal * Utility to help parse given `entry` result */ function parseEntryResult(entry) { let result; if (typeof entry !== "object" || entry == null || typeof entry.content !== "object" || entry.content == null) { return undefined; } const contentElementNames = Object.keys(entry.content).filter(function (key) { return key !== XML_METADATA_MARKER; }); if (contentElementNames && contentElementNames[0]) { const contentRootElementName = contentElementNames[0]; delete entry.content[contentRootElementName][XML_METADATA_MARKER]; result = entry.content[contentRootElementName]; if (result) { if (entry[XML_METADATA_MARKER]) { result[ATOM_METADATA_MARKER] = entry[XML_METADATA_MARKER]; } else { result[ATOM_METADATA_MARKER] = {}; } result[ATOM_METADATA_MARKER]["ContentRootElement"] = contentRootElementName; Object.keys(entry).forEach((property) => { if (property !== "content" && property !== XML_METADATA_MARKER) { result[ATOM_METADATA_MARKER][property] = entry[property]; } }); return result; } } return undefined; } /** * @internal * Utility to help parse link info from the given `feed` result */ function parseLinkInfo(feedLink, relationship) { if (!feedLink || !Array.isArray(feedLink)) { return undefined; } for (const linkInfo of feedLink) { if (linkInfo[XML_METADATA_MARKER].rel === relationship) { return linkInfo[XML_METADATA_MARKER].href; } } return undefined; } /** * @internal * Utility to help parse given `feed` result */ function parseFeedResult(feed) { const result = []; if (typeof feed === "object" && feed != null && feed.entry) { if (Array.isArray(feed.entry)) { feed.entry.forEach((entry) => { const parsedEntryResult = parseEntryResult(entry); if (parsedEntryResult) { result.push(parsedEntryResult); } }); } else { const parsedEntryResult = parseEntryResult(feed.entry); if (parsedEntryResult) { result.push(parsedEntryResult); } } result.nextLink = parseLinkInfo(feed.link, "next"); } return result; } /** * @internal */ function isKnownResponseCode(statusCode) { return !!HttpResponseCodes[statusCode]; } /** * @internal * Extracts the applicable entity name(s) from the URL based on the known structure * and instantiates the corresponding name properties to the deserialized response * * The pattern matching checks to extract entity names are based on following * constraints dictated by the service * - '/' is allowed in Queue and Topic names * - '/' is not allowed in Namespace, Subscription and Rule names * - Valid pathname URL structures used in the ATOM based management API are * - `<namespace-component>/<topic-name>/Subscriptions/<subscription-name>/Rules/<rule-name>` * - `<namespace-component>/<topic-name>/Subscriptions/<subscription-name>` * - `<namespace-component>/<any-entity-name>` * */ function setName(entry, nameProperties) { if (entry[ATOM_METADATA_MARKER]) { let rawUrl = entry[ATOM_METADATA_MARKER].id; // The parsedUrl gets constructed differently for browser vs Node. // It is specifically behaves different for some of the Atom based management API where // the received URL in "id" element is of type "sb:// ... " and not a standard HTTP one // Hence, normalizing the URL for parsing to work as expected in browser if (rawUrl.startsWith("sb://")) { rawUrl = "https://" + rawUrl.substring(5); } const parsedUrl = parseURL(rawUrl); const pathname = parsedUrl.pathname; const firstIndexOfDelimiter = pathname.indexOf("/"); if (pathname.match("(.*)/(.*)/Subscriptions/(.*)/Rules/(.*)")) { const lastIndexOfSubscriptionsDelimiter = pathname.lastIndexOf("/Subscriptions/"); const firstIndexOfRulesDelimiter = pathname.indexOf("/Rules/"); entry[nameProperties[0]] = pathname.substring(firstIndexOfDelimiter + 1, lastIndexOfSubscriptionsDelimiter); entry[nameProperties[1]] = pathname.substring(lastIndexOfSubscriptionsDelimiter + 15, firstIndexOfRulesDelimiter); entry[nameProperties[2]] = pathname.substring(firstIndexOfRulesDelimiter + 7); } else if (pathname.match("(.*)/(.*)/Subscriptions/(.*)")) { const lastIndexOfSubscriptionsDelimiter = pathname.lastIndexOf("/Subscriptions/"); entry[nameProperties[0]] = pathname.substring(firstIndexOfDelimiter + 1, lastIndexOfSubscriptionsDelimiter); entry[nameProperties[1]] = pathname.substring(lastIndexOfSubscriptionsDelimiter + 15); } else if (pathname.match("(.*)/(