2018-10-20 13:19:17 +03:00
|
|
|
/*
|
|
|
|
* Server side of the old-school SCP protocol.
|
|
|
|
*/
|
|
|
|
|
|
|
|
#include <assert.h>
|
|
|
|
#include <stdio.h>
|
|
|
|
#include <stdlib.h>
|
|
|
|
|
|
|
|
#include "putty.h"
|
|
|
|
#include "ssh.h"
|
|
|
|
#include "sshcr.h"
|
|
|
|
#include "sshchan.h"
|
|
|
|
#include "sftp.h"
|
|
|
|
|
|
|
|
/*
|
|
|
|
* I think it's worth actually documenting my understanding of what
|
|
|
|
* this protocol _is_, since I don't know of any other documentation
|
|
|
|
* of it anywhere.
|
|
|
|
*
|
|
|
|
* Format of data stream
|
|
|
|
* ---------------------
|
|
|
|
*
|
|
|
|
* The sending side of an SCP connection - the client, if you're
|
|
|
|
* uploading files, or the server if you're downloading - sends a data
|
|
|
|
* stream consisting of a sequence of 'commands', or header records,
|
|
|
|
* or whatever you want to call them, interleaved with file data.
|
|
|
|
*
|
|
|
|
* Each command starts with a letter indicating what type it is, and
|
|
|
|
* ends with a \n.
|
|
|
|
*
|
|
|
|
* The 'C' command introduces an actual file. It is followed by an
|
|
|
|
* octal file-permissions mask, then a space, then a decimal file
|
|
|
|
* size, then a space, then the file name up to the termating newline.
|
|
|
|
* For example, "C0644 12345 filename.txt\n" would be a plausible C
|
|
|
|
* command.
|
|
|
|
*
|
|
|
|
* After the 'C' command, the sending side will transmit exactly as
|
|
|
|
* many bytes of file data as specified by the size field in the
|
|
|
|
* header line, followed by a single zero byte.
|
|
|
|
*
|
|
|
|
* The 'D' command introduces a subdirectory. Its format is identical
|
|
|
|
* to 'C', including the size field, but the size field is sent as
|
|
|
|
* zero.
|
|
|
|
*
|
|
|
|
* After the 'D' command, all subsequent C and D commands are taken to
|
|
|
|
* indicate files that should be placed inside that subdirectory,
|
|
|
|
* until a terminating 'E' command.
|
|
|
|
*
|
|
|
|
* The 'E' command indicates the end of a subdirectory. It has no
|
|
|
|
* arguments at all (its format is always just "E\n"). After the E
|
|
|
|
* command, the receiver should revert to placing further downloaded
|
|
|
|
* files in whatever directory it was placing them before the
|
|
|
|
* subdirectory opened by the just-closed D.
|
|
|
|
*
|
|
|
|
* D and E commands match like parentheses: if you send, say,
|
|
|
|
*
|
|
|
|
* C0644 123 foo.txt ( followed by data )
|
|
|
|
* D0755 0 subdir
|
|
|
|
* C0644 123 bar.txt ( followed by data )
|
|
|
|
* D0755 0 subsubdir
|
|
|
|
* C0644 123 baz.txt ( followed by data )
|
|
|
|
* E
|
|
|
|
* C0644 123 quux.txt ( followed by data )
|
|
|
|
* E
|
|
|
|
* C0644 123 wibble.txt ( followed by data )
|
|
|
|
*
|
|
|
|
* then foo.txt, subdir and wibble.txt go in the top-level destination
|
|
|
|
* directory; bar.txt, subsubdir and quux.txt go in 'subdir'; and
|
|
|
|
* baz.txt goes in 'subdir/subsubdir'.
|
|
|
|
*
|
|
|
|
* The sender terminates the data stream with EOF when it has no more
|
|
|
|
* files to send. I believe it is not _required_ for all D to be
|
|
|
|
* closed by an E before this happens - you can elide a trailing
|
|
|
|
* sequence of E commands without provoking an error message from the
|
|
|
|
* receiver.
|
|
|
|
*
|
|
|
|
* Finally, the 'T' command is sent immediately before a C or D. It is
|
|
|
|
* followed by four space-separated decimal integers giving an mtime
|
|
|
|
* and atime to be applied to the file or directory created by the
|
|
|
|
* following C or D command. The first two integers give the mtime,
|
|
|
|
* encoded as seconds and microseconds (respectively) since the Unix
|
|
|
|
* epoch; the next two give the atime, encoded similarly. So
|
|
|
|
* "T1540373455 0 1540373457 0\n" is an example of a valid T command.
|
|
|
|
*
|
|
|
|
* Acknowledgments
|
|
|
|
* ---------------
|
|
|
|
*
|
|
|
|
* The sending side waits for an ack from the receiving side before
|
|
|
|
* sending each command; before beginning to send the file data
|
|
|
|
* following a C command; and before sending the final EOF.
|
|
|
|
*
|
|
|
|
* (In particular, the receiving side is expected to send an initial
|
|
|
|
* ack before _anything_ is sent.)
|
|
|
|
*
|
|
|
|
* Normally an ack consists of a single zero byte. It's also allowable
|
|
|
|
* to send a byte with value 1 or 2 followed by a \n-terminated error
|
|
|
|
* message (where 1 means a non-fatal error and 2 means a fatal one).
|
|
|
|
* I have to suppose that sending an error message from client to
|
|
|
|
* server is of limited use, but apparently it's allowed.
|
|
|
|
*
|
|
|
|
* Initiation
|
|
|
|
* ----------
|
|
|
|
*
|
|
|
|
* The protocol is begun by the client sending a command string to the
|
|
|
|
* server via the SSH-2 "exec" request (or the analogous
|
|
|
|
* SSH1_CMSG_EXEC_CMD), which indicates that this is an scp session
|
|
|
|
* rather than any other thing; specifies the direction of transfer;
|
|
|
|
* says what file(s) are to be sent by the server, or where the server
|
|
|
|
* should put files that the client is about to send; and a couple of
|
|
|
|
* other options.
|
|
|
|
*
|
|
|
|
* The command string takes the following form:
|
|
|
|
*
|
|
|
|
* Start with prefix "scp ", indicating that this is an SCP command at
|
|
|
|
* all. Otherwise it's a request to run some completely different
|
|
|
|
* command in the SSH session.
|
|
|
|
*
|
|
|
|
* Next the command can contain zero or more of the following options,
|
|
|
|
* each followed by a space:
|
|
|
|
*
|
|
|
|
* "-v" turns on verbose server diagnostics. Of course a server is not
|
|
|
|
* required to actually produce any, but this is an invitation for it
|
|
|
|
* to send any it might have available. Diagnostics are free-form, and
|
|
|
|
* sent as SSH standard-error extended data, so that they are separate
|
|
|
|
* from the actual data stream as described above.
|
|
|
|
*
|
|
|
|
* (Servers can send standard-error output anyway if they like, and in
|
|
|
|
* case of an actual error, they probably will with or without -v.)
|
|
|
|
*
|
|
|
|
* "-r" indicates recursive file transfer, i.e. potentially including
|
|
|
|
* subdirectories. For a download, this indicates that the client is
|
|
|
|
* willing to receive subdirectories (a D/E command pair bracketing
|
|
|
|
* further files and subdirs); without it, the server should only send
|
|
|
|
* C commands for individual files, followed by EOF.
|
|
|
|
*
|
|
|
|
* This flag must also be specified for a recursive upload, because I
|
|
|
|
* believe at least one server will reject D/E pairs sent by the
|
|
|
|
* client if the command didn't have -r in it. (Probably a consequence
|
|
|
|
* of sharing code between download and upload.)
|
|
|
|
*
|
|
|
|
* "-p" means preserve file times. In a download, this requests the
|
|
|
|
* server to send a T command before each C or D. I don't know whether
|
|
|
|
* any server will insist on having seen this option from the client
|
|
|
|
* before accepting T commands in an upload, but it is probably
|
|
|
|
* sensible to send it anyway.
|
|
|
|
*
|
|
|
|
* "-d", in an upload, means that the destination pathname (see below)
|
|
|
|
* is expected to be a directory, and that uploaded files (and
|
|
|
|
* subdirs) should be put inside it. Without -d, the semantics are
|
|
|
|
* that _if_ the destination exists and is a directory, then files
|
|
|
|
* will be put in it, whereas if it is not, then just a single file
|
|
|
|
* (or subdir) upload is expected, which will be placed at that exact
|
|
|
|
* pathname.
|
|
|
|
*
|
|
|
|
* In a download, I observe that clients tend to send -d if they are
|
|
|
|
* requesting multiple files or a wildcard, but as far as I know,
|
|
|
|
* servers ignore it.
|
|
|
|
*
|
|
|
|
* After all those optional options, there is a single mandatory
|
|
|
|
* option indicating the direction of transfer, which is either "-f"
|
|
|
|
* or "-t". "-f" indicates a download; "-t" indicates an upload.
|
|
|
|
*
|
|
|
|
* After that mandatory option, there is a single space, followed by
|
|
|
|
* the name(s) of files to transfer.
|
|
|
|
*
|
|
|
|
* This file name field is transmitted with NO QUOTING, in spite of
|
|
|
|
* the fact that a server will typically interpret it as a shell
|
|
|
|
* command. You'd think this couldn't possibly work, in the face of
|
|
|
|
* almost any filename with an interesting character in it - and you'd
|
|
|
|
* be right. Or rather (you might argue), it works 'as designed', but
|
|
|
|
* it's designed in a weird way, in that it's the user's
|
|
|
|
* responsibility to apply quoting on the client command line to get
|
|
|
|
* the filename through the shell that will decode things on the
|
|
|
|
* server side.
|
|
|
|
*
|
|
|
|
* But one effect of this is that if you issue a download command
|
|
|
|
* including a wildcard, say "scp -f somedir/foo*.txt", then the shell
|
|
|
|
* will expand the wildcard, and actually run the server-side scp
|
|
|
|
* program with multiple arguments, say "somedir/foo.txt
|
|
|
|
* somedir/quux.txt", leading to the download sending multiple C
|
|
|
|
* commands. This clearly _is_ intended: it's how a command such as
|
|
|
|
* 'scp server:somedir/foo*.txt destdir' can work at all.
|
|
|
|
*
|
|
|
|
* (You would think, given that, that it might also be legal to send
|
|
|
|
* multiple space-separated filenames in order to trigger a download
|
|
|
|
* of exactly those files. Given how scp is invoked in practice on a
|
|
|
|
* typical server, this would surely actually work, but my observation
|
|
|
|
* is that scp clients don't in fact try this - if you run OpenSSH's
|
|
|
|
* scp by saying 'scp server:foo server:bar destdir' then it will make
|
|
|
|
* two separate connections to the server for the two files, rather
|
|
|
|
* than sending a single space-separated remote command. PSCP won't
|
|
|
|
* even do that, and will make you do it in two separate runs.)
|
|
|
|
*
|
|
|
|
* So, some examples:
|
|
|
|
*
|
|
|
|
* - "scp -f filename.txt"
|
|
|
|
*
|
|
|
|
* Server should send a single C command (plus data) for that file.
|
|
|
|
* Client ought to ignore the filename in the C command, in favour
|
|
|
|
* of saving the file under the name implied by the user's command
|
|
|
|
* line.
|
|
|
|
*
|
|
|
|
* - "scp -f file*.txt"
|
|
|
|
*
|
|
|
|
* Server sends zero or more C commands, then EOF. Client will have
|
|
|
|
* been given a target directory to put them all in, and will name
|
|
|
|
* each one according to the name in the C command.
|
|
|
|
*
|
|
|
|
* (You'd like the client to validate the filenames against the
|
|
|
|
* wildcard it sent, to ensure a malicious server didn't try to
|
|
|
|
* overwrite some path like ".bashrc" when you thought you were
|
|
|
|
* downloading only normal text files. But wildcard semantics are
|
|
|
|
* chosen by the server, so this is essentially hopeless to do
|
|
|
|
* rigorously.)
|
|
|
|
*
|
|
|
|
* - "scp -f -r somedir"
|
|
|
|
*
|
|
|
|
* Assuming somedir is actually a directory, server sends a D/E
|
|
|
|
* pair, in between which are the contents of the directory
|
|
|
|
* (perhaps including further nested D/E pairs). Client probably
|
|
|
|
* ignores the name field of the outermost D
|
|
|
|
*
|
|
|
|
* - "scp -f -r some*wild*card*"
|
|
|
|
*
|
|
|
|
* Server sends multiple C or D-stuff-E, one for each top-level
|
|
|
|
* thing matching the wildcard, whether it's a file or a directory.
|
|
|
|
*
|
|
|
|
* - "scp -t -d some_dir"
|
|
|
|
*
|
|
|
|
* Client sends stuff, and server deposits each file at
|
|
|
|
* some_dir/<name from the C command>.
|
|
|
|
*
|
|
|
|
* - "scp -t some_path_name"
|
|
|
|
*
|
|
|
|
* Client sends one C command, and server deposits it at
|
|
|
|
* some_path_name itself, or in some_path_name/<name from C
|
|
|
|
* command>, depending whether some_path_name was already a
|
|
|
|
* directory or not.
|
|
|
|
*/
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Here's a useful debugging aid: run over a binary file containing
|
|
|
|
* the complete contents of the sender's data stream (e.g. extracted
|
|
|
|
* by contrib/logparse.pl -d), it removes the file contents, leaving
|
|
|
|
* only the list of commands, so you can see what the server sent.
|
|
|
|
*
|
|
|
|
* perl -pe 'read ARGV,$x,1+$1 if/^C\S+ (\d+)/'
|
|
|
|
*/
|
|
|
|
|
|
|
|
/* ----------------------------------------------------------------------
|
|
|
|
* Shared system for receiving replies from the SftpServer, and
|
|
|
|
* putting them into a set of ordinary variables rather than
|
|
|
|
* marshalling them into actual SFTP reply packets that we'd only have
|
|
|
|
* to unmarshal again.
|
|
|
|
*/
|
|
|
|
|
|
|
|
typedef struct ScpReplyReceiver ScpReplyReceiver;
|
|
|
|
struct ScpReplyReceiver {
|
|
|
|
int err;
|
|
|
|
unsigned code;
|
|
|
|
char *errmsg;
|
|
|
|
struct fxp_attrs attrs;
|
|
|
|
ptrlen name, handle, data;
|
|
|
|
|
|
|
|
SftpReplyBuilder srb;
|
|
|
|
};
|
|
|
|
|
|
|
|
static void scp_reply_ok(SftpReplyBuilder *srb)
|
|
|
|
{
|
|
|
|
ScpReplyReceiver *reply = container_of(srb, ScpReplyReceiver, srb);
|
|
|
|
reply->err = FALSE;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void scp_reply_error(
|
|
|
|
SftpReplyBuilder *srb, unsigned code, const char *msg)
|
|
|
|
{
|
|
|
|
ScpReplyReceiver *reply = container_of(srb, ScpReplyReceiver, srb);
|
|
|
|
reply->err = TRUE;
|
|
|
|
reply->code = code;
|
|
|
|
sfree(reply->errmsg);
|
|
|
|
reply->errmsg = dupstr(msg);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void scp_reply_name_count(SftpReplyBuilder *srb, unsigned count)
|
|
|
|
{
|
|
|
|
ScpReplyReceiver *reply = container_of(srb, ScpReplyReceiver, srb);
|
|
|
|
reply->err = FALSE;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void scp_reply_full_name(
|
|
|
|
SftpReplyBuilder *srb, ptrlen name,
|
|
|
|
ptrlen longname, struct fxp_attrs attrs)
|
|
|
|
{
|
|
|
|
ScpReplyReceiver *reply = container_of(srb, ScpReplyReceiver, srb);
|
|
|
|
char *p;
|
|
|
|
reply->err = FALSE;
|
|
|
|
sfree((void *)reply->name.ptr);
|
|
|
|
reply->name.ptr = p = mkstr(name);
|
|
|
|
reply->name.len = name.len;
|
|
|
|
reply->attrs = attrs;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void scp_reply_simple_name(SftpReplyBuilder *srb, ptrlen name)
|
|
|
|
{
|
|
|
|
ScpReplyReceiver *reply = container_of(srb, ScpReplyReceiver, srb);
|
|
|
|
reply->err = FALSE;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void scp_reply_handle(SftpReplyBuilder *srb, ptrlen handle)
|
|
|
|
{
|
|
|
|
ScpReplyReceiver *reply = container_of(srb, ScpReplyReceiver, srb);
|
|
|
|
char *p;
|
|
|
|
reply->err = FALSE;
|
|
|
|
sfree((void *)reply->handle.ptr);
|
|
|
|
reply->handle.ptr = p = mkstr(handle);
|
|
|
|
reply->handle.len = handle.len;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void scp_reply_data(SftpReplyBuilder *srb, ptrlen data)
|
|
|
|
{
|
|
|
|
ScpReplyReceiver *reply = container_of(srb, ScpReplyReceiver, srb);
|
|
|
|
char *p;
|
|
|
|
reply->err = FALSE;
|
|
|
|
sfree((void *)reply->data.ptr);
|
|
|
|
reply->data.ptr = p = mkstr(data);
|
|
|
|
reply->data.len = data.len;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void scp_reply_attrs(
|
|
|
|
SftpReplyBuilder *srb, struct fxp_attrs attrs)
|
|
|
|
{
|
|
|
|
ScpReplyReceiver *reply = container_of(srb, ScpReplyReceiver, srb);
|
|
|
|
reply->err = FALSE;
|
|
|
|
reply->attrs = attrs;
|
|
|
|
}
|
|
|
|
|
|
|
|
static const struct SftpReplyBuilderVtable ScpReplyReceiver_vt = {
|
|
|
|
scp_reply_ok,
|
|
|
|
scp_reply_error,
|
|
|
|
scp_reply_simple_name,
|
|
|
|
scp_reply_name_count,
|
|
|
|
scp_reply_full_name,
|
|
|
|
scp_reply_handle,
|
|
|
|
scp_reply_data,
|
|
|
|
scp_reply_attrs,
|
|
|
|
};
|
|
|
|
|
|
|
|
static void scp_reply_setup(ScpReplyReceiver *reply)
|
|
|
|
{
|
|
|
|
memset(reply, 0, sizeof(*reply));
|
|
|
|
reply->srb.vt = &ScpReplyReceiver_vt;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void scp_reply_cleanup(ScpReplyReceiver *reply)
|
|
|
|
{
|
|
|
|
sfree(reply->errmsg);
|
|
|
|
sfree((void *)reply->name.ptr);
|
|
|
|
sfree((void *)reply->handle.ptr);
|
|
|
|
sfree((void *)reply->data.ptr);
|
|
|
|
}
|
|
|
|
|
|
|
|
/* ----------------------------------------------------------------------
|
|
|
|
* Source end of the SCP protocol.
|
|
|
|
*/
|
|
|
|
|
|
|
|
#define SCP_MAX_BACKLOG 65536
|
|
|
|
|
|
|
|
typedef struct ScpSource ScpSource;
|
|
|
|
typedef struct ScpSourceStackEntry ScpSourceStackEntry;
|
|
|
|
|
|
|
|
struct ScpSource {
|
|
|
|
SftpServer *sf;
|
|
|
|
|
|
|
|
int acks, expect_newline, eof, throttled, finished;
|
|
|
|
|
|
|
|
SshChannel *sc;
|
|
|
|
ScpSourceStackEntry *head;
|
|
|
|
int recursive;
|
|
|
|
int send_file_times;
|
|
|
|
|
|
|
|
strbuf *pending_commands[3];
|
|
|
|
int n_pending_commands;
|
|
|
|
|
2018-10-27 01:08:58 +03:00
|
|
|
uint64_t file_offset, file_size;
|
2018-10-20 13:19:17 +03:00
|
|
|
|
|
|
|
ScpReplyReceiver reply;
|
|
|
|
|
|
|
|
ScpServer scpserver;
|
|
|
|
};
|
|
|
|
|
|
|
|
typedef enum ScpSourceNodeType ScpSourceNodeType;
|
|
|
|
enum ScpSourceNodeType { SCP_ROOTPATH, SCP_NAME, SCP_READDIR, SCP_READFILE };
|
|
|
|
|
|
|
|
struct ScpSourceStackEntry {
|
|
|
|
ScpSourceStackEntry *next;
|
|
|
|
ScpSourceNodeType type;
|
|
|
|
ptrlen pathname, handle;
|
|
|
|
const char *wildcard;
|
|
|
|
struct fxp_attrs attrs;
|
|
|
|
};
|
|
|
|
|
|
|
|
static void scp_source_push(ScpSource *scp, ScpSourceNodeType type,
|
|
|
|
ptrlen pathname, ptrlen handle,
|
|
|
|
const struct fxp_attrs *attrs, const char *wc)
|
|
|
|
{
|
|
|
|
size_t wc_len = wc ? strlen(wc)+1 : 0;
|
|
|
|
ScpSourceStackEntry *node = snew_plus(
|
|
|
|
ScpSourceStackEntry, pathname.len + handle.len + wc_len);
|
|
|
|
char *namebuf = snew_plus_get_aux(node);
|
|
|
|
memcpy(namebuf, pathname.ptr, pathname.len);
|
|
|
|
node->pathname = make_ptrlen(namebuf, pathname.len);
|
|
|
|
memcpy(namebuf + pathname.len, handle.ptr, handle.len);
|
|
|
|
node->handle = make_ptrlen(namebuf + pathname.len, handle.len);
|
|
|
|
if (wc) {
|
|
|
|
strcpy(namebuf + pathname.len + handle.len, wc);
|
|
|
|
node->wildcard = namebuf + pathname.len + handle.len;
|
|
|
|
} else {
|
|
|
|
node->wildcard = NULL;
|
|
|
|
}
|
|
|
|
node->attrs = attrs ? *attrs : no_attrs;
|
|
|
|
node->type = type;
|
|
|
|
node->next = scp->head;
|
|
|
|
scp->head = node;
|
|
|
|
}
|
|
|
|
|
|
|
|
static char *scp_source_err_base(ScpSource *scp, const char *fmt, va_list ap)
|
|
|
|
{
|
|
|
|
char *msg = dupvprintf(fmt, ap);
|
|
|
|
sshfwd_write_ext(scp->sc, TRUE, msg, strlen(msg));
|
|
|
|
sshfwd_write_ext(scp->sc, TRUE, "\012", 1);
|
|
|
|
return msg;
|
|
|
|
}
|
|
|
|
static void scp_source_err(ScpSource *scp, const char *fmt, ...)
|
|
|
|
{
|
|
|
|
va_list ap;
|
|
|
|
|
|
|
|
va_start(ap, fmt);
|
|
|
|
sfree(scp_source_err_base(scp, fmt, ap));
|
|
|
|
va_end(ap);
|
|
|
|
}
|
|
|
|
static void scp_source_abort(ScpSource *scp, const char *fmt, ...)
|
|
|
|
{
|
|
|
|
va_list ap;
|
|
|
|
char *msg;
|
|
|
|
|
|
|
|
va_start(ap, fmt);
|
|
|
|
msg = scp_source_err_base(scp, fmt, ap);
|
|
|
|
va_end(ap);
|
|
|
|
|
|
|
|
sshfwd_send_exit_status(scp->sc, 1);
|
|
|
|
sshfwd_write_eof(scp->sc);
|
|
|
|
sshfwd_initiate_close(scp->sc, msg);
|
|
|
|
|
|
|
|
scp->finished = TRUE;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void scp_source_push_name(
|
|
|
|
ScpSource *scp, ptrlen pathname, struct fxp_attrs attrs, const char *wc)
|
|
|
|
{
|
|
|
|
if (!(attrs.flags & SSH_FILEXFER_ATTR_PERMISSIONS)) {
|
|
|
|
scp_source_err(scp, "unable to read file permissions for %.*s",
|
|
|
|
PTRLEN_PRINTF(pathname));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (attrs.permissions & PERMS_DIRECTORY) {
|
|
|
|
if (!scp->recursive && !wc) {
|
|
|
|
scp_source_err(scp, "%.*s: is a directory",
|
|
|
|
PTRLEN_PRINTF(pathname));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
} else {
|
2018-10-26 17:37:32 +03:00
|
|
|
if (!(attrs.flags & SSH_FILEXFER_ATTR_SIZE)) {
|
2018-10-20 13:19:17 +03:00
|
|
|
scp_source_err(scp, "unable to read file size for %.*s",
|
|
|
|
PTRLEN_PRINTF(pathname));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
scp_source_push(scp, SCP_NAME, pathname, PTRLEN_LITERAL(""), &attrs, wc);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void scp_source_free(ScpServer *s);
|
|
|
|
static int scp_source_send(ScpServer *s, const void *data, size_t length);
|
|
|
|
static void scp_source_eof(ScpServer *s);
|
|
|
|
static void scp_source_throttle(ScpServer *s, int throttled);
|
|
|
|
|
|
|
|
static struct ScpServerVtable ScpSource_ScpServer_vt = {
|
|
|
|
scp_source_free,
|
|
|
|
scp_source_send,
|
|
|
|
scp_source_throttle,
|
|
|
|
scp_source_eof,
|
|
|
|
};
|
|
|
|
|
|
|
|
static ScpSource *scp_source_new(
|
|
|
|
SshChannel *sc, const SftpServerVtable *sftpserver_vt, ptrlen pathname)
|
|
|
|
{
|
|
|
|
ScpSource *scp = snew(ScpSource);
|
|
|
|
memset(scp, 0, sizeof(*scp));
|
|
|
|
|
|
|
|
scp->scpserver.vt = &ScpSource_ScpServer_vt;
|
|
|
|
scp_reply_setup(&scp->reply);
|
|
|
|
scp->sc = sc;
|
|
|
|
scp->sf = sftpsrv_new(sftpserver_vt);
|
|
|
|
scp->n_pending_commands = 0;
|
|
|
|
|
|
|
|
scp_source_push(scp, SCP_ROOTPATH, pathname, PTRLEN_LITERAL(""),
|
|
|
|
NULL, NULL);
|
|
|
|
|
|
|
|
return scp;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void scp_source_free(ScpServer *s)
|
|
|
|
{
|
|
|
|
ScpSource *scp = container_of(s, ScpSource, scpserver);
|
|
|
|
scp_reply_cleanup(&scp->reply);
|
|
|
|
while (scp->n_pending_commands > 0)
|
|
|
|
strbuf_free(scp->pending_commands[--scp->n_pending_commands]);
|
|
|
|
while (scp->head) {
|
|
|
|
ScpSourceStackEntry *node = scp->head;
|
|
|
|
scp->head = node->next;
|
|
|
|
sfree(node);
|
|
|
|
}
|
|
|
|
sfree(scp);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void scp_source_send_E(ScpSource *scp)
|
|
|
|
{
|
|
|
|
strbuf *cmd;
|
|
|
|
|
|
|
|
assert(scp->n_pending_commands == 0);
|
|
|
|
|
|
|
|
scp->pending_commands[scp->n_pending_commands++] = cmd = strbuf_new();
|
|
|
|
strbuf_catf(cmd, "E\012");
|
|
|
|
}
|
|
|
|
|
|
|
|
static void scp_source_send_CD(
|
|
|
|
ScpSource *scp, char cmdchar,
|
2018-10-27 01:08:58 +03:00
|
|
|
struct fxp_attrs attrs, uint64_t size, ptrlen name)
|
2018-10-20 13:19:17 +03:00
|
|
|
{
|
|
|
|
strbuf *cmd;
|
|
|
|
|
|
|
|
assert(scp->n_pending_commands == 0);
|
|
|
|
|
|
|
|
if (scp->send_file_times && (attrs.flags & SSH_FILEXFER_ATTR_ACMODTIME)) {
|
|
|
|
scp->pending_commands[scp->n_pending_commands++] = cmd = strbuf_new();
|
|
|
|
/* Our SFTP-based filesystem API doesn't support microsecond times */
|
|
|
|
strbuf_catf(cmd, "T%lu 0 %lu 0\012", attrs.mtime, attrs.atime);
|
|
|
|
}
|
|
|
|
|
|
|
|
const char *slash;
|
|
|
|
while ((slash = memchr(name.ptr, '/', name.len)) != NULL)
|
|
|
|
name = make_ptrlen(
|
|
|
|
slash+1, name.len - (slash+1 - (const char *)name.ptr));
|
|
|
|
|
|
|
|
scp->pending_commands[scp->n_pending_commands++] = cmd = strbuf_new();
|
2018-10-27 01:08:58 +03:00
|
|
|
strbuf_catf(cmd, "%c%04o %"PRIu64" %.*s\012", cmdchar,
|
2018-10-20 13:19:17 +03:00
|
|
|
(unsigned)(attrs.permissions & 07777),
|
2018-10-27 01:08:58 +03:00
|
|
|
size, PTRLEN_PRINTF(name));
|
2018-10-20 13:19:17 +03:00
|
|
|
|
|
|
|
if (cmdchar == 'C') {
|
|
|
|
/* We'll also wait for an ack before sending the file data,
|
|
|
|
* which we record by saving a zero-length 'command' to be
|
|
|
|
* sent after the C. */
|
|
|
|
scp->pending_commands[scp->n_pending_commands++] = cmd = strbuf_new();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void scp_source_process_stack(ScpSource *scp);
|
|
|
|
static void scp_source_process_stack_cb(void *vscp)
|
|
|
|
{
|
|
|
|
ScpSource *scp = (ScpSource *)vscp;
|
|
|
|
if (scp->finished)
|
|
|
|
return; /* this callback is out of date */
|
|
|
|
scp_source_process_stack(scp);
|
|
|
|
}
|
|
|
|
static void scp_requeue(ScpSource *scp)
|
|
|
|
{
|
|
|
|
queue_toplevel_callback(scp_source_process_stack_cb, scp);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void scp_source_process_stack(ScpSource *scp)
|
|
|
|
{
|
|
|
|
if (scp->throttled)
|
|
|
|
return;
|
|
|
|
|
|
|
|
while (scp->n_pending_commands > 0) {
|
|
|
|
/* Expect an ack, and consume it */
|
|
|
|
if (scp->eof) {
|
|
|
|
scp_source_abort(
|
|
|
|
scp, "scp: received client EOF, abandoning transfer");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (scp->acks == 0)
|
|
|
|
return;
|
|
|
|
scp->acks--;
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Now send the actual command (unless it was the phony
|
|
|
|
* zero-length one that indicates our need for an ack before
|
|
|
|
* beginning to send file data).
|
|
|
|
*/
|
|
|
|
|
|
|
|
if (scp->pending_commands[0]->len)
|
|
|
|
sshfwd_write(scp->sc, scp->pending_commands[0]->s,
|
|
|
|
scp->pending_commands[0]->len);
|
|
|
|
|
|
|
|
strbuf_free(scp->pending_commands[0]);
|
|
|
|
scp->n_pending_commands--;
|
|
|
|
if (scp->n_pending_commands > 0) {
|
|
|
|
/*
|
|
|
|
* We still have at least one pending command to send, so
|
|
|
|
* move up the queue.
|
|
|
|
*
|
|
|
|
* (We do that with a bodgy memmove, because there are at
|
|
|
|
* most a bounded number of commands ever pending at once,
|
|
|
|
* so no need to worry about quadratic time.)
|
|
|
|
*/
|
|
|
|
memmove(scp->pending_commands, scp->pending_commands+1,
|
|
|
|
scp->n_pending_commands * sizeof(*scp->pending_commands));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Mostly, we start by waiting for an ack byte from the receiver.
|
|
|
|
*/
|
2018-10-27 01:08:58 +03:00
|
|
|
if (scp->head && scp->head->type == SCP_READFILE && scp->file_offset) {
|
2018-10-20 13:19:17 +03:00
|
|
|
/*
|
|
|
|
* Exception: if we're already in the middle of transferring a
|
|
|
|
* file, we'll be called back here because the channel backlog
|
|
|
|
* has cleared; we don't need to wait for an ack.
|
|
|
|
*/
|
|
|
|
} else if (scp->head && scp->head->type == SCP_ROOTPATH) {
|
|
|
|
/*
|
|
|
|
* Another exception: the initial action node that makes us
|
|
|
|
* stat the root path. We'll translate it into an SCP_NAME,
|
|
|
|
* and _that_ will require an ack.
|
|
|
|
*/
|
|
|
|
ScpSourceStackEntry *node = scp->head;
|
|
|
|
scp->head = node->next;
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Start by checking if there's a wildcard involved in the
|
|
|
|
* root path.
|
|
|
|
*/
|
|
|
|
char *rootpath_str = mkstr(node->pathname);
|
|
|
|
char *rootpath_unesc = snewn(1+node->pathname.len, char);
|
|
|
|
ptrlen pathname;
|
|
|
|
const char *wildcard;
|
|
|
|
|
|
|
|
if (wc_unescape(rootpath_unesc, rootpath_str)) {
|
|
|
|
/*
|
|
|
|
* We successfully removed instances of the escape
|
|
|
|
* character used in our wildcard syntax, without
|
|
|
|
* encountering any actual wildcard chars - i.e. this is
|
|
|
|
* not a wildcard, just a single file. The simple case.
|
|
|
|
*/
|
|
|
|
pathname = ptrlen_from_asciz(rootpath_str);
|
|
|
|
wildcard = NULL;
|
|
|
|
} else {
|
|
|
|
/*
|
|
|
|
* This is a wildcard. Separate it into a directory name
|
|
|
|
* (which we enforce mustn't contain wc characters, for
|
|
|
|
* simplicity) and a wildcard to match leaf names.
|
|
|
|
*/
|
|
|
|
char *last_slash = strrchr(rootpath_str, '/');
|
|
|
|
|
|
|
|
if (last_slash) {
|
|
|
|
wildcard = last_slash + 1;
|
|
|
|
*last_slash = '\0';
|
|
|
|
if (!wc_unescape(rootpath_unesc, rootpath_str)) {
|
|
|
|
scp_source_abort(scp, "scp: wildcards in path components "
|
|
|
|
"before the file name not supported");
|
|
|
|
sfree(rootpath_str);
|
|
|
|
sfree(rootpath_unesc);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
pathname = ptrlen_from_asciz(rootpath_unesc);
|
|
|
|
} else {
|
|
|
|
pathname = PTRLEN_LITERAL(".");
|
|
|
|
wildcard = rootpath_str;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Now we know what directory we're scanning, and what
|
|
|
|
* wildcard (if any) we're using to match the filenames we get
|
|
|
|
* back.
|
|
|
|
*/
|
|
|
|
sftpsrv_stat(scp->sf, &scp->reply.srb, pathname, TRUE);
|
|
|
|
if (scp->reply.err) {
|
|
|
|
scp_source_abort(
|
|
|
|
scp, "%.*s: unable to access: %s",
|
|
|
|
PTRLEN_PRINTF(pathname), scp->reply.errmsg);
|
|
|
|
sfree(rootpath_str);
|
|
|
|
sfree(rootpath_unesc);
|
|
|
|
sfree(node);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
scp_source_push_name(scp, pathname, scp->reply.attrs, wildcard);
|
|
|
|
|
|
|
|
sfree(rootpath_str);
|
|
|
|
sfree(rootpath_unesc);
|
|
|
|
sfree(node);
|
|
|
|
scp_requeue(scp);
|
|
|
|
return;
|
|
|
|
} else {
|
|
|
|
}
|
|
|
|
|
|
|
|
if (scp->head && scp->head->type == SCP_READFILE) {
|
|
|
|
/*
|
|
|
|
* Transfer file data if our backlog hasn't filled up.
|
|
|
|
*/
|
|
|
|
int backlog;
|
2018-10-27 01:08:58 +03:00
|
|
|
uint64_t limit = scp->file_size - scp->file_offset;
|
|
|
|
if (limit > 4096)
|
|
|
|
limit = 4096;
|
|
|
|
if (limit > 0) {
|
2018-10-20 13:19:17 +03:00
|
|
|
sftpsrv_read(scp->sf, &scp->reply.srb, scp->head->handle,
|
2018-10-27 01:08:58 +03:00
|
|
|
scp->file_offset, limit);
|
2018-10-20 13:19:17 +03:00
|
|
|
if (scp->reply.err) {
|
|
|
|
scp_source_abort(
|
|
|
|
scp, "%.*s: unable to read: %s",
|
|
|
|
PTRLEN_PRINTF(scp->head->pathname), scp->reply.errmsg);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
backlog = sshfwd_write(
|
|
|
|
scp->sc, scp->reply.data.ptr, scp->reply.data.len);
|
2018-10-27 01:08:58 +03:00
|
|
|
scp->file_offset += scp->reply.data.len;
|
2018-10-20 13:19:17 +03:00
|
|
|
|
|
|
|
if (backlog < SCP_MAX_BACKLOG)
|
|
|
|
scp_requeue(scp);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* If we're done, send a terminating zero byte, close our file
|
|
|
|
* handle, and pop the stack.
|
|
|
|
*/
|
|
|
|
sshfwd_write(scp->sc, "\0", 1);
|
|
|
|
sftpsrv_close(scp->sf, &scp->reply.srb, scp->head->handle);
|
|
|
|
ScpSourceStackEntry *node = scp->head;
|
|
|
|
scp->head = node->next;
|
|
|
|
sfree(node);
|
|
|
|
scp_requeue(scp);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* If our queue is actually empty, send outgoing EOF.
|
|
|
|
*/
|
|
|
|
if (!scp->head) {
|
|
|
|
sshfwd_send_exit_status(scp->sc, 0);
|
|
|
|
sshfwd_write_eof(scp->sc);
|
|
|
|
sshfwd_initiate_close(scp->sc, NULL);
|
|
|
|
scp->finished = TRUE;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Otherwise, handle a command.
|
|
|
|
*/
|
|
|
|
ScpSourceStackEntry *node = scp->head;
|
|
|
|
scp->head = node->next;
|
|
|
|
|
|
|
|
if (node->type == SCP_READDIR) {
|
|
|
|
sftpsrv_readdir(scp->sf, &scp->reply.srb, node->handle, 1, TRUE);
|
|
|
|
if (scp->reply.err) {
|
|
|
|
if (scp->reply.code != SSH_FX_EOF)
|
|
|
|
scp_source_err(scp, "%.*s: unable to list directory: %s",
|
|
|
|
PTRLEN_PRINTF(node->pathname),
|
|
|
|
scp->reply.errmsg);
|
|
|
|
sftpsrv_close(scp->sf, &scp->reply.srb, node->handle);
|
|
|
|
|
|
|
|
if (!node->wildcard) {
|
|
|
|
/*
|
|
|
|
* Send 'pop stack' or 'end of directory' command,
|
|
|
|
* unless this was the topmost READDIR in a
|
|
|
|
* wildcard-based retrieval (in which case we didn't
|
|
|
|
* send a D command to start, so an E now would have
|
|
|
|
* no stack entry to pop).
|
|
|
|
*/
|
|
|
|
scp_source_send_E(scp);
|
|
|
|
}
|
|
|
|
} else if (ptrlen_eq_string(scp->reply.name, ".") ||
|
|
|
|
ptrlen_eq_string(scp->reply.name, "..") ||
|
|
|
|
(node->wildcard &&
|
|
|
|
!wc_match_pl(node->wildcard, scp->reply.name))) {
|
|
|
|
/* Skip special directory names . and .., and anything
|
|
|
|
* that doesn't match our wildcard (if we have one). */
|
|
|
|
scp->head = node; /* put back the unfinished READDIR */
|
|
|
|
node = NULL; /* and prevent it being freed */
|
|
|
|
} else {
|
|
|
|
ptrlen subpath;
|
|
|
|
subpath.len = node->pathname.len + 1 + scp->reply.name.len;
|
|
|
|
char *subpath_space = snewn(subpath.len, char);
|
|
|
|
subpath.ptr = subpath_space;
|
|
|
|
memcpy(subpath_space, node->pathname.ptr, node->pathname.len);
|
|
|
|
subpath_space[node->pathname.len] = '/';
|
|
|
|
memcpy(subpath_space + node->pathname.len + 1,
|
|
|
|
scp->reply.name.ptr, scp->reply.name.len);
|
|
|
|
|
|
|
|
scp->head = node; /* put back the unfinished READDIR */
|
|
|
|
node = NULL; /* and prevent it being freed */
|
|
|
|
scp_source_push_name(scp, subpath, scp->reply.attrs, NULL);
|
|
|
|
|
|
|
|
sfree(subpath_space);
|
|
|
|
}
|
|
|
|
} else if (node->attrs.permissions & PERMS_DIRECTORY) {
|
|
|
|
assert(scp->recursive || node->wildcard);
|
|
|
|
|
|
|
|
if (!node->wildcard)
|
2018-10-27 01:08:58 +03:00
|
|
|
scp_source_send_CD(scp, 'D', node->attrs, 0, node->pathname);
|
2018-10-20 13:19:17 +03:00
|
|
|
sftpsrv_opendir(scp->sf, &scp->reply.srb, node->pathname);
|
|
|
|
if (scp->reply.err) {
|
|
|
|
scp_source_err(
|
|
|
|
scp, "%.*s: unable to access: %s",
|
|
|
|
PTRLEN_PRINTF(node->pathname), scp->reply.errmsg);
|
|
|
|
|
|
|
|
if (!node->wildcard) {
|
|
|
|
/* Send 'pop stack' or 'end of directory' command. */
|
|
|
|
scp_source_send_E(scp);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
scp_source_push(
|
|
|
|
scp, SCP_READDIR, node->pathname,
|
|
|
|
scp->reply.handle, NULL, node->wildcard);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
sftpsrv_open(scp->sf, &scp->reply.srb,
|
|
|
|
node->pathname, SSH_FXF_READ, no_attrs);
|
|
|
|
if (scp->reply.err) {
|
|
|
|
scp_source_err(
|
|
|
|
scp, "%.*s: unable to open: %s",
|
|
|
|
PTRLEN_PRINTF(node->pathname), scp->reply.errmsg);
|
|
|
|
scp_requeue(scp);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
sftpsrv_fstat(scp->sf, &scp->reply.srb, scp->reply.handle);
|
|
|
|
if (scp->reply.err) {
|
|
|
|
scp_source_err(
|
|
|
|
scp, "%.*s: unable to stat: %s",
|
|
|
|
PTRLEN_PRINTF(node->pathname), scp->reply.errmsg);
|
|
|
|
sftpsrv_close(scp->sf, &scp->reply.srb, scp->reply.handle);
|
|
|
|
scp_requeue(scp);
|
|
|
|
return;
|
|
|
|
}
|
2018-10-27 01:08:58 +03:00
|
|
|
scp->file_offset = 0;
|
2018-10-20 13:19:17 +03:00
|
|
|
scp->file_size = scp->reply.attrs.size;
|
|
|
|
scp_source_send_CD(scp, 'C', node->attrs,
|
|
|
|
scp->file_size, node->pathname);
|
|
|
|
scp_source_push(
|
|
|
|
scp, SCP_READFILE, node->pathname, scp->reply.handle, NULL, NULL);
|
|
|
|
}
|
|
|
|
sfree(node);
|
|
|
|
scp_requeue(scp);
|
|
|
|
}
|
|
|
|
|
|
|
|
static int scp_source_send(ScpServer *s, const void *vdata, size_t length)
|
|
|
|
{
|
|
|
|
ScpSource *scp = container_of(s, ScpSource, scpserver);
|
|
|
|
const char *data = (const char *)vdata;
|
|
|
|
size_t i;
|
|
|
|
|
|
|
|
if (scp->finished)
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
for (i = 0; i < length; i++) {
|
|
|
|
if (scp->expect_newline) {
|
|
|
|
if (data[i] == '\012') {
|
|
|
|
/* End of an error message following a 1 byte */
|
|
|
|
scp->expect_newline = FALSE;
|
|
|
|
scp->acks++;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
switch (data[i]) {
|
|
|
|
case 0: /* ordinary ack */
|
|
|
|
scp->acks++;
|
|
|
|
break;
|
|
|
|
case 1: /* non-fatal error; consume it */
|
|
|
|
scp->expect_newline = TRUE;
|
|
|
|
break;
|
|
|
|
case 2:
|
|
|
|
scp_source_abort(
|
|
|
|
scp, "terminating on fatal error from client");
|
|
|
|
return 0;
|
|
|
|
default:
|
|
|
|
scp_source_abort(
|
|
|
|
scp, "unrecognised response code from client");
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
scp_source_process_stack(scp);
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void scp_source_throttle(ScpServer *s, int throttled)
|
|
|
|
{
|
|
|
|
ScpSource *scp = container_of(s, ScpSource, scpserver);
|
|
|
|
|
|
|
|
if (scp->finished)
|
|
|
|
return;
|
|
|
|
|
|
|
|
scp->throttled = throttled;
|
|
|
|
if (!throttled)
|
|
|
|
scp_source_process_stack(scp);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void scp_source_eof(ScpServer *s)
|
|
|
|
{
|
|
|
|
ScpSource *scp = container_of(s, ScpSource, scpserver);
|
|
|
|
|
|
|
|
if (scp->finished)
|
|
|
|
return;
|
|
|
|
|
|
|
|
scp->eof = TRUE;
|
|
|
|
scp_source_process_stack(scp);
|
|
|
|
}
|
|
|
|
|
|
|
|
/* ----------------------------------------------------------------------
|
|
|
|
* Sink end of the SCP protocol.
|
|
|
|
*/
|
|
|
|
|
|
|
|
typedef struct ScpSink ScpSink;
|
|
|
|
typedef struct ScpSinkStackEntry ScpSinkStackEntry;
|
|
|
|
|
|
|
|
struct ScpSink {
|
|
|
|
SftpServer *sf;
|
|
|
|
|
|
|
|
SshChannel *sc;
|
|
|
|
ScpSinkStackEntry *head;
|
|
|
|
|
2018-10-27 01:08:58 +03:00
|
|
|
uint64_t file_offset, file_size;
|
2018-10-20 13:19:17 +03:00
|
|
|
unsigned long atime, mtime;
|
|
|
|
int got_file_times;
|
|
|
|
|
|
|
|
bufchain data;
|
|
|
|
int input_eof;
|
|
|
|
strbuf *command;
|
|
|
|
char command_chr;
|
|
|
|
|
|
|
|
strbuf *filename_sb;
|
|
|
|
ptrlen filename;
|
|
|
|
struct fxp_attrs attrs;
|
|
|
|
|
|
|
|
char *errmsg;
|
|
|
|
|
|
|
|
int crState;
|
|
|
|
|
|
|
|
ScpReplyReceiver reply;
|
|
|
|
|
|
|
|
ScpServer scpserver;
|
|
|
|
};
|
|
|
|
|
|
|
|
struct ScpSinkStackEntry {
|
|
|
|
ScpSinkStackEntry *next;
|
|
|
|
ptrlen destpath;
|
|
|
|
|
|
|
|
/*
|
|
|
|
* If isdir is TRUE, then destpath identifies a directory that the
|
|
|
|
* files we receive should be created inside. If it's FALSE, then
|
|
|
|
* it identifies the exact pathname the next file we receive
|
|
|
|
* should be created _as_ - regardless of the filename in the 'C'
|
|
|
|
* command.
|
|
|
|
*/
|
|
|
|
int isdir;
|
|
|
|
};
|
|
|
|
|
|
|
|
static void scp_sink_push(ScpSink *scp, ptrlen pathname, int isdir)
|
|
|
|
{
|
|
|
|
ScpSinkStackEntry *node = snew_plus(ScpSinkStackEntry, pathname.len);
|
|
|
|
char *p = snew_plus_get_aux(node);
|
|
|
|
|
|
|
|
node->destpath.ptr = p;
|
|
|
|
node->destpath.len = pathname.len;
|
|
|
|
memcpy(p, pathname.ptr, pathname.len);
|
|
|
|
node->isdir = isdir;
|
|
|
|
|
|
|
|
node->next = scp->head;
|
|
|
|
scp->head = node;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void scp_sink_pop(ScpSink *scp)
|
|
|
|
{
|
|
|
|
ScpSinkStackEntry *node = scp->head;
|
|
|
|
scp->head = node->next;
|
|
|
|
sfree(node);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void scp_sink_free(ScpServer *s);
|
|
|
|
static int scp_sink_send(ScpServer *s, const void *data, size_t length);
|
|
|
|
static void scp_sink_eof(ScpServer *s);
|
|
|
|
static void scp_sink_throttle(ScpServer *s, int throttled) {}
|
|
|
|
|
|
|
|
static struct ScpServerVtable ScpSink_ScpServer_vt = {
|
|
|
|
scp_sink_free,
|
|
|
|
scp_sink_send,
|
|
|
|
scp_sink_throttle,
|
|
|
|
scp_sink_eof,
|
|
|
|
};
|
|
|
|
|
|
|
|
static void scp_sink_coroutine(ScpSink *scp);
|
|
|
|
static void scp_sink_start_callback(void *vscp)
|
|
|
|
{
|
|
|
|
scp_sink_coroutine((ScpSink *)vscp);
|
|
|
|
}
|
|
|
|
|
|
|
|
static ScpSink *scp_sink_new(
|
|
|
|
SshChannel *sc, const SftpServerVtable *sftpserver_vt, ptrlen pathname,
|
|
|
|
int pathname_is_definitely_dir)
|
|
|
|
{
|
|
|
|
ScpSink *scp = snew(ScpSink);
|
|
|
|
memset(scp, 0, sizeof(*scp));
|
|
|
|
|
|
|
|
scp->scpserver.vt = &ScpSink_ScpServer_vt;
|
|
|
|
scp_reply_setup(&scp->reply);
|
|
|
|
scp->sc = sc;
|
|
|
|
scp->sf = sftpsrv_new(sftpserver_vt);
|
|
|
|
bufchain_init(&scp->data);
|
|
|
|
scp->command = strbuf_new();
|
|
|
|
scp->filename_sb = strbuf_new();
|
|
|
|
|
|
|
|
if (!pathname_is_definitely_dir) {
|
|
|
|
/*
|
|
|
|
* If our root pathname is not already expected to be a
|
|
|
|
* directory because of the -d option in the command line,
|
|
|
|
* test it ourself to see whether it is or not.
|
|
|
|
*/
|
|
|
|
sftpsrv_stat(scp->sf, &scp->reply.srb, pathname, TRUE);
|
|
|
|
if (!scp->reply.err &&
|
|
|
|
(scp->reply.attrs.flags & SSH_FILEXFER_ATTR_PERMISSIONS) &&
|
|
|
|
(scp->reply.attrs.permissions & PERMS_DIRECTORY))
|
|
|
|
pathname_is_definitely_dir = TRUE;
|
|
|
|
}
|
|
|
|
scp_sink_push(scp, pathname, pathname_is_definitely_dir);
|
|
|
|
|
|
|
|
queue_toplevel_callback(scp_sink_start_callback, scp);
|
|
|
|
|
|
|
|
return scp;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void scp_sink_free(ScpServer *s)
|
|
|
|
{
|
|
|
|
ScpSink *scp = container_of(s, ScpSink, scpserver);
|
|
|
|
|
|
|
|
scp_reply_cleanup(&scp->reply);
|
|
|
|
bufchain_clear(&scp->data);
|
|
|
|
strbuf_free(scp->command);
|
|
|
|
strbuf_free(scp->filename_sb);
|
|
|
|
while (scp->head)
|
|
|
|
scp_sink_pop(scp);
|
|
|
|
sfree(scp->errmsg);
|
|
|
|
|
|
|
|
delete_callbacks_for_context(scp);
|
|
|
|
|
|
|
|
sfree(scp);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void scp_sink_coroutine(ScpSink *scp)
|
|
|
|
{
|
|
|
|
crBegin(scp->crState);
|
|
|
|
|
|
|
|
while (1) {
|
|
|
|
/*
|
|
|
|
* Send an ack, and read a command.
|
|
|
|
*/
|
|
|
|
sshfwd_write(scp->sc, "\0", 1);
|
|
|
|
scp->command->len = 0;
|
|
|
|
while (1) {
|
|
|
|
crMaybeWaitUntilV(scp->input_eof || bufchain_size(&scp->data) > 0);
|
|
|
|
if (scp->input_eof)
|
|
|
|
goto done;
|
|
|
|
|
|
|
|
void *vdata;
|
|
|
|
int len;
|
|
|
|
const char *cdata, *newline;
|
|
|
|
|
|
|
|
bufchain_prefix(&scp->data, &vdata, &len);
|
|
|
|
cdata = vdata;
|
|
|
|
newline = memchr(cdata, '\012', len);
|
|
|
|
if (newline)
|
|
|
|
len = (int)(newline+1 - cdata);
|
|
|
|
put_data(scp->command, cdata, len);
|
|
|
|
bufchain_consume(&scp->data, len);
|
|
|
|
|
|
|
|
if (newline)
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Parse the command.
|
|
|
|
*/
|
|
|
|
scp->command->len--; /* chomp the newline */
|
|
|
|
scp->command_chr = scp->command->len > 0 ? scp->command->s[0] : '\0';
|
|
|
|
if (scp->command_chr == 'T') {
|
|
|
|
unsigned long dummy1, dummy2;
|
|
|
|
if (sscanf(scp->command->s, "T%lu %lu %lu %lu",
|
|
|
|
&scp->mtime, &dummy1, &scp->atime, &dummy2) != 4)
|
|
|
|
goto parse_error;
|
|
|
|
scp->got_file_times = TRUE;
|
|
|
|
} else if (scp->command_chr == 'C' || scp->command_chr == 'D') {
|
|
|
|
/*
|
|
|
|
* Common handling of the start of this case, because the
|
|
|
|
* messages are parsed similarly. We diverge later.
|
|
|
|
*/
|
|
|
|
const char *q, *p = scp->command->s + 1; /* skip the 'C' */
|
|
|
|
|
|
|
|
scp->attrs.flags = SSH_FILEXFER_ATTR_PERMISSIONS;
|
|
|
|
scp->attrs.permissions = 0;
|
|
|
|
while (*p >= '0' && *p <= '7') {
|
|
|
|
scp->attrs.permissions =
|
|
|
|
scp->attrs.permissions * 8 + (*p - '0');
|
|
|
|
p++;
|
|
|
|
}
|
|
|
|
if (*p != ' ')
|
|
|
|
goto parse_error;
|
|
|
|
p++;
|
|
|
|
|
|
|
|
q = p;
|
|
|
|
while (*p >= '0' && *p <= '9')
|
|
|
|
p++;
|
|
|
|
if (*p != ' ')
|
|
|
|
goto parse_error;
|
|
|
|
p++;
|
2018-10-27 01:08:58 +03:00
|
|
|
scp->file_size = strtoull(q, NULL, 10);
|
2018-10-20 13:19:17 +03:00
|
|
|
|
|
|
|
ptrlen leafname = make_ptrlen(
|
|
|
|
p, scp->command->len - (p - scp->command->s));
|
|
|
|
scp->filename_sb->len = 0;
|
|
|
|
put_data(scp->filename_sb, scp->head->destpath.ptr,
|
|
|
|
scp->head->destpath.len);
|
|
|
|
if (scp->head->isdir) {
|
|
|
|
if (scp->filename_sb->len > 0 &&
|
|
|
|
scp->filename_sb->s[scp->filename_sb->len-1]
|
|
|
|
!= '/')
|
|
|
|
put_byte(scp->filename_sb, '/');
|
|
|
|
put_data(scp->filename_sb, leafname.ptr, leafname.len);
|
|
|
|
}
|
|
|
|
scp->filename = ptrlen_from_strbuf(scp->filename_sb);
|
|
|
|
|
|
|
|
if (scp->got_file_times) {
|
|
|
|
scp->attrs.mtime = scp->mtime;
|
|
|
|
scp->attrs.atime = scp->atime;
|
|
|
|
scp->attrs.flags |= SSH_FILEXFER_ATTR_ACMODTIME;
|
|
|
|
}
|
|
|
|
scp->got_file_times = FALSE;
|
|
|
|
|
|
|
|
if (scp->command_chr == 'D') {
|
|
|
|
sftpsrv_mkdir(scp->sf, &scp->reply.srb,
|
|
|
|
scp->filename, scp->attrs);
|
|
|
|
|
|
|
|
if (scp->reply.err) {
|
|
|
|
scp->errmsg = dupprintf(
|
|
|
|
"'%.*s': unable to create directory: %s",
|
|
|
|
PTRLEN_PRINTF(scp->filename), scp->reply.errmsg);
|
|
|
|
goto done;
|
|
|
|
}
|
|
|
|
|
|
|
|
scp_sink_push(scp, scp->filename, TRUE);
|
|
|
|
} else {
|
|
|
|
sftpsrv_open(scp->sf, &scp->reply.srb, scp->filename,
|
|
|
|
SSH_FXF_WRITE | SSH_FXF_CREAT | SSH_FXF_TRUNC,
|
|
|
|
scp->attrs);
|
|
|
|
if (scp->reply.err) {
|
|
|
|
scp->errmsg = dupprintf(
|
|
|
|
"'%.*s': unable to open file: %s",
|
|
|
|
PTRLEN_PRINTF(scp->filename), scp->reply.errmsg);
|
|
|
|
goto done;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Now send an ack, and read the file data.
|
|
|
|
*/
|
|
|
|
sshfwd_write(scp->sc, "\0", 1);
|
2018-10-27 01:08:58 +03:00
|
|
|
scp->file_offset = 0;
|
|
|
|
while (scp->file_offset < scp->file_size) {
|
2018-10-20 13:19:17 +03:00
|
|
|
void *vdata;
|
|
|
|
int len;
|
2018-10-27 01:08:58 +03:00
|
|
|
uint64_t this_len, remaining;
|
2018-10-20 13:19:17 +03:00
|
|
|
|
|
|
|
crMaybeWaitUntilV(
|
|
|
|
scp->input_eof || bufchain_size(&scp->data) > 0);
|
|
|
|
if (scp->input_eof) {
|
|
|
|
sftpsrv_close(scp->sf, &scp->reply.srb,
|
|
|
|
scp->reply.handle);
|
|
|
|
goto done;
|
|
|
|
}
|
|
|
|
|
|
|
|
bufchain_prefix(&scp->data, &vdata, &len);
|
2018-10-27 01:08:58 +03:00
|
|
|
this_len = len;
|
|
|
|
remaining = scp->file_size - scp->file_offset;
|
|
|
|
if (this_len > remaining)
|
2018-10-20 13:19:17 +03:00
|
|
|
this_len = remaining;
|
|
|
|
sftpsrv_write(scp->sf, &scp->reply.srb,
|
|
|
|
scp->reply.handle, scp->file_offset,
|
2018-10-27 01:08:58 +03:00
|
|
|
make_ptrlen(vdata, this_len));
|
2018-10-20 13:19:17 +03:00
|
|
|
if (scp->reply.err) {
|
|
|
|
scp->errmsg = dupprintf(
|
|
|
|
"'%.*s': unable to write to file: %s",
|
|
|
|
PTRLEN_PRINTF(scp->filename), scp->reply.errmsg);
|
|
|
|
goto done;
|
|
|
|
}
|
2018-10-27 01:08:58 +03:00
|
|
|
bufchain_consume(&scp->data, this_len);
|
|
|
|
scp->file_offset += this_len;
|
2018-10-20 13:19:17 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Wait for the trailing NUL byte.
|
|
|
|
*/
|
|
|
|
crMaybeWaitUntilV(
|
|
|
|
scp->input_eof || bufchain_size(&scp->data) > 0);
|
|
|
|
if (scp->input_eof) {
|
|
|
|
sftpsrv_close(scp->sf, &scp->reply.srb,
|
|
|
|
scp->reply.handle);
|
|
|
|
goto done;
|
|
|
|
}
|
|
|
|
bufchain_consume(&scp->data, 1);
|
|
|
|
}
|
|
|
|
} else if (scp->command_chr == 'E') {
|
|
|
|
if (!scp->head) {
|
|
|
|
scp->errmsg = dupstr("received E command without matching D");
|
|
|
|
goto done;
|
|
|
|
}
|
|
|
|
scp_sink_pop(scp);
|
|
|
|
scp->got_file_times = FALSE;
|
|
|
|
} else {
|
|
|
|
ptrlen cmd_pl;
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Also come here if any of the above cases run into
|
|
|
|
* parsing difficulties.
|
|
|
|
*/
|
|
|
|
parse_error:
|
|
|
|
cmd_pl = ptrlen_from_strbuf(scp->command);
|
|
|
|
scp->errmsg = dupprintf("unrecognised scp command '%.*s'",
|
|
|
|
PTRLEN_PRINTF(cmd_pl));
|
|
|
|
goto done;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
done:
|
|
|
|
if (scp->errmsg) {
|
|
|
|
sshfwd_write_ext(scp->sc, TRUE, scp->errmsg, strlen(scp->errmsg));
|
|
|
|
sshfwd_write_ext(scp->sc, TRUE, "\012", 1);
|
|
|
|
sshfwd_send_exit_status(scp->sc, 1);
|
|
|
|
} else {
|
|
|
|
sshfwd_send_exit_status(scp->sc, 0);
|
|
|
|
}
|
|
|
|
sshfwd_write_eof(scp->sc);
|
|
|
|
sshfwd_initiate_close(scp->sc, scp->errmsg);
|
|
|
|
while (1) crReturnV;
|
|
|
|
|
|
|
|
crFinishV;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int scp_sink_send(ScpServer *s, const void *data, size_t length)
|
|
|
|
{
|
|
|
|
ScpSink *scp = container_of(s, ScpSink, scpserver);
|
|
|
|
|
|
|
|
if (!scp->input_eof) {
|
|
|
|
bufchain_add(&scp->data, data, length);
|
|
|
|
scp_sink_coroutine(scp);
|
|
|
|
}
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void scp_sink_eof(ScpServer *s)
|
|
|
|
{
|
|
|
|
ScpSink *scp = container_of(s, ScpSink, scpserver);
|
|
|
|
|
|
|
|
scp->input_eof = TRUE;
|
|
|
|
scp_sink_coroutine(scp);
|
|
|
|
}
|
|
|
|
|
|
|
|
/* ----------------------------------------------------------------------
|
|
|
|
* Top-level error handler, instantiated in the case where the user
|
|
|
|
* sent a command starting with "scp " that we couldn't make sense of.
|
|
|
|
*/
|
|
|
|
|
|
|
|
typedef struct ScpError ScpError;
|
|
|
|
|
|
|
|
struct ScpError {
|
|
|
|
SshChannel *sc;
|
|
|
|
char *message;
|
|
|
|
ScpServer scpserver;
|
|
|
|
};
|
|
|
|
|
|
|
|
static void scp_error_free(ScpServer *s);
|
|
|
|
|
|
|
|
static int scp_error_send(ScpServer *s, const void *data, size_t length)
|
|
|
|
{ return 0; }
|
|
|
|
static void scp_error_eof(ScpServer *s) {}
|
|
|
|
static void scp_error_throttle(ScpServer *s, int throttled) {}
|
|
|
|
|
|
|
|
static struct ScpServerVtable ScpError_ScpServer_vt = {
|
|
|
|
scp_error_free,
|
|
|
|
scp_error_send,
|
|
|
|
scp_error_throttle,
|
|
|
|
scp_error_eof,
|
|
|
|
};
|
|
|
|
|
|
|
|
static void scp_error_send_message_cb(void *vscp)
|
|
|
|
{
|
|
|
|
ScpError *scp = (ScpError *)vscp;
|
|
|
|
sshfwd_write_ext(scp->sc, TRUE, scp->message, strlen(scp->message));
|
|
|
|
sshfwd_write_ext(scp->sc, TRUE, "\n", 1);
|
|
|
|
sshfwd_send_exit_status(scp->sc, 1);
|
|
|
|
sshfwd_write_eof(scp->sc);
|
|
|
|
sshfwd_initiate_close(scp->sc, scp->message);
|
|
|
|
}
|
|
|
|
|
|
|
|
static ScpError *scp_error_new(SshChannel *sc, const char *fmt, ...)
|
|
|
|
{
|
|
|
|
va_list ap;
|
|
|
|
ScpError *scp = snew(ScpError);
|
|
|
|
|
|
|
|
memset(scp, 0, sizeof(*scp));
|
|
|
|
|
|
|
|
scp->scpserver.vt = &ScpError_ScpServer_vt;
|
|
|
|
scp->sc = sc;
|
|
|
|
|
|
|
|
va_start(ap, fmt);
|
|
|
|
scp->message = dupvprintf(fmt, ap);
|
|
|
|
va_end(ap);
|
|
|
|
|
|
|
|
queue_toplevel_callback(scp_error_send_message_cb, scp);
|
|
|
|
|
|
|
|
return scp;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void scp_error_free(ScpServer *s)
|
|
|
|
{
|
|
|
|
ScpError *scp = container_of(s, ScpError, scpserver);
|
|
|
|
|
|
|
|
sfree(scp->message);
|
|
|
|
|
|
|
|
delete_callbacks_for_context(scp);
|
|
|
|
|
|
|
|
sfree(scp);
|
|
|
|
}
|
|
|
|
|
|
|
|
/* ----------------------------------------------------------------------
|
|
|
|
* Top-level entry point, which parses a command sent from the SSH
|
|
|
|
* client, and if it recognises it as an scp command, instantiates an
|
|
|
|
* appropriate ScpServer implementation and returns it.
|
|
|
|
*/
|
|
|
|
|
|
|
|
ScpServer *scp_recognise_exec(
|
|
|
|
SshChannel *sc, const SftpServerVtable *sftpserver_vt, ptrlen command)
|
|
|
|
{
|
|
|
|
int recursive = FALSE, preserve = FALSE;
|
|
|
|
int targetshouldbedirectory = FALSE;
|
|
|
|
ptrlen command_orig = command;
|
|
|
|
|
|
|
|
if (!ptrlen_startswith(command, PTRLEN_LITERAL("scp "), &command))
|
|
|
|
return NULL;
|
|
|
|
|
|
|
|
while (1) {
|
|
|
|
if (ptrlen_startswith(command, PTRLEN_LITERAL("-v "), &command)) {
|
|
|
|
/* Enable verbose mode in the server, which we ignore */
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (ptrlen_startswith(command, PTRLEN_LITERAL("-r "), &command)) {
|
|
|
|
recursive = TRUE;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (ptrlen_startswith(command, PTRLEN_LITERAL("-p "), &command)) {
|
|
|
|
preserve = TRUE;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (ptrlen_startswith(command, PTRLEN_LITERAL("-d "), &command)) {
|
|
|
|
targetshouldbedirectory = TRUE;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (ptrlen_startswith(command, PTRLEN_LITERAL("-t "), &command)) {
|
|
|
|
ScpSink *scp = scp_sink_new(sc, sftpserver_vt, command,
|
|
|
|
targetshouldbedirectory);
|
|
|
|
return &scp->scpserver;
|
|
|
|
} else if (ptrlen_startswith(command, PTRLEN_LITERAL("-f "), &command)) {
|
|
|
|
ScpSource *scp = scp_source_new(sc, sftpserver_vt, command);
|
|
|
|
scp->recursive = recursive;
|
|
|
|
scp->send_file_times = preserve;
|
|
|
|
return &scp->scpserver;
|
|
|
|
} else {
|
|
|
|
ScpError *scp = scp_error_new(
|
|
|
|
sc, "Unable to parse scp command: '%.*s'",
|
|
|
|
PTRLEN_PRINTF(command_orig));
|
|
|
|
return &scp->scpserver;
|
|
|
|
}
|
|
|
|
}
|