@@ -278,34 +278,28 @@ class CustomUserData extends UserData {
278
278
}
279
279
280
280
/**
281
- * Suggested content types, however any value is allowed .
281
+ * Options when creating `MultipartBody` .
282
282
*/
283
- export type MultipartContentType = 'text/x-shellscript; charset="utf-8"' | 'text/cloud-boothook; charset="utf-8"' | string ;
284
-
285
- /**
286
- * Options when creating `MultipartUserDataPart`.
287
- */
288
- export interface MultipartUserDataPartOptions {
283
+ export interface MultipartBodyOptions {
289
284
290
285
/**
291
286
* `Content-Type` header of this part.
292
287
*
293
- * For Linux shell scripts use `text/x-shellscript`
288
+ * Some examples of content types:
289
+ * * `text/x-shellscript; charset="utf-8"` (shell script)
290
+ * * `text/cloud-boothook; charset="utf-8"` (shell script executed during boot phase)
291
+ *
292
+ * For Linux shell scripts use `text/x-shellscript`.
294
293
*/
295
- readonly contentType : MultipartContentType ;
294
+ readonly contentType : string ;
296
295
297
296
/**
298
297
* `Content-Transfer-Encoding` header specifying part encoding.
299
298
*
300
299
* @default undefined - don't add this header
301
300
*/
302
301
readonly transferEncoding ?: string ;
303
- }
304
302
305
- /**
306
- * Options when creating `MultipartUserDataPart`.
307
- */
308
- export interface MultipartUserDataPartOptionsWithBody extends MultipartUserDataPartOptions {
309
303
/**
310
304
* The body of message.
311
305
*
@@ -314,66 +308,32 @@ export interface MultipartUserDataPartOptionsWithBody extends MultipartUserDataP
314
308
readonly body ?: string ,
315
309
}
316
310
317
- /**
318
- * Options when creating `MultipartUserDataPartWrapper`.
319
- */
320
- export interface MultipartUserDataPartWrapperOptions {
321
- /**
322
- * `Content-Type` header of this part.
323
- *
324
- * For Linux shell scripts typically it's `text/x-shellscript`.
325
- *
326
- * @default 'text/x-shellscript; charset="utf-8"'
327
- */
328
- readonly contentType ?: MultipartContentType ;
329
- }
330
-
331
- /**
332
- * Interface representing part of `MultipartUserData` user data.
333
- */
334
- export interface IMultipart {
335
- /**
336
- * The body of this MIME part.
337
- */
338
- readonly body : string | undefined ;
339
-
340
- /**
341
- * `Content-Type` header of this part.
342
- */
343
- readonly contentType : string ;
344
-
345
- /**
346
- * `Content-Transfer-Encoding` header specifying part encoding.
347
- *
348
- * @default undefined - don't add this header
349
- */
350
- readonly transferEncoding ?: string ;
351
- }
352
-
353
311
/**
354
312
* The base class for all classes which can be used as {@link MultipartUserData}.
355
313
*/
356
- export abstract class MultipartUserDataPart implements IMultipart {
314
+ export abstract class MultipartBody {
357
315
358
316
/**
359
- * Constructs the new `MultipartUserDataPart ` wrapping existing `UserData`. Modification to `UserData` are reflected
317
+ * Constructs the new `MultipartBody ` wrapping existing `UserData`. Modification to `UserData` are reflected
360
318
* in subsequent renders of the part.
361
319
*
362
- * For more information about content types see `MultipartUserDataPartOptionsWithBody`
320
+ * For more information about content types see {@link MultipartBodyOptions.contentType}.
321
+ *
322
+ * @param userData user data to wrap into body part
323
+ * @param contentType optional content type, if default one should not be used
363
324
*/
364
- public static fromUserData ( userData : UserData , opts ?: MultipartUserDataPartWrapperOptions ) : MultipartUserDataPart {
365
- opts = opts || { } ;
366
- return new MultipartUserDataPartWrapper ( userData , opts ) ;
325
+ public static fromUserData ( userData : UserData , contentType ?: string ) : MultipartBody {
326
+ return new MultipartBodyUserDataWrapper ( userData , contentType ) ;
367
327
}
368
328
369
329
/**
370
- * Constructs the raw `MultipartUserDataPart ` using specified body, content type and transfer encoding.
330
+ * Constructs the raw `MultipartBody ` using specified body, content type and transfer encoding.
371
331
*
372
332
* When transfer encoding is specified (typically as Base64), it's caller responsibility to convert body to
373
333
* Base64 either by wrapping with `Fn.base64` or by converting it by other converters.
374
334
*/
375
- public static fromRawBody ( opts : MultipartUserDataPartOptionsWithBody ) : MultipartUserDataPart {
376
- return new MultipartUserDataPartRaw ( opts ) ;
335
+ public static fromRawBody ( opts : MultipartBodyOptions ) : MultipartBody {
336
+ return new MultipartBodyRaw ( opts ) ;
377
337
}
378
338
379
339
protected static readonly DEFAULT_CONTENT_TYPE = 'text/x-shellscript; charset="utf-8"' ;
@@ -382,51 +342,76 @@ export abstract class MultipartUserDataPart implements IMultipart {
382
342
public abstract get body ( ) : string | undefined ;
383
343
384
344
/** `Content-Type` header of this part */
385
- public readonly contentType : string ;
345
+ public abstract get contentType ( ) : string ;
386
346
387
347
/**
388
348
* `Content-Transfer-Encoding` header specifying part encoding.
389
349
*
390
350
* @default undefined - don't add this header
391
351
*/
392
- public readonly transferEncoding ? : string ;
352
+ public abstract get transferEncoding ( ) : string | undefined ;
393
353
394
- public constructor ( props : MultipartUserDataPartOptions ) {
395
- this . contentType = props . contentType ;
396
- this . transferEncoding = props . transferEncoding ;
354
+ public constructor ( ) {
355
+ }
356
+
357
+ /**
358
+ * Render body part as the string.
359
+ *
360
+ * Subclasses should not add leading nor trailing new line characters (\r \n)
361
+ */
362
+ public renderBodyPart ( ) : string {
363
+ const result : string [ ] = [ ] ;
364
+
365
+ result . push ( `Content-Type: ${ this . contentType } ` ) ;
366
+
367
+ if ( this . transferEncoding != null ) {
368
+ result . push ( `Content-Transfer-Encoding: ${ this . transferEncoding } ` ) ;
369
+ }
370
+ // One line free after separator
371
+ result . push ( '' ) ;
372
+
373
+ if ( this . body != null ) {
374
+ result . push ( this . body ) ;
375
+ // The new line added after join will be consumed by encapsulating or closing boundary
376
+ }
377
+
378
+ return result . join ( '\n' ) ;
397
379
}
398
380
}
399
381
400
382
/**
401
383
* The raw part of multi-part user data, which can be added to {@link MultipartUserData}.
402
384
*/
403
- class MultipartUserDataPartRaw extends MultipartUserDataPart {
404
- private _body : string | undefined ;
385
+ class MultipartBodyRaw extends MultipartBody {
386
+ public readonly body : string | undefined ;
387
+ public readonly contentType : string ;
388
+ public readonly transferEncoding : string | undefined ;
405
389
406
- public constructor ( props : MultipartUserDataPartOptionsWithBody ) {
407
- super ( props ) ;
408
- this . _body = props . body ;
409
- }
390
+ public constructor ( props : MultipartBodyOptions ) {
391
+ super ( ) ;
410
392
411
- public get body ( ) : string | undefined {
412
- return this . _body ;
393
+ this . body = props . body ;
394
+ this . contentType = props . contentType ;
413
395
}
414
396
}
415
397
416
398
/**
417
399
* Wrapper for `UserData`.
418
400
*/
419
- class MultipartUserDataPartWrapper extends MultipartUserDataPart {
420
- public constructor ( public readonly userData : UserData , opts : MultipartUserDataPartWrapperOptions ) {
421
- super ( {
422
- contentType : opts . contentType || MultipartUserDataPart . DEFAULT_CONTENT_TYPE ,
423
- // Force Base64 in case userData will contain UTF-8 characters
424
- transferEncoding : 'base64' ,
425
- } ) ;
401
+ class MultipartBodyUserDataWrapper extends MultipartBody {
402
+
403
+ public readonly contentType : string ;
404
+ public readonly transferEncoding : string | undefined ;
405
+
406
+ public constructor ( public readonly userData : UserData , contentType ?: string ) {
407
+ super ( ) ;
408
+
409
+ this . contentType = contentType || MultipartBody . DEFAULT_CONTENT_TYPE ;
410
+ this . transferEncoding = 'base64' ;
426
411
}
427
412
428
413
public get body ( ) : string {
429
- // Wrap rendered user data with Base64 function, in case data contains tokens
414
+ // Wrap rendered user data with Base64 function, in case data contains non ASCII characters
430
415
return Fn . base64 ( this . userData . render ( ) ) ;
431
416
}
432
417
}
@@ -438,9 +423,11 @@ export interface MultipartUserDataOptions {
438
423
/**
439
424
* The string used to separate parts in multipart user data archive (it's like MIME boundary).
440
425
*
441
- * This string should contain [a-zA-Z0-9] characters only, and should not be present in any part, or in text content of archive.
426
+ * This string should contain [a-zA-Z0-9()+,-./:=?] characters only, and should not be present in any part, or in text content of archive.
427
+ *
428
+ * @default `+AWS+CDK+User+Data+Separator==`
442
429
*/
443
- readonly partsSeparator : string ;
430
+ readonly partsSeparator ? : string ;
444
431
}
445
432
446
433
/**
@@ -452,63 +439,66 @@ export interface MultipartUserDataOptions {
452
439
*/
453
440
export class MultipartUserData extends UserData {
454
441
private static readonly USE_PART_ERROR = 'MultipartUserData does not support this operation. Please add part using addPart.' ;
442
+ private static readonly BOUNDRY_PATTERN = '[^a-zA-Z0-9()+,-./:=?]' ;
455
443
456
- private parts : IMultipart [ ] = [ ] ;
444
+ private parts : MultipartBody [ ] = [ ] ;
457
445
458
446
private opts : MultipartUserDataOptions ;
459
447
460
- constructor ( opts : MultipartUserDataOptions ) {
448
+ constructor ( opts ? : MultipartUserDataOptions ) {
461
449
super ( ) ;
462
450
451
+ let partsSeparator : string ;
452
+
453
+ // Validate separator
454
+ if ( opts ?. partsSeparator != null ) {
455
+ if ( new RegExp ( MultipartUserData . BOUNDRY_PATTERN ) . test ( opts ! . partsSeparator ) ) {
456
+ throw new Error ( `Invalid characters in separator. Separator has to match pattern ${ MultipartUserData . BOUNDRY_PATTERN } ` ) ;
457
+ } else {
458
+ partsSeparator = opts ! . partsSeparator ;
459
+ }
460
+ } else {
461
+ partsSeparator = '+AWS+CDK+User+Data+Separator==' ;
462
+ }
463
+
463
464
this . opts = {
464
- ... opts ,
465
+ partsSeparator : partsSeparator ,
465
466
} ;
466
467
}
467
- /**
468
- * Adds existing `UserData`. Modification to `UserData` are reflected in subsequent renders of the part.
469
- *
470
- * For more information about content types see `MultipartUserDataPartOptionsWithBody`
471
- */
472
- public addUserDataPart ( userData : UserData , opts ?: MultipartUserDataPartWrapperOptions ) : this {
473
- this . parts . push ( MultipartUserDataPart . fromUserData ( userData , opts ) ) ;
474
-
475
- return this ;
476
- }
477
468
478
469
/**
479
- * Adds the 'raw' part using provided options .
470
+ * Adds a part to the list of parts .
480
471
*/
481
- public addPart ( opts : MultipartUserDataPartOptionsWithBody ) : this {
482
- this . parts . push ( MultipartUserDataPart . fromRawBody ( opts ) ) ;
472
+ public addPart ( part : MultipartBody ) : this {
473
+ this . parts . push ( part ) ;
483
474
484
475
return this ;
485
476
}
486
477
487
478
public render ( ) : string {
488
479
const boundary = this . opts . partsSeparator ;
489
-
490
480
// Now build final MIME archive - there are few changes from MIME message which are accepted by cloud-init:
491
481
// - MIME RFC uses CRLF to separate lines - cloud-init is fine with LF \n only
492
- var resultArchive = `Content-Type: multipart/mixed; boundary="${ boundary } "\n` ;
493
- resultArchive = resultArchive + 'MIME-Version: 1.0\n' ;
482
+ // Note: new lines matters, matters a lot.
483
+ var resultArchive = new Array < string > ( ) ;
484
+ resultArchive . push ( `Content-Type: multipart/mixed; boundary="${ boundary } "` ) ;
485
+ resultArchive . push ( 'MIME-Version: 1.0' ) ;
486
+
487
+ // Add new line, the next one will be boundary (encapsulating or closing)
488
+ // so this line will count into it.
489
+ resultArchive . push ( '' ) ;
494
490
495
491
// Add parts - each part starts with boundary
496
492
this . parts . forEach ( part => {
497
- resultArchive = resultArchive + '\n--' + boundary + '\n' + 'Content-Type: ' + part . contentType + '\n' ;
498
-
499
- if ( part . transferEncoding != null ) {
500
- resultArchive = resultArchive + `Content-Transfer-Encoding: ${ part . transferEncoding } \n` ;
501
- }
502
-
503
- if ( part . body != null ) {
504
- resultArchive = resultArchive + '\n' + part . body ;
505
- }
493
+ resultArchive . push ( `--${ boundary } ` ) ;
494
+ resultArchive . push ( part . renderBodyPart ( ) ) ;
506
495
} ) ;
507
496
508
497
// Add closing boundary
509
- resultArchive = resultArchive + `\n--${ boundary } --\n` ;
498
+ resultArchive . push ( `--${ boundary } --` ) ;
499
+ resultArchive . push ( '' ) ; // Force new line at the end
510
500
511
- return resultArchive ;
501
+ return resultArchive . join ( '\n' ) ;
512
502
}
513
503
514
504
public addS3DownloadCommand ( _params : S3DownloadOptions ) : string {
0 commit comments