New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

basic-ftp

Package Overview
Dependencies
Maintainers
1
Versions
112
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

basic-ftp - npm Package Compare versions

Comparing version 2.11.0 to 2.12.0

6

CHANGELOG.md
# Changelog
## 2.12.0
- Added: Support IPv6 for passive mode (EPSV).
- Added: Detect automatically whether to use EPSV or PASV.
- Added: Log server IP when connected.
## 2.11.0

@@ -4,0 +10,0 @@

187

lib/ftp.js
"use strict";
const Socket = require("net").Socket;
const net = require("net");
const tls = require("tls");

@@ -44,3 +44,3 @@ const fs = require("fs");

this.ftp = new FTPContext(timeout);
this.prepareTransfer = enterPassiveModeIPv4;
this.prepareTransfer = prepareTransferAutoDetect;
this.parseList = defaultParseList;

@@ -66,3 +66,3 @@ this._progressTracker = new ProgressTracker();

connect(host = "localhost", port = 21) {
this.ftp.socket.connect(port, host);
this.ftp.socket.connect({ host, port, family: this.ftp.ipFamily }, () => this.ftp.log(`Connected to ${this.ftp.socket.remoteAddress}`));
return this.ftp.handle(undefined, (res, task) => {

@@ -282,3 +282,3 @@ if (positiveCompletion(res.code)) {

async upload(readableStream, remoteFilename) {
await this.prepareTransfer(this.ftp);
await this.prepareTransfer(this);
return upload(this.ftp, this._progressTracker, readableStream, remoteFilename);

@@ -298,3 +298,3 @@ }

async download(writableStream, remoteFilename, startAt = 0) {
await this.prepareTransfer(this.ftp);
await this.prepareTransfer(this);
const command = startAt > 0 ? `REST ${startAt}` : `RETR ${remoteFilename}`;

@@ -310,3 +310,3 @@ return download(this.ftp, this._progressTracker, writableStream, command, remoteFilename);

async list() {
await this.prepareTransfer(this.ftp);
await this.prepareTransfer(this);
const writable = new StringWriter(this.ftp.encoding);

@@ -507,3 +507,3 @@ const progressTracker = nullObject(); // Don't track progress of list transfers.

*
* @param {Socket} socket
* @param {net.Socket} socket
* @param {Object} options Same options as in `tls.connect(options)`

@@ -535,68 +535,89 @@ * @returns {Promise<tls.TLSSocket>}

/**
* Prepare a data socket using passive mode.
* Try all available transfer strategies and pick the first one that works. Update `client` to
* use the working strategy for all successive transfer requests.
*
* @param {FTPContext} ftp
* @returns {Promise<PositiveResponse>}
* @param {Client} client
* @returns {Promise<PositiveResponse>} the response of the first successful strategy.
*/
function enterPassiveModeIPv4(ftp) {
return ftp.handle("PASV", (res, task) => {
if (positiveCompletion(res.code)) {
const target = parseIPv4PasvResponse(res.message);
if (!target) {
task.reject("Can't parse PASV response: " + res.message);
return;
async function prepareTransferAutoDetect(client) {
client.ftp.log("Trying to find optimal transfer strategy...");
for (const strategy of [ enterPassiveModeIPv6, enterPassiveModeIPv4 ]) {
try {
const res = await strategy(client);
client.ftp.log("Optimal transfer strategy found.");
client.prepareTransfer = strategy; // First strategy that works will be used from now on.
return res;
}
catch(err) {
if (!err.code) { // Don't log out FTP response error again.
client.ftp.log(err.toString());
}
// If the host in the PASV response has a local address while the control connection hasn't,
// we assume a NAT issue and use the IP of the control connection as the target for the data connection.
// We can't always perform this replacement because it's possible (although unlikely) that the FTP server
// indeed uses a different host for data connections.
if (ipIsPrivateAddress(target.host) && !ipIsPrivateAddress(ftp.socket.remoteAddress)) {
target.host = ftp.socket.remoteAddress;
}
const handleConnErr = function(err) {
task.reject("Can't open data connection in passive mode: " + err.message);
};
let socket = new Socket();
socket.on("error", handleConnErr);
socket.connect(target.port, target.host, () => {
if (ftp.hasTLS) {
socket = tls.connect(Object.assign({}, ftp.tlsOptions, {
// Upgrade the existing socket connection.
socket,
// Reuse the TLS session negotiated earlier when the control connection
// was upgraded. Servers expect this because it provides additional
// security. If a completely new session would be negotiated, a hacker
// could guess the port and connect to the new data connection before we do
// by just starting his/her own TLS session.
session: ftp.socket.getSession()
}));
// It's the responsibility of the transfer task to wait until the
// TLS socket issued the event 'secureConnect'. We can't do this
// here because some servers will start upgrading after the
// specific transfer request has been made. List and download don't
// have to wait for this event because the server sends whenever it
// is ready. But for upload this has to be taken into account,
// see the details in the upload() function below.
}
// Let the FTPContext listen to errors from now on, remove local handler.
socket.removeListener("error", handleConnErr);
ftp.dataSocket = socket;
task.resolve(res);
});
}
else {
task.reject(res);
}
});
}
throw new Error("None of the available transfer strategies work.");
}
/**
* Parse a PASV response message.
* Prepare a data socket using passive mode over IPv6.
*
* @param {Client} client
* @returns {Promise<PositiveResponse>}
*/
async function enterPassiveModeIPv6(client) {
const controlIP = client.ftp.socket.remoteAddress;
if (!net.isIPv6(controlIP)) {
throw new Error(`EPSV not possible, control connection is not using IPv6: ${controlIP}`);
}
const res = await client.send("EPSV");
const port = parseIPv6PasvResponse(res.message);
if (!port) {
throw new Error("Can't parse EPSV response: " + res.message);
}
await connectForPassiveTransfer(controlIP, port, client.ftp);
return res;
}
/**
* Parse an EPSV response. Returns only the port as in EPSV the host of the control connection is used.
*
* @param {string} message
* @returns {number} port
*/
function parseIPv6PasvResponse(message) {
// Get port from EPSV response, e.g. "229 Entering Extended Passive Mode (|||6446|)"
const groups = message.match(/\|{3}(.+)\|/);
return groups[1] ? parseInt(groups[1], 10) : undefined;
}
/**
* Prepare a data socket using passive mode over IPv6.
*
* @param {Client} client
* @returns {Promise<PositiveResponse>}
*/
async function enterPassiveModeIPv4(client) {
const res = await client.send("PASV");
const target = parseIPv4PasvResponse(res.message);
if (!target) {
throw new Error("Can't parse PASV response: " + res.message);
}
// If the host in the PASV response has a local address while the control connection hasn't,
// we assume a NAT issue and use the IP of the control connection as the target for the data connection.
// We can't always perform this replacement because it's possible (although unlikely) that the FTP server
// indeed uses a different host for data connections.
if (ipIsPrivateV4Address(target.host) && !ipIsPrivateV4Address(client.ftp.socket.remoteAddress)) {
target.host = client.ftp.socket.remoteAddress;
}
await connectForPassiveTransfer(target.host, target.port, client.ftp);
return res;
}
/**
* Parse a PASV response.
*
* @param {string} message
* @returns {{host: string, port: number}}
*/
function parseIPv4PasvResponse(message) {
// From something like "227 Entering Passive Mode (192,168,1,100,10,229)",
// extract host and port.
// Get host and port from PASV response, e.g. "227 Entering Passive Mode (192,168,1,100,10,229)"
const groups = message.match(/([-\d]+,[-\d]+,[-\d]+,[-\d]+),([-\d]+),([-\d]+)/);

@@ -613,3 +634,4 @@ if (!groups || groups.length !== 4) {

/**
* Returns true if an IP is a private address according to https://tools.ietf.org/html/rfc1918#section-3
* Returns true if an IP is a private address according to https://tools.ietf.org/html/rfc1918#section-3.
* This will handle IPv4-mapped IPv6 addresses correctly but return false for all other IPv6 addresses.
*

@@ -619,3 +641,7 @@ * @param {string} ip The IP as a string, e.g. "192.168.0.1"

*/
function ipIsPrivateAddress(ip = "") {
function ipIsPrivateV4Address(ip = "") {
// Handle IPv4-mapped IPv6 addresses like ::ffff:192.168.0.1
if (ip.startsWith("::ffff:")) {
ip = ip.substr(7); // Strip ::ffff: prefix
}
const octets = ip.split(".").map(o => parseInt(o, 10));

@@ -627,2 +653,37 @@ return octets[0] === 10 // 10.0.0.0 - 10.255.255.255

function connectForPassiveTransfer(host, port, ftp) {
return new Promise((resolve, reject) => {
const handleConnErr = function(err) {
reject("Can't open data connection in passive mode: " + err.message);
};
let socket = new net.Socket();
socket.on("error", handleConnErr);
socket.connect({ port, host, family: ftp.ipFamily }, () => {
if (ftp.hasTLS) {
socket = tls.connect(Object.assign({}, ftp.tlsOptions, {
// Upgrade the existing socket connection.
socket,
// Reuse the TLS session negotiated earlier when the control connection
// was upgraded. Servers expect this because it provides additional
// security. If a completely new session would be negotiated, a hacker
// could guess the port and connect to the new data connection before we do
// by just starting his/her own TLS session.
session: ftp.socket.getSession()
}));
// It's the responsibility of the transfer task to wait until the
// TLS socket issued the event 'secureConnect'. We can't do this
// here because some servers will start upgrading after the
// specific transfer request has been made. List and download don't
// have to wait for this event because the server sends whenever it
// is ready. But for upload this has to be taken into account,
// see the details in the upload() function below.
}
// Let the FTPContext listen to errors from now on, remove local handler.
socket.removeListener("error", handleConnErr);
ftp.dataSocket = socket;
resolve();
});
});
}
/**

@@ -629,0 +690,0 @@ * Upload stream data as a file. For example:

@@ -28,2 +28,3 @@ "use strict";

this.tlsOptions = {}; // Options for TLS connections.
this.ipFamily = 6; // IP version to prefer (4: IPv4, 6: IPv6).
this.verbose = false; // The client can log every outgoing and incoming message.

@@ -30,0 +31,0 @@ this.socket = new Socket(); // The control connection to the FTP server.

{
"name": "basic-ftp",
"version": "2.11.0",
"version": "2.12.0",
"description": "FTP client for Node.js with support for explicit FTPS over TLS.",

@@ -5,0 +5,0 @@ "main": "./lib/ftp",

@@ -5,3 +5,3 @@ # Basic FTP

This is an FTP client for Node.js. It supports explicit FTPS over TLS, has a Promise-based API and offers methods to operate on whole directories.
This is an FTP client for Node.js. It supports explicit FTPS over TLS, IPv6, has a Promise-based API, and offers methods to operate on whole directories.

@@ -27,3 +27,3 @@ ## Goals

await client.access({
host: "192.168.0.10",
host: "myftpserver.com",
user: "very"

@@ -48,5 +48,5 @@ password: "password",

```js
await client.ensureDir("my/remote/path")
await client.ensureDir("my/remote/directory")
await client.clearWorkingDir()
await client.uploadDir("my/local/path")
await client.uploadDir("my/local/directory")
```

@@ -90,3 +90,3 @@

Convenience method to get access to an FTP server. This method calls `connect`, `useTLS`, `login` and `useDefaultSettings`. The available options are:
Convenience method to get access to an FTP server. This method calls *connect*, *useTLS*, *login* and *useDefaultSettings*. It returns the response of the initial connect command. The available options are:

@@ -183,3 +183,3 @@ - `host`: Host to connect to

// Set a new callback function which resets the overall counter
// Set a new callback function which also resets the overall counter
client.trackProgress(info => console.log(info.bytesOverall))

@@ -233,3 +233,3 @@ await client.downloadDir("local/path")

FTP creates a socket connection for each single data transfer. Data transfers include directory listings, file uploads and downloads. This property holds the function that prepares this connection. Currently, this library only offers Passive Mode over IPv4, but this extension point makes support for Active Mode or IPv6 possible. The signature of the function is `(ftp: FTPContext) => Promise<void>` and its job is to set `ftp.dataSocket`. The section below about extending functionality explains what `FTPContext` is.
FTP creates a socket connection for each single data transfer. Data transfers include directory listings, file uploads and downloads. This property holds the function that prepares this connection. Currently, this library offers Passive Mode over IPv4 (PASV) and IPv6 (EPSV) but this extension point makes support for Active Mode possible. The signature of the function is `(ftp: FTPContext) => Promise<void>` and its job is to set `ftp.dataSocket`. The section below about extending functionality explains what `FTPContext` is.

@@ -236,0 +236,0 @@ `get/set client.parseList`

@@ -28,3 +28,3 @@ const assert = require("assert");

client = new Client();
client.prepareTransfer = () => {}; // Don't change
client.prepareTransfer = () => Promise.resolve(); // Don't change
client.ftp.socket = new SocketMock();

@@ -153,5 +153,5 @@ client.ftp.dataSocket = new SocketMock();

it("can connect", function() {
client.ftp.socket.connect = (port, host) => {
assert.equal(host, "host", "Socket host");
assert.equal(port, 22, "Socket port");
client.ftp.socket.connect = (options) => {
assert.equal(options.host, "host");
assert.equal(options.port, 22, "Socket port");
setTimeout(() => client.ftp.socket.emit("data", Buffer.from("200 OK")));

@@ -158,0 +158,0 @@ }

@@ -12,3 +12,3 @@ const assert = require("assert");

this.timeout(100);
var f;
const bufList = Buffer.from("12-05-96 05:03PM <DIR> myDir");

@@ -26,4 +26,5 @@ const expList = [

client = new Client();
client.prepareTransfer = ftp => {
ftp.dataSocket = new SocketMock();
client.prepareTransfer = client => {
client.ftp.dataSocket = new SocketMock();
return Promise.resolve();
};

@@ -30,0 +31,0 @@ client.ftp.socket = new SocketMock();

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc