Skip to content

Commit

Permalink
Merge pull request #30 from trs/fix-utf8
Browse files Browse the repository at this point in the history
Fix utf8
  • Loading branch information
trs authored Jun 27, 2017

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
2 parents 5394908 + e0b11ff commit c8526be
Showing 18 changed files with 188 additions and 38 deletions.
14 changes: 9 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -192,25 +192,29 @@ __Used in:__ `CWD`, `CDUP`
Returns a path to a newly created directory
__Used in:__ `MKD`

#### [`write(fileName, {append = false})`](src/fs.js#L68)
#### [`write(fileName, {append, start})`](src/fs.js#L68)
Returns a writable stream
Options: `append` if true, append to existing file
Options:
`append` if true, append to existing file
`start` if set, specifies the byte offset to write to
__Used in:__ `STOR`, `APPE`

#### [`read(fileName)`](src/fs.js#L75)
#### [`read(fileName, {start})`](src/fs.js#L75)
Returns a readable stream
Options:
`start` if set, specifies the byte offset to read from
__Used in:__ `RETR`

#### [`delete(path)`](src/fs.js#L87)
Delete a file or directory
__Used in:__ `DELE`

#### [`rename(from, to)`](src/fs.js#L102)
Rename a file or directory
Renames a file or directory
__Used in:__ `RNFR`, `RNTO`

#### [`chmod(path)`](src/fs.js#L108)
Modify a file or directory's permissions
Modifies a file or directory's permissions
__Used in:__ `SITE CHMOD`

#### [`getUniqueName()`](src/fs.js#L113)
9 changes: 2 additions & 7 deletions config/release/commitMessageConfig.js
Original file line number Diff line number Diff line change
@@ -15,12 +15,7 @@ module.exports = {
{value: 'WIP', name: 'WIP: Work in progress'}
],

scopes: [
{name: 'accounts'},
{name: 'admin'},
{name: 'exampleScope'},
{name: 'changeMe'}
],
scopes: [],

