Add urlPath util for building escaped URL paths
This commit is contained in:
		
					parent
					
						
							
								e51cde1770
							
						
					
				
			
			
				commit
				
					
						cd50c715a9
					
				
			
		
					 3 changed files with 144 additions and 73 deletions
				
			
		|  | @ -7,7 +7,8 @@ import { size } from '../../util/iterables'; | ||||||
| import { | import { | ||||||
|   maybeParseUrl, |   maybeParseUrl, | ||||||
|   setUrlSearchParams, |   setUrlSearchParams, | ||||||
|   urlPathFromComponents, |   urlPath, | ||||||
|  |   urlPathJoin, | ||||||
| } from '../../util/url'; | } from '../../util/url'; | ||||||
| 
 | 
 | ||||||
| describe('URL utilities', () => { | describe('URL utilities', () => { | ||||||
|  | @ -85,17 +86,43 @@ describe('URL utilities', () => { | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   describe('urlPathFromComponents', () => { |   describe('urlPath', () => { | ||||||
|     it('returns / if no components are provided', () => { |     it('escapes values', () => { | ||||||
|       assert.strictEqual(urlPathFromComponents([]), '/'); |       assert.strictEqual( | ||||||
|  |         urlPath`/path/to/${' %?&='}/${true}/${42}`.toString(), | ||||||
|  |         '/path/to/%20%25%3F%26%3D/true/42' | ||||||
|  |       ); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     it('joins components, percent-encoding them and removing empty components', () => { |     it('doesnt escape nested url paths', () => { | ||||||
|       const components = ['foo', '', '~', 'bar / baz qúx']; |  | ||||||
|       assert.strictEqual( |       assert.strictEqual( | ||||||
|         urlPathFromComponents(components), |         urlPath`${urlPath`/path?param=true`}&other=${' %?&='}`.toString(), | ||||||
|         '/foo/~/bar%20%2F%20baz%20q%C3%BAx' |         '/path?param=true&other=%20%25%3F%26%3D' | ||||||
|       ); |       ); | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
|  | 
 | ||||||
|  |   describe('urlPathJoin', () => { | ||||||
|  |     it('escapes values', () => { | ||||||
|  |       assert.strictEqual( | ||||||
|  |         urlPathJoin([' %?&=', true, 42], '&').toString(), | ||||||
|  |         '%20%25%3F%26%3D&true&42' | ||||||
|  |       ); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('doesnt escape nested url paths', () => { | ||||||
|  |       assert.strictEqual( | ||||||
|  |         urlPathJoin([urlPath`/path?param=true`, ' %?&='], '&').toString(), | ||||||
|  |         '/path?param=true&%20%25%3F%26%3D' | ||||||
|  |       ); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('works with empty arrays', () => { | ||||||
|  |       assert.strictEqual(urlPathJoin([], '&').toString(), ''); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('works with single items', () => { | ||||||
|  |       assert.strictEqual(urlPathJoin(['hi'], '&').toString(), 'hi'); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -67,7 +67,8 @@ import type { | ||||||
| } from './Types.d'; | } from './Types.d'; | ||||||
| import { handleStatusCode, translateError } from './Utils'; | import { handleStatusCode, translateError } from './Utils'; | ||||||
| import * as log from '../logging/log'; | import * as log from '../logging/log'; | ||||||
| import { maybeParseUrl, urlPathFromComponents } from '../util/url'; | import type { UrlPath } from '../util/url'; | ||||||
|  | import { urlPathJoin, maybeParseUrl, urlPath } from '../util/url'; | ||||||
| import { SECOND } from '../util/durations'; | import { SECOND } from '../util/durations'; | ||||||
| import { safeParseNumber } from '../util/numbers'; | import { safeParseNumber } from '../util/numbers'; | ||||||
| import { isStagingServer } from '../util/isStagingServer'; | import { isStagingServer } from '../util/isStagingServer'; | ||||||
|  | @ -717,7 +718,7 @@ type AjaxOptionsType = { | ||||||
|   responseType?: 'json' | 'bytes' | 'byteswithdetails' | 'stream'; |   responseType?: 'json' | 'bytes' | 'byteswithdetails' | 'stream'; | ||||||
|   schema?: unknown; |   schema?: unknown; | ||||||
|   timeout?: number; |   timeout?: number; | ||||||
|   urlParameters?: string; |   urlParameters?: UrlPath; | ||||||
|   username?: string; |   username?: string; | ||||||
|   validateResponse?: any; |   validateResponse?: any; | ||||||
|   isRegistration?: true; |   isRegistration?: true; | ||||||
|  | @ -1865,7 +1866,7 @@ export function initialize({ | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       if (!param.urlParameters) { |       if (!param.urlParameters) { | ||||||
|         param.urlParameters = ''; |         param.urlParameters = urlPath``; | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       const useWebSocketForEndpoint = |       const useWebSocketForEndpoint = | ||||||
|  | @ -1882,7 +1883,7 @@ export function initialize({ | ||||||
|         headers: param.headers, |         headers: param.headers, | ||||||
|         host: param.host || url, |         host: param.host || url, | ||||||
|         password: param.password ?? password, |         password: param.password ?? password, | ||||||
|         path: URL_CALLS[param.call] + param.urlParameters, |         path: URL_CALLS[param.call] + param.urlParameters.toString(), | ||||||
|         proxyUrl, |         proxyUrl, | ||||||
|         responseType: param.responseType, |         responseType: param.responseType, | ||||||
|         timeout: param.timeout, |         timeout: param.timeout, | ||||||
|  | @ -2045,7 +2046,7 @@ export function initialize({ | ||||||
|         httpType: 'GET', |         httpType: 'GET', | ||||||
|         responseType: 'json', |         responseType: 'json', | ||||||
|         validateResponse: { certificate: 'string' }, |         validateResponse: { certificate: 'string' }, | ||||||
|         ...(omitE164 ? { urlParameters: '?includeE164=false' } : {}), |         ...(omitE164 ? { urlParameters: urlPath`?includeE164=false` } : {}), | ||||||
|       })) as GetSenderCertificateResultType; |       })) as GetSenderCertificateResultType; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -2084,8 +2085,8 @@ export function initialize({ | ||||||
|         httpType: 'GET', |         httpType: 'GET', | ||||||
|         responseType: 'byteswithdetails', |         responseType: 'byteswithdetails', | ||||||
|         urlParameters: greaterThanVersion |         urlParameters: greaterThanVersion | ||||||
|           ? `/version/${greaterThanVersion}` |           ? urlPath`/version/${greaterThanVersion}` | ||||||
|           : '', |           : urlPath``, | ||||||
|         ...credentials, |         ...credentials, | ||||||
|       }); |       }); | ||||||
| 
 | 
 | ||||||
|  | @ -2177,13 +2178,11 @@ export function initialize({ | ||||||
|         profileKeyCredentialRequest, |         profileKeyCredentialRequest, | ||||||
|       }: GetProfileCommonOptionsType |       }: GetProfileCommonOptionsType | ||||||
|     ) { |     ) { | ||||||
|       let profileUrl = `/${serviceId}`; |       let profileUrl = urlPath`/${serviceId}`; | ||||||
|       if (profileKeyVersion !== undefined) { |       if (profileKeyVersion !== undefined) { | ||||||
|         profileUrl += `/${profileKeyVersion}`; |         profileUrl = urlPath`${profileUrl}/${profileKeyVersion}`; | ||||||
|         if (profileKeyCredentialRequest !== undefined) { |         if (profileKeyCredentialRequest !== undefined) { | ||||||
|           profileUrl += |           profileUrl = urlPath`${profileUrl}/${profileKeyCredentialRequest}?credentialType=expiringProfileKey`; | ||||||
|             `/${profileKeyCredentialRequest}` + |  | ||||||
|             '?credentialType=expiringProfileKey'; |  | ||||||
|         } |         } | ||||||
|       } else { |       } else { | ||||||
|         strictAssert( |         strictAssert( | ||||||
|  | @ -2226,7 +2225,7 @@ export function initialize({ | ||||||
|         await _ajax({ |         await _ajax({ | ||||||
|           call: 'username', |           call: 'username', | ||||||
|           httpType: 'GET', |           httpType: 'GET', | ||||||
|           urlParameters: `/${hashBase64}`, |           urlParameters: urlPath`/${hashBase64}`, | ||||||
|           responseType: 'json', |           responseType: 'json', | ||||||
|           redactUrl: _createRedactor(hashBase64), |           redactUrl: _createRedactor(hashBase64), | ||||||
|           unauthenticated: true, |           unauthenticated: true, | ||||||
|  | @ -2443,7 +2442,7 @@ export function initialize({ | ||||||
|         await _ajax({ |         await _ajax({ | ||||||
|           httpType: 'GET', |           httpType: 'GET', | ||||||
|           call: 'usernameLink', |           call: 'usernameLink', | ||||||
|           urlParameters: `/${encodeURIComponent(serverId)}`, |           urlParameters: urlPath`/${serverId}`, | ||||||
|           responseType: 'json', |           responseType: 'json', | ||||||
|           unauthenticated: true, |           unauthenticated: true, | ||||||
|           accessKey: undefined, |           accessKey: undefined, | ||||||
|  | @ -2462,7 +2461,7 @@ export function initialize({ | ||||||
|       await _ajax({ |       await _ajax({ | ||||||
|         call: 'reportMessage', |         call: 'reportMessage', | ||||||
|         httpType: 'POST', |         httpType: 'POST', | ||||||
|         urlParameters: urlPathFromComponents([senderAci, serverGuid]), |         urlParameters: urlPath`/${senderAci}/${serverGuid}`, | ||||||
|         responseType: 'bytes', |         responseType: 'bytes', | ||||||
|         jsonData, |         jsonData, | ||||||
|       }); |       }); | ||||||
|  | @ -2493,7 +2492,7 @@ export function initialize({ | ||||||
|         await _ajax({ |         await _ajax({ | ||||||
|           call: 'verificationSession', |           call: 'verificationSession', | ||||||
|           httpType: 'PATCH', |           httpType: 'PATCH', | ||||||
|           urlParameters: `/${encodeURIComponent(session.id)}`, |           urlParameters: urlPath`/${session.id}`, | ||||||
|           responseType: 'json', |           responseType: 'json', | ||||||
|           jsonData: { |           jsonData: { | ||||||
|             captcha, |             captcha, | ||||||
|  | @ -2514,7 +2513,7 @@ export function initialize({ | ||||||
|         await _ajax({ |         await _ajax({ | ||||||
|           call: 'verificationSession', |           call: 'verificationSession', | ||||||
|           httpType: 'POST', |           httpType: 'POST', | ||||||
|           urlParameters: `/${encodeURIComponent(session.id)}/code`, |           urlParameters: urlPath`/${session.id}/code`, | ||||||
|           responseType: 'json', |           responseType: 'json', | ||||||
|           jsonData: { |           jsonData: { | ||||||
|             client: 'ios', |             client: 'ios', | ||||||
|  | @ -2536,7 +2535,7 @@ export function initialize({ | ||||||
|         await _ajax({ |         await _ajax({ | ||||||
|           httpType: 'HEAD', |           httpType: 'HEAD', | ||||||
|           call: 'accountExistence', |           call: 'accountExistence', | ||||||
|           urlParameters: `/${serviceId}`, |           urlParameters: urlPath`/${serviceId}`, | ||||||
|           unauthenticated: true, |           unauthenticated: true, | ||||||
|           accessKey: undefined, |           accessKey: undefined, | ||||||
|           groupSendToken: undefined, |           groupSendToken: undefined, | ||||||
|  | @ -2622,7 +2621,7 @@ export function initialize({ | ||||||
|           isRegistration: true, |           isRegistration: true, | ||||||
|           call: 'verificationSession', |           call: 'verificationSession', | ||||||
|           httpType: 'PUT', |           httpType: 'PUT', | ||||||
|           urlParameters: `/${encodeURIComponent(sessionId)}/code`, |           urlParameters: urlPath`/${sessionId}/code`, | ||||||
|           responseType: 'json', |           responseType: 'json', | ||||||
|           jsonData: { |           jsonData: { | ||||||
|             code, |             code, | ||||||
|  | @ -2739,7 +2738,7 @@ export function initialize({ | ||||||
|       await _ajax({ |       await _ajax({ | ||||||
|         call: 'devices', |         call: 'devices', | ||||||
|         httpType: 'DELETE', |         httpType: 'DELETE', | ||||||
|         urlParameters: `/${deviceId}`, |         urlParameters: urlPath`/${deviceId}`, | ||||||
|       }); |       }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -2824,7 +2823,7 @@ export function initialize({ | ||||||
|       await _ajax({ |       await _ajax({ | ||||||
|         isRegistration: true, |         isRegistration: true, | ||||||
|         call: 'keys', |         call: 'keys', | ||||||
|         urlParameters: `?${serviceIdKindToQuery(serviceIdKind)}`, |         urlParameters: urlPath`?${serviceIdKindToQuery(serviceIdKind)}`, | ||||||
|         httpType: 'PUT', |         httpType: 'PUT', | ||||||
|         jsonData: keys, |         jsonData: keys, | ||||||
|       }); |       }); | ||||||
|  | @ -2854,7 +2853,7 @@ export function initialize({ | ||||||
|       abortSignal, |       abortSignal, | ||||||
|     }: GetBackupStreamOptionsType): Promise<Readable> { |     }: GetBackupStreamOptionsType): Promise<Readable> { | ||||||
|       return _getAttachment({ |       return _getAttachment({ | ||||||
|         cdnPath: `/backups/${encodeURIComponent(backupDir)}/${encodeURIComponent(backupName)}`, |         cdnPath: urlPath`/backups/${backupDir}/${backupName}`, | ||||||
|         cdnNumber: cdn, |         cdnNumber: cdn, | ||||||
|         redactor: _createRedactor(backupDir, backupName), |         redactor: _createRedactor(backupDir, backupName), | ||||||
|         headers, |         headers, | ||||||
|  | @ -2954,9 +2953,7 @@ export function initialize({ | ||||||
|       const res = await _ajax({ |       const res = await _ajax({ | ||||||
|         call: 'getBackupCredentials', |         call: 'getBackupCredentials', | ||||||
|         httpType: 'GET', |         httpType: 'GET', | ||||||
|         urlParameters: |         urlParameters: urlPath`?redemptionStartSeconds=${startDayInSeconds}&redemptionEndSeconds=${endDayInSeconds}`, | ||||||
|           `?redemptionStartSeconds=${startDayInSeconds}&` + |  | ||||||
|           `redemptionEndSeconds=${endDayInSeconds}`, |  | ||||||
|         responseType: 'json', |         responseType: 'json', | ||||||
|       }); |       }); | ||||||
| 
 | 
 | ||||||
|  | @ -2974,7 +2971,7 @@ export function initialize({ | ||||||
|         accessKey: undefined, |         accessKey: undefined, | ||||||
|         groupSendToken: undefined, |         groupSendToken: undefined, | ||||||
|         headers, |         headers, | ||||||
|         urlParameters: `?cdn=${cdn}`, |         urlParameters: urlPath`?cdn=${cdn}`, | ||||||
|         responseType: 'json', |         responseType: 'json', | ||||||
|       }); |       }); | ||||||
| 
 | 
 | ||||||
|  | @ -3080,12 +3077,12 @@ export function initialize({ | ||||||
|       cursor, |       cursor, | ||||||
|       limit, |       limit, | ||||||
|     }: BackupListMediaOptionsType) { |     }: BackupListMediaOptionsType) { | ||||||
|       const params = new Array<string>(); |       const params: Array<UrlPath> = []; | ||||||
| 
 | 
 | ||||||
|       if (cursor != null) { |       if (cursor != null) { | ||||||
|         params.push(`cursor=${encodeURIComponent(cursor)}`); |         params.push(urlPath`cursor=${cursor}`); | ||||||
|       } |       } | ||||||
|       params.push(`limit=${limit}`); |       params.push(urlPath`limit=${limit}`); | ||||||
| 
 | 
 | ||||||
|       const res = await _ajax({ |       const res = await _ajax({ | ||||||
|         call: 'backupMedia', |         call: 'backupMedia', | ||||||
|  | @ -3095,7 +3092,7 @@ export function initialize({ | ||||||
|         groupSendToken: undefined, |         groupSendToken: undefined, | ||||||
|         headers, |         headers, | ||||||
|         responseType: 'json', |         responseType: 'json', | ||||||
|         urlParameters: `?${params.join('&')}`, |         urlParameters: urlPath`?${urlPathJoin(params, '&')}`, | ||||||
|       }); |       }); | ||||||
| 
 | 
 | ||||||
|       return backupListMediaResponseSchema.parse(res); |       return backupListMediaResponseSchema.parse(res); | ||||||
|  | @ -3128,7 +3125,7 @@ export function initialize({ | ||||||
|     ): Promise<ServerKeyCountType> { |     ): Promise<ServerKeyCountType> { | ||||||
|       const result = (await _ajax({ |       const result = (await _ajax({ | ||||||
|         call: 'keys', |         call: 'keys', | ||||||
|         urlParameters: `?${serviceIdKindToQuery(serviceIdKind)}`, |         urlParameters: urlPath`?${serviceIdKindToQuery(serviceIdKind)}`, | ||||||
|         httpType: 'GET', |         httpType: 'GET', | ||||||
|         responseType: 'json', |         responseType: 'json', | ||||||
|         validateResponse: { count: 'number', pqCount: 'number' }, |         validateResponse: { count: 'number', pqCount: 'number' }, | ||||||
|  | @ -3230,7 +3227,7 @@ export function initialize({ | ||||||
|       const keys = (await _ajax({ |       const keys = (await _ajax({ | ||||||
|         call: 'keys', |         call: 'keys', | ||||||
|         httpType: 'GET', |         httpType: 'GET', | ||||||
|         urlParameters: `/${serviceId}/${deviceId || '*'}`, |         urlParameters: urlPath`/${serviceId}/${deviceId || '*'}`, | ||||||
|         responseType: 'json', |         responseType: 'json', | ||||||
|         validateResponse: { identityKey: 'string', devices: 'object' }, |         validateResponse: { identityKey: 'string', devices: 'object' }, | ||||||
|       })) as ServerKeyResponseType; |       })) as ServerKeyResponseType; | ||||||
|  | @ -3248,7 +3245,7 @@ export function initialize({ | ||||||
|       const keys = (await _ajax({ |       const keys = (await _ajax({ | ||||||
|         call: 'keys', |         call: 'keys', | ||||||
|         httpType: 'GET', |         httpType: 'GET', | ||||||
|         urlParameters: `/${serviceId}/${deviceId || '*'}`, |         urlParameters: urlPath`/${serviceId}/${deviceId || '*'}`, | ||||||
|         responseType: 'json', |         responseType: 'json', | ||||||
|         validateResponse: { identityKey: 'string', devices: 'object' }, |         validateResponse: { identityKey: 'string', devices: 'object' }, | ||||||
|         unauthenticated: true, |         unauthenticated: true, | ||||||
|  | @ -3284,7 +3281,7 @@ export function initialize({ | ||||||
|       await _ajax({ |       await _ajax({ | ||||||
|         call: 'messages', |         call: 'messages', | ||||||
|         httpType: 'PUT', |         httpType: 'PUT', | ||||||
|         urlParameters: `/${destination}?story=${booleanToString(story)}`, |         urlParameters: urlPath`/${destination}?story=${story}`, | ||||||
|         jsonData, |         jsonData, | ||||||
|         responseType: 'json', |         responseType: 'json', | ||||||
|         unauthenticated: true, |         unauthenticated: true, | ||||||
|  | @ -3313,16 +3310,12 @@ export function initialize({ | ||||||
|       await _ajax({ |       await _ajax({ | ||||||
|         call: 'messages', |         call: 'messages', | ||||||
|         httpType: 'PUT', |         httpType: 'PUT', | ||||||
|         urlParameters: `/${destination}?story=${booleanToString(story)}`, |         urlParameters: urlPath`/${destination}?story=${story}`, | ||||||
|         jsonData, |         jsonData, | ||||||
|         responseType: 'json', |         responseType: 'json', | ||||||
|       }); |       }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     function booleanToString(value: boolean | undefined): string { |  | ||||||
|       return value ? 'true' : 'false'; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     async function sendWithSenderKey( |     async function sendWithSenderKey( | ||||||
|       data: Uint8Array, |       data: Uint8Array, | ||||||
|       accessKeys: Uint8Array | undefined, |       accessKeys: Uint8Array | undefined, | ||||||
|  | @ -3338,16 +3331,16 @@ export function initialize({ | ||||||
|         urgent?: boolean; |         urgent?: boolean; | ||||||
|       } |       } | ||||||
|     ): Promise<MultiRecipient200ResponseType> { |     ): Promise<MultiRecipient200ResponseType> { | ||||||
|       const onlineParam = `&online=${booleanToString(online)}`; |       const onlineParam = urlPath`&online=${online ?? false}`; | ||||||
|       const urgentParam = `&urgent=${booleanToString(urgent)}`; |       const urgentParam = urlPath`&urgent=${urgent}`; | ||||||
|       const storyParam = `&story=${booleanToString(story)}`; |       const storyParam = urlPath`&story=${story}`; | ||||||
| 
 | 
 | ||||||
|       const response = await _ajax({ |       const response = await _ajax({ | ||||||
|         call: 'multiRecipient', |         call: 'multiRecipient', | ||||||
|         httpType: 'PUT', |         httpType: 'PUT', | ||||||
|         contentType: 'application/vnd.signal-messenger.mrm', |         contentType: 'application/vnd.signal-messenger.mrm', | ||||||
|         data, |         data, | ||||||
|         urlParameters: `?ts=${timestamp}${onlineParam}${urgentParam}${storyParam}`, |         urlParameters: urlPath`?ts=${timestamp}${onlineParam}${urgentParam}${storyParam}`, | ||||||
|         responseType: 'json', |         responseType: 'json', | ||||||
|         unauthenticated: true, |         unauthenticated: true, | ||||||
|         accessKey: accessKeys != null ? Bytes.toBase64(accessKeys) : undefined, |         accessKey: accessKeys != null ? Bytes.toBase64(accessKeys) : undefined, | ||||||
|  | @ -3486,7 +3479,7 @@ export function initialize({ | ||||||
|         call: 'getStickerPackUpload', |         call: 'getStickerPackUpload', | ||||||
|         responseType: 'json', |         responseType: 'json', | ||||||
|         httpType: 'GET', |         httpType: 'GET', | ||||||
|         urlParameters: `/${encryptedStickers.length}`, |         urlParameters: urlPath`/${encryptedStickers.length}`, | ||||||
|       }); |       }); | ||||||
| 
 | 
 | ||||||
|       const { packId, manifest, stickers } = |       const { packId, manifest, stickers } = | ||||||
|  | @ -3552,7 +3545,7 @@ export function initialize({ | ||||||
|       }; |       }; | ||||||
|     }) { |     }) { | ||||||
|       return _getAttachment({ |       return _getAttachment({ | ||||||
|         cdnPath: `/attachments/${cdnKey}`, |         cdnPath: urlPath`/attachments/${cdnKey}`, | ||||||
|         cdnNumber: cdnNumber ?? 0, |         cdnNumber: cdnNumber ?? 0, | ||||||
|         redactor: _createRedactor(cdnKey), |         redactor: _createRedactor(cdnKey), | ||||||
|         options, |         options, | ||||||
|  | @ -3579,7 +3572,7 @@ export function initialize({ | ||||||
|       }; |       }; | ||||||
|     }) { |     }) { | ||||||
|       return _getAttachment({ |       return _getAttachment({ | ||||||
|         cdnPath: `/backups/${backupDir}/${mediaDir}/${mediaId}`, |         cdnPath: urlPath`/backups/${backupDir}/${mediaDir}/${mediaId}`, | ||||||
|         cdnNumber, |         cdnNumber, | ||||||
|         headers, |         headers, | ||||||
|         redactor: _createRedactor(backupDir, mediaDir, mediaId), |         redactor: _createRedactor(backupDir, mediaDir, mediaId), | ||||||
|  | @ -3594,7 +3587,7 @@ export function initialize({ | ||||||
|       redactor, |       redactor, | ||||||
|       options, |       options, | ||||||
|     }: { |     }: { | ||||||
|       cdnPath: string; |       cdnPath: UrlPath; | ||||||
|       cdnNumber: number; |       cdnNumber: number; | ||||||
|       headers?: Record<string, string>; |       headers?: Record<string, string>; | ||||||
|       redactor: RedactUrl; |       redactor: RedactUrl; | ||||||
|  | @ -3627,7 +3620,7 @@ export function initialize({ | ||||||
|         if (options?.downloadOffset) { |         if (options?.downloadOffset) { | ||||||
|           targetHeaders.range = `bytes=${options.downloadOffset}-`; |           targetHeaders.range = `bytes=${options.downloadOffset}-`; | ||||||
|         } |         } | ||||||
|         streamWithDetails = await _outerAjax(`${cdnUrl}${cdnPath}`, { |         streamWithDetails = await _outerAjax(`${cdnUrl}${cdnPath.toString()}`, { | ||||||
|           headers: targetHeaders, |           headers: targetHeaders, | ||||||
|           certificateAuthority, |           certificateAuthority, | ||||||
|           disableRetries: options?.disableRetries, |           disableRetries: options?.disableRetries, | ||||||
|  | @ -3689,7 +3682,7 @@ export function initialize({ | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       const timeoutStream = getTimeoutStream({ |       const timeoutStream = getTimeoutStream({ | ||||||
|         name: `getAttachment(${redactor(cdnPath)})`, |         name: `getAttachment(${redactor(cdnPath.toString())})`, | ||||||
|         timeout: GET_ATTACHMENT_CHUNK_TIMEOUT, |         timeout: GET_ATTACHMENT_CHUNK_TIMEOUT, | ||||||
|         abortController, |         abortController, | ||||||
|       }); |       }); | ||||||
|  | @ -3954,10 +3947,7 @@ export function initialize({ | ||||||
|       const endDayInSeconds = endDayInMs / durations.SECOND; |       const endDayInSeconds = endDayInMs / durations.SECOND; | ||||||
|       const response = (await _ajax({ |       const response = (await _ajax({ | ||||||
|         call: 'getGroupCredentials', |         call: 'getGroupCredentials', | ||||||
|         urlParameters: |         urlParameters: urlPath`?redemptionStartSeconds=${startDayInSeconds}&redemptionEndSeconds=${endDayInSeconds}&zkcCredential=true`, | ||||||
|           `?redemptionStartSeconds=${startDayInSeconds}&` + |  | ||||||
|           `redemptionEndSeconds=${endDayInSeconds}&` + |  | ||||||
|           'zkcCredential=true', |  | ||||||
|         httpType: 'GET', |         httpType: 'GET', | ||||||
|         responseType: 'json', |         responseType: 'json', | ||||||
|       })) as GetGroupCredentialsResultType; |       })) as GetGroupCredentialsResultType; | ||||||
|  | @ -4150,7 +4140,7 @@ export function initialize({ | ||||||
|         httpType: 'GET', |         httpType: 'GET', | ||||||
|         responseType: 'bytes', |         responseType: 'bytes', | ||||||
|         urlParameters: safeInviteLinkPassword |         urlParameters: safeInviteLinkPassword | ||||||
|           ? `${safeInviteLinkPassword}` |           ? urlPath`${safeInviteLinkPassword}` | ||||||
|           : undefined, |           : undefined, | ||||||
|         redactUrl: _createRedactor(safeInviteLinkPassword), |         redactUrl: _createRedactor(safeInviteLinkPassword), | ||||||
|       }); |       }); | ||||||
|  | @ -4182,7 +4172,7 @@ export function initialize({ | ||||||
|         httpType: 'PATCH', |         httpType: 'PATCH', | ||||||
|         responseType: 'bytes', |         responseType: 'bytes', | ||||||
|         urlParameters: safeInviteLinkPassword |         urlParameters: safeInviteLinkPassword | ||||||
|           ? `?inviteLinkPassword=${safeInviteLinkPassword}` |           ? urlPath`?inviteLinkPassword=${safeInviteLinkPassword}` | ||||||
|           : undefined, |           : undefined, | ||||||
|         redactUrl: safeInviteLinkPassword |         redactUrl: safeInviteLinkPassword | ||||||
|           ? _createRedactor(safeInviteLinkPassword) |           ? _createRedactor(safeInviteLinkPassword) | ||||||
|  | @ -4243,11 +4233,7 @@ export function initialize({ | ||||||
|         headers: { |         headers: { | ||||||
|           'Cached-Send-Endorsements': String(cachedEndorsementsExpiration ?? 0), |           'Cached-Send-Endorsements': String(cachedEndorsementsExpiration ?? 0), | ||||||
|         }, |         }, | ||||||
|         urlParameters: |         urlParameters: urlPath`/${startVersion}?includeFirstState=${Boolean(includeFirstState)}&includeLastState=${Boolean(includeLastState)}&maxSupportedChangeEpoch=${Number(maxSupportedChangeEpoch)}`, | ||||||
|           `/${startVersion}?` + |  | ||||||
|           `includeFirstState=${Boolean(includeFirstState)}&` + |  | ||||||
|           `includeLastState=${Boolean(includeLastState)}&` + |  | ||||||
|           `maxSupportedChangeEpoch=${Number(maxSupportedChangeEpoch)}`, |  | ||||||
|       }); |       }); | ||||||
|       const { data, response } = withDetails; |       const { data, response } = withDetails; | ||||||
|       const changes = Proto.GroupChanges.decode(data); |       const changes = Proto.GroupChanges.decode(data); | ||||||
|  | @ -4292,7 +4278,7 @@ export function initialize({ | ||||||
|       const data = await _ajax({ |       const data = await _ajax({ | ||||||
|         call: 'subscriptions', |         call: 'subscriptions', | ||||||
|         httpType: 'GET', |         httpType: 'GET', | ||||||
|         urlParameters: `/${formattedId}`, |         urlParameters: urlPath`/${formattedId}`, | ||||||
|         responseType: 'json', |         responseType: 'json', | ||||||
|         unauthenticated: true, |         unauthenticated: true, | ||||||
|         accessKey: undefined, |         accessKey: undefined, | ||||||
|  |  | ||||||
|  | @ -1,6 +1,8 @@ | ||||||
| // Copyright 2021 Signal Messenger, LLC
 | // Copyright 2021 Signal Messenger, LLC
 | ||||||
| // SPDX-License-Identifier: AGPL-3.0-only
 | // SPDX-License-Identifier: AGPL-3.0-only
 | ||||||
| 
 | 
 | ||||||
|  | import { strictAssert } from './assert'; | ||||||
|  | 
 | ||||||
| export function maybeParseUrl(value: string): undefined | URL { | export function maybeParseUrl(value: string): undefined | URL { | ||||||
|   if (typeof value === 'string') { |   if (typeof value === 'string') { | ||||||
|     try { |     try { | ||||||
|  | @ -32,8 +34,64 @@ function cloneUrl(url: Readonly<URL>): URL { | ||||||
|   return new URL(url.href); |   return new URL(url.href); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function urlPathFromComponents( | class UrlPath { | ||||||
|   components: ReadonlyArray<string> |   #urlPath: string; | ||||||
| ): string { | 
 | ||||||
|   return `/${components.filter(Boolean).map(encodeURIComponent).join('/')}`; |   constructor(escapedUrlPath: string) { | ||||||
|  |     this.#urlPath = escapedUrlPath; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   toString(): string { | ||||||
|  |     return this.#urlPath; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export type { UrlPath }; | ||||||
|  | 
 | ||||||
|  | export type UrlPathInput = boolean | number | string | UrlPath; | ||||||
|  | 
 | ||||||
|  | export function isUrlPath(value: unknown): value is UrlPath { | ||||||
|  |   return value instanceof UrlPath; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function escapeValueForUrlPath(value: UrlPathInput): string { | ||||||
|  |   if (typeof value === 'boolean' || typeof value === 'number') { | ||||||
|  |     return String(value); | ||||||
|  |   } | ||||||
|  |   if (typeof value === 'string') { | ||||||
|  |     return encodeURIComponent(value); | ||||||
|  |   } | ||||||
|  |   if (isUrlPath(value)) { | ||||||
|  |     return value.toString(); | ||||||
|  |   } | ||||||
|  |   throw new TypeError('Unexpected url path component'); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function urlPath( | ||||||
|  |   strings: TemplateStringsArray, | ||||||
|  |   ...components: ReadonlyArray<UrlPathInput> | ||||||
|  | ): UrlPath { | ||||||
|  |   let result = ''; | ||||||
|  |   for (let index = 0; index < strings.length; index += 1) { | ||||||
|  |     result += strings[index]; | ||||||
|  |     if (index < components.length) { | ||||||
|  |       result += escapeValueForUrlPath(components[index]); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   return new UrlPath(result); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function urlPathJoin( | ||||||
|  |   values: ReadonlyArray<UrlPathInput>, | ||||||
|  |   separator: string | ||||||
|  | ): UrlPath { | ||||||
|  |   strictAssert(isUrlPath(separator), 'Separator must be an EscapedUrlPath'); | ||||||
|  |   let result = ''; | ||||||
|  |   for (let index = 0; index < values.length; index += 1) { | ||||||
|  |     result += escapeValueForUrlPath(values[index]); | ||||||
|  |     if (index < values.length - 1) { | ||||||
|  |       result += separator; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   return new UrlPath(result); | ||||||
| } | } | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Jamie Kyle
				Jamie Kyle