extracted server id validation

This commit is contained in:
Danny Coates 2018-02-05 16:36:44 -08:00
parent 807c44f057
commit aae61f9451
No known key found for this signature in database
GPG Key ID: 4C442633C62E00CB
15 changed files with 83 additions and 388 deletions

197
package-lock.json generated
View File

@ -445,9 +445,9 @@
}
},
"aws-sdk": {
"version": "2.188.0",
"resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.188.0.tgz",
"integrity": "sha1-kGKrx9umOTRZ+i80I89dKU8ARhE=",
"version": "2.189.0",
"resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.189.0.tgz",
"integrity": "sha1-TfZJuM+iAI/uCfNX/jqYNUZcTaQ=",
"requires": {
"buffer": "4.9.1",
"events": "1.1.1",
@ -3436,9 +3436,9 @@
}
},
"eslint": {
"version": "4.16.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-4.16.0.tgz",
"integrity": "sha512-YVXV4bDhNoHHcv0qzU4Meof7/P26B4EuaktMi5L1Tnt52Aov85KmYA8c5D+xyZr/BkhvwUqr011jDSD/QTULxg==",
"version": "4.17.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-4.17.0.tgz",
"integrity": "sha512-AyxBUCANU/o/xC0ijGMKavo5Ls3oK6xykiOITlMdjFjrKOsqLrA7Nf5cnrDgcKrHzBirclAZt63XO7YZlVUPwA==",
"dev": true,
"requires": {
"ajv": "5.5.0",
@ -3450,7 +3450,7 @@
"doctrine": "2.1.0",
"eslint-scope": "3.7.1",
"eslint-visitor-keys": "1.0.0",
"espree": "3.5.2",
"espree": "3.5.3",
"esquery": "1.0.0",
"esutils": "2.0.2",
"file-entry-cache": "2.0.0",
@ -3612,13 +3612,21 @@
"dev": true
},
"espree": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/espree/-/espree-3.5.2.tgz",
"integrity": "sha512-sadKeYwaR/aJ3stC2CdvgXu1T16TdYN+qwCpcWbMnGJ8s0zNWemzrvb2GbD4OhmJ/fwpJjudThAlLobGbWZbCQ==",
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/espree/-/espree-3.5.3.tgz",
"integrity": "sha512-Zy3tAJDORxQZLl2baguiRU1syPERAIg0L+JB2MWorORgTu/CplzvxS9WWA7Xh4+Q+eOQihNs/1o1Xep8cvCxWQ==",
"dev": true,
"requires": {
"acorn": "5.2.1",
"acorn": "5.4.1",
"acorn-jsx": "3.0.1"
},
"dependencies": {
"acorn": {
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-5.4.1.tgz",
"integrity": "sha512-XLmq3H/BVvW6/GbxKryGxWORz1ebilSsUDlyC27bXhWGWAZWkGwS6FLHjOlwFXNFoWFQEO/Df4u0YYd0K3BQgQ==",
"dev": true
}
}
},
"esprima": {
@ -5391,16 +5399,10 @@
"integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=",
"dev": true
},
"graceful-readlink": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz",
"integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=",
"dev": true
},
"growl": {
"version": "1.9.2",
"resolved": "https://registry.npmjs.org/growl/-/growl-1.9.2.tgz",
"integrity": "sha1-Dqd0NxXbjY3ixe3hd14bRayFwC8=",
"version": "1.10.3",
"resolved": "https://registry.npmjs.org/growl/-/growl-1.10.3.tgz",
"integrity": "sha512-hKlsbA5Vu3xsh1Cg3J7jSmX/WaW6A5oBeqzM88oNbCRQFz+zUaXm6yxS4RVytp1scBoJzSYl4YAEOQIt6O8V1Q==",
"dev": true
},
"handle-thing": {
@ -6965,40 +6967,6 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz",
"integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4="
},
"lodash._baseassign": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz",
"integrity": "sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=",
"dev": true,
"requires": {
"lodash._basecopy": "3.0.1",
"lodash.keys": "3.1.2"
}
},
"lodash._basecopy": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz",
"integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=",
"dev": true
},
"lodash._basecreate": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash._basecreate/-/lodash._basecreate-3.0.3.tgz",
"integrity": "sha1-G8ZhYU2qf8MRt9A78WgGoCE8+CE=",
"dev": true
},
"lodash._getnative": {
"version": "3.9.1",
"resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz",
"integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=",
"dev": true
},
"lodash._isiterateecall": {
"version": "3.0.9",
"resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz",
"integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=",
"dev": true
},
"lodash.camelcase": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
@ -7010,46 +6978,12 @@
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
"integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8="
},
"lodash.create": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/lodash.create/-/lodash.create-3.1.1.tgz",
"integrity": "sha1-1/KEnw29p+BGgruM1yqwIkYd6+c=",
"dev": true,
"requires": {
"lodash._baseassign": "3.2.0",
"lodash._basecreate": "3.0.3",
"lodash._isiterateecall": "3.0.9"
}
},
"lodash.get": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
"integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=",
"dev": true
},
"lodash.isarguments": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
"integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=",
"dev": true
},
"lodash.isarray": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz",
"integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=",
"dev": true
},
"lodash.keys": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz",
"integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=",
"dev": true,
"requires": {
"lodash._getnative": "3.9.1",
"lodash.isarguments": "3.1.0",
"lodash.isarray": "3.0.4"
}
},
"lodash.memoize": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
@ -7626,70 +7560,51 @@
}
},
"mocha": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/mocha/-/mocha-3.5.3.tgz",
"integrity": "sha512-/6na001MJWEtYxHOV1WLfsmR4YIynkUEhBwzsb+fk2qmQ3iqsi258l/Q2MWHJMImAcNpZ8DEdYAK72NHoIQ9Eg==",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/mocha/-/mocha-5.0.0.tgz",
"integrity": "sha512-ukB2dF+u4aeJjc6IGtPNnJXfeby5d4ZqySlIBT0OEyva/DrMjVm5HkQxKnHDLKEfEQBsEnwTg9HHhtPHJdTd8w==",
"dev": true,
"requires": {
"browser-stdout": "1.3.0",
"commander": "2.9.0",
"debug": "2.6.8",
"diff": "3.2.0",
"commander": "2.11.0",
"debug": "3.1.0",
"diff": "3.3.1",
"escape-string-regexp": "1.0.5",
"glob": "7.1.1",
"growl": "1.9.2",
"glob": "7.1.2",
"growl": "1.10.3",
"he": "1.1.1",
"json3": "3.3.2",
"lodash.create": "3.1.1",
"mkdirp": "0.5.1",
"supports-color": "3.1.2"
"supports-color": "4.4.0"
},
"dependencies": {
"commander": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz",
"integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=",
"dev": true,
"requires": {
"graceful-readlink": "1.0.1"
}
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz",
"integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==",
"dev": true
},
"debug": {
"version": "2.6.8",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz",
"integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=",
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"dev": true,
"requires": {
"ms": "2.0.0"
}
},
"glob": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.1.tgz",
"integrity": "sha1-gFIR3wT6rxxjo2ADBs31reULLsg=",
"dev": true,
"requires": {
"fs.realpath": "1.0.0",
"inflight": "1.0.6",
"inherits": "2.0.3",
"minimatch": "3.0.4",
"once": "1.4.0",
"path-is-absolute": "1.0.1"
}
},
"has-flag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz",
"integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=",
"diff": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/diff/-/diff-3.3.1.tgz",
"integrity": "sha512-MKPHZDMB0o6yHyDryUOScqZibp914ksXwAMYMTHj6KO8UeKsRYNJD3oNCKjTqZon+V488P7N/HzXF8t7ZR95ww==",
"dev": true
},
"supports-color": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.1.2.tgz",
"integrity": "sha1-cqJiiU2dQIuVbKBf83su2KbiotU=",
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.4.0.tgz",
"integrity": "sha512-rKC3+DyXWgK0ZLKwmRsrkyHVZAjNkfzeehuFWdGGcqGDTZFH73+RH6S/RDAAxl9GusSjZSUWYLmT9N5pzXFOXQ==",
"dev": true,
"requires": {
"has-flag": "1.0.0"
"has-flag": "2.0.0"
}
}
}
@ -9307,15 +9222,27 @@
}
},
"postcss-loader": {
"version": "2.0.10",
"resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-2.0.10.tgz",
"integrity": "sha512-xQaDcEgJ/2JqFY18zpFkik8vyYs7oS5ZRbrjvDqkP97k2wYWfPT4+qA0m4o3pTSCsz0u26PNqs8ZO9FRUWAqrA==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-2.1.0.tgz",
"integrity": "sha512-S/dKzpDwGFmP9g8eyCu9sUIV+/+3UooeTpYlsKf23qKDdrhHuA4pTSfytVu0rEJ0iDqUavXrgtOPq5KhNyNMOw==",
"dev": true,
"requires": {
"loader-utils": "1.1.0",
"postcss": "6.0.14",
"postcss-load-config": "1.2.0",
"schema-utils": "0.3.0"
"schema-utils": "0.4.3"
},
"dependencies": {
"schema-utils": {
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.3.tgz",
"integrity": "sha512-sgv/iF/T4/SewJkaVpldKC4WjSkz0JsOh2eKtxCPpCO1oR05+7MOF+H476HVRbLArkgA7j5TRJJ4p2jdFkUGQQ==",
"dev": true,
"requires": {
"ajv": "5.5.0",
"ajv-keywords": "2.1.1"
}
}
}
},
"postcss-media-query-parser": {

View File

@ -57,7 +57,7 @@
"css-loader": "^0.28.9",
"css-mqpacker": "^6.0.2",
"cssnano": "^3.10.0",
"eslint": "^4.16.0",
"eslint": "^4.17.0",
"eslint-plugin-mocha": "^4.11.0",
"eslint-plugin-node": "^5.2.1",
"eslint-plugin-security": "^1.4.0",
@ -70,10 +70,10 @@
"html-loader": "^0.5.5",
"husky": "^0.14.3",
"lint-staged": "^4.3.0",
"mocha": "^3.5.3",
"mocha": "^5.0.0",
"nanobus": "^4.3.2",
"npm-run-all": "^4.1.2",
"postcss-loader": "^2.0.10",
"postcss-loader": "^2.1.0",
"prettier": "^1.10.2",
"proxyquire": "^1.8.0",
"raven-js": "^3.22.1",
@ -94,7 +94,7 @@
"webpack-unassert-loader": "^1.2.0"
},
"dependencies": {
"aws-sdk": "^2.188.0",
"aws-sdk": "^2.189.0",
"body-parser": "^1.18.2",
"choo": "^6.7.0",
"cldr-core": "^32.0.0",

View File

@ -1,17 +1,8 @@
const storage = require('../storage');
function validateID(route_id) {
return route_id.match(/^[0-9a-fA-F]{10}$/) !== null;
}
module.exports = async function(req, res) {
const id = req.params.id;
if (!validateID(id)) {
res.sendStatus(404);
return;
}
const ownerToken = req.body.owner_token || req.body.delete_token;
if (!ownerToken) {

View File

@ -3,15 +3,8 @@ const mozlog = require('../log');
const log = mozlog('send.download');
const crypto = require('crypto');
function validateID(route_id) {
return route_id.match(/^[0-9a-fA-F]{10}$/) !== null;
}
module.exports = async function(req, res) {
const id = req.params.id;
if (!validateID(id)) {
return res.sendStatus(404);
}
try {
const auth = req.header('Authorization').split(' ')[1];

View File

@ -1,14 +1,7 @@
const storage = require('../storage');
function validateID(route_id) {
return route_id.match(/^[0-9a-fA-F]{10}$/) !== null;
}
module.exports = async (req, res) => {
const id = req.params.id;
if (!validateID(id)) {
return res.sendStatus(404);
}
try {
const meta = await storage.metadata(id);

View File

@ -5,6 +5,7 @@ const languages = require('../languages');
const storage = require('../storage');
const config = require('../config');
const pages = require('./pages');
const validation = require('../validation');
const { negotiateLanguages } = require('fluent-langneg');
const IS_DEV = config.env === 'development';
const acceptLanguages = /(([a-zA-Z]+(-[a-zA-Z0-9]+){0,2})|\*)(;q=[0-1](\.[0-9]+)?)?/g;
@ -81,6 +82,7 @@ module.exports = function(app) {
next();
});
app.use(bodyParser.json());
app.use(validation.middleware);
app.get('/', pages.index);
app.get('/legal', pages.legal);
app.get('/jsconfig.js', require('./jsconfig'));

View File

@ -1,14 +1,7 @@
const storage = require('../storage');
function validateID(route_id) {
return route_id.match(/^[0-9a-fA-F]{10}$/) !== null;
}
module.exports = async function(req, res) {
const id = req.params.id;
if (!validateID(id)) {
return res.sendStatus(404);
}
const ownerToken = req.body.owner_token;
if (!ownerToken) {
return res.sendStatus(400);

View File

@ -1,15 +1,8 @@
const storage = require('../storage');
const crypto = require('crypto');
function validateID(route_id) {
return route_id.match(/^[0-9a-fA-F]{10}$/) !== null;
}
module.exports = async function(req, res) {
const id = req.params.id;
if (!validateID(id)) {
return res.sendStatus(404);
}
try {
const auth = req.header('Authorization').split(' ')[1];

View File

@ -2,10 +2,6 @@ const routes = require('../../app/routes');
const storage = require('../storage');
const state = require('../state');
function validateID(route_id) {
return route_id.match(/^[0-9a-fA-F]{10}$/) !== null;
}
function stripEvents(str) {
// For CSP we need to remove all the event handler placeholders.
// It's ok, app.js will add them when it attaches to the DOM.
@ -23,9 +19,6 @@ module.exports = {
download: async function(req, res, next) {
const id = req.params.id;
if (!validateID(id)) {
return next();
}
try {
const { nonce, pwd } = await storage.metadata(id);

View File

@ -1,14 +1,7 @@
const storage = require('../storage');
function validateID(route_id) {
return route_id.match(/^[0-9a-fA-F]{10}$/) !== null;
}
module.exports = async function(req, res) {
const id = req.params.id;
if (!validateID(id)) {
return res.sendStatus(404);
}
const ownerToken = req.body.owner_token;
if (!ownerToken) {
return res.sendStatus(400);

View File

@ -1,14 +1,7 @@
const storage = require('../storage');
function validateID(route_id) {
return route_id.match(/^[0-9a-fA-F]{10}$/) !== null;
}
module.exports = async function(req, res) {
const id = req.params.id;
if (!validateID(id)) {
return res.sendStatus(404);
}
const ownerToken = req.body.owner_token;
if (!ownerToken) {
return res.sendStatus(404);

12
server/validation.js Normal file
View File

@ -0,0 +1,12 @@
function validateID(route_id) {
return route_id.match(/^[0-9a-fA-F]{10}$/) !== null;
}
module.exports = {
middleware: function(req, res, next) {
if (req.params.id && !validateID(req.params.id)) {
return res.sendStatus(404);
}
next();
}
};

View File

@ -1,190 +0,0 @@
const assert = require('assert');
const sinon = require('sinon');
const proxyquire = require('proxyquire');
const request = require('supertest');
const fs = require('fs');
const logStub = {};
logStub.info = sinon.stub();
logStub.error = sinon.stub();
const storage = proxyquire('../../server/storage', {
'./log.js': function() {
return logStub;
}
});
storage.flushall();
describe('Server integration tests', function() {
let server;
let storage;
let uuid;
let fileId;
before(function() {
const app = proxyquire('../../server/server', {
'./log.js': function() {
return logStub;
}
});
server = app.server;
storage = app.storage;
});
after(function() {
storage.flushall();
storage.quit();
server.close();
});
function upload() {
return request(server)
.post('/upload')
.field('fname', 'test_upload.txt')
.set(
'X-File-Metadata',
JSON.stringify({
id: '111111111111111111111111',
filename: 'test_upload.txt'
})
)
.attach('file', './test/test_upload.txt');
}
it('Responds with a 200 when the service is up', function() {
return request(server)
.get('/')
.expect(200);
});
it('Rejects with a 404 when a file id is not valid', function() {
return request(server)
.post('/upload/123')
.field('fname', 'test_upload.txt')
.set(
'X-File-Metadata',
JSON.stringify({
silly: 'text'
})
)
.attach('file', './test/test_upload.txt')
.expect(404);
});
it('Accepts a file and stores it when properly uploaded', function(done) {
upload().then(res => {
assert(res.body.hasOwnProperty('delete'));
uuid = res.body.delete;
assert(res.body.hasOwnProperty('url'));
assert(res.body.hasOwnProperty('id'));
fileId = res.body.id;
fs.access('./static/' + fileId, fs.constants.F_OK, err => {
if (err) {
done(new Error('The file does not exist'));
} else {
done();
}
});
});
});
it('Responds with a 200 if a file exists', function() {
return request(server)
.get('/exists/' + fileId)
.expect(200);
});
it('Exists in the redis server', function() {
return storage
.exists(fileId)
.then(() => assert(1))
.catch(err => assert.fail());
});
it('Fails delete if the delete token does not match', function() {
return request(server)
.post('/delete/' + fileId)
.send({ delete_token: 11 })
.expect(404);
});
it('Fails delete if the id is invalid', function() {
return request(server)
.post('/delete/1')
.expect(404);
});
it('Successfully deletes if the id is valid and the delete token matches', function(done) {
request(server)
.post('/delete/' + fileId)
.send({ delete_token: uuid })
.expect(200)
.then(() => {
fs.access('./static/' + fileId, fs.constants.F_OK, err => {
if (err) {
done();
} else {
done(new Error('The file does not exist'));
}
});
});
});
it('Responds with a 404 if a file does not exist', function() {
return request(server)
.get('/exists/notfound')
.expect(404);
});
it('Uploads properly after a delete', function(done) {
upload().then(res => {
assert(res.body.hasOwnProperty('delete'));
uuid = res.body.delete;
assert(res.body.hasOwnProperty('url'));
assert(res.body.hasOwnProperty('id'));
fileId = res.body.id;
fs.access('./static/' + fileId, fs.constants.F_OK, err => {
if (err) {
done(new Error('The file does not exist'));
} else {
done();
}
});
});
});
it('Responds with a 200 for the download page', function() {
return request(server)
.get('/download/' + fileId)
.expect(200);
});
it('Downloads a file properly', function() {
return request(server)
.get('/assets/download/' + fileId)
.then(res => {
assert(res.header.hasOwnProperty('content-disposition'));
assert(res.header.hasOwnProperty('content-type'));
assert(res.header.hasOwnProperty('content-length'));
assert(res.header.hasOwnProperty('x-file-metadata'));
assert.equal(
res.header['content-disposition'],
'attachment; filename=test_upload.txt'
);
assert.equal(res.header['content-type'], 'application/octet-stream');
});
});
it('The file is deleted after one download', function() {
assert(!fs.existsSync('./static/' + fileId));
});
it('No longer exists in the redis server', function() {
return storage
.exists(fileId)
.then(() => assert.fail())
.catch(err => assert(1));
});
});

View File

@ -45,6 +45,7 @@ const awsStub = {
const storage = proxyquire('../../server/storage', {
redis: redisStub,
'redis-mock': redisStub,
fs: fsStub,
'./log': function() {
return logStub;
@ -100,7 +101,7 @@ describe('Testing Get using aws', function() {
describe('Testing Set using aws', function() {
beforeEach(function() {
expire.reset();
expire.resetHistory();
});
after(function() {

View File

@ -32,6 +32,7 @@ logStub.error = sinon.stub();
const storage = proxyquire('../../server/storage', {
redis: redisStub,
'redis-mock': redisStub,
fs: fsStub,
'./log': function() {
return logStub;