/* * Implement the SftpServer abstraction, in the 'live' form (i.e. * really operating on the Unix filesystem). */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include "putty.h" #include "ssh.h" #include "ssh/server.h" #include "ssh/sftp.h" #include "tree234.h" typedef struct UnixSftpServer UnixSftpServer; struct UnixSftpServer { unsigned *fdseqs; bool *fdsopen; size_t fdsize; tree234 *dirhandles; int last_dirhandle_index; char handlekey[8]; SftpServer srv; }; struct uss_dirhandle { int index; DIR *dp; }; #define USS_DIRHANDLE_SEQ (0xFFFFFFFFU) static int uss_dirhandle_cmp(void *av, void *bv) { struct uss_dirhandle *a = (struct uss_dirhandle *)av; struct uss_dirhandle *b = (struct uss_dirhandle *)bv; if (a->index < b->index) return -1; if (a->index > b->index) return +1; return 0; } static SftpServer *uss_new(const SftpServerVtable *vt) { UnixSftpServer *uss = snew(UnixSftpServer); memset(uss, 0, sizeof(UnixSftpServer)); uss->dirhandles = newtree234(uss_dirhandle_cmp); uss->srv.vt = vt; make_unix_sftp_filehandle_key(uss->handlekey, sizeof(uss->handlekey)); return &uss->srv; } static void uss_free(SftpServer *srv) { UnixSftpServer *uss = container_of(srv, UnixSftpServer, srv); struct uss_dirhandle *udh; for (size_t i = 0; i < uss->fdsize; i++) if (uss->fdsopen[i]) close(i); sfree(uss->fdseqs); while ((udh = delpos234(uss->dirhandles, 0)) != NULL) { closedir(udh->dp); sfree(udh); } sfree(uss); } static void uss_return_handle_raw( UnixSftpServer *uss, SftpReplyBuilder *reply, int index, unsigned seq) { unsigned char handlebuf[8]; PUT_32BIT_MSB_FIRST(handlebuf, index); PUT_32BIT_MSB_FIRST(handlebuf + 4, seq); des_encrypt_xdmauth(uss->handlekey, handlebuf, 8); fxp_reply_handle(reply, make_ptrlen(handlebuf, 8)); } static bool uss_decode_handle( UnixSftpServer *uss, ptrlen handle, int *index, unsigned *seq) { unsigned char handlebuf[8]; if (handle.len != 8) return false; memcpy(handlebuf, handle.ptr, 8); des_decrypt_xdmauth(uss->handlekey, handlebuf, 8); *index = toint(GET_32BIT_MSB_FIRST(handlebuf)); *seq = GET_32BIT_MSB_FIRST(handlebuf + 4); return true; } static void uss_return_new_handle( UnixSftpServer *uss, SftpReplyBuilder *reply, int fd) { assert(fd >= 0); if (fd >= uss->fdsize) { size_t old_size = uss->fdsize; sgrowarray(uss->fdseqs, uss->fdsize, fd); uss->fdsopen = sresize(uss->fdsopen, uss->fdsize, bool); while (old_size < uss->fdsize) { uss->fdseqs[old_size] = 0; uss->fdsopen[old_size] = false; old_size++; } } assert(!uss->fdsopen[fd]); uss->fdsopen[fd] = true; if (++uss->fdseqs[fd] == USS_DIRHANDLE_SEQ) uss->fdseqs[fd] = 0; uss_return_handle_raw(uss, reply, fd, uss->fdseqs[fd]); } static int uss_try_lookup_fd(UnixSftpServer *uss, ptrlen handle) { int fd; unsigned seq; if (!uss_decode_handle(uss, handle, &fd, &seq) || fd < 0 || fd >= uss->fdsize || !uss->fdsopen[fd] || uss->fdseqs[fd] != seq) return -1; return fd; } static int uss_lookup_fd(UnixSftpServer *uss, SftpReplyBuilder *reply, ptrlen handle) { int fd = uss_try_lookup_fd(uss, handle); if (fd < 0) fxp_reply_error(reply, SSH_FX_FAILURE, "invalid file handle"); return fd; } static void uss_return_new_dirhandle( UnixSftpServer *uss, SftpReplyBuilder *reply, DIR *dp) { struct uss_dirhandle *udh = snew(struct uss_dirhandle); udh->index = uss->last_dirhandle_index++; udh->dp = dp; struct uss_dirhandle *added = add234(uss->dirhandles, udh); assert(added == udh); uss_return_handle_raw(uss, reply, udh->index, USS_DIRHANDLE_SEQ); } static struct uss_dirhandle *uss_try_lookup_dirhandle( UnixSftpServer *uss, ptrlen handle) { struct uss_dirhandle key, *udh; unsigned seq; if (!uss_decode_handle(uss, handle, &key.index, &seq) || seq != USS_DIRHANDLE_SEQ || (udh = find234(uss->dirhandles, &key, NULL)) == NULL) return NULL; return udh; } static struct uss_dirhandle *uss_lookup_dirhandle( UnixSftpServer *uss, SftpReplyBuilder *reply, ptrlen handle) { struct uss_dirhandle *udh = uss_try_lookup_dirhandle(uss, handle); if (!udh) fxp_reply_error(reply, SSH_FX_FAILURE, "invalid file handle"); return udh; } static void uss_error(UnixSftpServer *uss, SftpReplyBuilder *reply) { unsigned code = SSH_FX_FAILURE; switch (errno) { case ENOENT: code = SSH_FX_NO_SUCH_FILE; break; case EPERM: code = SSH_FX_PERMISSION_DENIED; break; } fxp_reply_error(reply, code, strerror(errno)); } static void uss_realpath(SftpServer *srv, SftpReplyBuilder *reply, ptrlen path) { UnixSftpServer *uss = container_of(srv, UnixSftpServer, srv); char *inpath = mkstr(path); char *outpath = realpath(inpath, NULL); free(inpath); if (!outpath) { uss_error(uss, reply); } else { fxp_reply_simple_name(reply, ptrlen_from_asciz(outpath)); free(outpath); } } static void uss_open(SftpServer *srv, SftpReplyBuilder *reply, ptrlen path, unsigned flags, struct fxp_attrs attrs) { UnixSftpServer *uss = container_of(srv, UnixSftpServer, srv); int openflags = 0; if (!((SSH_FXF_READ | SSH_FXF_WRITE) &~ flags)) openflags |= O_RDWR; else if (flags & SSH_FXF_WRITE) openflags |= O_WRONLY; else if (flags & SSH_FXF_READ) openflags |= O_RDONLY; if (flags & SSH_FXF_APPEND) openflags |= O_APPEND; if (flags & SSH_FXF_CREAT) openflags |= O_CREAT; if (flags & SSH_FXF_TRUNC) openflags |= O_TRUNC; if (flags & SSH_FXF_EXCL) openflags |= O_EXCL; char *pathstr = mkstr(path); int fd = open(pathstr, openflags, GET_PERMISSIONS(attrs, 0777)); free(pathstr); if (fd < 0) { uss_error(uss, reply); } else { uss_return_new_handle(uss, reply, fd); } } static void uss_opendir(SftpServer *srv, SftpReplyBuilder *reply, ptrlen path) { UnixSftpServer *uss = container_of(srv, UnixSftpServer, srv); char *pathstr = mkstr(path); DIR *dp = opendir(pathstr); free(pathstr); if (!dp) { uss_error(uss, reply); } else { uss_return_new_dirhandle(uss, reply, dp); } } static void uss_close(SftpServer *srv, SftpReplyBuilder *reply, ptrlen handle) { UnixSftpServer *uss = container_of(srv, UnixSftpServer, srv); int fd; struct uss_dirhandle *udh; if ((udh = uss_try_lookup_dirhandle(uss, handle)) != NULL) { closedir(udh->dp); del234(uss->dirhandles, udh); sfree(udh); fxp_reply_ok(reply); } else if ((fd = uss_lookup_fd(uss, reply, handle)) >= 0) { close(fd); assert(0 <= fd && fd <= uss->fdsize); uss->fdsopen[fd] = false; fxp_reply_ok(reply); } /* if both failed, uss_lookup_fd will have filled in an error response */ } static void uss_mkdir(SftpServer *srv, SftpReplyBuilder *reply, ptrlen path, struct fxp_attrs attrs) { UnixSftpServer *uss = container_of(srv, UnixSftpServer, srv); char *pathstr = mkstr(path); int status = mkdir(pathstr, GET_PERMISSIONS(attrs, 0777)); free(pathstr); if (status < 0) { uss_error(uss, reply); } else { fxp_reply_ok(reply); } } static void uss_rmdir(SftpServer *srv, SftpReplyBuilder *reply, ptrlen path) { UnixSftpServer *uss = container_of(srv, UnixSftpServer, srv); char *pathstr = mkstr(path); int status = rmdir(pathstr); free(pathstr); if (status < 0) { uss_error(uss, reply); } else { fxp_reply_ok(reply); } } static void uss_remove(SftpServer *srv, SftpReplyBuilder *reply, ptrlen path) { UnixSftpServer *uss = container_of(srv, UnixSftpServer, srv); char *pathstr = mkstr(path); int status = unlink(pathstr); free(pathstr); if (status < 0) { uss_error(uss, reply); } else { fxp_reply_ok(reply); } } static void uss_rename(SftpServer *srv, SftpReplyBuilder *reply, ptrlen srcpath, ptrlen dstpath) { UnixSftpServer *uss = container_of(srv, UnixSftpServer, srv); char *srcstr = mkstr(srcpath), *dststr = mkstr(dstpath); int status = rename(srcstr, dststr); free(srcstr); free(dststr); if (status < 0) { uss_error(uss, reply); } else { fxp_reply_ok(reply); } } static struct fxp_attrs uss_translate_struct_stat(const struct stat *st) { struct fxp_attrs attrs; attrs.flags = (SSH_FILEXFER_ATTR_SIZE | SSH_FILEXFER_ATTR_PERMISSIONS | SSH_FILEXFER_ATTR_UIDGID | SSH_FILEXFER_ATTR_ACMODTIME); attrs.size = st->st_size; attrs.permissions = st->st_mode; attrs.uid = st->st_uid; attrs.gid = st->st_gid; attrs.atime = st->st_atime; attrs.mtime = st->st_mtime; return attrs; } static void uss_reply_struct_stat(SftpReplyBuilder *reply, const struct stat *st) { fxp_reply_attrs(reply, uss_translate_struct_stat(st)); } static void uss_stat(SftpServer *srv, SftpReplyBuilder *reply, ptrlen path, bool follow_symlinks) { UnixSftpServer *uss = container_of(srv, UnixSftpServer, srv); struct stat st; char *pathstr = mkstr(path); int status = (follow_symlinks ? stat : lstat) (pathstr, &st); free(pathstr); if (status < 0) { uss_error(uss, reply); } else { uss_reply_struct_stat(reply, &st); } } static void uss_fstat(SftpServer *srv, SftpReplyBuilder *reply, ptrlen handle) { UnixSftpServer *uss = container_of(srv, UnixSftpServer, srv); struct stat st; int fd; if ((fd = uss_lookup_fd(uss, reply, handle)) < 0) return; int status = fstat(fd, &st); if (status < 0) { uss_error(uss, reply); } else { uss_reply_struct_stat(reply, &st); } } #if !HAVE_FUTIMES static inline int futimes(int fd, const struct timeval tv[2]) { /* If the OS doesn't support futimes(3) then we have to pretend it * always returns failure */ errno = EINVAL; return -1; } #endif /* * The guts of setstat and fsetstat, macroised so that they can call * fchown(fd,...) or chown(path,...) depending on parameters. */ #define SETSTAT_GUTS(api_prefix, api_arg, attrs, success) do \ { \ if (attrs.flags & SSH_FILEXFER_ATTR_SIZE) \ if (api_prefix(truncate)(api_arg, attrs.size) < 0) \ success = false; \ if (attrs.flags & SSH_FILEXFER_ATTR_UIDGID) \ if (api_prefix(chown)(api_arg, attrs.uid, attrs.gid) < 0) \ success = false; \ if (attrs.flags & SSH_FILEXFER_ATTR_PERMISSIONS) \ if (api_prefix(chmod)(api_arg, attrs.permissions) < 0) \ success = false; \ if (attrs.flags & SSH_FILEXFER_ATTR_ACMODTIME) { \ struct timeval tv[2]; \ tv[0].tv_sec = attrs.atime; \ tv[1].tv_sec = attrs.mtime; \ tv[0].tv_usec = tv[1].tv_usec = 0; \ if (api_prefix(utimes)(api_arg, tv) < 0) \ success = false; \ } \ } while (0) #define PATH_PREFIX(func) func #define FD_PREFIX(func) f ## func static void uss_setstat(SftpServer *srv, SftpReplyBuilder *reply, ptrlen path, struct fxp_attrs attrs) { UnixSftpServer *uss = container_of(srv, UnixSftpServer, srv); char *pathstr = mkstr(path); bool success = true; SETSTAT_GUTS(PATH_PREFIX, pathstr, attrs, success); free(pathstr); if (!success) { uss_error(uss, reply); } else { fxp_reply_ok(reply); } } static void uss_fsetstat(SftpServer *srv, SftpReplyBuilder *reply, ptrlen handle, struct fxp_attrs attrs) { UnixSftpServer *uss = container_of(srv, UnixSftpServer, srv); int fd; if ((fd = uss_lookup_fd(uss, reply, handle)) < 0) return; bool success = true; SETSTAT_GUTS(FD_PREFIX, fd, attrs, success); if (!success) { uss_error(uss, reply); } else { fxp_reply_ok(reply); } } static void uss_read(SftpServer *srv, SftpReplyBuilder *reply, ptrlen handle, uint64_t offset, unsigned length) { UnixSftpServer *uss = container_of(srv, UnixSftpServer, srv); int fd; char *buf; if ((fd = uss_lookup_fd(uss, reply, handle)) < 0) return; if ((buf = malloc(length)) == NULL) { /* A rare case in which I bother to check malloc failure, * because in this case we can localise the problem easily by * turning it into a failure response from this one sftp * request */ fxp_reply_error(reply, SSH_FX_FAILURE, "Out of memory for read buffer"); return; } char *p = buf; int status = lseek(fd, offset, SEEK_SET); if (status >= 0 || errno == ESPIPE) { bool seekable = (status >= 0); while (length > 0) { status = read(fd, p, length); if (status <= 0) break; unsigned bytes_read = status; assert(bytes_read <= length); length -= bytes_read; p += bytes_read; if (!seekable) { /* * If the seek failed because the file is fundamentally * not a seekable kind of thing, abandon this loop after * one attempt, i.e. we just read whatever we could get * and we don't mind returning a short buffer. */ } } } if (status < 0) { uss_error(uss, reply); } else if (p == buf) { fxp_reply_error(reply, SSH_FX_EOF, "End of file"); } else { fxp_reply_data(reply, make_ptrlen(buf, p - buf)); } free(buf); } static void uss_write(SftpServer *srv, SftpReplyBuilder *reply, ptrlen handle, uint64_t offset, ptrlen data) { UnixSftpServer *uss = container_of(srv, UnixSftpServer, srv); int fd; if ((fd = uss_lookup_fd(uss, reply, handle)) < 0) return; const char *p = data.ptr; unsigned length = data.len; int status = lseek(fd, offset, SEEK_SET); if (status >= 0 || errno == ESPIPE) { while (length > 0) { status = write(fd, p, length); assert(status != 0); if (status < 0) break; unsigned bytes_written = status; assert(bytes_written <= length); length -= bytes_written; p += bytes_written; } } if (status < 0) { uss_error(uss, reply); } else { fxp_reply_ok(reply); } } static void uss_readdir(SftpServer *srv, SftpReplyBuilder *reply, ptrlen handle, int max_entries, bool omit_longname) { UnixSftpServer *uss = container_of(srv, UnixSftpServer, srv); struct dirent *de; struct uss_dirhandle *udh; if ((udh = uss_lookup_dirhandle(uss, reply, handle)) == NULL) return; errno = 0; de = readdir(udh->dp); if (!de) { if (errno == 0) { fxp_reply_error(reply, SSH_FX_EOF, "End of directory"); } else { uss_error(uss, reply); } } else { ptrlen longname = PTRLEN_LITERAL(""); char *longnamebuf = NULL; struct fxp_attrs attrs = no_attrs; #if defined HAVE_FSTATAT && defined HAVE_DIRFD struct stat st; if (!fstatat(dirfd(udh->dp), de->d_name, &st, AT_SYMLINK_NOFOLLOW)) { char perms[11], *uidbuf = NULL, *gidbuf = NULL; struct passwd *pwd; struct group *grp; const char *user, *group; struct tm tm; attrs = uss_translate_struct_stat(&st); if (!omit_longname) { strcpy(perms, "----------"); switch (st.st_mode & S_IFMT) { case S_IFBLK: perms[0] = 'b'; break; case S_IFCHR: perms[0] = 'c'; break; case S_IFDIR: perms[0] = 'd'; break; case S_IFIFO: perms[0] = 'p'; break; case S_IFLNK: perms[0] = 'l'; break; case S_IFSOCK: perms[0] = 's'; break; } if (st.st_mode & S_IRUSR) perms[1] = 'r'; if (st.st_mode & S_IWUSR) perms[2] = 'w'; if (st.st_mode & S_IXUSR) perms[3] = (st.st_mode & S_ISUID ? 's' : 'x'); else perms[3] = (st.st_mode & S_ISUID ? 'S' : '-'); if (st.st_mode & S_IRGRP) perms[4] = 'r'; if (st.st_mode & S_IWGRP) perms[5] = 'w'; if (st.st_mode & S_IXGRP) perms[6] = (st.st_mode & S_ISGID ? 's' : 'x'); else perms[6] = (st.st_mode & S_ISGID ? 'S' : '-'); if (st.st_mode & S_IROTH) perms[7] = 'r'; if (st.st_mode & S_IWOTH) perms[8] = 'w'; if (st.st_mode & S_IXOTH) perms[9] = 'x'; if ((pwd = getpwuid(st.st_uid)) != NULL) user = pwd->pw_name; else user = uidbuf = dupprintf("%u", (unsigned)st.st_uid); if ((grp = getgrgid(st.st_gid)) != NULL) group = grp->gr_name; else group = gidbuf = dupprintf("%u", (unsigned)st.st_gid); tm = *localtime(&st.st_mtime); longnamebuf = dupprintf( "%s %3u %-8s %-8s %8"PRIuMAX" %.3s %2d %02d:%02d %s", perms, (unsigned)st.st_nlink, user, group, (uintmax_t)st.st_size, (&"JanFebMarAprMayJunJulAugSepOctNovDec"[3*tm.tm_mon]), tm.tm_mday, tm.tm_hour, tm.tm_min, de->d_name); longname = ptrlen_from_asciz(longnamebuf); sfree(uidbuf); sfree(gidbuf); } } #endif /* FIXME: be able to return more than one, in which case we * must also check max_entries */ fxp_reply_name_count(reply, 1); fxp_reply_full_name(reply, ptrlen_from_asciz(de->d_name), longname, attrs); sfree(longnamebuf); } } const SftpServerVtable unix_live_sftpserver_vt = { .new = uss_new, .free = uss_free, .realpath = uss_realpath, .open = uss_open, .opendir = uss_opendir, .close = uss_close, .mkdir = uss_mkdir, .rmdir = uss_rmdir, .remove = uss_remove, .rename = uss_rename, .stat = uss_stat, .fstat = uss_fstat, .setstat = uss_setstat, .fsetstat = uss_fsetstat, .read = uss_read, .write = uss_write, .readdir = uss_readdir, };