Skip to content

Commit 5a3c157

Browse files
committed
Checkpoint
1 parent 15a3c05 commit 5a3c157

2 files changed

Lines changed: 95 additions & 51 deletions

File tree

lib/browser/mqtt_shared_browser.spec.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ test('MQTT topic properties - invalid topic', async () => {
7676
test('metrics username construction - undefined username', async () => {
7777
let username = mqtt_shared_browser.buildFinalUsernameFromMetrics(new mqtt_shared.AwsIoTDeviceSDKMetrics());
7878
expect(username.startsWith(`?SDK=${mqtt_shared.SDK_NAME}&Platform=Browser`)).toBeTruthy();
79+
expect(username.indexOf("&Metadata=(Browser=")).toBeGreaterThanOrEqual(0);
7980
});
8081

8182
test('metrics username construction - empty username', async () => {
@@ -104,8 +105,8 @@ test('metrics username construction - query username with no overlap 3', async (
104105
});
105106

106107
test('metrics username construction - query username with sdk overlap 1', async () => {
107-
let username = mqtt_shared_browser.buildFinalUsernameFromMetrics(new mqtt_shared.AwsIoTDeviceSDKMetrics(), "hello?a=b&SDK=derp");
108-
expect(username.startsWith(`hello?a=b&SDK=derp&Platform=Browser`)).toBeTruthy();
108+
let username = mqtt_shared_browser.buildFinalUsernameFromMetrics(new mqtt_shared.AwsIoTDeviceSDKMetrics(), "hello?a=b&SDK");
109+
expect(username.startsWith(`hello?a=b&SDK=&Platform=Browser`)).toBeTruthy();
109110
});
110111

111112
test('metrics username construction - query username with sdk overlap 2', async () => {

lib/browser/mqtt_shared_browser.ts

Lines changed: 92 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -175,34 +175,37 @@ export function calculateNextReconnectDelay(context: ReconnectDelayContext) : nu
175175
class ParsedUsername {
176176
prefix: string = "";
177177
queryParams: [string, string][] = new Array<[string, string]>;
178-
// metadata: [string, string][] = new Array<[string, string]>;
178+
metadata?: [string, string][] = undefined;
179179
}
180180

181181
const METADATA_KEY : string = "Metadata";
182182
const SDK_KEY : string = "SDK";
183183
const PLATFORM_KEY : string = "Platform";
184184
const BROWSER_PLATFORM_VALUE : string = "Browser";
185185
const BROWSER_METADATA_KEY : string = "Browser";
186-
187-
function parseDelimitedKeyValueString(input: string, pairDelimiter: string, kvDelimeter: string) : Array<[string, string]> | null {
186+
const QUERY_PARAM_START_DELIMITER : string = "?";
187+
const QUERY_PAIR_DELIMITER : string = "&";
188+
const METADATA_PAIR_DELIMITER : string = ";";
189+
const KEY_VALUE_SEPARATOR : string = "=";
190+
const METADATA_PREFIX : string = "(";
191+
const METADATA_SUFFIX : string = ")";
192+
193+
function parseDelimitedKeyValueString(input: string, pairDelimiter: string, kvDelimiter: string) : Array<[string, string]> {
188194
let kvPairs = new Array<[string, string]>;
189195

190196
let pairs = input.split(pairDelimiter);
191197
for (let pair of pairs) {
192-
let kvDelimeterIndex = pair.indexOf(kvDelimeter);
193-
if (kvDelimeterIndex < 0) {
198+
let kvDelimiterIndex = pair.indexOf(kvDelimiter);
199+
// just a key, no value?
200+
if (kvDelimiterIndex < 0) {
194201
kvPairs.push([pair, ""]);
195202
}
196203

197-
let value = pair.substring(kvDelimeterIndex + 1);
198-
kvPairs.push([pair.substring(0, kvDelimeterIndex), value]);
199-
}
200-
201-
if (kvPairs.length > 0) {
202-
return kvPairs;
204+
let value = pair.substring(kvDelimiterIndex + 1);
205+
kvPairs.push([pair.substring(0, kvDelimiterIndex), value]);
203206
}
204207

205-
return null;
208+
return kvPairs;
206209
}
207210

208211
function parseUsername(username?: string) : ParsedUsername {
@@ -212,80 +215,120 @@ function parseUsername(username?: string) : ParsedUsername {
212215
return parsed;
213216
}
214217

215-
let queryIndex = username.lastIndexOf('?');
218+
let queryIndex = username.lastIndexOf(QUERY_PARAM_START_DELIMITER);
216219
if (queryIndex < 0) {
220+
// no '?'
217221
parsed.prefix = username;
218222
return parsed;
219223
}
220224

221225
parsed.prefix = username.substring(0, queryIndex);
222226

223227
let remaining = username.substring(queryIndex + 1);
224-
let topLevelPairs = parseDelimitedKeyValueString(remaining, "&", "=");
225-
if (topLevelPairs) {
226-
parsed.queryParams = topLevelPairs;
227-
/*
228-
let metadataValue = topLevelPairs.get(METADATA_KEY);
229-
if (metadataValue !== undefined && metadataValue.length >= 2 && metadataValue[0] == '(' && metadataValue[metadataValue.length - 1] == ')') {
230-
metadataValue = metadataValue.substring(1, metadataValue.length - 2);
231-
let metadataMap = parseDelimitedKeyValueString(metadataValue, ";", "=");
232-
if (metadataMap) {
233-
parsed.metadata = metadataMap;
234-
parsed.queryParams.delete(METADATA_KEY);
228+
parsed.queryParams = parseDelimitedKeyValueString(remaining, QUERY_PAIR_DELIMITER, KEY_VALUE_SEPARATOR);
229+
let seenMetadata = false;
230+
for (let pair of parsed.queryParams) {
231+
if (pair[0] === METADATA_KEY && !seenMetadata) {
232+
// only process the first instance of metadata
233+
seenMetadata = true;
234+
let metadataValue = pair[1];
235+
if (metadataValue !== undefined && metadataValue.length >= 2 && metadataValue.startsWith(METADATA_PREFIX) && metadataValue.endsWith(METADATA_SUFFIX)) {
236+
metadataValue = metadataValue.substring(METADATA_PREFIX.length, metadataValue.length - (METADATA_PREFIX.length + METADATA_SUFFIX.length));
237+
parsed.metadata = parseDelimitedKeyValueString(metadataValue, METADATA_PAIR_DELIMITER, KEY_VALUE_SEPARATOR);
235238
}
236-
}*/
239+
break;
240+
}
237241
}
238242

239243
return parsed;
240244
}
241245

242-
function addTopLevelPairIfNonexistent(parsed: ParsedUsername, key: string, value: string) {
243-
if (parsed.queryParams.has(key)) {
244-
return;
246+
// if this is ever public, we need to handle attempting to set metadata in a special way
247+
function addTopLevelPair(parsed: ParsedUsername, key: string, value: string) {
248+
// no need to check for duplicates; when we build the final username we use the first value only
249+
parsed.queryParams.push([key, value]);
250+
}
251+
252+
function addMetadataPair(parsed: ParsedUsername, key: string, value: string) {
253+
let hasMetadataKey = false;
254+
for (let pair of parsed.queryParams) {
255+
if (pair[0] === METADATA_KEY) {
256+
hasMetadataKey = true;
257+
break;
258+
}
245259
}
246260

247-
parsed.queryParams.set(key, value);
248-
}
261+
// If we have malformed metadata (top level entry, but no parsed pairs), then don't do anything
262+
if (!parsed.metadata) {
263+
if (hasMetadataKey) {
264+
return;
265+
}
249266

250-
function addMetadataPairIfNonExistent(parsed: ParsedUsername, key: string, value: string) {
251-
// can't add metadata pairs if there's an existing top-level metadata entry that is malformed
252-
if (parsed.queryParams.has(METADATA_KEY)) {
253-
return;
267+
parsed.metadata = new Array<[string, string]>();
254268
}
255269

256-
let metadataValue = parsed.metadata.get(key);
257-
if (metadataValue !== undefined) {
258-
return;
270+
// no need to check for duplicates; when we build the final username we use the first value only
271+
let strippedValue = value.replace(METADATA_PAIR_DELIMITER, "");
272+
parsed.metadata.push([key, strippedValue]);
273+
}
274+
275+
function buildOrderedKeyValues(pairs?: Array<[string, string]>) : [Array<string>, Map<string, string>] {
276+
let keys: Array<string> = new Array<string>();
277+
let kvMap = new Map<string, string>();
278+
if (!pairs) {
279+
return [keys, kvMap];
259280
}
260281

261-
parsed.metadata.set(key, value);
282+
pairs.forEach((pair) => {
283+
if (!kvMap.has(pair[0])) {
284+
kvMap.set(pair[0], pair[1]);
285+
keys.push(pair[0]);
286+
}
287+
});
288+
289+
return [keys, kvMap];
262290
}
263291

264292
function buildUsernameFromQueryParse(parsed: ParsedUsername) : string {
265-
let metadataValue = undefined;
266-
if (!parsed.queryParams.get(METADATA_KEY) && parsed.metadata.size > 0) {
267-
let innerValue = Array.from(parsed.metadata.entries()).map((pair) => pair.join("=")).join(";");
268-
metadataValue = "(" + innerValue + ")";
293+
let [queryKeys, queryValues] = buildOrderedKeyValues(parsed.queryParams);
294+
let [metadataKeys, metadataValues] = buildOrderedKeyValues(parsed.metadata);
295+
296+
if (parsed.queryParams.length == 0 && !parsed.metadata) {
297+
return parsed.prefix;
298+
}
299+
300+
// if we have valid metadata pairs, build the final metadata value
301+
let metadataValue : string | undefined = undefined;
302+
if (parsed.metadata && metadataKeys.length > 0) {
303+
let innerValue = metadataKeys.map((key) => {
304+
return `${key}${KEY_VALUE_SEPARATOR}${metadataValues.get(key) ?? ""}`;
305+
}).join(METADATA_PAIR_DELIMITER);
306+
metadataValue = METADATA_PREFIX + innerValue + METADATA_SUFFIX;
269307
}
270308

271-
let topLevelParamArray = Array.from(parsed.queryParams.entries());
309+
// if we have a constructed metadata value, replace the value and make sure it's in the key list
272310
if (metadataValue !== undefined) {
273-
topLevelParamArray.push([METADATA_KEY, metadataValue]);
311+
if (queryValues.get(METADATA_KEY) === undefined) {
312+
queryKeys.push(METADATA_KEY);
313+
}
314+
queryValues.set(METADATA_KEY, metadataValue);
274315
}
275316

276-
let queryParamValue = topLevelParamArray.map((pair) => pair.join("=")).join(":");
317+
let queryParamValue = queryKeys.map((key) => {
318+
return `${key}${KEY_VALUE_SEPARATOR}${queryValues.get(key) ?? ""}`;
319+
}).join(QUERY_PAIR_DELIMITER);
277320

278-
return parsed.prefix + "?" + queryParamValue;
321+
return parsed.prefix + QUERY_PARAM_START_DELIMITER + queryParamValue;
279322
}
280323

281324
export function buildFinalUsernameFromMetrics(metrics: mqtt_shared.AwsIoTDeviceSDKMetrics, username?: string) : string {
282325
let parsed = parseUsername(username);
283326

284-
addTopLevelPairIfNonexistent(parsed, SDK_KEY, metrics.libraryName);
285-
addTopLevelPairIfNonexistent(parsed, PLATFORM_KEY, BROWSER_PLATFORM_VALUE);
327+
addTopLevelPair(parsed, SDK_KEY, metrics.libraryName);
328+
addTopLevelPair(parsed, PLATFORM_KEY, BROWSER_PLATFORM_VALUE);
286329

287330
let browserInfo = window?.navigator?.userAgent ?? "unknown";
288-
addMetadataPairIfNonExistent(parsed, BROWSER_METADATA_KEY, browserInfo);
331+
addMetadataPair(parsed, BROWSER_METADATA_KEY, browserInfo);
289332

290333
return buildUsernameFromQueryParse(parsed);
291334
}

0 commit comments

Comments
 (0)