companion.js 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. const nock = require('nock')
  2. const request = require('supertest')
  3. const mockOauthState = require('../mockoauthstate')
  4. const { version } = require('../../package.json')
  5. const { nockGoogleDownloadFile } = require('../fixtures/drive')
  6. const defaults = require('../fixtures/constants')
  7. jest.mock('tus-js-client')
  8. jest.mock('../../src/server/helpers/oauth-state', () => ({
  9. ...jest.requireActual('../../src/server/helpers/oauth-state'),
  10. ...mockOauthState(),
  11. }))
  12. const fakeLocalhost = 'localhost.com'
  13. jest.mock('node:dns', () => {
  14. const actual = jest.requireActual('node:dns')
  15. return {
  16. ...actual,
  17. lookup: (hostname, options, callback) => {
  18. if (fakeLocalhost === hostname || hostname === 'localhost') {
  19. return callback(null, '127.0.0.1', 4)
  20. }
  21. return callback(new Error(`Unexpected call to hostname ${hostname}`))
  22. },
  23. }
  24. })
  25. const tokenService = require('../../src/server/helpers/jwt')
  26. const { getServer } = require('../mockserver')
  27. // todo don't share server between tests. rewrite to not use env variables
  28. const authServer = getServer({ COMPANION_CLIENT_SOCKET_CONNECT_TIMEOUT: '0' })
  29. const authData = {
  30. dropbox: { accessToken: 'token value' },
  31. box: { accessToken: 'token value' },
  32. drive: { accessToken: 'token value' },
  33. }
  34. const token = tokenService.generateEncryptedAuthToken(authData, process.env.COMPANION_SECRET)
  35. const OAUTH_STATE = 'some-cool-nice-encrytpion'
  36. afterAll(() => {
  37. nock.cleanAll()
  38. nock.restore()
  39. })
  40. describe('validate upload data', () => {
  41. test('access token expired or invalid when starting provider download', () => {
  42. const meta = {
  43. size: null,
  44. mimeType: 'video/mp4',
  45. id: defaults.ITEM_ID,
  46. }
  47. nock('https://www.googleapis.com').get(`/drive/v3/files/${defaults.ITEM_ID}`).query(() => true).reply(200, meta)
  48. nock('https://www.googleapis.com').get(`/drive/v3/files/${defaults.ITEM_ID}?alt=media&supportsAllDrives=true`).reply(401, {
  49. "error": {
  50. "code": 401,
  51. "message": "Request had invalid authentication credentials. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.",
  52. "status": "UNAUTHENTICATED"
  53. }
  54. })
  55. return request(authServer)
  56. .post('/drive/get/DUMMY-FILE-ID')
  57. .set('uppy-auth-token', token)
  58. .set('Content-Type', 'application/json')
  59. .send({
  60. endpoint: 'http://url.myendpoint.com/files',
  61. protocol: 'tus',
  62. httpMethod: 'POST',
  63. })
  64. .expect(401)
  65. .then((res) => expect(res.body.message).toBe('HTTP 401: invalid access token detected by Provider'))
  66. })
  67. test('invalid upload protocol gets rejected', () => {
  68. nockGoogleDownloadFile()
  69. return request(authServer)
  70. .post('/drive/get/DUMMY-FILE-ID')
  71. .set('uppy-auth-token', token)
  72. .set('Content-Type', 'application/json')
  73. .send({
  74. endpoint: 'http://url.myendpoint.com/files',
  75. protocol: 'tusInvalid',
  76. })
  77. .expect(400)
  78. .then((res) => expect(res.body.message).toBe('unsupported protocol specified'))
  79. })
  80. test('invalid upload fieldname gets rejected', () => {
  81. nockGoogleDownloadFile()
  82. return request(authServer)
  83. .post('/drive/get/DUMMY-FILE-ID')
  84. .set('uppy-auth-token', token)
  85. .set('Content-Type', 'application/json')
  86. .send({
  87. endpoint: 'http://url.myendpoint.com/files',
  88. protocol: 'tus',
  89. fieldname: 390,
  90. })
  91. .expect(400)
  92. .then((res) => expect(res.body.message).toBe('fieldname must be a string'))
  93. })
  94. test('invalid upload metadata gets rejected', () => {
  95. nockGoogleDownloadFile()
  96. return request(authServer)
  97. .post('/drive/get/DUMMY-FILE-ID')
  98. .set('uppy-auth-token', token)
  99. .set('Content-Type', 'application/json')
  100. .send({
  101. endpoint: 'http://url.myendpoint.com/files',
  102. protocol: 'tus',
  103. metadata: 'I am a string instead of object',
  104. })
  105. .expect(400)
  106. .then((res) => expect(res.body.message).toBe('metadata must be an object'))
  107. })
  108. test('invalid upload headers get rejected', () => {
  109. nockGoogleDownloadFile()
  110. return request(authServer)
  111. .post('/drive/get/DUMMY-FILE-ID')
  112. .set('uppy-auth-token', token)
  113. .set('Content-Type', 'application/json')
  114. .send({
  115. endpoint: 'http://url.myendpoint.com/files',
  116. protocol: 'tus',
  117. headers: 'I am a string instead of object',
  118. })
  119. .expect(400)
  120. .then((res) => expect(res.body.message).toBe('headers must be an object'))
  121. })
  122. test('invalid upload HTTP Method gets rejected', () => {
  123. nockGoogleDownloadFile()
  124. return request(authServer)
  125. .post('/drive/get/DUMMY-FILE-ID')
  126. .set('uppy-auth-token', token)
  127. .set('Content-Type', 'application/json')
  128. .send({
  129. endpoint: 'http://url.myendpoint.com/files',
  130. protocol: 'tus',
  131. httpMethod: 'DELETE',
  132. })
  133. .expect(400)
  134. .then((res) => expect(res.body.message).toBe('unsupported HTTP METHOD specified'))
  135. })
  136. test('valid upload data is allowed - tus', () => {
  137. nockGoogleDownloadFile()
  138. return request(authServer)
  139. .post('/drive/get/DUMMY-FILE-ID')
  140. .set('uppy-auth-token', token)
  141. .set('Content-Type', 'application/json')
  142. .send({
  143. endpoint: 'http://url.myendpoint.com/files',
  144. protocol: 'tus',
  145. httpMethod: 'POST',
  146. headers: {
  147. customheader: 'header value',
  148. },
  149. metadata: {
  150. mymetadata: 'matadata value',
  151. },
  152. fieldname: 'uploadField',
  153. })
  154. .expect(200)
  155. })
  156. test('valid upload data is allowed - s3-multipart', () => {
  157. nockGoogleDownloadFile()
  158. return request(authServer)
  159. .post('/drive/get/DUMMY-FILE-ID')
  160. .set('uppy-auth-token', token)
  161. .set('Content-Type', 'application/json')
  162. .send({
  163. endpoint: 'http://url.myendpoint.com/files',
  164. protocol: 's3-multipart',
  165. httpMethod: 'PUT',
  166. headers: {
  167. customheader: 'header value',
  168. },
  169. metadata: {
  170. mymetadata: 'matadata value',
  171. },
  172. fieldname: 'uploadField',
  173. })
  174. .expect(200)
  175. })
  176. })
  177. describe('handle main oauth redirect', () => {
  178. const serverWithMainOauth = getServer({
  179. COMPANION_OAUTH_DOMAIN: 'localhost:3040',
  180. })
  181. test('redirect to a valid uppy instance', () => {
  182. return request(serverWithMainOauth)
  183. .get(`/dropbox/redirect?state=${OAUTH_STATE}`)
  184. .set('uppy-auth-token', token)
  185. .expect(302)
  186. .expect('Location', `http://localhost:3020/connect/dropbox/callback?state=${OAUTH_STATE}`)
  187. })
  188. test('do not redirect to invalid uppy instances', () => {
  189. const state = 'state-with-invalid-instance-url' // see mock ../../src/server/helpers/oauth-state above
  190. return request(serverWithMainOauth)
  191. .get(`/dropbox/redirect?state=${state}`)
  192. .set('uppy-auth-token', token)
  193. .expect(400)
  194. })
  195. })
  196. it('periodically pings', (done) => {
  197. nock('http://localhost').post('/ping', (body) => (
  198. body.some === 'value'
  199. && body.version === version
  200. && typeof body.processId === 'string'
  201. )).reply(200, () => done())
  202. getServer({
  203. COMPANION_PERIODIC_PING_URLS: 'http://localhost/ping',
  204. COMPANION_PERIODIC_PING_STATIC_JSON_PAYLOAD: '{"some": "value"}',
  205. COMPANION_PERIODIC_PING_INTERVAL: '10',
  206. COMPANION_PERIODIC_PING_COUNT: '1',
  207. })
  208. }, 3000)
  209. async function runUrlMetaTest (url) {
  210. const server = getServer()
  211. return request(server)
  212. .post('/url/meta')
  213. .send({ url })
  214. }
  215. async function runUrlGetTest (url) {
  216. const server = getServer()
  217. return request(server)
  218. .post('/url/get')
  219. .send({
  220. fileId: url,
  221. metadata: {},
  222. endpoint: 'http://url.myendpoint.com/files',
  223. protocol: 'tus',
  224. size: null,
  225. url,
  226. })
  227. }
  228. it('respects allowLocalUrls, localhost', async () => {
  229. let res = await runUrlMetaTest('http://localhost/')
  230. expect(res.statusCode).toBe(400)
  231. expect(res.body).toEqual({ error: 'Invalid request body' })
  232. res = await runUrlGetTest('http://localhost/')
  233. expect(res.statusCode).toBe(400)
  234. expect(res.body).toEqual({ error: 'Invalid request body' })
  235. })
  236. describe('respects allowLocalUrls, valid hostname that resolves to localhost', () => {
  237. test('meta', async () => {
  238. const res = await runUrlMetaTest(`http://${fakeLocalhost}/`)
  239. expect(res.statusCode).toBe(500)
  240. expect(res.body).toEqual({ message: 'failed to fetch URL metadata' })
  241. })
  242. test('get', async () => {
  243. const res = await runUrlGetTest(`http://${fakeLocalhost}/`)
  244. expect(res.statusCode).toBe(500)
  245. expect(res.body).toEqual({ message: 'failed to fetch URL' })
  246. })
  247. })