// it needs to match the value for field type. Eg.: 'fix'
/*
@@ -39,5 +34,5 @@ module.exports = {
allowBreakingChanges: ['feat', 'fix'],

// Appends the branch name to the footer of the commit. Useful for tracking commits after branches have been merged
appendBranchNameToCommitMessage: true
appendBranchNameToCommitMessage: false
};
3 changes: 2 additions & 1 deletion src/commands/registration/auth.js
Original file line number Diff line number Diff line change
@@ -20,7 +20,8 @@ module.exports = {
};

function handleTLS() {
if (!this.server._tls) return this.reply(504);
if (!this.server._tls) return this.reply(502);
if (this.secure) return this.reply(202);

return this.reply(234)
.then(() => {
5 changes: 4 additions & 1 deletion src/commands/registration/feat.js
Original file line number Diff line number Diff line change
@@ -10,7 +10,10 @@ module.exports = {
if (feat) return _.concat(feats, feat);
return feats;
}, ['UTF8'])
.map(feat => ` ${feat}`);
.map(feat => ({
message: ` ${feat}`,
raw: true
}));
return features.length
? this.reply(211, 'Extensions supported', ...features, 'End')
: this.reply(211, 'No features');
30 changes: 28 additions & 2 deletions src/commands/registration/opts.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,34 @@
const _ = require('lodash');

const OPTIONS = {
UTF8: utf8,
'UTF-8': utf8
};

module.exports = {
directive: 'OPTS',
handler: function () {
return this.reply(501);
handler: function ({command} = {}) {
if (!_.has(command, 'arg')) return this.reply(501);

const [_option, ...args] = command.arg.split(' ');
const option = _.toUpper(_option);

if (!OPTIONS.hasOwnProperty(option)) return this.reply(500);
return OPTIONS[option].call(this, args);
},
syntax: '{{cmd}}',
description: 'Select options for a feature'
};

function utf8([setting] = []) {
switch (_.toUpper(setting)) {
case 'ON':
this.encoding = 'utf8';
return this.reply(200, 'UTF8 encoding on');
case 'OFF':
this.encoding = 'ascii';
return this.reply(200, 'UTF8 encoding off');
default:
return this.reply(501, 'Unknown setting for option');
}
}
3 changes: 2 additions & 1 deletion src/commands/registration/pbsz.js
Original file line number Diff line number Diff line change
@@ -8,6 +8,7 @@ module.exports = {
syntax: '{{cmd}}',
description: 'Protection Buffer Size',
flags: {
no_auth: true
no_auth: true,
feat: 'PBSZ'
}
};
3 changes: 2 additions & 1 deletion src/commands/registration/prot.js
Original file line number Diff line number Diff line change
@@ -17,6 +17,7 @@ module.exports = {
syntax: '{{cmd}}',
description: 'Data Channel Protection Level',
flags: {
no_auth: true
no_auth: true,
feat: 'PROT'
}
};
16 changes: 16 additions & 0 deletions src/commands/registration/rest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const _ = require('lodash');

module.exports = {
directive: 'REST',
handler: function ({command} = {}) {
const arg = _.get(command, 'arg');
const byteCount = parseInt(arg, 10);

if (isNaN(byteCount) || byteCount < 0) return this.reply(501, 'Byte count must be 0 or greater');

this.restByteCount = byteCount;
return this.reply(350, `Resarting next transfer at ${byteCount}`);
},
syntax: '{{cmd}} <byte-count>',
description: 'Restart transfer from the specified point. Resets after any STORE or RETRIEVE'
};
3 changes: 2 additions & 1 deletion src/commands/registration/retr.js
Original file line number Diff line number Diff line change
@@ -12,8 +12,9 @@ module.exports = {
this.commandSocket.pause();
dataSocket = socket;
})
.then(() => when.try(this.fs.read.bind(this.fs), command.arg))
.then(() => when.try(this.fs.read.bind(this.fs), command.arg, {start: this.restByteCount}))
.then(stream => {
this.restByteCount = 0;
return when.promise((resolve, reject) => {
dataSocket.on('error', err => stream.emit('error', err));

3 changes: 2 additions & 1 deletion src/commands/registration/stor.js
Original file line number Diff line number Diff line change
@@ -15,8 +15,9 @@ module.exports = {
this.commandSocket.pause();
dataSocket = socket;
})
.then(() => when.try(this.fs.write.bind(this.fs), fileName, {append}))
.then(() => when.try(this.fs.write.bind(this.fs), fileName, {append, start: this.restByteCount}))
.then(stream => {
this.restByteCount = 0;
return when.promise((resolve, reject) => {
stream.once('error', err => dataSocket.emit('error', err));
stream.once('finish', () => resolve(this.reply(226, fileName)));
1 change: 0 additions & 1 deletion src/commands/registration/type.js
Original file line number Diff line number Diff line change
@@ -2,7 +2,6 @@
module.exports = {
directive: 'TYPE',
handler: function ({command} = {}) {

if (/^A[0-9]?$/i.test(command.arg)) {
this.transferType = 'ascii';
} else if (/^L[0-9]?$/i.test(command.arg) || /^I$/i.test(command.arg)) {
1 change: 1 addition & 0 deletions src/commands/registry.js
Original file line number Diff line number Diff line change
@@ -21,6 +21,7 @@ const commands = [
require('./registration/port'),
require('./registration/pwd'),
require('./registration/quit'),
require('./registration/rest'),
require('./registration/retr'),
require('./registration/rmd'),
require('./registration/rnfr'),
23 changes: 20 additions & 3 deletions src/connection.js
Original file line number Diff line number Diff line change
@@ -16,7 +16,10 @@ class FtpConnection {
this.log = options.log.child({id: this.id, ip: this.ip});
this.commands = new Commands(this);
this.transferType = 'binary';
this.encoding = 'utf8';
this.bufferSize = false;
this._restByteCount = 0;
this._secure = false;

this.connector = new BaseConnector(this);

@@ -34,7 +37,7 @@ class FtpConnection {
}

_handleData(data) {
const messages = _.compact(data.toString('utf8').split('\r\n'));
const messages = _.compact(data.toString(this.encoding).split('\r\n'));
this.log.trace(messages);
return sequence(messages.map(message => this.commands.handle.bind(this.commands, message)));
}
@@ -47,6 +50,20 @@ class FtpConnection {
}
}

get restByteCount() {
return this._restByteCount > 0 ? this._restByteCount : undefined;
}
set restByteCount(rbc) {
this._restByteCount = rbc;
}

get secure() {
return this.server.isTLS || this._secure;
}
set secure(sec) {
this._secure = sec;
}

close(code = 421, message = 'Closing connection') {
return when
.resolve(code)
@@ -58,7 +75,7 @@ class FtpConnection {
return when.try(() => {
const loginListeners = this.server.listeners('login');
if (!loginListeners || !loginListeners.length) {
if (!this.server.options.anoymous) throw new errors.GeneralError('No "login" listener setup', 500);
if (!this.server.options.anonymous) throw new errors.GeneralError('No "login" listener setup', 500);
} else {
return this.server.emitPromise('login', {connection: this, username, password});
}
@@ -84,7 +101,7 @@ class FtpConnection {

if (!letter.socket) letter.socket = options.socket ? options.socket : this.commandSocket;
if (!letter.message) letter.message = DEFAULT_MESSAGE[options.code] || 'No information';
if (!letter.encoding) letter.encoding = 'utf8';
if (!letter.encoding) letter.encoding = this.encoding;
return when(letter.message) // allow passing in a promise as a message
.then(message => {
letter.message = message;
8 changes: 4 additions & 4 deletions src/connector/passive.js
Original file line number Diff line number Diff line change
@@ -41,7 +41,7 @@ class Passive extends Connector {
return this.connection.reply(550, 'Remote addresses do not match')
.finally(() => this.connection.close());
}
this.log.debug({port}, 'Passive connection fulfilled.');
this.log.trace({port, remoteAddress: socket.remoteAddress}, 'Passive connection fulfilled.');

if (this.connection.secure) {
const secureContext = tls.createSecureContext(this.server._tls);
@@ -57,7 +57,7 @@ class Passive extends Connector {
this.dataSocket.setEncoding(this.connection.transferType);
this.dataSocket.on('error', err => this.server.emit('client-error', {connection: this.connection, context: 'dataSocket', error: err}));
this.dataSocket.on('close', () => {
this.log.debug('Passive connection closed');
this.log.trace('Passive connection closed');
this.end();
});
};
@@ -67,15 +67,15 @@ class Passive extends Connector {
this.dataServer.maxConnections = 1;
this.dataServer.on('error', err => this.server.emit('client-error', {connection: this.connection, context: 'dataServer', error: err}));
this.dataServer.on('close', () => {
this.log.debug('Passive server closed');
this.log.trace('Passive server closed');
this.dataServer = null;
});

return when.promise((resolve, reject) => {
this.dataServer.listen(port, err => {
if (err) reject(err);
else {
this.log.info({port}, 'Passive connection listening');
this.log.debug({port}, 'Passive connection listening');
resolve(this.dataServer);
}
});
8 changes: 4 additions & 4 deletions src/fs.js
Original file line number Diff line number Diff line change
@@ -65,22 +65,22 @@ class FileSystem {
});
}

write(fileName, {append = false} = {}) {
write(fileName, {append = false, start = undefined} = {}) {
const {fsPath} = this._resolvePath(fileName);
const stream = syncFs.createWriteStream(fsPath, {flags: !append ? 'w+' : 'a+'});
const stream = syncFs.createWriteStream(fsPath, {flags: !append ? 'w+' : 'a+', start});
stream.once('error', () => fs.unlink(fsPath));
stream.once('close', () => stream.end());
return stream;
}

read(fileName) {
read(fileName, {start = undefined} = {}) {
const {fsPath} = this._resolvePath(fileName);
return fs.stat(fsPath)
.tap(stat => {
if (stat.isDirectory()) throw new errors.FileSystemError('Cannot read a directory');
})
.then(() => {
const stream = syncFs.createReadStream(fsPath, {flags: 'r'});
const stream = syncFs.createReadStream(fsPath, {flags: 'r', start});
return stream;
});
}
6 changes: 1 addition & 5 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -47,17 +47,13 @@ class FtpServer {

this.server = (this.isTLS ? tls : net).createServer(serverOptions, serverConnectionHandler);
this.server.on('error', err => this.log.error(err, '[Event] error'));
if (this.isTLS) {
this.server.on('tlsClientError', err => this.log.error(err, '[Event] tlsClientError'));
}
this.on = this.server.on.bind(this.server);
this.once = this.server.once.bind(this.server);
this.listeners = this.server.listeners.bind(this.server);

process.on('SIGTERM', () => this.close());
process.on('SIGINT', () => this.close());
process.on('SIGBREAK', () => this.close());
process.on('SIGHUP', () => this.close());
process.on('SIGQUIT', () => this.close());
}

get isTLS() {
32 changes: 31 additions & 1 deletion test/commands/registration/opts.spec.js
Original file line number Diff line number Diff line change
@@ -19,10 +19,40 @@ describe(CMD, function () {
sandbox.restore();
});

it('// successful', () => {
it('// unsuccessful', () => {
return cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(501);
});
});

it('BAD // unsuccessful', () => {
return cmdFn({command: {arg: 'BAD', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(500);
});
});

it('UTF8 BAD // unsuccessful', () => {
return cmdFn({command: {arg: 'UTF8 BAD', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(501);
});
});

it('UTF8 OFF // successful', () => {
return cmdFn({command: {arg: 'UTF8 OFF', directive: CMD}})
.then(() => {
expect(mockClient.encoding).to.equal('ascii');
expect(mockClient.reply.args[0][0]).to.equal(200);
});
});

it('UTF8 ON // successful', () => {
return cmdFn({command: {arg: 'UTF8 ON', directive: CMD}})
.then(() => {
expect(mockClient.encoding).to.equal('utf8');
expect(mockClient.reply.args[0][0]).to.equal(200);
});
});
});
Loading

0 comments on commit c8526be

Please sign in to comment.