Skip to content

Commit 06be1a1

Browse files
authored
[EventHubs] Helper function to parse connection string (Azure#12684)
## What Add the following to the Event Hubs package: - A helper methods `parseEventHubConnectionString` that validates and parses a given connection string - An interface `EventHubConnectionStringProperties` that defines the output of the above method. ## Why In a [recent PR](Azure#11949) the same change has been made for ServiceBus to provide a parsing utility that can help transform a raw connection string for use with credential-based client creation. Implements Azure#11894
1 parent 7262928 commit 06be1a1

File tree

8 files changed

+246
-21
lines changed

8 files changed

+246
-21
lines changed

sdk/eventhub/event-hubs/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## 5.4.0 (Unreleased)
44

5+
- A helper method `parseEventHubConnectionString` has been added which validates and
6+
parses a given connection string for Azure Event Hubs.
7+
Resolves [#11894](https://github.com/Azure/azure-sdk-for-js/issues/11894)
8+
59
- Adds the `customEndpointAddress` field to `EventHubClientOptions`.
610
This allows for specifying a custom endpoint to use when communicating
711
with the Event Hubs service, which is useful when your network does not

sdk/eventhub/event-hubs/review/event-hubs.api.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,16 @@ export interface EventHubClientOptions {
8080
webSocketOptions?: WebSocketOptions;
8181
}
8282

83+
// @public
84+
export interface EventHubConnectionStringProperties {
85+
endpoint: string;
86+
eventHubName?: string;
87+
fullyQualifiedNamespace: string;
88+
sharedAccessKey?: string;
89+
sharedAccessKeyName?: string;
90+
sharedAccessSignature?: string;
91+
}
92+
8393
// @public
8494
export class EventHubConsumerClient {
8595
constructor(consumerGroup: string, connectionString: string, options?: EventHubConsumerClientOptions);
@@ -176,6 +186,9 @@ export interface OperationOptions {
176186
tracingOptions?: OperationTracingOptions;
177187
}
178188

189+
// @public
190+
export function parseEventHubConnectionString(connectionString: string): Readonly<EventHubConnectionStringProperties>;
191+
179192
// @public
180193
export interface PartitionContext {
181194
readonly consumerGroup: string;

sdk/eventhub/event-hubs/src/connectionContext.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@
77
import { logger, logErrorStackTrace } from "./log";
88
import { getRuntimeInfo } from "./util/runtimeInfo";
99
import { packageJsonInfo } from "./util/constants";
10+
import { parseEventHubConnectionString } from "./util/connectionStringUtils";
1011
import { EventHubReceiver } from "./eventHubReceiver";
1112
import { EventHubSender } from "./eventHubSender";
1213
import {
1314
ConnectionContextBase,
1415
Constants,
1516
CreateConnectionContextBaseParameters,
16-
parseConnectionString,
1717
ConnectionConfig
1818
} from "@azure/core-amqp";
1919
import { TokenCredential, isTokenCredential } from "@azure/core-auth";
@@ -447,23 +447,26 @@ export function createConnectionContext(
447447
hostOrConnectionString = String(hostOrConnectionString);
448448

449449
if (!isTokenCredential(credentialOrOptions)) {
450-
const parsedCS = parseConnectionString<{ EntityPath?: string }>(hostOrConnectionString);
450+
const parsedCS = parseEventHubConnectionString(hostOrConnectionString);
451451
if (
452-
!(parsedCS.EntityPath || (typeof eventHubNameOrOptions === "string" && eventHubNameOrOptions))
452+
!(
453+
parsedCS.eventHubName ||
454+
(typeof eventHubNameOrOptions === "string" && eventHubNameOrOptions)
455+
)
453456
) {
454457
throw new TypeError(
455458
`Either provide "eventHubName" or the "connectionString": "${hostOrConnectionString}", ` +
456459
`must contain "EntityPath=<your-event-hub-name>".`
457460
);
458461
}
459462
if (
460-
parsedCS.EntityPath &&
463+
parsedCS.eventHubName &&
461464
typeof eventHubNameOrOptions === "string" &&
462465
eventHubNameOrOptions &&
463-
parsedCS.EntityPath !== eventHubNameOrOptions
466+
parsedCS.eventHubName !== eventHubNameOrOptions
464467
) {
465468
throw new TypeError(
466-
`The entity path "${parsedCS.EntityPath}" in connectionString: "${hostOrConnectionString}" ` +
469+
`The entity path "${parsedCS.eventHubName}" in connectionString: "${hostOrConnectionString}" ` +
467470
`doesn't match with eventHubName: "${eventHubNameOrOptions}".`
468471
);
469472
}

sdk/eventhub/event-hubs/src/eventhubSharedKeyCredential.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT license.
33

4-
import { parseConnectionString } from "@azure/core-amqp";
4+
import { parseEventHubConnectionString } from "./util/connectionStringUtils";
55
import { AccessToken } from "@azure/core-auth";
66
import { Buffer } from "buffer";
77
import isBuffer from "is-buffer";
@@ -78,16 +78,12 @@ export class SharedKeyCredential {
7878
* @param {string} connectionString - The EventHub/ServiceBus connection string
7979
*/
8080
static fromConnectionString(connectionString: string): SharedKeyCredential {
81-
const parsed = parseConnectionString<{
82-
SharedAccessSignature: string;
83-
SharedAccessKeyName: string;
84-
SharedAccessKey: string;
85-
}>(connectionString);
81+
const parsed = parseEventHubConnectionString(connectionString);
8682

87-
if (parsed.SharedAccessSignature == null) {
88-
return new SharedKeyCredential(parsed.SharedAccessKeyName, parsed.SharedAccessKey);
83+
if (parsed.sharedAccessSignature == null) {
84+
return new SharedKeyCredential(parsed.sharedAccessKeyName!, parsed.sharedAccessKey!);
8985
} else {
90-
return new SharedAccessSignatureCredential(parsed.SharedAccessSignature);
86+
return new SharedAccessSignatureCredential(parsed.sharedAccessSignature);
9187
}
9288
}
9389
}

sdk/eventhub/event-hubs/src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,7 @@ export { CloseReason } from "./models/public";
3838
export { MessagingError, RetryOptions, WebSocketOptions } from "@azure/core-amqp";
3939
export { TokenCredential } from "@azure/core-auth";
4040
export { logger } from "./log";
41+
export {
42+
parseEventHubConnectionString,
43+
EventHubConnectionStringProperties
44+
} from "./util/connectionStringUtils";
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
import { parseConnectionString } from "@azure/core-amqp";
5+
6+
/**
7+
* The set of properties that comprise an Event Hub connection string.
8+
*/
9+
export interface EventHubConnectionStringProperties {
10+
/**
11+
* The fully qualified Event Hub namespace extracted from the "Endpoint" in the
12+
* connection string. This is likely to be similar to "{yournamespace}.servicebus.windows.net".
13+
* This is typically used to construct the EventHub{Producer|Consumer}Client.
14+
*/
15+
fullyQualifiedNamespace: string;
16+
/**
17+
* The value for "Endpoint" in the connection string.
18+
*/
19+
endpoint: string;
20+
/**
21+
* The value for "EntityPath" in the connection string which would be the name of the event hub instance associated with the connection string.
22+
* Connection string from a Shared Access Policy created at the namespace level
23+
* will not have the EntityPath in it.
24+
*/
25+
eventHubName?: string;
26+
/**
27+
* The value for "SharedAccessKey" in the connection string. This along with the "SharedAccessKeyName"
28+
* in the connection string is used to generate a SharedAccessSignature which can be used authorize
29+
* the connection to the service.
30+
*/
31+
sharedAccessKey?: string;
32+
/**
33+
* The value for "SharedAccessKeyName" in the connection string. This along with the "SharedAccessKey"
34+
* in the connection string is used to generate a SharedAccessSignature which can be used authorize
35+
* the connection to the service.
36+
*/
37+
sharedAccessKeyName?: string;
38+
/**
39+
* The value for "SharedAccessSignature" in the connection string. This is typically not present in the
40+
* connection string generated for a Shared Access Policy. It is instead generated by the
41+
* user and appended to the connection string for ease of use.
42+
*/
43+
sharedAccessSignature?: string;
44+
}
45+
46+
/**
47+
* Parses given connection string into the different properties applicable to Azure Event Hubs.
48+
* The properties are useful to then construct an EventHub{Producer|Consumer}Client.
49+
* @param connectionString The connection string associated with the Shared Access Policy created
50+
* for the Event Hubs namespace.
51+
*/
52+
export function parseEventHubConnectionString(
53+
connectionString: string
54+
): Readonly<EventHubConnectionStringProperties> {
55+
const parsedResult = parseConnectionString<{
56+
Endpoint: string;
57+
EntityPath?: string;
58+
SharedAccessSignature?: string;
59+
SharedAccessKey?: string;
60+
SharedAccessKeyName?: string;
61+
}>(connectionString);
62+
63+
validateProperties(
64+
parsedResult.Endpoint,
65+
parsedResult.SharedAccessSignature,
66+
parsedResult.SharedAccessKey,
67+
parsedResult.SharedAccessKeyName
68+
);
69+
70+
const output: EventHubConnectionStringProperties = {
71+
fullyQualifiedNamespace: (parsedResult.Endpoint.match(".*://([^/]*)") || [])[1],
72+
endpoint: parsedResult.Endpoint
73+
};
74+
75+
if (parsedResult.EntityPath) {
76+
output.eventHubName = parsedResult.EntityPath;
77+
}
78+
79+
if (parsedResult.SharedAccessSignature) {
80+
output.sharedAccessSignature = parsedResult.SharedAccessSignature;
81+
}
82+
83+
if (parsedResult.SharedAccessKey && parsedResult.SharedAccessKeyName) {
84+
output.sharedAccessKey = parsedResult.SharedAccessKey;
85+
output.sharedAccessKeyName = parsedResult.SharedAccessKeyName;
86+
}
87+
88+
return output;
89+
}
90+
91+
/**
92+
* @internal
93+
* @ignore
94+
*/
95+
function validateProperties(
96+
endpoint?: string,
97+
sharedAccessSignature?: string,
98+
sharedAccessKey?: string,
99+
sharedAccessKeyName?: string
100+
): void {
101+
if (!endpoint) {
102+
throw new Error("Connection string should have an Endpoint key.");
103+
}
104+
105+
if (sharedAccessSignature) {
106+
if (sharedAccessKey || sharedAccessKeyName) {
107+
throw new Error(
108+
"Connection string cannot have both SharedAccessSignature and SharedAccessKey keys."
109+
);
110+
}
111+
} else if (sharedAccessKey && !sharedAccessKeyName) {
112+
throw new Error("Connection string with SharedAccessKey should have SharedAccessKeyName.");
113+
} else if (!sharedAccessKey && sharedAccessKeyName) {
114+
throw new Error(
115+
"Connection string with SharedAccessKeyName should have SharedAccessKey as well."
116+
);
117+
}
118+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
import {
5+
parseEventHubConnectionString,
6+
EventHubConnectionStringProperties
7+
} from "../src/util/connectionStringUtils";
8+
import chai from "chai";
9+
10+
const assert = chai.assert;
11+
12+
describe("parseEventHubConnectionString", () => {
13+
const namespace = "my.servicebus.windows.net";
14+
const sharedAccessKey = "shared-access-key";
15+
const sharedAccessKeyName = "shared-access-key-name";
16+
const sharedAccessSignature = "shared-access-signature";
17+
const endpoint = "sb://my.servicebus.windows.net";
18+
const eventHubName = "event-hub-name";
19+
20+
describe("with valid data", () => {
21+
it("parses a full connection string correctly", () => {
22+
const expected: EventHubConnectionStringProperties = {
23+
fullyQualifiedNamespace: namespace,
24+
endpoint: endpoint,
25+
eventHubName: eventHubName,
26+
sharedAccessKeyName: sharedAccessKeyName,
27+
sharedAccessKey: sharedAccessKey
28+
};
29+
30+
const connectionString = `Endpoint=${endpoint};EntityPath=${eventHubName};SharedAccessKeyName=${sharedAccessKeyName};SharedAccessKey=${sharedAccessKey}`;
31+
32+
assert.deepEqual(parseEventHubConnectionString(connectionString), expected);
33+
});
34+
35+
it("parses a minimal connection string correctly", () => {
36+
const expected: EventHubConnectionStringProperties = {
37+
fullyQualifiedNamespace: namespace,
38+
endpoint: endpoint,
39+
sharedAccessSignature: sharedAccessSignature
40+
};
41+
42+
const connectionString = `Endpoint=${endpoint};SharedAccessSignature=${sharedAccessSignature}`;
43+
44+
assert.deepEqual(parseEventHubConnectionString(connectionString), expected);
45+
});
46+
});
47+
48+
describe("with invalid data", () => {
49+
it("throws when Endpoint is missing", () => {
50+
const connectionString = `SharedAccessSignature=${sharedAccessSignature}`;
51+
assert.throws(() => {
52+
parseEventHubConnectionString(connectionString);
53+
}, /Connection string/);
54+
});
55+
56+
it("throws when both SharedAccessSignature and SharedAccessKey are provided", () => {
57+
const connectionString = `Endpoint=${endpoint};SharedAccessSignature=${sharedAccessSignature};SharedAccessKey=${sharedAccessKey}`;
58+
assert.throws(() => {
59+
parseEventHubConnectionString(connectionString);
60+
}, /Connection string/);
61+
});
62+
63+
it("throws when both SharedAccessSignature and SharedAccessKeyName are provided", () => {
64+
const connectionString = `Endpoint=${endpoint};SharedAccessSignature=${sharedAccessSignature};SharedAccessKeyName=${sharedAccessKeyName}`;
65+
assert.throws(() => {
66+
parseEventHubConnectionString(connectionString);
67+
}, /Connection string/);
68+
});
69+
70+
it("throws when SharedAccessKey is provided without SharedAccessKeyName", () => {
71+
const connectionString = `Endpoint=${endpoint};SharedAccessKey=${sharedAccessKey}`;
72+
assert.throws(() => {
73+
parseEventHubConnectionString(connectionString);
74+
}, /Connection string/);
75+
});
76+
77+
it("throws when SharedAccessKeyName is provided without SharedAccessKey", () => {
78+
const connectionString = `Endpoint=${endpoint};SharedAccessKeyName=${sharedAccessKeyName}`;
79+
assert.throws(() => {
80+
parseEventHubConnectionString(connectionString);
81+
}, /Connection string/);
82+
});
83+
});
84+
});

sdk/eventhub/event-hubs/test/internal/auth.spec.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT license.
33

4-
import { parseConnectionString } from "@azure/core-amqp";
5-
import { EventHubConsumerClient, EventHubProducerClient } from "../../src";
4+
import {
5+
EventHubConsumerClient,
6+
EventHubProducerClient,
7+
parseEventHubConnectionString
8+
} from "../../src";
69
import { EnvVarKeys, getEnvVars } from "../public/utils/testUtils";
710
import chai from "chai";
811
import { SharedKeyCredential } from "../../src/eventhubSharedKeyCredential";
@@ -14,9 +17,9 @@ describe("Authentication via SAS", () => {
1417
const service = {
1518
connectionString: env[EnvVarKeys.EVENTHUB_CONNECTION_STRING],
1619
path: env[EnvVarKeys.EVENTHUB_NAME],
17-
fqdn: parseConnectionString<{ Endpoint: string }>(
20+
endpoint: parseEventHubConnectionString(
1821
env[EnvVarKeys.EVENTHUB_CONNECTION_STRING]
19-
).Endpoint.replace(/\/+$/, "")
22+
).endpoint.replace(/\/+$/, "")
2023
};
2124

2225
before(() => {
@@ -58,9 +61,9 @@ describe("Authentication via SAS", () => {
5861

5962
function getSasConnectionString(): string {
6063
const sas = SharedKeyCredential.fromConnectionString(service.connectionString).getToken(
61-
`${service.fqdn}/${service.path}`
64+
`${service.endpoint}/${service.path}`
6265
).token;
6366

64-
return `Endpoint=${service.fqdn}/;SharedAccessSignature=${sas}`;
67+
return `Endpoint=${service.endpoint}/;SharedAccessSignature=${sas}`;
6568
}
6669
});

0 commit comments

Comments
 (0)