Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[EventHubs] Helper function to parse connection string #12684

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions sdk/eventhub/event-hubs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## 5.4.0 (Unreleased)

- A helper method `parseEventHubConnectionString` has been added which validates and
parses a given connection string for Azure Event Hubs.
Resolves [#11894](https://github.com/Azure/azure-sdk-for-js/issues/11894)

- Adds the `customEndpointAddress` field to `EventHubClientOptions`.
This allows for specifying a custom endpoint to use when communicating
with the Event Hubs service, which is useful when your network does not
Expand Down
13 changes: 13 additions & 0 deletions sdk/eventhub/event-hubs/review/event-hubs.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,16 @@ export interface EventHubClientOptions {
webSocketOptions?: WebSocketOptions;
}

// @public
export interface EventHubConnectionStringProperties {
endpoint: string;
eventHubName?: string;
fullyQualifiedNamespace: string;
sharedAccessKey?: string;
sharedAccessKeyName?: string;
sharedAccessSignature?: string;
}

// @public
export class EventHubConsumerClient {
constructor(consumerGroup: string, connectionString: string, options?: EventHubConsumerClientOptions);
Expand Down Expand Up @@ -176,6 +186,9 @@ export interface OperationOptions {
tracingOptions?: OperationTracingOptions;
}

// @public
export function parseEventHubConnectionString(connectionString: string): Readonly<EventHubConnectionStringProperties>;

// @public
export interface PartitionContext {
readonly consumerGroup: string;
Expand Down
15 changes: 9 additions & 6 deletions sdk/eventhub/event-hubs/src/connectionContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@
import { logger, logErrorStackTrace } from "./log";
import { getRuntimeInfo } from "./util/runtimeInfo";
import { packageJsonInfo } from "./util/constants";
import { parseEventHubConnectionString } from "./util/connectionStringUtils";
import { EventHubReceiver } from "./eventHubReceiver";
import { EventHubSender } from "./eventHubSender";
import {
ConnectionContextBase,
Constants,
CreateConnectionContextBaseParameters,
parseConnectionString,
ConnectionConfig
} from "@azure/core-amqp";
import { TokenCredential, isTokenCredential } from "@azure/core-auth";
Expand Down Expand Up @@ -447,23 +447,26 @@ export function createConnectionContext(
hostOrConnectionString = String(hostOrConnectionString);

if (!isTokenCredential(credentialOrOptions)) {
const parsedCS = parseConnectionString<{ EntityPath?: string }>(hostOrConnectionString);
const parsedCS = parseEventHubConnectionString(hostOrConnectionString);
if (
!(parsedCS.EntityPath || (typeof eventHubNameOrOptions === "string" && eventHubNameOrOptions))
!(
parsedCS.eventHubName ||
(typeof eventHubNameOrOptions === "string" && eventHubNameOrOptions)
)
) {
throw new TypeError(
`Either provide "eventHubName" or the "connectionString": "${hostOrConnectionString}", ` +
`must contain "EntityPath=<your-event-hub-name>".`
);
}
if (
parsedCS.EntityPath &&
parsedCS.eventHubName &&
typeof eventHubNameOrOptions === "string" &&
eventHubNameOrOptions &&
parsedCS.EntityPath !== eventHubNameOrOptions
parsedCS.eventHubName !== eventHubNameOrOptions
) {
throw new TypeError(
`The entity path "${parsedCS.EntityPath}" in connectionString: "${hostOrConnectionString}" ` +
`The entity path "${parsedCS.eventHubName}" in connectionString: "${hostOrConnectionString}" ` +
`doesn't match with eventHubName: "${eventHubNameOrOptions}".`
);
}
Expand Down
14 changes: 5 additions & 9 deletions sdk/eventhub/event-hubs/src/eventhubSharedKeyCredential.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import { parseConnectionString } from "@azure/core-amqp";
import { parseEventHubConnectionString } from "./util/connectionStringUtils";
import { AccessToken } from "@azure/core-auth";
import { Buffer } from "buffer";
import isBuffer from "is-buffer";
Expand Down Expand Up @@ -78,16 +78,12 @@ export class SharedKeyCredential {
* @param {string} connectionString - The EventHub/ServiceBus connection string
*/
static fromConnectionString(connectionString: string): SharedKeyCredential {
const parsed = parseConnectionString<{
SharedAccessSignature: string;
SharedAccessKeyName: string;
SharedAccessKey: string;
}>(connectionString);
const parsed = parseEventHubConnectionString(connectionString);

if (parsed.SharedAccessSignature == null) {
return new SharedKeyCredential(parsed.SharedAccessKeyName, parsed.SharedAccessKey);
if (parsed.sharedAccessSignature == null) {
return new SharedKeyCredential(parsed.sharedAccessKeyName!, parsed.sharedAccessKey!);
} else {
return new SharedAccessSignatureCredential(parsed.SharedAccessSignature);
return new SharedAccessSignatureCredential(parsed.sharedAccessSignature);
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions sdk/eventhub/event-hubs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,7 @@ export { CloseReason } from "./models/public";
export { MessagingError, RetryOptions, WebSocketOptions } from "@azure/core-amqp";
export { TokenCredential } from "@azure/core-auth";
export { logger } from "./log";
export {
parseEventHubConnectionString,
EventHubConnectionStringProperties
} from "./util/connectionStringUtils";
118 changes: 118 additions & 0 deletions sdk/eventhub/event-hubs/src/util/connectionStringUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import { parseConnectionString } from "@azure/core-amqp";

/**
* The set of properties that comprise an Event Hub connection string.
*/
export interface EventHubConnectionStringProperties {
/**
* The fully qualified Event Hub namespace extracted from the "Endpoint" in the
* connection string. This is likely to be similar to "{yournamespace}.servicebus.windows.net".
* This is typically used to construct the EventHub{Producer|Consumer}Client.
*/
fullyQualifiedNamespace: string;
/**
* The value for "Endpoint" in the connection string.
*/
endpoint: string;
/**
* The value for "EntityPath" in the connection string which would be the name of the event hub instance associated with the connection string.
* Connection string from a Shared Access Policy created at the namespace level
* will not have the EntityPath in it.
*/
eventHubName?: string;
/**
* The value for "SharedAccessKey" in the connection string. This along with the "SharedAccessKeyName"
* in the connection string is used to generate a SharedAccessSignature which can be used authorize
* the connection to the service.
*/
sharedAccessKey?: string;
/**
* The value for "SharedAccessKeyName" in the connection string. This along with the "SharedAccessKey"
* in the connection string is used to generate a SharedAccessSignature which can be used authorize
* the connection to the service.
*/
sharedAccessKeyName?: string;
/**
* The value for "SharedAccessSignature" in the connection string. This is typically not present in the
* connection string generated for a Shared Access Policy. It is instead generated by the
* user and appended to the connection string for ease of use.
*/
sharedAccessSignature?: string;
}

/**
* Parses given connection string into the different properties applicable to Azure Event Hubs.
* The properties are useful to then construct an EventHub{Producer|Consumer}Client.
* @param connectionString The connection string associated with the Shared Access Policy created
* for the Event Hubs namespace.
*/
export function parseEventHubConnectionString(
connectionString: string
): Readonly<EventHubConnectionStringProperties> {
const parsedResult = parseConnectionString<{
Endpoint: string;
EntityPath?: string;
SharedAccessSignature?: string;
SharedAccessKey?: string;
SharedAccessKeyName?: string;
}>(connectionString);

validateProperties(
parsedResult.Endpoint,
parsedResult.SharedAccessSignature,
parsedResult.SharedAccessKey,
parsedResult.SharedAccessKeyName
);

const output: EventHubConnectionStringProperties = {
fullyQualifiedNamespace: (parsedResult.Endpoint.match(".*://([^/]*)") || [])[1],
endpoint: parsedResult.Endpoint
};

if (parsedResult.EntityPath) {
output.eventHubName = parsedResult.EntityPath;
}

if (parsedResult.SharedAccessSignature) {
output.sharedAccessSignature = parsedResult.SharedAccessSignature;
}

if (parsedResult.SharedAccessKey && parsedResult.SharedAccessKeyName) {
output.sharedAccessKey = parsedResult.SharedAccessKey;
output.sharedAccessKeyName = parsedResult.SharedAccessKeyName;
}

return output;
}

/**
* @internal
* @ignore
*/
function validateProperties(
endpoint?: string,
sharedAccessSignature?: string,
sharedAccessKey?: string,
sharedAccessKeyName?: string
): void {
if (!endpoint) {
throw new Error("Connection string should have an Endpoint key.");
}

if (sharedAccessSignature) {
if (sharedAccessKey || sharedAccessKeyName) {
throw new Error(
"Connection string cannot have both SharedAccessSignature and SharedAccessKey keys."
);
}
} else if (sharedAccessKey && !sharedAccessKeyName) {
throw new Error("Connection string with SharedAccessKey should have SharedAccessKeyName.");
} else if (!sharedAccessKey && sharedAccessKeyName) {
throw new Error(
"Connection string with SharedAccessKeyName should have SharedAccessKey as well."
);
}
}
84 changes: 84 additions & 0 deletions sdk/eventhub/event-hubs/test/connectionStringUtils.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import {
parseEventHubConnectionString,
EventHubConnectionStringProperties
} from "../src/util/connectionStringUtils";
import chai from "chai";

const assert = chai.assert;

describe("parseEventHubConnectionString", () => {
const namespace = "my.servicebus.windows.net";
const sharedAccessKey = "shared-access-key";
const sharedAccessKeyName = "shared-access-key-name";
const sharedAccessSignature = "shared-access-signature";
const endpoint = "sb://my.servicebus.windows.net";
const eventHubName = "event-hub-name";

describe("with valid data", () => {
it("parses a full connection string correctly", () => {
const expected: EventHubConnectionStringProperties = {
fullyQualifiedNamespace: namespace,
endpoint: endpoint,
eventHubName: eventHubName,
sharedAccessKeyName: sharedAccessKeyName,
sharedAccessKey: sharedAccessKey
};

const connectionString = `Endpoint=${endpoint};EntityPath=${eventHubName};SharedAccessKeyName=${sharedAccessKeyName};SharedAccessKey=${sharedAccessKey}`;

assert.deepEqual(parseEventHubConnectionString(connectionString), expected);
});

it("parses a minimal connection string correctly", () => {
const expected: EventHubConnectionStringProperties = {
fullyQualifiedNamespace: namespace,
endpoint: endpoint,
sharedAccessSignature: sharedAccessSignature
};

const connectionString = `Endpoint=${endpoint};SharedAccessSignature=${sharedAccessSignature}`;

assert.deepEqual(parseEventHubConnectionString(connectionString), expected);
});
});

describe("with invalid data", () => {
it("throws when Endpoint is missing", () => {
const connectionString = `SharedAccessSignature=${sharedAccessSignature}`;
assert.throws(() => {
parseEventHubConnectionString(connectionString);
}, /Connection string/);
});

it("throws when both SharedAccessSignature and SharedAccessKey are provided", () => {
const connectionString = `Endpoint=${endpoint};SharedAccessSignature=${sharedAccessSignature};SharedAccessKey=${sharedAccessKey}`;
assert.throws(() => {
parseEventHubConnectionString(connectionString);
}, /Connection string/);
});

it("throws when both SharedAccessSignature and SharedAccessKeyName are provided", () => {
const connectionString = `Endpoint=${endpoint};SharedAccessSignature=${sharedAccessSignature};SharedAccessKeyName=${sharedAccessKeyName}`;
assert.throws(() => {
parseEventHubConnectionString(connectionString);
}, /Connection string/);
});

it("throws when SharedAccessKey is provided without SharedAccessKeyName", () => {
const connectionString = `Endpoint=${endpoint};SharedAccessKey=${sharedAccessKey}`;
assert.throws(() => {
parseEventHubConnectionString(connectionString);
}, /Connection string/);
});

it("throws when SharedAccessKeyName is provided without SharedAccessKey", () => {
const connectionString = `Endpoint=${endpoint};SharedAccessKeyName=${sharedAccessKeyName}`;
assert.throws(() => {
parseEventHubConnectionString(connectionString);
}, /Connection string/);
});
});
});
15 changes: 9 additions & 6 deletions sdk/eventhub/event-hubs/test/internal/auth.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import { parseConnectionString } from "@azure/core-amqp";
import { EventHubConsumerClient, EventHubProducerClient } from "../../src";
import {
EventHubConsumerClient,
EventHubProducerClient,
parseEventHubConnectionString
} from "../../src";
import { EnvVarKeys, getEnvVars } from "../public/utils/testUtils";
import chai from "chai";
import { SharedKeyCredential } from "../../src/eventhubSharedKeyCredential";
Expand All @@ -14,9 +17,9 @@ describe("Authentication via SAS", () => {
const service = {
connectionString: env[EnvVarKeys.EVENTHUB_CONNECTION_STRING],
path: env[EnvVarKeys.EVENTHUB_NAME],
fqdn: parseConnectionString<{ Endpoint: string }>(
endpoint: parseEventHubConnectionString(
env[EnvVarKeys.EVENTHUB_CONNECTION_STRING]
).Endpoint.replace(/\/+$/, "")
).endpoint.replace(/\/+$/, "")
};

before(() => {
Expand Down Expand Up @@ -58,9 +61,9 @@ describe("Authentication via SAS", () => {

function getSasConnectionString(): string {
const sas = SharedKeyCredential.fromConnectionString(service.connectionString).getToken(
`${service.fqdn}/${service.path}`
`${service.endpoint}/${service.path}`
).token;

return `Endpoint=${service.fqdn}/;SharedAccessSignature=${sas}`;
return `Endpoint=${service.endpoint}/;SharedAccessSignature=${sas}`;
}
});