/* * Server side of the old-school SCP protocol. */ #include #include #include #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/. * * - "scp -t some_path_name" * * Client sends one C command, and server deposits it at * some_path_name itself, or in some_path_name/, 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 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; 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 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 { 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 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, bool 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, 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 */ 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(); strbuf_catf(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 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, 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 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, bool 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, 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); 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++; scp->file_size = strtoull(q, NULL, 10); ptrlen leafname = make_ptrlen( p, scp->command->len - (p - scp->command->s)); scp->filename_sb->len = 0; 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) { void *vdata; int len; 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; } bufchain_prefix(&scp->data, &vdata, &len); this_len = 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(vdata, 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 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, bool 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) { 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; } }