app.ts 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. import express from 'express'
  2. import path from 'path'
  3. import cors from 'cors'
  4. import { Express, NextFunction } from 'express-serve-static-core'
  5. import * as OpenApiValidator from 'express-openapi-validator'
  6. import { HttpError, OpenAPIV3 } from 'express-openapi-validator/dist/framework/types'
  7. import { KeyringPair } from '@polkadot/keyring/types'
  8. import { ApiPromise } from '@polkadot/api'
  9. import { RequestData, verifyTokenSignature, parseUploadToken, UploadToken } from '../helpers/auth'
  10. import { checkRemoveNonce } from '../../services/helpers/tokenNonceKeeper'
  11. import { httpLogger, errorLogger } from '../../services/logger'
  12. /**
  13. * Creates Express web application. Uses the OAS spec file for the API.
  14. *
  15. * @param api - runtime API promise
  16. * @param account - KeyringPair instance
  17. * @param workerId - storage provider ID (worker ID)
  18. * @param uploadsDir - directory for the file uploading
  19. * @param maxFileSize - max allowed file size
  20. * @returns Express promise.
  21. */
  22. export async function createApp(
  23. api: ApiPromise,
  24. account: KeyringPair,
  25. workerId: number,
  26. uploadsDir: string,
  27. maxFileSize: number
  28. ): Promise<Express> {
  29. const spec = path.join(__dirname, './../../api-spec/openapi.yaml')
  30. const app = express()
  31. app.use(cors())
  32. app.use(express.json())
  33. app.use(httpLogger())
  34. app.use(
  35. // Set parameters for each request.
  36. (req: express.Request, res: express.Response, next: NextFunction) => {
  37. res.locals.uploadsDir = uploadsDir
  38. res.locals.storageProviderAccount = account
  39. res.locals.workerId = workerId
  40. res.locals.api = api
  41. next()
  42. },
  43. // Setup OpenAPiValidator
  44. OpenApiValidator.middleware({
  45. apiSpec: spec,
  46. validateApiSpec: true,
  47. validateResponses: true,
  48. validateRequests: true,
  49. operationHandlers: {
  50. basePath: path.join(__dirname, './controllers'),
  51. resolver: OpenApiValidator.resolvers.modulePathResolver,
  52. },
  53. fileUploader: {
  54. dest: uploadsDir,
  55. // Busboy library settings
  56. limits: {
  57. // For multipart forms, the max number of file fields (Default: Infinity)
  58. files: 1,
  59. // For multipart forms, the max file size (in bytes) (Default: Infinity)
  60. fileSize: maxFileSize,
  61. },
  62. },
  63. validateSecurity: {
  64. handlers: {
  65. UploadAuth: validateUpload(api, account),
  66. },
  67. },
  68. })
  69. ) // Required signature.
  70. app.use(errorLogger())
  71. /* eslint-disable @typescript-eslint/no-unused-vars */
  72. app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
  73. // Express error handling recommendation:
  74. // https://expressjs.com/en/guide/error-handling.html
  75. if (res.headersSent) {
  76. return next(err)
  77. }
  78. // Request error handler.
  79. if (err instanceof HttpError) {
  80. res.status(err.status || 500).json({
  81. type: 'request_exception',
  82. message: err.message,
  83. })
  84. } else {
  85. res.status(500).json({
  86. type: 'unknown_error',
  87. message: err.message,
  88. })
  89. }
  90. next()
  91. })
  92. return app
  93. }
  94. // Defines a signature for a upload validation function.
  95. type ValidateUploadFunction = (
  96. req: express.Request,
  97. scopes: string[],
  98. schema: OpenAPIV3.SecuritySchemeObject
  99. ) => boolean | Promise<boolean>
  100. /**
  101. * Creates upload validation function with captured parameters from the request.
  102. *
  103. * @param api - runtime API promise
  104. * @param account - KeyringPair instance
  105. * @returns ValidateUploadFunction.
  106. */
  107. function validateUpload(api: ApiPromise, account: KeyringPair): ValidateUploadFunction {
  108. // We don't use these variables yet.
  109. /* eslint-disable @typescript-eslint/no-unused-vars */
  110. return (req: express.Request, scopes: string[], schema: OpenAPIV3.SecuritySchemeObject) => {
  111. const tokenString = req.headers['x-api-key'] as string
  112. const token = parseUploadToken(tokenString)
  113. const sourceTokenRequest: RequestData = {
  114. dataObjectId: parseInt(req.body.dataObjectId),
  115. storageBucketId: parseInt(req.body.storageBucketId),
  116. bagId: req.body.bagId,
  117. }
  118. verifyUploadTokenData(account.address, token, sourceTokenRequest)
  119. return true
  120. }
  121. }
  122. /**
  123. * Verifies upload request token. Throws exceptions on errors.
  124. *
  125. * @param accountAddress - account address (public key)
  126. * @param token - token object
  127. * @param request - data from the request to validate token
  128. */
  129. function verifyUploadTokenData(accountAddress: string, token: UploadToken, request: RequestData): void {
  130. if (!verifyTokenSignature(token, accountAddress)) {
  131. throw new Error('Invalid signature')
  132. }
  133. if (token.data.dataObjectId !== request.dataObjectId) {
  134. throw new Error('Unexpected dataObjectId')
  135. }
  136. if (token.data.storageBucketId !== request.storageBucketId) {
  137. throw new Error('Unexpected storageBucketId')
  138. }
  139. if (token.data.bagId !== request.bagId) {
  140. throw new Error('Unexpected bagId')
  141. }
  142. if (token.data.validUntil < Date.now()) {
  143. throw new Error('Token expired')
  144. }
  145. if (!checkRemoveNonce(token.data.nonce)) {
  146. throw new Error('Nonce not found')
  147. }
  148. }