163 lines
5.6 KiB
JavaScript
163 lines
5.6 KiB
JavaScript
import _ from 'lodash'
|
|
import uuid from 'uuid'
|
|
import Promise from 'bluebird'
|
|
import { EventEmitter } from 'node:events'
|
|
import BaseConnector from './connector/base.js'
|
|
import { FileSystem } from './fs.js'
|
|
import Commands from './commands/index.js'
|
|
import errors from './errors.js'
|
|
import DEFAULT_MESSAGE from './messages.js'
|
|
|
|
export default class FtpConnection extends EventEmitter {
|
|
constructor(server, options) {
|
|
super()
|
|
this.server = server
|
|
this.id = uuid.v4()
|
|
this.commandSocket = options.socket
|
|
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)
|
|
|
|
this.commandSocket.on('error', (err) => {
|
|
this.log.error(err, 'Client error')
|
|
this.server.emit('client-error', { connection: this, context: 'commandSocket', error: err })
|
|
})
|
|
this.commandSocket.on('data', this._handleData.bind(this))
|
|
this.commandSocket.on('timeout', () => {
|
|
this.log.trace('Client timeout')
|
|
this.close()
|
|
})
|
|
this.commandSocket.on('close', () => {
|
|
if (this.connector) this.connector.end()
|
|
if (this.commandSocket && !this.commandSocket.destroyed) this.commandSocket.destroy()
|
|
this.removeAllListeners()
|
|
})
|
|
}
|
|
|
|
_handleData(data) {
|
|
const messages = _.compact(data.toString(this.encoding).split('\r\n'))
|
|
this.log.trace(messages)
|
|
return Promise.mapSeries(messages, (message) => this.commands.handle(message))
|
|
}
|
|
|
|
get ip() {
|
|
try {
|
|
return this.commandSocket ? this.commandSocket.remoteAddress : undefined
|
|
} catch (ex) {
|
|
return null
|
|
}
|
|
}
|
|
|
|
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 Promise.resolve(code)
|
|
.then((_code) => _code && this.reply(_code, message))
|
|
.then(() => this.commandSocket && this.commandSocket.destroy())
|
|
}
|
|
|
|
login(username, password) {
|
|
return Promise.try(() => {
|
|
const loginListeners = this.server.listeners('login')
|
|
if (!loginListeners || !loginListeners.length) {
|
|
if (!this.server.options.anonymous) throw new errors.GeneralError('No "login" listener setup', 500)
|
|
} else {
|
|
return this.server.emitPromise('login', { connection: this, username, password })
|
|
}
|
|
}).then(({ root, cwd, fs, blacklist = [], whitelist = [] } = {}) => {
|
|
this.authenticated = true
|
|
this.commands.blacklist = _.concat(this.commands.blacklist, blacklist)
|
|
this.commands.whitelist = _.concat(this.commands.whitelist, whitelist)
|
|
this.fs = fs || new FileSystem(this, { root, cwd })
|
|
})
|
|
}
|
|
|
|
reply(options = {}, ...letters) {
|
|
const satisfyParameters = () => {
|
|
if (typeof options === 'number') options = { code: options } // allow passing in code as first param
|
|
if (!Array.isArray(letters)) letters = [letters]
|
|
if (!letters.length) letters = [{}]
|
|
return Promise.map(letters, (promise, index) => {
|
|
return Promise.resolve(promise).then((letter) => {
|
|
if (!letter) letter = {}
|
|
else if (typeof letter === 'string') letter = { message: letter } // allow passing in message as first param
|
|
|
|
if (!letter.socket) letter.socket = options.socket ? options.socket : this.commandSocket
|
|
if (!options.useEmptyMessage) {
|
|
if (!letter.message) letter.message = DEFAULT_MESSAGE[options.code] || 'No information'
|
|
if (!letter.encoding) letter.encoding = this.encoding
|
|
}
|
|
return Promise.resolve(letter.message) // allow passing in a promise as a message
|
|
.then((message) => {
|
|
if (!options.useEmptyMessage) {
|
|
const seperator = !options.hasOwnProperty('eol')
|
|
? letters.length - 1 === index
|
|
? ' '
|
|
: '-'
|
|
: options.eol
|
|
? ' '
|
|
: '-'
|
|
message = !letter.raw
|
|
? _.compact([letter.code || options.code, message]).join(seperator)
|
|
: message
|
|
letter.message = message
|
|
} else {
|
|
letter.message = ''
|
|
}
|
|
return letter
|
|
})
|
|
})
|
|
})
|
|
}
|
|
|
|
const processLetter = (letter) => {
|
|
return new Promise((resolve, reject) => {
|
|
if (letter.socket && letter.socket.writable) {
|
|
this.log.trace(
|
|
{ port: letter.socket.address().port, encoding: letter.encoding, message: letter.message },
|
|
'Reply'
|
|
)
|
|
letter.socket.write(letter.message + '\r\n', letter.encoding, (error) => {
|
|
if (error) {
|
|
this.log.error('[Process Letter] Socket Write Error', { error: error.message })
|
|
return reject(error)
|
|
}
|
|
resolve()
|
|
})
|
|
} else {
|
|
this.log.trace({ message: letter.message }, 'Could not write message')
|
|
reject(new errors.SocketError('Socket not writable'))
|
|
}
|
|
})
|
|
}
|
|
|
|
return satisfyParameters()
|
|
.then((satisfiedLetters) =>
|
|
Promise.mapSeries(satisfiedLetters, (letter, index) => {
|
|
return processLetter(letter, index)
|
|
})
|
|
)
|
|
.catch((error) => {
|
|
this.log.error('Satisfy Parameters Error', { error: error.message })
|
|
})
|
|
}
|
|
}
|