Skip to content
Snippets Groups Projects
sftp.cpp 59.50 KiB
#include <iomanip>
#include <iostream>
#include <sstream>
#include <string>
#include <stdexcept>
#include <memory>
#include <vector>

#include "sbbs.h"
#include "xpprintf.h" // for asprintf() on Win32

constexpr uint32_t lib_flag {UINT32_C(1)<<31};
constexpr uint32_t lib_mask {~lib_flag};
constexpr uint32_t users_gid {UINT32_MAX};

#define SLASH_FILES "/files"
#define SLASH_VFILES "/vfiles"
#define SLASH_FLS "/fls"
#define SLASH_HOME "/home"
#define MAX_FILES_PER_READDIR 25

constexpr int32_t no_more_files = -3;
constexpr int32_t dot = -2;
constexpr int32_t dotdot = -1;

/*
 * This does all the work of mapping an sftp path received from the client
 * to an opendir handle.  It will also enforce permissions.
 */
typedef enum map_path_result {
	MAP_FAILED,
	MAP_BAD_PATH,
	MAP_PERMISSION_DENIED,
	MAP_ALLOC_FAILED,
	MAP_INVALID_ARGS,
	MAP_SMB_FAILED,
	// The rest should be in this order at the end
	MAP_SUCCESS,
	MAP_TO_FILE = MAP_SUCCESS,
	MAP_TO_DIR,
	MAP_TO_SYMLINK,
} map_path_result_t;

typedef enum map_path_mode {
	MAP_STAT,
	MAP_READ,
	MAP_WRITE,
	MAP_RDWR,
} map_path_mode_t;

struct pathmap {
	const char *sftp_patt;	// %s is replaced with user alias
	const char *real_patt;	// %s is replaced with datadir, %d with user number
	const char *link_patt;	// %s is replaced with datadir, %d with user number
	sftp_file_attr_t (*get_attrs)(sbbs_t *sbbs, const char *path);
	bool is_dynamic;
};

static sftp_file_attr_t rootdir_attrs(sbbs_t *sbbs, const char *path);
static sftp_file_attr_t homedir_attrs(sbbs_t *sbbs, const char *path);
static sftp_file_attr_t homefile_attrs(sbbs_t *sbbs, const char *path);
static sftp_file_attr_t sshkeys_attrs(sbbs_t *sbbs, const char *path);
static char *sftp_parse_crealpath(sbbs_t *sbbs, const char *filename);
static char *expand_slash(const char *orig);
static bool is_in_filebase(const char *path);
static enum sftp_dir_tree in_tree(const char *path);
static char * get_longname(sbbs_t *sbbs, const char *path, const char *link, sftp_file_attr_t attr);
static sftp_file_attr_t get_attrs(sbbs_t *sbbs, const char *path, char **link);

const char files_path[] = SLASH_FILES;
constexpr size_t files_path_len = (sizeof(files_path) - 1);
const char vfiles_path[] = SLASH_VFILES;
constexpr size_t vfiles_path_len = (sizeof(vfiles_path) - 1);
const char fls_path[] = SLASH_FLS;
constexpr size_t fls_path_len = (sizeof(fls_path) - 1);

static struct pathmap static_files[] = {
	// TODO: ftpalias.cfg
	// TODO: User to user file transfers
	// TODO: Upload to sysop
	{"/", nullptr, nullptr, rootdir_attrs},
	{SLASH_FILES "/", nullptr, nullptr, rootdir_attrs},
	//{SLASH_FLS "/", nullptr, nullptr, rootdir_attrs},
	{SLASH_HOME "/", nullptr, nullptr, homedir_attrs},
	{SLASH_HOME "/%s/", nullptr, nullptr, homedir_attrs},
	// TODO: Some way for a sysop/mod authour to map things in here
	{SLASH_HOME "/%s/.ssh/", nullptr, nullptr, homedir_attrs},
	{SLASH_HOME "/%s/.ssh/authorized_keys", "%suser/%04d.sshkeys", SLASH_HOME "/%s/sshkeys", sshkeys_attrs},
	{SLASH_HOME "/%s/sshkeys", "%suser/%04d.sshkeys", nullptr, homefile_attrs},
	{SLASH_HOME "/%s/plan", "%suser/%04d.plan", nullptr, homefile_attrs},
	{SLASH_HOME "/%s/signature", "%suser/%04d.sig", nullptr, homefile_attrs},
	{SLASH_HOME "/%s/smtptags", "%suser/%04d.smtptags", nullptr, homefile_attrs},
	//{SLASH_VFILES "/", nullptr, nullptr, rootdir_attrs},
};
constexpr size_t static_files_sz = (sizeof(static_files) / sizeof(static_files[0]));

class path_map {
	bool is_static_ {true};
	enum sftp_dir_tree tree_ {SFTP_DTREE_FULL};
	map_path_result_t result_ {MAP_FAILED};

	// Too lazy to write a std::expected thing here.
	int find_lib_sz_(const char *libnam, size_t lnsz)
	{
		for (int l = 0; l < sbbs->cfg.total_libs; l++) {
			if (!can_user_access_lib(&sbbs->cfg, l, &sbbs->useron, &sbbs->client))
				continue;
			char *exp {};
			switch (tree_) {
				case SFTP_DTREE_FULL:
					exp = expand_slash(sbbs->cfg.lib[l]->lname);
					break;
				case SFTP_DTREE_SHORT:
					exp = expand_slash(sbbs->cfg.lib[l]->sname);
					break;
				case SFTP_DTREE_VIRTUAL:
					exp = expand_slash(sbbs->cfg.lib[l]->vdir);
					break;
			}
			if (exp == nullptr)
				return -1;
			if ((memcmp(libnam, exp, lnsz) == 0)
			    && (exp[lnsz] == 0)) {
				free(exp);
				return l;
			}
			free(exp);
		}
		return -1;
	}

	int find_dir_sz_(const char *dirnam, int lib, size_t dnsz)
	{
		for (int d = 0; d < sbbs->cfg.total_dirs; d++) {
			if (sbbs->cfg.dir[d]->lib != lib)
				continue;
			if (!can_user_access_dir(&sbbs->cfg, d, &sbbs->useron, &sbbs->client))
				continue;
			char *exp {};
			switch (tree_) {
				case SFTP_DTREE_FULL:
					exp = expand_slash(sbbs->cfg.dir[d]->lname);
					break;
				case SFTP_DTREE_SHORT:
					exp = expand_slash(sbbs->cfg.dir[d]->sname);
					break;
				case SFTP_DTREE_VIRTUAL:
					exp = expand_slash(sbbs->cfg.dir[d]->vdir);
					break;
			}
			if (exp == nullptr)
				return -1;
			if ((memcmp(dirnam, exp, dnsz) == 0)
			    && (exp[dnsz] == 0)) {
				free(exp);
				return d;
			}
			free(exp);
		}
		return -1;
	}

public:
	const map_path_mode_t mode;
	sbbs_t * const sbbs{};
	char * local_path{};
	char * sftp_path{};
	char * sftp_link_target{};
	union {
		struct {
			uint32_t offset;
			int32_t idx;
			int lib;
			int dir;
		} filebase {0,no_more_files,-1,-1};
		struct {
			struct pathmap *mapping;
		} rootdir;
	} info;

	path_map() = delete;
	path_map(sbbs_t *sbbsptr, const char* path, map_path_mode_t mode) : mode(mode),  sbbs(sbbsptr)
	{
		path_map(sbbs, reinterpret_cast<const uint8_t*>(path), mode);
	}

	path_map(sbbs_t *sbbsptr, const uint8_t* path, map_path_mode_t mode) : mode(mode), sbbs(sbbsptr)
	{
		const char *c;
		const char *cpath = reinterpret_cast<const char *>(path);

		if (path == nullptr || sbbs == nullptr) {
			result_ = MAP_INVALID_ARGS;
			return;
		}

		this->sftp_path = sftp_parse_crealpath(sbbs, cpath);
		if (this->sftp_path == nullptr) {
			return;
		}
		if (is_in_filebase(this->sftp_path)) {
			tree_ = in_tree(this->sftp_path);
			// This is in the file base.
			if (mode == MAP_RDWR) {
				result_ = MAP_PERMISSION_DENIED;
				return;
			}
			this->is_static_ = false;
			this->info.filebase.dir = -1;
			this->info.filebase.lib = -1;
			this->info.filebase.idx = dot;
			char *lib;
			size_t sz{files_path_len}; // Initialize in case tree_ isn't valid (which should have trown an exception)
			switch (tree_) {
				case SFTP_DTREE_FULL:
					sz = files_path_len;
					break;
				case SFTP_DTREE_SHORT:
					sz = fls_path_len;
					break;
				case SFTP_DTREE_VIRTUAL:
					sz = vfiles_path_len;
					break;
			}
			if (this->sftp_path[sz] == 0 || this->sftp_path[sz+1] == 0) {
				// Root...
				result_ = MAP_TO_DIR;
				return;
			}
			lib = &this->sftp_path[sz + 1];
			c = strchr(lib, '/');
			size_t libsz;
			if (c == nullptr) {
				libsz = strlen(lib);
			}
			else {
				libsz = c - (lib);
			}
			this->info.filebase.lib = find_lib_sz_(lib, libsz);
			if (this->info.filebase.lib == -1) {
				result_ = MAP_BAD_PATH;
				return;
			}
			if (c == nullptr || c[1] == 0) {
				result_ = MAP_TO_DIR;
				return;
			}
			// There's a dir name too...
			const char *dir = &lib[libsz + 1];
			c = strchr(dir, '/');
			size_t dirsz;
			if (c == nullptr) {
				dirsz = strlen(dir);
			}
			else {
				dirsz = c - dir;
			}
			this->info.filebase.dir = find_dir_sz_(dir, this->info.filebase.lib, dirsz);
			if (this->info.filebase.dir == -1) {
				result_ = MAP_BAD_PATH;
				return;
			}
			if (c == nullptr || c[1] == 0) {
				result_ = MAP_TO_DIR;
				return;
			}
			// There's a filename too!  What fun!
			smb_t     smb{};
			smbfile_t file{};
			result_ = MAP_TO_FILE;
			const char *fname = &dir[dirsz + 1];
			if (asprintf(&this->local_path, "%s/%s", sbbs->cfg.dir[this->info.filebase.dir]->path, fname) == -1) {
				result_ = MAP_ALLOC_FAILED;
				return;
			}
			if (smb_open_dir(&sbbs->cfg, &smb, this->info.filebase.dir) != SMB_SUCCESS) {
				result_ = MAP_SMB_FAILED;
				return;
			}
			if (smb_findfile(&smb, fname, &file) != SMB_SUCCESS) {
				/*
				 * If it doesn't exist, and we're trying to write,
				 * this is success
				 */
				if ((mode == MAP_READ) || (mode == MAP_STAT)) {
					result_ = MAP_BAD_PATH;
					return;
				}
				if (access(this->local_path, F_OK)) {
					// File already exists...
					result_ = MAP_PERMISSION_DENIED;
					return;
				}
				return;
			}
			this->info.filebase.idx = file.file_idx.idx.number;
			this->info.filebase.offset = file.file_idx.idx.offset;
			// TODO: Keep this around?
			smb_freefilemem(&file);
			smb_close(&smb);
			/* TODO: Sometimes some users can overwrite some files...
			 *       but for now, nobody can.
			 */
			if (mode == MAP_WRITE || mode == MAP_RDWR) {
				result_ = MAP_PERMISSION_DENIED;
				return;
			}
			if (mode == MAP_READ) {
				if (!can_user_download(&sbbs->cfg, this->info.filebase.dir, &sbbs->useron, &sbbs->client, nullptr)) {
					result_ = MAP_PERMISSION_DENIED;
					return;
				}
			}
			return;
		}
		else {
			// Static files
			unsigned sfidx;

			this->is_static_ = true;
			size_t pathlen = strlen(this->sftp_path);
			for (sfidx = 0; sfidx < static_files_sz; sfidx++) {
				char tmpdir[MAX_PATH + 1];
				snprintf(tmpdir, sizeof(tmpdir), static_files[sfidx].sftp_patt, sbbs->useron.alias);
				if (strncmp(this->sftp_path, tmpdir, pathlen) == 0
				    && (tmpdir[pathlen] == 0
				    || (tmpdir[pathlen] == '/' && tmpdir[pathlen + 1] == 0))) {
					this->info.rootdir.mapping = &static_files[sfidx];
					if (static_files[sfidx].real_patt) {
						if (asprintf(&this->local_path, static_files[sfidx].real_patt, sbbs->cfg.data_dir, sbbs->useron.number) == -1) {
							result_ = MAP_ALLOC_FAILED;
							return;
						}
						result_ = MAP_TO_FILE;
					}
					else
						result_ = MAP_TO_DIR;
					if ((mode == MAP_READ || mode == MAP_STAT)
					    && (result_ == MAP_TO_FILE)) {
						if (access(this->local_path, R_OK)) {
							result_ = MAP_BAD_PATH;
							return;
						}
					}
					if (static_files[sfidx].link_patt) {
						if (asprintf(&this->sftp_link_target, static_files[sfidx].link_patt, sbbs->useron.alias) == -1) {
							result_ = MAP_ALLOC_FAILED;
							return;
						}
					}
					return;
				}
			}
			result_ = MAP_FAILED;
			return;
		}
	}

	bool cleanup()
	{
		switch(result_) {
			case MAP_BAD_PATH:
				return sftps_send_error(sbbs->sftp_state, SSH_FX_NO_SUCH_FILE, "No such file");
			case MAP_PERMISSION_DENIED:
				return sftps_send_error(sbbs->sftp_state, SSH_FX_PERMISSION_DENIED, "No such file");
			default:
				if (result_ >= MAP_SUCCESS) {
					return sftps_send_error(sbbs->sftp_state, SSH_FX_PERMISSION_DENIED, "No such file");
				}
				return sftps_send_error(sbbs->sftp_state, SSH_FX_FAILURE, "Mapping failure");
		}
	}

	const map_path_result_t &result(void) const {
		return result_;
	}

	const bool &is_static(void) const {
		return is_static_;
	}

	const enum sftp_dir_tree &tree(void) const {
		return tree_;
	}

	const bool success(void) {
		return result_ >= MAP_SUCCESS;
	}

	~path_map() {
		free(local_path);
		free(sftp_path);
		free(sftp_link_target);
	}
};


class file_names {
	uint32_t entries_{0};
	sbbs_t * const sbbs{};
	std::vector<char *> fnames;
	std::vector<char *> lnames;
	std::vector<sftp_file_attr_t> attrs;
public:
	uint32_t entries(void) { return entries_; }

	bool add_name(char *fname, char *lname, sftp_file_attr_t attr) {
		unsigned added = 0;
		try {
			fnames.push_back(fname);
			added++;
			lnames.push_back(lname);
			added++;
			attrs.push_back(attr);
			added++;
			entries_ += 1;
		}
		catch(...) {
			if (added < 1)
				free(fname);
			if (added < 2)
				free(lname);
			if (added < 3)
				sftp_fattr_free(attr);
			return false;
		}
		return true;
	}

	bool
	generic_dot_attr_entry(char *fname, sftp_file_attr_t attr, char **link, int32_t& idx)
	{
		if (attr == nullptr) {
			free(fname);
			return false;
		}
		char *lname = get_longname(sbbs, fname, link ? *link : nullptr, attr);
		if (lname == nullptr) {
			free(fname);
			sftp_fattr_free(attr);
			return false;
		}
		bool ret = add_name(fname, lname, attr);
		if (ret)
			idx++;
		return ret;
	}

	bool
	generic_dot_entry(char *fname, const char *path, int32_t& idx)
	{
		char *link = nullptr;
		sftp_file_attr_t attr = get_attrs(sbbs, path, &link);
		if (attr == nullptr) {
			free(link);
			free(fname);
			return false;
		}
		bool ret = generic_dot_attr_entry(fname, attr, &link, idx);
		free(link);
		return ret;
	}

	bool
	generic_dot_realpath_entry(char *fname, const char *path, int32_t& idx)
	{
		char *vpath = sftp_parse_crealpath(sbbs, path);
		if (vpath == nullptr) {
			free(fname);
			return false;
		}
		bool ret = generic_dot_entry(fname, vpath, idx);
		free(vpath);
		return ret;
	}

	bool send(void) {
		if (entries_ < 1)
			return false;
		return sftps_send_name(sbbs->sftp_state, entries_, &fnames[0], &lnames[0], &attrs[0]);
	}

	file_names() = delete;
	file_names(sbbs_t *sbbsptr) : sbbs(sbbsptr) {}

	~file_names(void) {
		for (auto&& name : fnames)
			free(name);
		for (auto&& name : lnames)
			free(name);
		for (auto&& attr : attrs)
			sftp_fattr_free(attr);
	}
};

static std::string
sftp_attr_string(sftp_file_attr_t attr)
{
	uint64_t u64;
	uint32_t u32;
	sftp_str_t str;
	std::ostringstream ret;
	std::string rstr;

	try {
		if (sftp_fattr_get_size(attr, &u64))
			ret << "size=" << u64 << ", ";
		if (sftp_fattr_get_uid(attr, &u32))
			ret << "uid=" << u32 << ", ";
		if (sftp_fattr_get_gid(attr, &u32))
			ret << "gid=" << u32 << ", ";
		if (sftp_fattr_get_permissions(attr, &u32))
			ret << "perm=0" << std::oct << u32 << ", " << std::dec;
		/*
		 * We can't use std::put_time() because apparently std::gmtime_r() isn't
		 * available on Win32.
		 */
		if (sftp_fattr_get_atime(attr, &u32)) {
			struct tm t;
			time_t tt = u32;
			if (gmtime_r(&tt, &t))
				ret << "atime=" << std::put_time(&t, "%c") << u32 << ", ";
			else
				ret << "atime=" << u32 << ", ";
		}
		if (sftp_fattr_get_mtime(attr, &u32)) {
			struct tm t;
			time_t tt = u32;
			if (gmtime_r(&tt, &t))
				ret << "mtime=" << std::put_time(&t, "%c") << u32 << ", ";
			else
				ret << "mtime=" << u32 << ", ";
		}
		u32 = sftp_fattr_get_ext_count(attr);
		for (uint32_t idx = 0; idx < u32; idx++) {
			str = sftp_fattr_get_ext_type(attr, idx);
			if (str)
				ret << str->c_str << "=";
			else
				ret << "<null>=";
			free_sftp_str(str);
			str = sftp_fattr_get_ext_data(attr, idx);
			if (str)
				ret << str->c_str << ", ";
			else
				ret << "<null>, ";
			free_sftp_str(str);
		}
		std::string rstr = ret.str();
		if (rstr.length() > 2)
			rstr.erase(rstr.length() - 2);
	}
	catch(...) {
		rstr = "<error>"; // TODO: This could throw as well. :(
	}
	return rstr;
}

static bool
is_in_filebase(const char *path)
{
	if (memcmp(files_path, path, files_path_len) == 0) {
		if (path[files_path_len] == 0 || path[files_path_len] == '/')
			return true;
	}
	if (memcmp(fls_path, path, fls_path_len) == 0) {
		if (path[fls_path_len] == 0 || path[fls_path_len] == '/')
			return true;
	}
	if (memcmp(vfiles_path, path, vfiles_path_len) == 0) {
		if (path[vfiles_path_len] == 0 || path[vfiles_path_len] == '/')
			return true;
	}
	return false;
}

static enum sftp_dir_tree
in_tree(const char *path)
{
	if (memcmp(files_path, path, files_path_len) == 0) {
		if (path[files_path_len] == 0 || path[files_path_len] == '/')
			return SFTP_DTREE_FULL;
	}
	if (memcmp(fls_path, path, fls_path_len) == 0) {
		if (path[fls_path_len] == 0 || path[fls_path_len] == '/')
			return SFTP_DTREE_SHORT;
	}
	if (memcmp(vfiles_path, path, vfiles_path_len) == 0) {
		if (path[vfiles_path_len] == 0 || path[vfiles_path_len] == '/')
			return SFTP_DTREE_VIRTUAL;
	}
	throw std::invalid_argument( "Invalid path" );
}

/*
 * Replaces a slash with a dash.
 * 
 * The SFTP protocol requires the use of Solidus as a path separator,
 * and dir and lib names can contain it.  Rather than a visually
 * different and one-way mapping as used in the FTP server, take
 * advantage of the fact that dir and lib names aren't unicode to have
 * a visially similar reversible mapping.
 *
 * Even in the future, should these support unicode, it will at least
 * still be visually more similar, even if it's not reversible.
 */
static char *
expand_slash(const char *orig)
{
	char *p;
	const char *p2;
	char *p3;
	unsigned slashes = 0;

	for (p2 = orig; *p2; p2++) {
		if (*p2 == '/')
			slashes++;
	}
	p = static_cast<char *>(malloc(strlen(orig) + (slashes * 2) + 1));
	if (p == nullptr)
		return p;
	p3 = p;
	for (p2 = orig; *p2; p2++) {
		if (*p2 == '/')
			*p3++ = '-';
		else
			*p3++ = *p2;
	}
	*p3 = 0;
	return p;
}

static sftp_file_attr_t
dummy_attrs(void)
{
	return sftp_fattr_alloc();
}

static sftp_file_attr_t
homedir_attrs(sbbs_t *sbbs, const char *path)
{
	sftp_file_attr_t attr = sftp_fattr_alloc();

	if (attr == nullptr)
		return nullptr;
	sftp_fattr_set_permissions(attr, S_IFDIR | S_IRWXU | S_IRUSR | S_IWUSR | S_IXUSR);
	sftp_fattr_set_uid_gid(attr, sbbs->useron.number, users_gid);
	return attr;
}

static sftp_file_attr_t
rootdir_attrs(sbbs_t *sbbs, const char *path)
{
	sftp_file_attr_t attr = sftp_fattr_alloc();

	if (attr == nullptr)
		return nullptr;
	sftp_fattr_set_permissions(attr, S_IFDIR | S_IRWXU | S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH);
	sftp_fattr_set_uid_gid(attr, 1, users_gid);
	return attr;
}

static sftp_file_attr_t
homefile_attrs(sbbs_t *sbbs, const char *path)
{
	sftp_file_attr_t attr = sftp_fattr_alloc();

	if (attr == nullptr)
		return nullptr;
	sftp_fattr_set_permissions(attr, S_IFREG | S_IRWXU | S_IRUSR | S_IWUSR);
	sftp_fattr_set_uid_gid(attr, sbbs->useron.number, users_gid);
	sftp_fattr_set_size(attr, flength(path));
	uint32_t fd = static_cast<uint32_t>(fdate(path));
	sftp_fattr_set_times(attr, fd, fd);
	return attr;
}

static sftp_file_attr_t
sshkeys_attrs(sbbs_t *sbbs, const char *path)
{
	sftp_file_attr_t attr = sftp_fattr_alloc();

	if (attr == nullptr)
		return nullptr;
	sftp_fattr_set_permissions(attr, S_IFLNK | S_IRWXU | S_IRUSR | S_IWUSR);
	sftp_fattr_set_uid_gid(attr, sbbs->useron.number, users_gid);
	sftp_fattr_set_size(attr, flength(path));
	uint32_t fd = static_cast<uint32_t>(fdate(path));
	sftp_fattr_set_times(attr, fd, fd);
	return attr;
}

void
remove_trailing_slash(char *str)
{
	size_t end = strlen(str);

	if (end > 0)
		end--;
	while (str[end] == '/' && end > 0)
		str[end] = 0;
}

/*
 * This is mostly copied from the xpdev _fullpath() implementation
 */
static char *
sftp_resolve_path(char *target, const char *path, size_t size)
{
	char	*out;
	char	*p;

	if(target==NULL) {
		size = MAX_PATH + 1;
		if((target=(char*)malloc(size))==NULL) {
			return(NULL);
		}
	}
	strncpy(target, path, size);
	target[size-1] = 0;
	out=target;

	for(;*out;out++) {
		while(*out=='/') {
			if(*(out+1)=='/')
				memmove(out,out+1,strlen(out));
			else if(*(out+1)=='.' && (*(out+2)=='/' || *(out+2)==0))
				memmove(out,out+2,strlen(out)-1);
			else if(*(out+1)=='.' && *(out+2)=='.' && (*(out+3)=='/' || *(out+3)==0))  {
				*out=0;
				p=strrchr(target,'/');
				if (p!=NULL) {
					memmove(p,out+3,strlen(out+3)+1);
					out=p;
				}
			}
			else  {
				out++;
			}
		}
	}
	if (size > 1 && *target == 0) {
		target[0] = '/';
		target[1] = 0;
	}
	return(target);
}

static char *
sftp_parse_crealpath(sbbs_t *sbbs, const char *filename)
{
	char *ret;
	char *tmp;

	if (sbbs->sftp_cwd == nullptr) {
		if (asprintf(&sbbs->sftp_cwd, SLASH_HOME "/%s", sbbs->useron.alias) == -1)
			sbbs->sftp_cwd = nullptr;
	}
	if (sbbs->sftp_cwd == nullptr)
		return nullptr;
	if (!isfullpath(filename)) {
		if (asprintf(&tmp, "%s/%s", sbbs->sftp_cwd, filename) == -1)
			return tmp;
		ret = sftp_resolve_path(nullptr, tmp, 0);
		free(tmp);
	}
	else {
		ret = sftp_resolve_path(nullptr, filename, 0);
	}
	if (ret == nullptr)
		return ret;
	if (ret[0] == 0) {
		free(ret);
		return nullptr;
	}
	remove_trailing_slash(ret);

	return ret;
}

static char *
sftp_parse_realpath(sbbs_t *sbbs, sftp_str_t filename)
{
	return sftp_parse_crealpath(sbbs, reinterpret_cast<char *>(filename->c_str));
}

static unsigned
parse_file_handle(sbbs_t *sbbs, sftp_filehandle_t handle)
{
	constexpr size_t nfdes = sizeof(sbbs->sftp_filedes) / sizeof(sbbs->sftp_filedes[0]);

	long tmp = strtol(reinterpret_cast<char *>(handle->c_str), nullptr, 10);
	if (tmp <= 0)
		return UINT_MAX;
	if (tmp > UINT_MAX)
		return UINT_MAX;
	if (static_cast<size_t>(tmp) > nfdes)
		return UINT_MAX;
	if (sbbs->sftp_filedes[tmp - 1] == nullptr)
		return UINT_MAX;
	return tmp - 1;
}

static unsigned
parse_dir_handle(sbbs_t *sbbs, sftp_dirhandle_t handle)
{
	constexpr size_t nfdes = sizeof(sbbs->sftp_dirdes) / sizeof(sbbs->sftp_dirdes[0]);

	if (handle->len < 3)
		return UINT_MAX;
	if (memcmp(handle->c_str, "D:", 2) != 0)
		return UINT_MAX;
	long tmp = strtol(reinterpret_cast<char *>(handle->c_str + 2), nullptr, 10);
	if (tmp <= 0)
		return UINT_MAX;
	if (tmp > UINT_MAX)
		return UINT_MAX;
	if (static_cast<size_t>(tmp) > nfdes)
		return UINT_MAX;
	if (sbbs->sftp_dirdes[tmp - 1] == nullptr)
		return UINT_MAX;
	return tmp - 1;
}

/*
 * From FreeBSD ls
 */
void
format_time(sbbs_t *sbbs, time_t ftime, char *longstring, size_t sz)
{
	static time_t now;
	const char *format;
	static int d_first = (sbbs->cfg.sys_date_fmt == DDMMYY);

	now = time(NULL);

#define SIXMONTHS       ((365 / 2) * 86400)
	if (ftime + SIXMONTHS > now && ftime < now + SIXMONTHS)
		/* mmm dd hh:mm || dd mmm hh:mm */
		format = d_first ? "%e %b %R" : "%b %e %R";
	else
		/* mmm dd  yyyy || dd mmm  yyyy */
		format = d_first ? "%e %b  %Y" : "%b %e  %Y";
	strftime(longstring, sz, format, localtime(&ftime));
}

static void
uid_to_string(sbbs_t *sbbs, uint32_t uid, char *buf)
{
	if (uid == 0)
		strcpy(buf, "<nobody>");
	if (username(&sbbs->cfg, uid, buf) == nullptr || buf[0] == 0)
		strcpy(buf, "unknown");
}

static void
gid_to_string(sbbs_t *sbbs, uint32_t gid, char *buf)
{
	if (gid == users_gid)
		strcpy(buf, "users");
	else if (gid & lib_flag)
		strcpy(buf, sbbs->cfg.lib[gid & lib_mask]->vdir);
	else
		strcpy(buf, sbbs->cfg.dir[gid]->code);
}

static char *
get_longname(sbbs_t *sbbs, const char *path, const char *link, sftp_file_attr_t attr)
{
	char *ret;
	const char *fname;
	uint32_t perms;
	uint32_t mtime;
	uint64_t sz;
	char pstr[11];
	char szstr[21];
	char datestr[20];
	char owner[LEN_ALIAS + 1];
	char group[LEN_EXTCODE + 1];

	memset(pstr, '-', sizeof(pstr) - 1);
	pstr[sizeof(pstr) - 1] = 0;
	if (sftp_fattr_get_permissions(attr, &perms)) {
		switch (perms & S_IFMT) {
			case S_IFSOCK:
				pstr[0] = 's';
				break;
			case S_IFLNK:
				pstr[0] = 'l';
				break;
			case S_IFREG:
				pstr[0] = '-';
				break;
			case S_IFBLK:
				pstr[0] = 'b';
				break;
			case S_IFDIR:
				pstr[0] = 'd';
				break;
			case S_IFCHR:
				pstr[0] = 'c';
				break;
			case S_IFIFO:
				pstr[0] = 'p';
				break;
		}
		if (perms & S_IRUSR)
			pstr[1] = 'r';
		if (perms & S_IWUSR)
			pstr[2] = 'w';
		if (((perms & S_IXUSR) == 0) && (perms & S_ISUID))
			pstr[3] = 'S';
		else if ((perms & S_IXUSR) && (perms & S_ISUID))
			pstr[3] = 's';
		else if (perms & S_IXUSR)
			pstr[3] = 'x';
		if (perms & S_IRGRP)
			pstr[4] = 'r';
		if (perms & S_IWGRP)
			pstr[5] = 'w';
		if (((perms & S_IXGRP) == 0) && (perms & S_ISGID))
			pstr[6] = 'S';
		else if ((perms & S_IXGRP) && (perms & S_ISGID))
			pstr[6] = 's';
		else if (perms & S_IXGRP)
			pstr[6] = 'x';
		if (perms & S_IROTH)
			pstr[7] = 'r';
		if (perms & S_IWOTH)
			pstr[8] = 'w';
		if (perms & S_IXOTH) {
			if (perms & S_ISVTX)
				pstr[9] = 't';
			else
				pstr[9] = 'x';
		}
		else if (perms & S_ISVTX)
			pstr[9] = 'T';
	}
	sz = 0;
	sftp_fattr_get_size(attr, &sz);
	sprintf(szstr, "%8" PRIu64, sz);
	mtime = 0;
	sftp_fattr_get_mtime(attr, &mtime);
	format_time(sbbs, mtime, datestr, sizeof(datestr));
	uint32_t uid{0};
	sftp_fattr_get_uid(attr, &uid);
	uid_to_string(sbbs, uid, owner);
	uid = 0;
	sftp_fattr_get_gid(attr, &uid);
	gid_to_string(sbbs, uid, group);
	fname = getfname(path);
	if (fname[0] == 0)
		fname = path;
	if (asprintf(&ret, "%s   0 %-8.8s %-8.8s %-8s %-12s %s%s%s", pstr, owner, group, szstr, datestr, fname, pstr[0] == 'l' ? " -> " : "", link ? link : "") == -1)
		return nullptr;
	return ret;
}

static sftp_file_attr_t
get_lib_attrs(sbbs_t *sbbs, int lib)
{
	sftp_file_attr_t attr = sftp_fattr_alloc();

	if (attr == nullptr)
		return nullptr;
	sftp_fattr_set_permissions(attr, S_IFDIR | S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP);
	sftp_fattr_set_uid_gid(attr, 1, static_cast<uint32_t>(lib) | lib_flag);
	return attr;
}

static sftp_file_attr_t
get_dir_attrs(sbbs_t *sbbs, int32_t dir)
{
	sftp_file_attr_t attr = sftp_fattr_alloc();
	uint32_t perms = S_IFDIR | S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP;

	if (attr == nullptr)
		return nullptr;
	if (can_user_upload(&sbbs->cfg, dir, &sbbs->useron, &sbbs->client, nullptr))
		perms |= S_IWGRP;
	sftp_fattr_set_permissions(attr, perms);
	sftp_fattr_set_uid_gid(attr, 1, static_cast<uint32_t>(dir));
	return attr;
}

static sftp_file_attr_t
get_filebase_attrs(sbbs_t *sbbs, int32_t dir, smbfile_t *file)
{
	sftp_file_attr_t attr = sftp_fattr_alloc();
	uint32_t perms = S_IFREG | S_IRUSR | S_IWUSR;
	uint32_t atime;
	uint32_t mtime;

	if (attr == nullptr)
		return nullptr;
	if (can_user_download(&sbbs->cfg, dir, &sbbs->useron, &sbbs->client, nullptr))
		perms |= S_IRGRP;
	if (can_user_upload(&sbbs->cfg, dir, &sbbs->useron, &sbbs->client, nullptr))
		perms |= S_IWGRP;
	sftp_fattr_set_permissions(attr, perms);
	sftp_fattr_set_size(attr, smb_getfilesize(&file->idx));
	sftp_fattr_set_uid_gid(attr, 0, static_cast<uint32_t>(dir));
	atime = file->hdr.last_downloaded; // Is this a time_t?
	mtime = file->hdr.when_written.time;
	sftp_fattr_set_times(attr, atime, mtime);
	// TODO: How to get user number of uploader if available?  For uid
	//       Answer, from_ext... be sure to check if it's anonymous etc.
	//       Real answer: We don't store the user number of uploader,
	//                    look up the usernumber from uploader's username.

	return attr;
}

static int
find_lib(sbbs_t *sbbs, const char *path, enum sftp_dir_tree tree)
{
	char *p = strdup(path);
	char *c = strchr(p, '/');
	char *exp {};
	int l;

	if (c)
		*c = 0;
	for (l = 0; l < sbbs->cfg.total_libs; l++) {
		if (!can_user_access_lib(&sbbs->cfg, l, &sbbs->useron, &sbbs->client))
			continue;
		switch (tree) {
			case SFTP_DTREE_FULL:
				exp = expand_slash(sbbs->cfg.lib[l]->lname);
				break;
			case SFTP_DTREE_SHORT:
				exp = expand_slash(sbbs->cfg.lib[l]->sname);
				break;
			case SFTP_DTREE_VIRTUAL:
				exp = expand_slash(sbbs->cfg.lib[l]->vdir);
				break;
		}
		if (exp == nullptr) {
			free(p);
			return -1;
		}
		if (strcmp(p, exp)) {
			free(exp);
			continue;
		}
		free(exp);
		break;
	}
	free(p);
	if (l < sbbs->cfg.total_libs)
		return l;
	return -1;
}

static int
find_dir(sbbs_t *sbbs, const char *path, int lib, enum sftp_dir_tree tree)
{
	char *p = strdup(path);
	char *c;
	char *e;
	int d;
	char *exp{};

	if (p == nullptr)
		return -1;
	remove_trailing_slash(p);
	c = strchr(p, '/');
	if (c == nullptr || c[1] == 0) {
		free(p);
		return -1;
	}
	c++;
	e = strchr(c, '/');
	if (e != nullptr)
		*e = 0;
	for (d = 0; d < sbbs->cfg.total_dirs; d++) {
		if (sbbs->cfg.dir[d]->lib != lib)
			continue;
		if (!can_user_access_dir(&sbbs->cfg, d, &sbbs->useron, &sbbs->client))
			continue;
		switch (tree) {
			case SFTP_DTREE_FULL:
				exp = expand_slash(sbbs->cfg.dir[d]->lname);
				break;
			case SFTP_DTREE_SHORT:
				exp = expand_slash(sbbs->cfg.dir[d]->sname);
				break;
			case SFTP_DTREE_VIRTUAL:
				exp = expand_slash(sbbs->cfg.dir[d]->vdir);
				break;
		}
		if (exp == nullptr) {
			free(p);
			return -1;
		}
		if (strcmp(c, exp)) {
			free(exp);
			continue;
		}
		free(exp);
		break;
	}
	free(p);
	if (d < sbbs->cfg.total_dirs)
		return d;
	return -1;
}

static struct pathmap *
get_pathmap_ptr(sbbs_t *sbbs, const char *filename)
{
	unsigned sf;
	char vpath[MAX_PATH + 1];

	for (sf = 0; sf < static_files_sz; sf++) {
		snprintf(vpath, sizeof(vpath), static_files[sf].sftp_patt, sbbs->useron.alias);
		remove_trailing_slash(vpath);
		if (strcmp(vpath, filename) == 0)
			return &static_files[sf];
	}
	return nullptr;
}

// TODO: This should be overhauled as well...
static sftp_file_attr_t
get_attrs(sbbs_t *sbbs, const char *path, char **link)
{
	struct pathmap *pm;
	char ppath[MAX_PATH + 1];
	sftp_file_attr_t ret;

	if (link)
		*link = nullptr;
	pm = get_pathmap_ptr(sbbs, path);
	if (pm == nullptr) {
		int lib;
		int dir;
		const char *libp;

		if (!is_in_filebase(path))
			return nullptr;
		enum sftp_dir_tree tree = in_tree(path);
		switch (tree) {
			case SFTP_DTREE_FULL:
				libp = path + files_path_len + 1;
				break;
			case SFTP_DTREE_SHORT:
				libp = path + fls_path_len + 1;
				break;
			case SFTP_DTREE_VIRTUAL:
				libp = path + vfiles_path_len + 1;
				break;
			default:
				return nullptr;
		}
		lib = find_lib(sbbs, libp, tree);
		if (lib == -1) {
			return nullptr;
		}
		const char *c = strchr(libp, '/');
		if (c == nullptr || c[1] == 0)
			return get_lib_attrs(sbbs, lib);
		dir = find_dir(sbbs, libp, lib, tree);
		if (dir == -1)
			return nullptr;
		c = strchr(c + 1, '/');
		if (c == nullptr || c[1] == 0)
			return get_dir_attrs(sbbs, dir);
		smb_t smb{};
		smbfile_t file{};
		if (smb_open_dir(&sbbs->cfg, &smb, dir) != SMB_SUCCESS)
			return nullptr;
		if (smb_findfile(&smb, &c[1], &file) != SMB_SUCCESS) {
			smb_close(&smb);
			return nullptr;
		}
		if (smb_getfile(&smb, &file, file_detail_normal) != SMB_SUCCESS) {
			smb_close(&smb);
			return nullptr;
		}
		ret = get_filebase_attrs(sbbs, dir, &file);
		smb_freefilemem(&file);
		smb_close(&smb);
		return ret;
	}
	if (pm->real_patt)
		snprintf(ppath, sizeof(ppath), pm->real_patt, sbbs->cfg.data_dir, sbbs->useron.number);
	else
		ppath[0] = 0;
	ret = pm->get_attrs(sbbs, ppath);
	if (link && pm->link_patt) {
		if (asprintf(link, pm->link_patt, sbbs->useron.alias) == -1) {
			sftp_fattr_free(ret);
			ret = nullptr;
		}
	}
	return ret;
}

static sftp_file_attr_t
get_attrs(sbbs_t *sbbs, const char *path)
{
	return get_attrs(sbbs, path, nullptr);
}

static void
copy_path(char *p, const char *fp)
{
	char *last;

	strcpy(p, fp);
	last = strrchr(p, '/');
	if (last == nullptr) {
		return;
	}
	*last = 0;
}

static void
copy_path_from_dir(char *p, const char *fp)
{
	char *last;

	strcpy(p, fp);
	last = strrchr(p, '/');
	if (last == nullptr) {
		return;
	}
	if (last[1] == 0) {
		*last = 0;
		last = strrchr(p, '/');
		if (last == nullptr)
			return;
	}
	*last = 0;
}

