Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix timeout on RETR with TLS #233

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open

Conversation

ldm314
Copy link

@ldm314 ldm314 commented Dec 17, 2020

The issue looks to be either a race condition or sequencing error. I changed the RETR command to send the 150 response after opening the file, but before waiting for the socket to be connected. This resolved this issue with both WinSCP and an old hardware device I am working with.

@matt-forster
Copy link
Contributor

@ldm314 I am still planning on getting this merged in, I will have some time next week to address this.

@@ -7,57 +7,59 @@ module.exports = {
if (!this.fs.read) return this.reply(402, 'Not supported by file system');

const filePath = command.arg;
return this.fs.read(filePath, {start: this.restByteCount})
Copy link
Contributor

@matt-forster matt-forster Aug 9, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We had this.fs.read wrapped in a Promise.try block previously because it isn't necessarily going to return a promise in every implementation - best to include that, and your tests will begin passing again most likely.

@matt-forster matt-forster self-assigned this Aug 9, 2021
@ideadesignmedia
Copy link

Found solution, replace src/commands/registration/retr.js:

const Promise = require('bluebird');

module.exports = {
  directive: 'RETR',
  handler: function ({log, command} = {}) {
    if (!this.fs) return this.reply(550, 'File system not instantiated');
    if (!this.fs.read) return this.reply(402, 'Not supported by file system');

    const filePath = command.arg;
this.reply(150).then(() => this.connector.socket && this.connector.socket.resume())
    return this.connector.waitForConnection()
    .tap(() => this.commandSocket.pause())
    .then(() => Promise.try(() => this.fs.read(filePath, {start: this.restByteCount})))
    .then((fsResponse) => {
      let {stream, clientPath} = fsResponse;
      if (!stream && !clientPath) {
        stream = fsResponse;
        clientPath = filePath;
      }
      const serverPath = stream.path || filePath;

      const destroyConnection = (connection, reject) => (err) => {
        if (connection) connection.destroy(err);
        reject(err);
      };

      const eventsPromise = new Promise((resolve, reject) => {
        stream.on('data', (data) => {
          if (stream) stream.pause();
          if (this.connector.socket) {
            this.connector.socket.write(data, () => stream && stream.resume());
          }
        });
        stream.once('end', () => resolve());
        stream.once('error', destroyConnection(this.connector.socket, reject));

        this.connector.socket.once('error', destroyConnection(stream, reject));
      });

      this.restByteCount = 0;

      return eventsPromise.tap(() => this.emit('RETR', null, serverPath))
      .then(() => this.reply(226, clientPath))
      .finally(() => stream.destroy && stream.destroy());
    })
    .catch(Promise.TimeoutError, (err) => {
      log.error(err);
      return this.reply(425, 'No connection established');
    })
    .catch((err) => {
      log.error(err);
      this.emit('RETR', err);
      return this.reply(551, err.message);
    })
    .finally(() => {
      this.connector.end();
      this.commandSocket.resume();
    });
  },
  syntax: '{{cmd}} <path>',
  description: 'Retrieve a copy of the file'
};

@matt-forster
Copy link
Contributor

@ideadesignmedia Feel free to create another PR that eclipses this one!

@matt-forster matt-forster removed their assignment Dec 13, 2022
@andersnm
Copy link

andersnm commented Feb 28, 2024

[edit; amended my comment so as to not say anything too wrong on the internet] So it seems WinSCP expects a 150 response before STOR and RETR with TLS+passive. For some hysterical reason, WinSCP does not wait for 150 without TLS. On the other hand, FileZilla does not wait for a 150 response up front, but unlike WinSCP it breaks if you send two 150's (which my comment suggested initially, altho the PR already took care of it)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants