headers.ts 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  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 configKey = `attachments:contentDisposition:${actualContentTypeString}:inline` as ConfigKey;
  34. const rawConfigValue = await instance.configManager.getConfig(configKey);
  35. const requestedInline = opts?.inline ?? false;
  36. let systemAllowsInline: boolean;
  37. const ALL_POSSIBLE_INLINE_MIME_TYPES = new Set<string>([
  38. ...DEFAULT_ALLOWLIST_MIME_TYPES,
  39. ...SAFE_INLINE_CONFIGURABLE_MIME_TYPES,
  40. ]);
  41. if (!ALL_POSSIBLE_INLINE_MIME_TYPES.has(actualContentTypeString)) {
  42. systemAllowsInline = false;
  43. }
  44. else if (typeof rawConfigValue === 'boolean') {
  45. systemAllowsInline = rawConfigValue;
  46. }
  47. else {
  48. systemAllowsInline = DEFAULT_ALLOWLIST_MIME_TYPES.has(actualContentTypeString);
  49. }
  50. const shouldBeInline = requestedInline && systemAllowsInline;
  51. instance.contentDisposition = {
  52. field: 'Content-Disposition',
  53. value: shouldBeInline
  54. ? 'inline'
  55. : `attachment;filename*=UTF-8''${encodeURIComponent(filename)}`,
  56. };
  57. instance.contentSecurityPolicy = {
  58. field: 'Content-Security-Policy',
  59. value: `script-src 'unsafe-hashes';
  60. style-src 'self' 'unsafe-inline';
  61. object-src 'none';
  62. require-trusted-types-for 'script';
  63. media-src 'self';
  64. default-src 'none';`,
  65. };
  66. instance.xContentTypeOptions = {
  67. field: 'X-Content-Type-Options',
  68. value: 'nosniff',
  69. };
  70. if (attachment.fileSize) {
  71. instance.contentLength = {
  72. field: 'Content-Length',
  73. value: attachment.fileSize.toString(),
  74. };
  75. }
  76. return instance;
  77. }
  78. /**
  79. * Convert to ExpressHttpHeader[]
  80. */
  81. toExpressHttpHeaders(): ExpressHttpHeader[] {
  82. return [
  83. this.contentType,
  84. this.contentLength,
  85. this.contentSecurityPolicy,
  86. this.contentDisposition,
  87. this.xContentTypeOptions,
  88. ]
  89. // exclude undefined
  90. .filter((member): member is NonNullable<typeof member> => member != null);
  91. }
  92. }
  93. /**
  94. * Convert Record to ExpressHttpHeader[]
  95. */
  96. export const toExpressHttpHeaders = (records: Record<string, string | string[]>): ExpressHttpHeader[] => {
  97. return Object.entries(records).map(([field, value]) => { return { field, value } });
  98. };
  99. export const applyHeaders = (res: Response, headers: ExpressHttpHeader[]): void => {
  100. headers.forEach((header) => {
  101. res.header(header.field, header.value);
  102. });
  103. };