diff options
Diffstat (limited to 'ssh/scpserver.c')
-rw-r--r-- | ssh/scpserver.c | 1399 |
1 files changed, 1399 insertions, 0 deletions
diff --git a/ssh/scpserver.c b/ssh/scpserver.c new file mode 100644 index 00000000..15633ecb --- /dev/null +++ b/ssh/scpserver.c @@ -0,0 +1,1399 @@ +/* + * 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 "channel.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 { + bool 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 SftpReplyBuilderVtable ScpReplyReceiver_vt = { + .reply_ok = scp_reply_ok, + .reply_error = scp_reply_error, + .reply_simple_name = scp_reply_simple_name, + .reply_name_count = scp_reply_name_count, + .reply_full_name = scp_reply_full_name, + .reply_handle = scp_reply_handle, + .reply_data = scp_reply_data, + .reply_attrs = 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; + bool expect_newline, eof, throttled, finished; + + SshChannel *sc; + ScpSourceStackEntry *head; + bool recursive; + bool send_file_times; + + strbuf *pending_commands[3]; + int n_pending_commands; + + uint64_t file_offset, file_size; + + 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 PRINTF_LIKE(2, 3) 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 PRINTF_LIKE(2, 3) 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 { + if (!(attrs.flags & SSH_FILEXFER_ATTR_SIZE)) { + 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 size_t 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, bool throttled); + +static const ScpServerVtable ScpSource_ScpServer_vt = { + .free = scp_source_free, + .send = scp_source_send, + .throttle = scp_source_throttle, + .eof = 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); + } + + delete_callbacks_for_context(scp); + + 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(); + put_fmt(cmd, "E\012"); +} + +static void scp_source_send_CD( + ScpSource *scp, char cmdchar, + struct fxp_attrs attrs, uint64_t size, ptrlen name) +{ + 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 */ + put_fmt(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(); + put_fmt(cmd, "%c%04o %"PRIu64" %.*s\012", cmdchar, + (unsigned)(attrs.permissions & 07777), + size, PTRLEN_PRINTF(name)); + + 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. + */ + if (scp->head && scp->head->type == SCP_READFILE && scp->file_offset) { + /* + * 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; + uint64_t limit = scp->file_size - scp->file_offset; + if (limit > 4096) + limit = 4096; + if (limit > 0) { + sftpsrv_read(scp->sf, &scp->reply.srb, scp->head->handle, + scp->file_offset, limit); + 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); + scp->file_offset += scp->reply.data.len; + + 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) + scp_source_send_CD(scp, 'D', node->attrs, 0, node->pathname); + 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; + } + scp->file_offset = 0; + 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 size_t 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, bool 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; + + uint64_t file_offset, file_size; + unsigned long atime, mtime; + bool got_file_times; + + bufchain data; + bool 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. + */ + bool isdir; +}; + +static void scp_sink_push(ScpSink *scp, ptrlen pathname, bool 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 size_t 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, bool throttled) {} + +static const ScpServerVtable ScpSink_ScpServer_vt = { + .free = scp_sink_free, + .send = scp_sink_send, + .throttle = scp_sink_throttle, + .eof = 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, + bool 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); + strbuf_clear(scp->command); + while (1) { + crMaybeWaitUntilV(scp->input_eof || bufchain_size(&scp->data) > 0); + if (scp->input_eof) + goto done; + + ptrlen data = bufchain_prefix(&scp->data); + const char *cdata = data.ptr; + const char *newline = memchr(cdata, '\012', data.len); + if (newline) + data.len = (int)(newline+1 - cdata); + put_data(scp->command, cdata, data.len); + bufchain_consume(&scp->data, data.len); + + if (newline) + break; + } + + /* + * Parse the command. + */ + strbuf_chomp(scp->command, '\n'); + 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++; + scp->file_size = strtoull(q, NULL, 10); + + ptrlen leafname = make_ptrlen( + p, scp->command->len - (p - scp->command->s)); + strbuf_clear(scp->filename_sb); + put_datapl(scp->filename_sb, scp->head->destpath); + 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_datapl(scp->filename_sb, leafname); + } + 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); + scp->file_offset = 0; + while (scp->file_offset < scp->file_size) { + ptrlen data; + uint64_t this_len, remaining; + + 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; + } + + data = bufchain_prefix(&scp->data); + this_len = data.len; + remaining = scp->file_size - scp->file_offset; + if (this_len > remaining) + this_len = remaining; + sftpsrv_write(scp->sf, &scp->reply.srb, + scp->reply.handle, scp->file_offset, + make_ptrlen(data.ptr, this_len)); + if (scp->reply.err) { + scp->errmsg = dupprintf( + "'%.*s': unable to write to file: %s", + PTRLEN_PRINTF(scp->filename), scp->reply.errmsg); + goto done; + } + bufchain_consume(&scp->data, this_len); + scp->file_offset += this_len; + } + + /* + * 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 size_t 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 size_t 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, bool throttled) {} + +static const ScpServerVtable ScpError_ScpServer_vt = { + .free = scp_error_free, + .send = scp_error_send, + .throttle = scp_error_throttle, + .eof = 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 PRINTF_LIKE(2, 3) 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) +{ + bool recursive = false, preserve = false; + bool 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; + } +} |