diff --git a/client.js b/client.js index cf9c9085..d45981fa 100644 --- a/client.js +++ b/client.js @@ -1,5 +1,6 @@ import { Mongo } from 'meteor/mongo'; import { Meteor } from 'meteor/meteor'; +import { DDP } from 'meteor/ddp-client'; import { Cookies } from 'meteor/ostrio:cookies'; import { check, Match } from 'meteor/check'; import { UploadInstance } from './upload.js'; @@ -113,29 +114,20 @@ export class FilesCollection extends FilesCollectionCore { } const setTokenCookie = () => { - if ((!cookie.has('x_mtok') && Meteor.connection._lastSessionId) || (cookie.has('x_mtok') && (cookie.get('x_mtok') !== Meteor.connection._lastSessionId))) { - cookie.set('x_mtok', Meteor.connection._lastSessionId, { - path: '/' - }); - } - }; - - const unsetTokenCookie = () => { - if (cookie.has('x_mtok')) { - cookie.remove('x_mtok', '/'); + if (Meteor.connection._lastSessionId) { + cookie.set('x_mtok', Meteor.connection._lastSessionId, { path: '/' }); + if (Meteor.isCordova) { + cookie.send(); + } } }; if (typeof Accounts !== 'undefined' && Accounts !== null) { - Meteor.startup(() => { - setTokenCookie(); - }); - Accounts.onLogin(() => { - setTokenCookie(); - }); - Accounts.onLogout(() => { - unsetTokenCookie(); + DDP.onReconnect((conn) => { + conn.onReconnect = setTokenCookie; }); + Meteor.startup(setTokenCookie); + Accounts.onLogin(setTokenCookie); } check(this.onbeforeunloadMessage, Match.OneOf(String, Function)); diff --git a/docs/constructor.md b/docs/constructor.md index 6547c08f..c194695c 100644 --- a/docs/constructor.md +++ b/docs/constructor.md @@ -726,7 +726,24 @@ Use for security reasons when only upload from Client to Server usage is needed, and files shouldn't be downloaded by any user. - + + + config.allowedOrigins {Regex|Boolean} + + + Server + + + Regex of Origins that are allowed CORS access or `false` to disable completely. + + + /^http:\/\/localhost:12\d\d\d$/ + + + Defaults to `localhost:12000`-`localhost:13000` for allowing Meteor-Cordova builds access. + + + config._preCollection {Mongo.Collection} diff --git a/package.js b/package.js index e71e0903..f7c9e1fd 100755 --- a/package.js +++ b/package.js @@ -16,7 +16,7 @@ Npm.depends({ Package.onUse(function(api) { api.versionsFrom('1.6.1'); api.use('webapp', 'server'); - api.use(['reactive-var', 'tracker', 'http'], 'client'); + api.use(['reactive-var', 'tracker', 'http', 'ddp-client'], 'client'); api.use(['mongo', 'check', 'random', 'ecmascript', 'ostrio:cookies@2.3.0'], ['client', 'server']); api.addAssets('worker.min.js', 'client'); api.mainModule('server.js', 'server'); diff --git a/server.js b/server.js index de00adee..8b218bad 100644 --- a/server.js +++ b/server.js @@ -58,7 +58,8 @@ const NOOP = () => { }; * @param config.downloadCallback {Function} - [Server] Callback triggered each time file is requested, return truthy value to continue download, or falsy to abort * @param config.interceptDownload {Function} - [Server] Intercept download request, so you can serve file from third-party resource, arguments {http: {request: {...}, response: {...}}, fileRef: {...}} * @param config.disableUpload {Boolean} - Disable file upload, useful for server only solutions - * @param config.disableDownload {Boolean} - Disable file download (serving), useful for file management only solutions + * @param config.disableDownload {Boolean} - Disable file download (serving), useful for file management only solutions + * @param config.allowedOrigins {Regex|Boolean} - [Server] Regex of Origins that are allowed CORS access or `false` to disable completely. Defaults to `localhost:12000`-`localhost:13000` for allowing Meteor-Cordova builds access. * @param config._preCollection {Mongo.Collection} - [Server] Mongo preCollection Instance * @param config._preCollectionName {String} - [Server] preCollection name * @summary Create new instance of FilesCollection @@ -90,6 +91,7 @@ export class FilesCollection extends FilesCollectionCore { namingFunction: this.namingFunction, responseHeaders: this.responseHeaders, disableDownload: this.disableDownload, + allowedOrigins: this.allowedOrigins, allowClientCode: this.allowClientCode, downloadCallback: this.downloadCallback, onInitiateUpload: this.onInitiateUpload, @@ -205,6 +207,10 @@ export class FilesCollection extends FilesCollectionCore { this.disableDownload = false; } + if (!this.allowedOrigins) { + this.allowedOrigins = /^http:\/\/localhost:12\d\d\d$/; + } + if (!helpers.isObject(this._currentUploads)) { this._currentUploads = {}; } @@ -435,6 +441,23 @@ export class FilesCollection extends FilesCollectionCore { return; } WebApp.connectHandlers.use((httpReq, httpResp, next) => { + if (this.allowedOrigins && !!~httpReq._parsedUrl.path.indexOf(`${this.downloadRoute}/`) && !httpResp.headersSent) { + if (this.allowedOrigins.test(httpReq.headers.origin)) { + httpResp.setHeader('Access-Control-Allow-Credentials', 'true'); + httpResp.setHeader('Access-Control-Allow-Origin', httpReq.headers.origin); + } + + if (httpReq.method === 'OPTIONS') { + httpResp.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + httpResp.setHeader('Access-Control-Allow-Headers', 'Range, Content-Type, x-mtok, x-start, x-chunkid, x-fileid, x-eof'); + httpResp.setHeader('Access-Control-Expose-Headers', 'Accept-Ranges, Content-Encoding, Content-Length, Content-Range'); + httpResp.setHeader('Allow', 'GET, POST, OPTIONS'); + httpResp.writeHead(200); + httpResp.end(); + return; + } + } + if (!this.disableUpload && !!~httpReq._parsedUrl.path.indexOf(`${this.downloadRoute}/${this.collectionName}/__upload`)) { if (httpReq.method === 'POST') { const handleError = (_error) => { diff --git a/upload.js b/upload.js index ff472b3e..1c92f8fb 100644 --- a/upload.js +++ b/upload.js @@ -377,6 +377,10 @@ export class UploadInstance extends EventEmitter { 'x-fileid': opts.fileId, 'x-chunkid': opts.chunkId, 'content-type': 'text/plain' + }, + beforeSend(xhr) { + xhr.withCredentials = true; + return true; } }, (error) => { this.transferTime += +new Date() - this.startTime[opts.chunkId]; @@ -423,6 +427,10 @@ export class UploadInstance extends EventEmitter { 'x-mtok': (helpers.isObject(Meteor.connection) ? Meteor.connection._lastSessionId : void 0) || null, 'x-fileId': opts.fileId, 'content-type': 'text/plain' + }, + beforeSend(xhr) { + xhr.withCredentials = true; + return true; } }, (error, _result) => { let result; @@ -607,6 +615,10 @@ export class UploadInstance extends EventEmitter { headers: { 'x-start': '1', 'x-mtok': (helpers.isObject(Meteor.connection) ? Meteor.connection._lastSessionId : void 0) || null + }, + beforeSend(xhr) { + xhr.withCredentials = true; + return true; } }, handleStart); }