headers.ts 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. import type { Response } from 'express';
  2. import type { ExpressHttpHeader, IContentHeaders } from '~/server/interfaces/attachment';
  3. import type { IAttachmentDocument } from '~/server/models/attachment';
  4. import type { ConfigManager } from '~/server/service/config-manager';
  5. import type { ConfigKey } from '../../config-manager/config-definition';
  6. import { DEFAULT_ALLOWLIST_MIME_TYPES, SAFE_INLINE_CONFIGURABLE_MIME_TYPES } from './security';
  7. export class ContentHeaders implements IContentHeaders {
  8. contentType?: ExpressHttpHeader<'Content-Type'>;
  9. contentLength?: ExpressHttpHeader<'Content-Length'>;
  10. contentSecurityPolicy?: ExpressHttpHeader<'Content-Security-Policy'>;
  11. contentDisposition?: ExpressHttpHeader<'Content-Disposition'>;
  12. xContentTypeOptions?: ExpressHttpHeader<'X-Content-Type-Options'>;
  13. private configManager: ConfigManager;
  14. private constructor(configManager: ConfigManager) {
  15. this.configManager = configManager;
  16. }
  17. static async create(
  18. configManager: ConfigManager,
  19. attachment: IAttachmentDocument,
  20. opts?: {
  21. inline?: boolean,
  22. },
  23. ): Promise<ContentHeaders> {
  24. // Create instance, passing the configManager to the private constructor
  25. const instance = new ContentHeaders(configManager);
  26. const attachmentContentType = attachment.fileFormat;
  27. const filename = attachment.originalName;
  28. const actualContentTypeString: string = attachmentContentType || 'application/octet-stream';
  29. instance.contentType = {
  30. field: 'Content-Type',
  31. value: actualContentTypeString,
  32. };
  33. const requestedInline = opts?.inline ?? false;
  34. const configKey = `attachments:contentDisposition:${actualContentTypeString}:inline` as ConfigKey;
  35. // AWAIT the config value here
  36. const rawConfigValue = await instance.configManager.getConfig(configKey); // Use instance's configManager
  37. let isConfiguredInline: boolean;
  38. if (typeof rawConfigValue === 'boolean') {
  39. isConfiguredInline = rawConfigValue;
  40. }
  41. else {
  42. isConfiguredInline = DEFAULT_ALLOWLIST_MIME_TYPES.has(actualContentTypeString);
  43. }
  44. const shouldBeInline = requestedInline
  45. && isConfiguredInline
  46. && SAFE_INLINE_CONFIGURABLE_MIME_TYPES.has(actualContentTypeString);
  47. instance.contentDisposition = {
  48. field: 'Content-Disposition',
  49. value: shouldBeInline
  50. ? 'inline'
  51. : `attachment;filename*=UTF-8''${encodeURIComponent(filename)}`,
  52. };
  53. instance.contentSecurityPolicy = {
  54. field: 'Content-Security-Policy',
  55. value: `script-src 'unsafe-hashes';
  56. style-src 'self' 'unsafe-inline';
  57. object-src 'none';
  58. require-trusted-types-for 'script';
  59. media-src 'self';
  60. default-src 'none';`,
  61. };
  62. instance.xContentTypeOptions = {
  63. field: 'X-Content-Type-Options',
  64. value: 'nosniff',
  65. };
  66. if (attachment.fileSize) {
  67. instance.contentLength = {
  68. field: 'Content-Length',
  69. value: attachment.fileSize.toString(),
  70. };
  71. }
  72. return instance;
  73. }
  74. /**
  75. * Convert to ExpressHttpHeader[]
  76. */
  77. toExpressHttpHeaders(): ExpressHttpHeader[] {
  78. return [
  79. this.contentType,
  80. this.contentLength,
  81. this.contentSecurityPolicy,
  82. this.contentDisposition,
  83. this.xContentTypeOptions,
  84. ]
  85. // exclude undefined
  86. .filter((member): member is NonNullable<typeof member> => member != null);
  87. }
  88. }
  89. /**
  90. * Convert Record to ExpressHttpHeader[]
  91. */
  92. export const toExpressHttpHeaders = (records: Record<string, string | string[]>): ExpressHttpHeader[] => {
  93. return Object.entries(records).map(([field, value]) => { return { field, value } });
  94. };
  95. export const applyHeaders = (res: Response, headers: ExpressHttpHeader[]): void => {
  96. headers.forEach((header) => {
  97. res.header(header.field, header.value);
  98. });
  99. };