static void
record_transfer(sbbs_t *sbbs, sftp_filedescriptor_t desc, bool upload)
{
	if (desc->dir == -1) {
		char str[MAX_PATH + 1];
		snprintf(str, sizeof str, "%sloaded %s (%" PRId64 " bytes)"
			, upload ? "up" : "down", desc->local_path, flength(desc->local_path));
		sbbs->logline(upload ? "U+" : "D-", str);
		return;
	}
	char *nptr = strrchr(desc->local_path, '/');
	if (nptr != nullptr) {
		file_t file{};
		nptr++;
		file.name = nptr;
		file.dir = desc->dir;
		file.size = flength(desc->local_path);
		file.file_idx.idx.offset = desc->idx_offset;
		file.file_idx.idx.number = desc->idx_number;
		if (upload)
			sbbs->uploadfile(&file);
		else
			sbbs->downloadedfile(&file);
		file.name = nullptr;
		// We shouldn't need to call this, but it doesn't hurt.
		smb_freefilemem(&file);
	}
}

extern "C" {

static bool
sftp_send(uint8_t *buf, size_t len, void *cb_data)
{
	sbbs_t *sbbs = (sbbs_t *)cb_data;
	size_t sent = 0;
	int i;
	int status;

	if (sbbs->sftp_channel == -1)
		return false;
	while (sent < len) {
		pthread_mutex_lock(&sbbs->ssh_mutex);
		status = cryptSetAttribute(sbbs->ssh_session, CRYPT_SESSINFO_SSH_CHANNEL, sbbs->sftp_channel);
		if (cryptStatusError(status)) {
			pthread_mutex_unlock(&sbbs->ssh_mutex);
			return false;
		}
		size_t sendbytes = len - sent;
#define SENDBYTES_MAX 0x2000
		if (sendbytes > SENDBYTES_MAX)
			sendbytes = SENDBYTES_MAX;
		status = cryptSetAttribute(sbbs->ssh_session, CRYPT_OPTION_NET_WRITETIMEOUT, 5);
		if(cryptStatusError(status)) {
			pthread_mutex_unlock(&sbbs->ssh_mutex);
			return false;
		}
		status = cryptPushData(sbbs->ssh_session, (char*)buf + sent, sendbytes, &i);
		if(cryptStatusError(status)) {
			pthread_mutex_unlock(&sbbs->ssh_mutex);
			return false;
		}
		status = cryptFlushData(sbbs->ssh_session);
		if(cryptStatusError(status)) {
			pthread_mutex_unlock(&sbbs->ssh_mutex);
			return false;
		}
		status = cryptSetAttribute(sbbs->ssh_session, CRYPT_OPTION_NET_WRITETIMEOUT, 0);
		if(cryptStatusError(status)) {
			pthread_mutex_unlock(&sbbs->ssh_mutex);
			return false;
		}
		pthread_mutex_unlock(&sbbs->ssh_mutex);
		sent += i;
	}
	return true;
}

static void
sftp_lprint(void *arg, uint32_t errcode, const char *msg)
{
	sbbs_t *sbbs = (sbbs_t *)arg;
	int level = LOG_DEBUG;

	switch (errcode) {
		case SSH_FX_PERMISSION_DENIED:
			level = LOG_INFO;
			break;
		case SSH_FX_FAILURE:
		case SSH_FX_BAD_MESSAGE:
			level = LOG_WARNING;
			break;
	}

	sbbs->lprintf(level, "SFTP error code %" PRIu32 " (%s) %s", errcode, sftp_get_errcode_name(errcode), msg);
}

static void
sftp_cleanup_callback(void *cb_data)
{
	sbbs_t *sbbs = (sbbs_t *)cb_data;
	constexpr size_t nfdes = sizeof(sbbs->sftp_filedes) / sizeof(sbbs->sftp_filedes[0]);
	constexpr size_t nddes = sizeof(sbbs->sftp_dirdes) / sizeof(sbbs->sftp_dirdes[0]);

	for (unsigned i = 0; i < nfdes; i++) {
		if (sbbs->sftp_filedes[i] != nullptr) {
			close(sbbs->sftp_filedes[i]->fd);
			if (sbbs->sftp_filedes[i]->created && sbbs->sftp_filedes[i]->local_path) {
				// If we were uploading, delete the incomplete file
				remove(sbbs->sftp_filedes[i]->local_path);
			}
			free(sbbs->sftp_filedes[i]->local_path);
			free(sbbs->sftp_filedes[i]);
			sbbs->sftp_filedes[i] = nullptr;
		}
	}
	for (unsigned i = 0; i < nddes; i++) {
		free(sbbs->sftp_dirdes[i]);
		sbbs->sftp_dirdes[i] = nullptr;
	}
	free(sbbs->sftp_cwd);
}

static bool
sftp_open(sftp_str_t filename, uint32_t flags, sftp_file_attr_t attributes, void *cb_data)
{
	sbbs_t *sbbs = (sbbs_t *)cb_data;
	constexpr size_t nfdes = sizeof(sbbs->sftp_filedes) / sizeof(sbbs->sftp_filedes[0]);
	unsigned fdidx;
	mode_t omode = 0;
	int oflags = O_BINARY;
	sftp_str_t handle;
	bool ret;
	map_path_mode_t mmode;

	sbbs->lprintf(LOG_DEBUG, "SFTP open(%.*s, %x, %s)", filename->len, filename->c_str, flags, sftp_attr_string(attributes).c_str());

	// See if there's an available file descriptor
	for (fdidx = 0; fdidx < nfdes; fdidx++) {
		if (sbbs->sftp_filedes[fdidx] == nullptr)
			break;
	}
	if (fdidx == nfdes) {
		return sftps_send_error(sbbs->sftp_state, SSH_FX_FAILURE, "Too many open file descriptors");
	}
	switch (flags & (SSH_FXF_READ | SSH_FXF_WRITE)) {
		case SSH_FXF_READ:
			oflags |= O_RDONLY;
			mmode = MAP_READ;
			break;
		case SSH_FXF_WRITE:
			oflags |= O_WRONLY;
			mmode = MAP_WRITE;
			break;
		case (SSH_FXF_READ | SSH_FXF_WRITE):
			oflags |= O_RDWR;
			mmode = MAP_RDWR;
			break;
		default:
			return sftps_send_error(sbbs->sftp_state, SSH_FX_OP_UNSUPPORTED, "Invalid flags (not read or write)");
	}
	if (flags & SSH_FXF_APPEND)
		oflags |= O_APPEND;
	if (flags & SSH_FXF_CREAT)
		oflags |= O_CREAT;
	if (flags & SSH_FXF_TRUNC)
		oflags |= O_TRUNC;
	if (flags & SSH_FXF_EXCL)
		oflags |= O_EXCL;
	path_map pmap(sbbs, filename->c_str, mmode);
	if (pmap.result() != MAP_TO_FILE)
		return pmap.cleanup();
	if (oflags & O_CREAT) {
		uint32_t perms;
		if (!sftp_fattr_get_permissions(attributes, &perms)) {
			omode = DEFFILEMODE;
		}
		else {
			if (perms & 0444) {
				omode |= S_IREAD;
			}
			if (perms & 0222) {
				omode |= S_IWRITE;
			}
			if (perms & ~(0666)) {
				return sftps_send_error(sbbs->sftp_state, SSH_FX_OP_UNSUPPORTED, "Invalid permissions");
			}
		}
		if (sftp_fattr_get_size(attributes, nullptr)) {
			return sftps_send_error(sbbs->sftp_state, SSH_FX_OP_UNSUPPORTED, "Specifying size in open not supported");
		}
		if (sftp_fattr_get_uid(attributes, nullptr)) {
			return sftps_send_error(sbbs->sftp_state, SSH_FX_OP_UNSUPPORTED, "Specifying uid/gid in open not supported");
		}
		if (sftp_fattr_get_atime(attributes, nullptr)) {
			return sftps_send_error(sbbs->sftp_state, SSH_FX_OP_UNSUPPORTED, "Specifying times in open not supported");
		}
		if (sftp_fattr_get_ext_count(attributes)) {
			return sftps_send_error(sbbs->sftp_state, SSH_FX_OP_UNSUPPORTED, "Specifying extended attributes in open not supported");
		}
	}
	sbbs->sftp_filedes[fdidx] = static_cast<sftp_filedescriptor_t>(calloc(1, sizeof(*sbbs->sftp_filedes[0])));
	if (sbbs->sftp_filedes[fdidx] == nullptr) {
		return sftps_send_error(sbbs->sftp_state, SSH_FX_FAILURE, "Unable to allocate file handle");
	}
	if (pmap.is_static())
		sbbs->sftp_filedes[fdidx]->dir = -1;
	else {
		sbbs->sftp_filedes[fdidx]->dir = pmap.info.filebase.dir;
		sbbs->sftp_filedes[fdidx]->idx_offset = pmap.info.filebase.offset;
		sbbs->sftp_filedes[fdidx]->idx_number = pmap.info.filebase.idx;
	}
	if (access(pmap.local_path, F_OK) != 0) {
		// File did not exist, and we're creating
		if (oflags & O_CREAT) {
			sbbs->sftp_filedes[fdidx]->created = true;
		}
	}
	if (sbbs->sftp_filedes[fdidx] == nullptr) {
		return sftps_send_error(sbbs->sftp_state, SSH_FX_FAILURE, "Unable to allocate file handle");
	}
	sbbs->sftp_filedes[fdidx]->local_path = strdup(pmap.local_path);
	if (sbbs->sftp_filedes[fdidx]->local_path == nullptr) {
		free(sbbs->sftp_filedes[fdidx]);
		return sftps_send_error(sbbs->sftp_state, SSH_FX_FAILURE, "Allocation failure");
	}
	sbbs->sftp_filedes[fdidx]->fd = open(pmap.local_path, oflags, omode);
	if (sbbs->sftp_filedes[fdidx]->fd == -1) {
		free(sbbs->sftp_filedes[fdidx]->local_path);
		free(sbbs->sftp_filedes[fdidx]);
		return sftps_send_error(sbbs->sftp_state, SSH_FX_FAILURE, "Operation failed");
	}
	handle = sftp_asprintf("%u", fdidx + 1);
	if (handle == nullptr) {
		close(sbbs->sftp_filedes[fdidx]->fd);
		free(sbbs->sftp_filedes[fdidx]->local_path);
		free(sbbs->sftp_filedes[fdidx]);
		return sftps_send_error(sbbs->sftp_state, SSH_FX_FAILURE, "Out of resources");
	}
	ret = sftps_send_handle(sbbs->sftp_state, handle);
	free_sftp_str(handle);
	return ret;
}

static bool
sftp_close(sftp_str_t handle, void *cb_data)
{
	sbbs_t *sbbs = (sbbs_t *)cb_data;

	sbbs->lprintf(LOG_DEBUG, "SFTP close(%.*s)", handle->len, handle->c_str);
	if (isdigit(handle->c_str[0])) {
		unsigned fidx = parse_file_handle(sbbs, handle);
		if (fidx == UINT_MAX) {
			return sftps_send_error(sbbs->sftp_state, SSH_FX_FAILURE, "Invalid file handle");
		}
		int rval = close(sbbs->sftp_filedes[fidx]->fd);
		if (sbbs->sftp_filedes[fidx]->created)
			record_transfer(sbbs, sbbs->sftp_filedes[fidx], true);
		free(sbbs->sftp_filedes[fidx]->local_path);
		free(sbbs->sftp_filedes[fidx]);
		sbbs->sftp_filedes[fidx] = nullptr;
		if (rval)
			return sftps_send_error(sbbs->sftp_state, SSH_FX_FAILURE, "Close failed");
		else
			return sftps_send_error(sbbs->sftp_state, SSH_FX_OK, "Closed");
	}
	else {
		unsigned didx = parse_dir_handle(sbbs, handle);
		if (didx == UINT_MAX) {
			return sftps_send_error(sbbs->sftp_state, SSH_FX_FAILURE, "Invalid handle");
		}
		free(sbbs->sftp_dirdes[didx]);
		sbbs->sftp_dirdes[didx] = nullptr;
		return sftps_send_error(sbbs->sftp_state, SSH_FX_OK, "Closed");
	}
}

static bool
sftp_read(sftp_filehandle_t handle, uint64_t offset, uint32_t len, void *cb_data)
{
	sbbs_t *sbbs = (sbbs_t *)cb_data;
	unsigned fidx = parse_file_handle(sbbs, handle);
	ssize_t rlen;

	sbbs->lprintf(LOG_DEBUG, "SFTP read(%.*s, %" PRIu64 ", %" PRIu32 ")", handle->len, handle->c_str, offset, len);
	if (fidx == UINT_MAX) {
		return sftps_send_error(sbbs->sftp_state, SSH_FX_FAILURE, "Invalid file handle");
	}
	int fd = sbbs->sftp_filedes[fidx]->fd;
	if (fd == -1) {
		return sftps_send_error(sbbs->sftp_state, SSH_FX_FAILURE, "Invalid file handle");
	}
	if (lseek(fd, offset, SEEK_SET) == -1) {
		return sftps_send_error(sbbs->sftp_state, SSH_FX_FAILURE, "Unable to seek to correct position");
	}
	sftp_str_t data = sftp_alloc_str(len);
	if (data == nullptr) {
		return sftps_send_error(sbbs->sftp_state, SSH_FX_FAILURE, "Unable to allocate buffer");
	}
	rlen = read(fd, data->c_str, len);
	if (rlen == 0) {
		// EOF
		free_sftp_str(data);
		return sftps_send_error(sbbs->sftp_state, SSH_FX_EOF, "End of file");
	}
	if (rlen == -1) {
		// Error
		free_sftp_str(data);
		return sftps_send_error(sbbs->sftp_state, SSH_FX_FAILURE, "Failed");
	}
	data->len = rlen;
	bool ret = sftps_send_data(sbbs->sftp_state, data);
	free_sftp_str(data);
	/*
	 * A successful transfer is defined as the last byte of the file
	 * being transmitted to the remote.
	 */
	uint8_t byte;
	if (read(fd, &byte, 1) == 0)
		record_transfer(sbbs, sbbs->sftp_filedes[fidx], false);

	return ret;
}

static bool
sftp_write(sftp_filehandle_t handle, uint64_t offset, sftp_str_t data, void *cb_data)
{
	sbbs_t *sbbs = (sbbs_t *)cb_data;
	unsigned fidx = parse_file_handle(sbbs, handle);
	ssize_t rlen;

	sbbs->lprintf(LOG_DEBUG, "SFTP write(%.*s, %" PRIu64 ", %" PRIu32 ")", handle->len, handle->c_str, offset, data->len);
	if (data->len == 0) {
		return sftps_send_error(sbbs->sftp_state, SSH_FX_OK, "Nothing done, as requested");
	}
	if (fidx == UINT_MAX) {
		return sftps_send_error(sbbs->sftp_state, SSH_FX_FAILURE, "Invalid file handle");
	}
	int fd = sbbs->sftp_filedes[fidx]->fd;
	if (fd == -1) {
		return sftps_send_error(sbbs->sftp_state, SSH_FX_FAILURE, "Invalid file handle");
	}
	if (lseek(fd, offset, SEEK_SET) == -1) {
		return sftps_send_error(sbbs->sftp_state, SSH_FX_FAILURE, "Unable to seek to correct position");
	}
	rlen = write(fd, data->c_str, data->len);
	if (rlen == -1) {
		// Error
		return sftps_send_error(sbbs->sftp_state, SSH_FX_FAILURE, "Failed");
	}
	if (rlen != data->len) {
		return sftps_send_error(sbbs->sftp_state, SSH_FX_EOF, "Short write... I dunno.");
	}
	return sftps_send_error(sbbs->sftp_state, SSH_FX_OK, "Wrote");
}

static bool
sftp_realpath(sftp_str_t path, void *cb_data)
{
	sbbs_t *sbbs = (sbbs_t *)cb_data;
	char *rp = sftp_parse_realpath(sbbs, path);
	sbbs->lprintf(LOG_DEBUG, "SFTP realpath(%.*s)", path->len, path->c_str);
	if (rp == nullptr) {
		return sftps_send_error(sbbs->sftp_state, SSH_FX_FAILURE, "No idea where that is boss");
	}
	sftp_file_attr_t attr = dummy_attrs();
	if (attr == nullptr) {
		free(rp);
		return sftps_send_error(sbbs->sftp_state, SSH_FX_FAILURE, "Unable to allocate attribute");
	}
	bool ret = sftps_send_name(sbbs->sftp_state, 1, &rp, &rp, &attr);
	free(rp);
	sftp_fattr_free(attr);

	return ret;
}

static bool
sftp_opendir(sftp_str_t path, void *cb_data)
{
	sbbs_t *sbbs = (sbbs_t *)cb_data;
	constexpr size_t nddes = sizeof(sbbs->sftp_dirdes) / sizeof(sbbs->sftp_dirdes[0]);
	unsigned ddidx;
	sftp_str_t h;

	sbbs->lprintf(LOG_DEBUG, "SFTP opendir(%.*s)", path->len, path->c_str);
	// See if there's an available file descriptor
	for (ddidx = 0; ddidx < nddes; ddidx++) {
		if (sbbs->sftp_dirdes[ddidx] == nullptr)
			break;
	}
	if (ddidx == nddes) {
		return sftps_send_error(sbbs->sftp_state, SSH_FX_FAILURE, "Too many open file descriptors");
	}
	path_map pmap(sbbs, path->c_str, MAP_READ);
	if (pmap.result() != MAP_TO_DIR)
		return pmap.cleanup();
	sbbs->sftp_dirdes[ddidx] = static_cast<sftp_dirdescriptor_t>(malloc(sizeof(*sbbs->sftp_dirdes[ddidx])));
	sbbs->sftp_dirdes[ddidx]->tree = pmap.tree();
	if (pmap.is_static()) {
		sbbs->sftp_dirdes[ddidx]->is_static = true;
		sbbs->sftp_dirdes[ddidx]->info.rootdir.mapping = pmap.info.rootdir.mapping;
		sbbs->sftp_dirdes[ddidx]->info.rootdir.idx = dot;
	}
	else {
		sbbs->sftp_dirdes[ddidx]->is_static = false;
		sbbs->sftp_dirdes[ddidx]->info.filebase.lib = pmap.info.filebase.lib;
		sbbs->sftp_dirdes[ddidx]->info.filebase.dir = pmap.info.filebase.dir;
		sbbs->sftp_dirdes[ddidx]->info.filebase.idx = dot;
	}
	h = sftp_asprintf("D:%u", ddidx + 1);
	if (h == nullptr) {
		return sftps_send_error(sbbs->sftp_state, SSH_FX_FAILURE, "Handle allocation failure");
	}
	bool ret = sftps_send_handle(sbbs->sftp_state, h);
	free_sftp_str(h);
	return ret;
}

// TODO: This is still too ugly... should be split into multiple functions.
static bool
sftp_readdir(sftp_dirhandle_t handle, void *cb_data)
{
	sbbs_t *sbbs = (sbbs_t *)cb_data;
	unsigned didx = parse_dir_handle(sbbs, handle);
	sftp_file_attr_t attr;
	sftp_dirdescriptor_t dd;
	char tmppath[MAX_PATH + 1];
	char cwd[MAX_PATH + 1];
	char *vpath;
	char *lname;
	char *ename;
	struct pathmap *pm;
	file_names fn(sbbs);

	sbbs->lprintf(LOG_DEBUG, "SFTP readdir(%.*s)", handle->len, handle->c_str);
	if (didx == UINT_MAX)
		return sftps_send_error(sbbs->sftp_state, SSH_FX_FAILURE, "Invalid handle");
	dd = sbbs->sftp_dirdes[didx];
	pm = static_cast<struct pathmap *>(dd->info.rootdir.mapping);
	if (dd->is_static) {
		char *link;

		if (dd->info.rootdir.idx == no_more_files) {
			if (fn.entries() > 0)
				return fn.send();
			return sftps_send_error(sbbs->sftp_state, SSH_FX_EOF, "No more files");
		}
		if (dd->info.rootdir.idx == dot) {
			const char *dir = ".";
			snprintf(tmppath, sizeof(tmppath), pm->sftp_patt, sbbs->useron.alias);
			remove_trailing_slash(tmppath);
			if (!fn.generic_dot_entry(strdup(dir), tmppath, dd->info.rootdir.idx))
				return sftps_send_error(sbbs->sftp_state, SSH_FX_FAILURE, "Unable to add dot dir");
		}
		if (dd->info.rootdir.idx == dotdot) {
			if (pm->sftp_patt[1]) {
				const char *dir = "..";
				snprintf(tmppath, sizeof(tmppath) - 3 /* for dir */, pm->sftp_patt, sbbs->useron.alias);
				tmppath[sizeof(tmppath) - 2] = 0;
				strcat(tmppath, dir);
				if (!fn.generic_dot_realpath_entry(strdup(dir), tmppath, dd->info.rootdir.idx))
					return sftps_send_error(sbbs->sftp_state, SSH_FX_FAILURE, "Unable to add dotdot dir");
			}
			else
				dd->info.rootdir.idx++;
		}
		if (dd->info.rootdir.idx == 0) {
			unsigned sf;
			for (sf = 0; sf < static_files_sz; sf++) {
				if (&static_files[sf] == pm) {
					dd->info.rootdir.idx = sf;
					break;
				}
			}
			if (sf == static_files_sz)
				return sftps_send_error(sbbs->sftp_state, SSH_FX_FAILURE, "Corrupt directory handle");
		}
		copy_path(cwd, pm->sftp_patt);
		while (static_files[dd->info.rootdir.idx].sftp_patt != nullptr && fn.entries() < MAX_FILES_PER_READDIR) {
			dd->info.rootdir.idx++;
			if (static_cast<size_t>(dd->info.rootdir.idx) >= static_files_sz)
				break;
			if (static_files[dd->info.rootdir.idx].sftp_patt == nullptr) {
				dd->info.rootdir.idx = no_more_files;
				if (fn.entries() > 0)
					return fn.send();
				return sftps_send_error(sbbs->sftp_state, SSH_FX_EOF, "No more files");
			}
			copy_path_from_dir(tmppath, static_files[dd->info.rootdir.idx].sftp_patt);
			if (strcmp(cwd, tmppath))
				continue;
			if (static_files[dd->info.rootdir.idx].real_patt) {
				sprintf(tmppath, static_files[dd->info.rootdir.idx].real_patt, sbbs->cfg.data_dir, sbbs->useron.number);
				if (access(tmppath, F_OK))
					continue;
			}
			sprintf(tmppath, static_files[dd->info.rootdir.idx].sftp_patt, sbbs->useron.alias);
			remove_trailing_slash(tmppath);
			attr = get_attrs(sbbs, tmppath, &link);
			if (attr == nullptr) {
				free(link);
				return sftps_send_error(sbbs->sftp_state, SSH_FX_FAILURE, "Attributes allocation failure");
			}
			lname = get_longname(sbbs, tmppath, link, attr);
			free(link);
			if (lname == nullptr) {
				sftp_fattr_free(attr);
				return sftps_send_error(sbbs->sftp_state, SSH_FX_FAILURE, "Longname allocation failure");
			}
			vpath = getfname(tmppath);
			if (!fn.add_name(strdup(vpath), lname, attr))
				return sftps_send_error(sbbs->sftp_state, SSH_FX_FAILURE, "adding static file");
		}
	}
	else {
		if (dd->info.filebase.lib == -1) {
			// /files/ (ie: list of libs)
			if (dd->info.filebase.idx == no_more_files) {
				if (fn.entries() > 0)
					return fn.send();
				return sftps_send_error(sbbs->sftp_state, SSH_FX_EOF, "No more files");
			}
			if (dd->info.filebase.idx == dot) {
				const char *dir = ".";
				switch (dd->tree) {
					case SFTP_DTREE_FULL:
						strcpy(tmppath, SLASH_FILES);
						break;
					case SFTP_DTREE_SHORT:
						strcpy(tmppath, SLASH_FLS);
						break;
					case SFTP_DTREE_VIRTUAL:
						strcpy(tmppath, SLASH_VFILES);
						break;
				}
				if (!fn.generic_dot_entry(strdup(dir), tmppath, dd->info.filebase.idx))
					return sftps_send_error(sbbs->sftp_state, SSH_FX_EOF, "Error adding topdoot");
			}
			if (dd->info.filebase.idx == dotdot) {
				const char *dir = "..";
				strcpy(tmppath, "/");
				if (!fn.generic_dot_entry(strdup(dir), tmppath, dd->info.filebase.idx))
					return sftps_send_error(sbbs->sftp_state, SSH_FX_EOF, "Error adding topdootdoot");
			}
			while (dd->info.filebase.idx < sbbs->cfg.total_libs && fn.entries() < MAX_FILES_PER_READDIR) {
				if (dd->info.filebase.idx >= sbbs->cfg.total_libs) {
					dd->info.filebase.idx = no_more_files;
					if (fn.entries() > 0)
						return fn.send();
					return sftps_send_error(sbbs->sftp_state, SSH_FX_EOF, "No more files");
				}
				if (!can_user_access_lib(&sbbs->cfg, dd->info.filebase.idx, &sbbs->useron, &sbbs->client)) {
					dd->info.filebase.idx++;
					continue;
				}
				attr = get_lib_attrs(sbbs, dd->info.filebase.idx);
				if (attr == nullptr)
					return sftps_send_error(sbbs->sftp_state, SSH_FX_FAILURE, "Attributes allocation failure");
				switch (dd->tree) {
					case SFTP_DTREE_FULL:
						ename = expand_slash(sbbs->cfg.lib[dd->info.filebase.idx]->lname);
						break;
					case SFTP_DTREE_SHORT:
						ename = expand_slash(sbbs->cfg.lib[dd->info.filebase.idx]->sname);
						break;
					case SFTP_DTREE_VIRTUAL:
						ename = expand_slash(sbbs->cfg.lib[dd->info.filebase.idx]->vdir);
						break;
					default:
						ename = nullptr;
						break;
				}
				if (ename == nullptr) {
					sftp_fattr_free(attr);
					return sftps_send_error(sbbs->sftp_state, SSH_FX_FAILURE, "Ename allocation failure");
				}
				lname = get_longname(sbbs, ename, nullptr, attr);
				if (lname == nullptr) {
					free(ename);
					sftp_fattr_free(attr);
					return sftps_send_error(sbbs->sftp_state, SSH_FX_FAILURE, "Longname allocation failure");
				}
				if (!fn.add_name(ename, lname, attr))
					return sftps_send_error(sbbs->sftp_state, SSH_FX_EOF, "Error adding lib");
				dd->info.filebase.idx++;
			}
		}
		else if (dd->info.filebase.dir == -1) {
			// /files/somelib (ie: list of dirs)
			if (dd->info.filebase.idx == no_more_files) {
				if (fn.entries() > 0)
					return fn.send();
				return sftps_send_error(sbbs->sftp_state, SSH_FX_EOF, "No more files");
			}
			if (dd->info.filebase.idx == dot) {
				const char *dir = ".";
				attr = get_lib_attrs(sbbs, dd->info.filebase.lib);
				if (!fn.generic_dot_attr_entry(strdup(dir), attr, nullptr, dd->info.filebase.idx))
					return sftps_send_error(sbbs->sftp_state, SSH_FX_EOF, "Adding libdoot");
			}
			if (dd->info.filebase.idx == dotdot) {
				const char *dir = "..";
				switch (dd->tree) {
					case SFTP_DTREE_FULL:
						strcpy(tmppath, SLASH_FILES);
						break;
					case SFTP_DTREE_SHORT:
						strcpy(tmppath, SLASH_FLS);
						break;
					case SFTP_DTREE_VIRTUAL:
						strcpy(tmppath, SLASH_VFILES);
						break;
				}
				if (!fn.generic_dot_entry(strdup(dir), tmppath, dd->info.filebase.idx))
					return sftps_send_error(sbbs->sftp_state, SSH_FX_EOF, "Adding libdootdoot");
			}
			while (dd->info.filebase.idx < sbbs->cfg.total_dirs && fn.entries() < MAX_FILES_PER_READDIR) {
				if (dd->info.filebase.idx >= sbbs->cfg.total_dirs) {
					dd->info.filebase.idx = no_more_files;
					if (fn.entries() > 0)
						return fn.send();
					return sftps_send_error(sbbs->sftp_state, SSH_FX_EOF, "No more files");
				}
				if (sbbs->cfg.dir[dd->info.filebase.idx]->lib != dd->info.filebase.lib) {
					dd->info.filebase.idx++;
					continue;
				}
				if (!can_user_access_dir(&sbbs->cfg, dd->info.filebase.idx, &sbbs->useron, &sbbs->client)) {
					dd->info.filebase.idx++;
					continue;
				}
				attr = get_dir_attrs(sbbs, dd->info.filebase.idx);
				if (attr == nullptr)
					return sftps_send_error(sbbs->sftp_state, SSH_FX_FAILURE, "Attributes allocation failure");
				switch (dd->tree) {
					case SFTP_DTREE_FULL:
						ename = expand_slash(sbbs->cfg.dir[dd->info.filebase.idx]->lname);
						break;
					case SFTP_DTREE_SHORT:
						ename = expand_slash(sbbs->cfg.dir[dd->info.filebase.idx]->sname);
						break;
					case SFTP_DTREE_VIRTUAL:
						ename = expand_slash(sbbs->cfg.dir[dd->info.filebase.idx]->vdir);
						break;
					default:
						sftp_fattr_free(attr);
						return sftps_send_error(sbbs->sftp_state, SSH_FX_FAILURE, "Invalid tree type");
				}
				if (ename == nullptr) {
					sftp_fattr_free(attr);
					return sftps_send_error(sbbs->sftp_state, SSH_FX_FAILURE, "EName allocation failure");
				}
				lname = get_longname(sbbs, ename, nullptr, attr);
				if (lname == nullptr) {
					free(ename);
					sftp_fattr_free(attr);
					return sftps_send_error(sbbs->sftp_state, SSH_FX_FAILURE, "Longname allocation failure");
				}
				if (!fn.add_name(ename, lname, attr))
					return sftps_send_error(sbbs->sftp_state, SSH_FX_FAILURE, "Add dir name failure");
				dd->info.filebase.idx++;
			}
		}
		else {
			// /files/somelib/somedir (ie: list of files)
			if (dd->info.filebase.idx == no_more_files) {
				if (fn.entries() > 0)
					return fn.send();
				return sftps_send_error(sbbs->sftp_state, SSH_FX_EOF, "No more files");
			}
			if (dd->info.filebase.idx == dot) {
				const char *dir = ".";
				attr = get_dir_attrs(sbbs, dd->info.filebase.dir);
				if (!fn.generic_dot_attr_entry(strdup(dir), attr, nullptr, dd->info.filebase.idx))
					return sftps_send_error(sbbs->sftp_state, SSH_FX_EOF, "Adding dirdoot");
			}
			if (dd->info.filebase.idx == dotdot) {
				const char *dir = "..";
				attr = get_lib_attrs(sbbs, dd->info.filebase.lib);
				if (!fn.generic_dot_attr_entry(strdup(dir), attr, nullptr, dd->info.filebase.idx))
					return sftps_send_error(sbbs->sftp_state, SSH_FX_EOF, "Adding dirdootdoot");
			}
			// Find the "next"* file number.
			smb_t     smb{};
			idxrec_t  idx{};
			smbfile_t file{};
			if (smb_open_dir(&sbbs->cfg, &smb, dd->info.filebase.dir) != SMB_SUCCESS) {
				return sftps_send_error(sbbs->sftp_state, SSH_FX_FAILURE, "Can't open dir");
			}
			do  {
				if (dd->info.filebase.idx == 0) {
					if (smb_getfirstidx(&smb, &idx) != SMB_SUCCESS) {
						smb_close(&smb);
						dd->info.filebase.idx = no_more_files;
						if (fn.entries() > 0)
							return fn.send();
						return sftps_send_error(sbbs->sftp_state, SSH_FX_EOF, "No files at all");
					}
					file.hdr.number = idx.number;
				}
				else {
					file.hdr.number = dd->info.filebase.idx;
					if (smb_getmsgidx(&smb, &file) != SMB_SUCCESS) {
						smb_close(&smb);
						return sftps_send_error(sbbs->sftp_state, SSH_FX_FAILURE, "Can't find previous file in index");
					}
					file.hdr.number = 0;
					file.idx_offset++;
				}
				int result = smb_getmsgidx(&smb, &file);
				if (result == SMB_ERR_HDR_OFFSET) {
					smb_close(&smb);
					dd->info.filebase.idx = no_more_files;
					if (fn.entries() > 0)
						return fn.send();
					return sftps_send_error(sbbs->sftp_state, SSH_FX_EOF, "No more files");
				}
				if (result != SMB_SUCCESS) {
					smb_close(&smb);
					return sftps_send_error(sbbs->sftp_state, SSH_FX_FAILURE, "Can't find next file in index");
				}
				dd->info.filebase.idx = file.file_idx.idx.number;
				if (!(file.file_idx.idx.attr & MSG_FILE))
					continue;
				if (file.file_idx.idx.attr & (MSG_DELETE | MSG_PRIVATE))
					continue;
				if ((file.file_idx.idx.attr & (MSG_MODERATED | MSG_VALIDATED)) == MSG_MODERATED)
					continue;
				if (file.hdr.auxattr & MSG_NODISP)
					continue;
				if (smb_getfile(&smb, &file, file_detail_normal) != SMB_SUCCESS) {
					smb_close(&smb);
					return sftps_send_error(sbbs->sftp_state, SSH_FX_FAILURE, "Can't get file header");
				}
				attr = get_filebase_attrs(sbbs, dd->info.filebase.dir, &file);
				if (attr == nullptr) {
					smb_freefilemem(&file);
					smb_close(&smb);
					return sftps_send_error(sbbs->sftp_state, SSH_FX_FAILURE, "Can't get file attributes");
				}
				strcpy(tmppath, file.name);
				sprintf(cwd, "%s/%s", sbbs->cfg.dir[dd->info.filebase.dir]->path, file.name);
				smb_freefilemem(&file);
				if (access(cwd, R_OK)) {
					sftp_fattr_free(attr);
					continue;
				}
				char *lname = get_longname(sbbs, cwd, nullptr, attr);
				if (lname == nullptr) {
					sftp_fattr_free(attr);
					smb_close(&smb);
					return sftps_send_error(sbbs->sftp_state, SSH_FX_FAILURE, "Can't get file header");
				}
				if (!fn.add_name(strdup(tmppath), lname, attr)) {
					smb_close(&smb);
					return sftps_send_error(sbbs->sftp_state, SSH_FX_FAILURE, "Can't get file header");
				}
			} while (fn.entries() < MAX_FILES_PER_READDIR);
			smb_close(&smb);
		}
	}
	if (fn.entries() > 0)
		return fn.send();
	return sftps_send_error(sbbs->sftp_state, SSH_FX_EOF, "No more files");
}

static bool
sftp_stat(sftp_str_t path, void *cb_data)
{
	sbbs_t *sbbs = (sbbs_t *)cb_data;
	unsigned lcnt = 0;

	sbbs->lprintf(LOG_DEBUG, "SFTP stat(%.*s)", path->len, path->c_str);

	std::unique_ptr<path_map> cpmap(new path_map(sbbs, path->c_str, MAP_STAT));

	if (!cpmap->success())
		return cpmap->cleanup();
	while (cpmap->sftp_link_target != nullptr) {
		lcnt++;
		if (lcnt > 50) {
			return sftps_send_error(sbbs->sftp_state, SSH_FX_FAILURE, "Too many symbolic links");
		}
		std::unique_ptr<path_map> newpmap(new path_map(sbbs, cpmap->sftp_link_target, MAP_STAT));
		if (!newpmap->success())
			return newpmap->cleanup();
		cpmap = std::move(newpmap);
	}
	sftp_file_attr_t attr = get_attrs(sbbs, cpmap->sftp_path);
	if (attr == nullptr)
		return sftps_send_error(sbbs->sftp_state, SSH_FX_FAILURE, "Unable to allocate attribute");
	bool ret = sftps_send_attrs(sbbs->sftp_state, attr);
	sftp_fattr_free(attr);

	return ret;
}

static bool
sftp_lstat(sftp_str_t path, void *cb_data)
{
	sbbs_t *sbbs = (sbbs_t *)cb_data;
	sbbs->lprintf(LOG_DEBUG, "SFTP lstat(%.*s)", path->len, path->c_str);
	path_map pmap(sbbs, path->c_str, MAP_STAT);
	if (!pmap.success())
		return pmap.cleanup();
	sftp_file_attr_t attr = get_attrs(sbbs, pmap.sftp_path);
	if (attr == nullptr)
		return sftps_send_error(sbbs->sftp_state, SSH_FX_FAILURE, "Unable to allocate attribute");
	bool ret = sftps_send_attrs(sbbs->sftp_state, attr);
	sftp_fattr_free(attr);

	return ret;
}

static bool
sftp_readlink(sftp_str_t path, void *cb_data)
{
	sbbs_t *sbbs = (sbbs_t *)cb_data;
	sbbs->lprintf(LOG_DEBUG, "SFTP readlink(%.*s)", path->len, path->c_str);
	path_map pmap(sbbs, path->c_str, MAP_STAT);
	if (pmap.result() != MAP_TO_SYMLINK)
		return sftps_send_error(sbbs->sftp_state, SSH_FX_FAILURE, "Not a symlink");
	sftp_file_attr_t attr = dummy_attrs();
	if (attr == nullptr)
		return sftps_send_error(sbbs->sftp_state, SSH_FX_FAILURE, "Unable to allocate attribute");
	bool ret = sftps_send_name(sbbs->sftp_state, 1, &pmap.sftp_link_target, &pmap.sftp_link_target, &attr);
	sftp_fattr_free(attr);

	return ret;
}

#if NOTYET
static bool
sftp_fstat(sftp_filehandle_t handle, void *cb_data)
{
	sbbs_t *sbbs = (sbbs_t *)cb_data;
	return true;
}

static bool
sftp_remove(sftp_str_t filename, void *cb_data)
{
	sbbs_t *sbbs = (sbbs_t *)cb_data;
	return true;
}

static bool
sftp_rename(sftp_str_t oldpath, sftp_str_t newpath, void *cb_data)
{
	sbbs_t *sbbs = (sbbs_t *)cb_data;
	return true;
}

static bool
sftp_extended(sftp_str_t request, sftp_rx_pkt_t pkt, void *cb_data)
{
	sbbs_t *sbbs = (sbbs_t *)cb_data;
	return true;
}

static bool
sftp_setstat(sftp_str_t path, sftp_file_attr_t attributes, void *cb_data)
{
	sbbs_t *sbbs = (sbbs_t *)cb_data;
	return true;
}

static bool
sftp_fsetstat(sftp_filehandle_t handle, sftp_file_attr_t attributes, void *cb_data)
{
	sbbs_t *sbbs = (sbbs_t *)cb_data;
	return true;
}

static bool
sftp_mkdir(sftp_str_t path, sftp_file_attr_t attributes, void *cb_data)
{
	sbbs_t *sbbs = (sbbs_t *)cb_data;
	return true;
}

static bool
sftp_rmdir(sftp_str_t path, void *cb_data)
{
	sbbs_t *sbbs = (sbbs_t *)cb_data;
	return true;
}

static bool
sftp_symlink(sftp_str_t linkpath, sftp_str_t targetpath, void *cb_data)
{
	sbbs_t *sbbs = (sbbs_t *)cb_data;
	return true;
}
#endif

}

bool
sbbs_t::init_sftp(int cid)
{
	if (sftp_state != nullptr)
		return true;
	sftp_state = sftps_begin(sftp_send, this);
	if (sftp_state != nullptr) {
		sftp_state->lprint = sftp_lprint;
		sftp_state->cleanup_callback = sftp_cleanup_callback;
		sftp_state->realpath = sftp_realpath;
		sftp_state->open = sftp_open;
		sftp_state->close = sftp_close;
		sftp_state->read = sftp_read;
		sftp_state->write = sftp_write;
		sftp_state->opendir = sftp_opendir;
		sftp_state->readdir = sftp_readdir;
		sftp_state->stat = sftp_stat;
		sftp_state->lstat = sftp_lstat;
		sftp_state->readlink = sftp_readlink;
		sftp_channel = cid;
		lprintf(LOG_INFO, "SFTP initialized on channel %d", cid);
		return true;
	}
	return false;
}

bool
sbbs_t::sftp_end(void)
{
	sftps_end(sftp_state);
	sftp_state = nullptr;
	return true;
}