question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

[Bug] `OPTS MLST type;...` command may have multiple responses.

See original GitHub issue

Describe the bug While experimenting with adding implicit connection support, some request and response messages got out of sync at this point:

await this.sendIgnoringError("OPTS MLST type;size;modify;unique;unix.mode;unix.owner;unix.group;unix.ownername;unix.groupname;") // Make sure MLSD listings include all we can parse

Which for my server responded twice with:

< 500 Unknown option: MLST
< 500 Unknown option: OPTS UTF8 type;size;modify;unique;unix.mode;unix.owner;unix.group;unix.ownername;unix.groupname;

Console output

The . RESOLVED: ... lines below indicate that the task was marked resolved at that point. However I added a 1 second delay before it actually finished with setTimeout(() => resolvePromise(...args), 1000)

...
> OPTS UTF8 ON
< 200 OK.
. RESOLVED: OPTS UTF8 ON
> OPTS MLST type;size;modify;unique;unix.mode;unix.owner;unix.group;unix.ownername;unix.groupname;
. RESOLVED: OPTS MLST type;size;modify;unique;unix.mode;unix.owner;unix.group;unix.ownername;unix.groupname;
< 500 Unknown option: MLST
< 500 Unknown option: OPTS UTF8 type;size;modify;unique;unix.mode;unix.owner;unix.group;unix.ownername;unix.groupname;
> PBSZ 0
< 200 OK.
...

Without the 1 second delay then this is the result.

...
> OPTS UTF8 ON
< 200 OK.
. RESOLVED: OPTS UTF8 ON
> OPTS MLST type;size;modify;unique;unix.mode;unix.owner;unix.group;unix.ownername;unix.groupname;
< 500 Unknown option: MLST
. RESOLVED: OPTS MLST type;size;modify;unique;unix.mode;unix.owner;unix.group;unix.ownername;unix.groupname;
> PBSZ 0
< 500 Unknown option: OPTS UTF8 type;size;modify;unique;unix.mode;unix.owner;unix.group;unix.ownername;unix.groupname;
. RESOLVED: PBSZ 0
> PROT P
< 200 OK.
. RESOLVED: PROT P
...

Which version of Node.js are you using? e.g. Node 12.14.0

Issue Analytics

  • State:closed
  • Created 4 years ago
  • Comments:6 (6 by maintainers)

github_iconTop GitHub Comments

1reaction
sparebytescommented, Jan 9, 2020

@patrickjuchli Thank you for the helping with this use-case.

I combined the workaround for the ShareFile server and the Implicit Encryption support in #121.

Example:

async function example() {
    let client: Client | undefined
    try {
        client = new Client()
        client.ftp.verbose = true
        await ftpsConnectLogin(client, {
            host: "127.0.0.1",
            port: 990,
            user: "...",
            password: "...",
            secure: "implicit" as const,
            secureOptions: { rejectUnauthorized: true },
        })
        await shareFileServerSettings(client)
        // ...
    } finally {
        if (client) client.close()
    }
}

Functions:

import { AccessOptions, Client, FTPError, FTPResponse } from "basic-ftp"
import { Socket } from "net"
import { connect as connectTLS, ConnectionOptions } from "tls"

export type AccessOptionsWithImplicit = Omit<AccessOptions, "secure"> & {
    secure?: boolean | "implicit" | "explicit"
}

export async function ftpsConnect(
    client: Client,
    host = "localhost",
    port = 21,
    secureOptions?: ConnectionOptions,
): Promise<FTPResponse> {
    if (secureOptions == null) {
        client.ftp.reset()

        client.ftp.socket.connect(
            {
                host,
                port,
                family: client.ftp.ipFamily,
            },
            () => client.ftp.log(`Connected to ${describeAddress(client.ftp.socket)}`),
        )
    } else {
        try {
            client.ftp.socket = connectTLS(port, host, secureOptions, () =>
                client.ftp.log(`Connected to ${describeAddress(client.ftp.socket)} over TLS`),
            )
            client.ftp.tlsOptions = secureOptions
        } catch (error) {
            client.ftp.reset()
            throw error
        }
    }

    return client.ftp.handle(undefined, (res, task) => {
        if (res instanceof Error) {
            // The connection has been destroyed by the FTPContext at this point.
            task.reject(res)
        } else if (positiveCompletion(res.code)) {
            task.resolve(res)
        }
        // Reject all other codes, including 120 "Service ready in nnn minutes".
        else {
            // Don't stay connected but don't replace the socket yet by using reset()
            // so the user can inspect properties of this instance.
            client.ftp.socket.destroy()
            task.reject(new FTPError(res))
        }
    })
}

export async function ftpsConnectLogin(
    client: Client,
    options: AccessOptionsWithImplicit,
): Promise<FTPResponse> {
    const welcomeSecureOptions =
        options.secure !== "implicit" ? undefined : options.secureOptions != null ? options.secureOptions : {}
    // const welcome = await client.connect(options.host, options.port)
    const welcome = await ftpsConnect(client, options.host, options.port, welcomeSecureOptions)
    if (options.secure === true || options.secure === "explicit") {
        await client.useTLS(options.secureOptions)
    }
    await client.login(options.user, options.password)
    return welcome
}

export async function shareFileServerSettings(client: Client): Promise<void> {
    // Don't use "await this.useDefaultSettings()", apply custom settings instead:
    await client.send("TYPE I")
    await client.sendIgnoringError("STRU F")
    await client.sendIgnoringError("OPTS UTF8 ON")
    if (client.useTLS != null) {
        await client.sendIgnoringError("PBSZ 0")
        await client.sendIgnoringError("PROT P")
    }
}

/**
 * Returns a string describing the remote address of a socket.
 */
function describeAddress(socket: Socket): string {
    if (socket.remoteFamily === "IPv6") {
        return `[${socket.remoteAddress}]:${socket.remotePort}`
    }
    return `${socket.remoteAddress}:${socket.remotePort}`
}

/**
 * Return true if an FTP return code describes a positive completion.
 */
function positiveCompletion(code: number): boolean {
    return code >= 200 && code < 300
}
0reactions
patrickjuchlicommented, Jan 8, 2020

I will close this issue because this is a bug with the FTP server and not this library. I’ve also shown a way how to handle this specific case.

Read more comments on GitHub >

github_iconTop Results From Across the Web

FTP Commands: APPE, MLSD, MLST, LIST, RETR, STOR, ...
This article explains the use of FTP commands: · APPE FTP command · MLSD FTP command · MLST FTP command · LIST FTP...
Read more >
Extensions to FTP RFC 3659
When the MLST command is supported, as indicated in the response to the FEAT command [6], pathnames are to be transferred in one...
Read more >
RFC 2389: Feature negotiation mechanism for the File ...
6 4 The OPTS Command . ... Two new commands are added: "FEAT" and "OPTS". ... set for use in various commands and...
Read more >
Command-line syntax overview for System.CommandLine
Arguments can have expected types, and System.CommandLine displays an error message if an argument can't be parsed into the expected type.
Read more >
Results-Reports Visualizations
Embedded data fields don't have multiple data source options, but the type of data captured in the field can affect what types of...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found