app.ts 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  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. * @returns Express promise.
  20. */
  21. export async function createApp(
  22. api: ApiPromise,
  23. account: KeyringPair,
  24. workerId: number,
  25. uploadsDir: string
  26. ): Promise<Express> {
  27. const spec = path.join(__dirname, './../../api-spec/openapi.yaml')
  28. const app = express()
  29. app.use(cors())
  30. app.use(express.json())
  31. app.use(httpLogger())
  32. app.use(
  33. // Set parameters for each request.
  34. (req: express.Request, res: express.Response, next: NextFunction) => {
  35. res.locals.uploadsDir = uploadsDir
  36. res.locals.storageProviderAccount = account
  37. res.locals.workerId = workerId
  38. res.locals.api = api
  39. next()
  40. },
  41. // Setup OpenAPiValidator
  42. OpenApiValidator.middleware({
  43. apiSpec: spec,
  44. validateApiSpec: true,
  45. validateResponses: true,
  46. validateRequests: true,
  47. operationHandlers: {
  48. basePath: path.join(__dirname, './controllers'),
  49. resolver: OpenApiValidator.resolvers.modulePathResolver,
  50. },
  51. fileUploader: { dest: uploadsDir },
  52. validateSecurity: {
  53. handlers: {
  54. UploadAuth: validateUpload(api, account),
  55. },
  56. },
  57. })
  58. ) // Required signature.
  59. app.use(errorLogger())
  60. /* eslint-disable @typescript-eslint/no-unused-vars */
  61. app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
  62. // Request validation error handler.
  63. if (err instanceof HttpError) {
  64. res.status(err.status).json({
  65. type: 'request_validation',
  66. message: err.message,
  67. errors: err.errors,
  68. })
  69. } else {
  70. res.status(500).json({
  71. type: 'unknown_error',
  72. message: err.message,
  73. })
  74. }
  75. next()
  76. })
  77. return app
  78. }
  79. // Defines a signature for a upload validation function.
  80. type ValidateUploadFunction = (
  81. req: express.Request,
  82. scopes: string[],
  83. schema: OpenAPIV3.SecuritySchemeObject
  84. ) => boolean | Promise<boolean>
  85. /**
  86. * Creates upload validation function with captured parameters from the request.
  87. *
  88. * @param api - runtime API promise
  89. * @param account - KeyringPair instance
  90. * @returns ValidateUploadFunction.
  91. */
  92. function validateUpload(api: ApiPromise, account: KeyringPair): ValidateUploadFunction {
  93. // We don't use these variables yet.
  94. /* eslint-disable @typescript-eslint/no-unused-vars */
  95. return (req: express.Request, scopes: string[], schema: OpenAPIV3.SecuritySchemeObject) => {
  96. const tokenString = req.headers['x-api-key'] as string
  97. const token = parseUploadToken(tokenString)
  98. const sourceTokenRequest: RequestData = {
  99. dataObjectId: parseInt(req.body.dataObjectId),
  100. storageBucketId: parseInt(req.body.storageBucketId),
  101. bagId: req.body.bagId,
  102. }
  103. verifyUploadTokenData(account.address, token, sourceTokenRequest)
  104. return true
  105. }
  106. }
  107. /**
  108. * Verifies upload request token. Throws exceptions on errors.
  109. *
  110. * @param accountAddress - account address (public key)
  111. * @param token - token object
  112. * @param request - data from the request to validate token
  113. */
  114. function verifyUploadTokenData(accountAddress: string, token: UploadToken, request: RequestData): void {
  115. if (!verifyTokenSignature(token, accountAddress)) {
  116. throw new Error('Invalid signature')
  117. }
  118. if (token.data.dataObjectId !== request.dataObjectId) {
  119. throw new Error('Unexpected dataObjectId')
  120. }
  121. if (token.data.storageBucketId !== request.storageBucketId) {
  122. throw new Error('Unexpected storageBucketId')
  123. }
  124. if (token.data.bagId !== request.bagId) {
  125. throw new Error('Unexpected bagId')
  126. }
  127. if (token.data.validUntil < Date.now()) {
  128. throw new Error('Token expired')
  129. }
  130. if (!checkRemoveNonce(token.data.nonce)) {
  131. throw new Error('Nonce not found')
  132. }
  133. }