/* Internal SMB protocol implementation */ #ifndef _GNU_SOURCE #define _GNU_SOURCE /* Needed for asprintf() */ #endif #error SMB protocol support is vulnerable to CVE-2006-5925. Do not use. #error If you want to use SMB, please wait for ELinks 0.12.0 or see bug 844. #ifdef HAVE_CONFIG_H #include "config.h" #endif #include #include #include #include #include #ifdef HAVE_FCNTL_H #include /* OS/2 needs this after sys/types.h */ #endif #ifdef HAVE_UNISTD_H #include #endif #include "elinks.h" #include "cache/cache.h" #include "config/options.h" #include "intl/gettext/libintl.h" #include "main/module.h" #include "main/select.h" #include "network/connection.h" #include "network/socket.h" #include "osdep/osdep.h" #include "protocol/common.h" #include "protocol/protocol.h" #include "protocol/smb/smb.h" #include "protocol/uri.h" #include "util/memory.h" #include "util/snprintf.h" #include "util/string.h" /* XXX: Nice cleanup target --pasky */ /* FIXME: we rely on smbclient output which may change in future, * so i think we should use libsmbclient instead (or better in addition) * This stuff is a quick hack, but it works ;). --Zas */ enum smb_list_type { SMB_LIST_NONE, SMB_LIST_SHARES, SMB_LIST_DIR, }; struct smb_connection_info { enum smb_list_type list_type; /* If this is 1, it means one socket is already off. The second one * should call end_smb_connection() when it goes off as well. */ int closing; size_t textlen; unsigned char text[1]; }; static void end_smb_connection(struct connection *conn); struct option_info smb_options[] = { INIT_OPT_TREE("protocol", N_("SMB"), "smb", 0, N_("SAMBA specific options.")), INIT_OPT_STRING("protocol.smb", N_("Credentials"), "credentials", 0, "", N_("Credentials file passed to smbclient via -A option.")), NULL_OPTION_INFO, }; struct module smb_protocol_module = struct_module( /* name: */ N_("SMB"), /* options: */ smb_options, /* hooks: */ NULL, /* submodules: */ NULL, /* data: */ NULL, /* init: */ NULL, /* done: */ NULL ); /* Return 0 if @conn->cached was set. */ static int smb_get_cache(struct connection *conn) { if (conn->cached) return 0; conn->cached = get_cache_entry(conn->uri); if (conn->cached) return 0; abort_connection(conn, S_OUT_OF_MEM); return -1; } #define READ_SIZE 4096 static ssize_t smb_read_data(struct connection *conn, int sock, unsigned char *dst) { ssize_t r; struct smb_connection_info *si = conn->info; r = read(sock, dst, READ_SIZE); if (r == -1) { retry_connection(conn, -errno); return -1; } if (r == 0) { if (!si->closing) { si->closing = 1; clear_handlers(sock); return 0; } end_smb_connection(conn); return 0; } return r; } static void smb_read_text(struct connection *conn, int sock) { ssize_t r; struct smb_connection_info *si = conn->info; /* We add 2 here to handle LF and NUL chars that are added in * smb_end_connection(). */ si = mem_realloc(si, sizeof(*si) + si->textlen + READ_SIZE + 2); if (!si) { abort_connection(conn, S_OUT_OF_MEM); return; } conn->info = si; r = smb_read_data(conn, sock, si->text + si->textlen); if (r <= 0) return; if (!conn->from) set_connection_state(conn, S_GETH); si->textlen += r; } static void smb_got_data(struct connection *conn) { struct smb_connection_info *si = conn->info; unsigned char buffer[READ_SIZE]; ssize_t r; if (si->list_type != SMB_LIST_NONE) { smb_read_text(conn, conn->data_socket->fd); return; } r = smb_read_data(conn, conn->data_socket->fd, buffer); if (r <= 0) return; set_connection_state(conn, S_TRANS); if (smb_get_cache(conn)) return; conn->received += r; if (add_fragment(conn->cached, conn->from, buffer, r) == 1) conn->tries = 0; conn->from += r; } #undef READ_SIZE static void smb_got_text(struct connection *conn) { smb_read_text(conn, conn->socket->fd); } /* Search for @str1 followed by @str2 in @line. * It returns NULL if not found, or pointer to start * of @str2 in @line if found. */ static unsigned char * find_strs(unsigned char *line, unsigned char *str1, unsigned char *str2) { unsigned char *p = strstr(line, str1); if (!p) return NULL; p = strstr(p + strlen(str1), str2); return p; } static void parse_smbclient_output(struct uri *uri, struct smb_connection_info *si, struct string *page) { unsigned char *line_start, *line_end; enum { SMB_TYPE_NONE, SMB_TYPE_SHARE, SMB_TYPE_SERVER, SMB_TYPE_WORKGROUP } type = SMB_TYPE_NONE; size_t pos = 0; int stop; assert(uri && si && page); if_assert_failed return; add_to_string(page, "/"); add_bytes_to_string(page, uri->data, uri->datalen); add_to_string(page, "
");

	line_start = si->text;
	stop = !si->textlen;	/* Nothing to parse. */
	while (!stop && (line_end = strchr(line_start, ASCII_LF))) {
		unsigned char *line = line_start;
		size_t line_len;
		size_t start_offset = 0;

		/* Handle \r\n case. Normally, do not occur on *nix. */
		if (line_end > line_start && line_end[-1] == ASCII_CR)
			 line_end--, start_offset++;

		line_len = line_end - line_start;

		/* Here we modify si->text content, this should not be
		 * a problem as it is only used here. This prevents
		 * allocation of memory for the line. */
		*line_end = '\0';

		/* And I got bored here with cleaning it up. --pasky */

		if (si->list_type == SMB_LIST_SHARES) {
			unsigned char *ll, *lll, *found;

			if (!*line) type = SMB_TYPE_NONE;

			found = find_strs(line, "Sharename", "Type");
			if (found) {
				pos = found - line;
				type = SMB_TYPE_SHARE;
				goto print_as_is;
			}

			found = find_strs(line, "Server", "Comment");
			if (found) {
				type = SMB_TYPE_SERVER;
				goto print_as_is;
			}

			found = find_strs(line, "Workgroup", "Master");
			if (found) {
				pos = found - line;
				type = SMB_TYPE_WORKGROUP;
				goto print_as_is;
			}

			if (type == SMB_TYPE_NONE)
				goto print_as_is;

			for (ll = line; *ll; ll++)
				if (!isspace(*ll) && *ll != '-')
					goto print_next;

			goto print_as_is;

print_next:
			for (ll = line; *ll; ll++)
				if (!isspace(*ll))
					break;

			for (lll = ll; *lll; lll++)
				if (isspace(*lll))
					break;

			switch (type) {
			case SMB_TYPE_SHARE:
			{
				unsigned char *llll;

				if (!strstr(lll, "Disk"))
					goto print_as_is;

				if (pos && pos < line_len
				    && isspace(*(llll = line + pos - 1))
				    && llll > ll) {
					while (llll > ll && isspace(*llll))
						llll--;
					if (!isspace(*llll))
						lll = llll + 1;
				}

				add_bytes_to_string(page, line, ll - line);
				add_to_string(page, "");
				add_bytes_to_string(page, ll, lll - ll);
				add_to_string(page, "");
				add_to_string(page, lll);
				break;
			}

			case SMB_TYPE_WORKGROUP:
				if (pos < line_len && pos
				    && isspace(line[pos - 1])
				    && !isspace(line[pos])) {
					ll = line + pos;
				} else {
					for (ll = lll; *ll; ll++)
						if (!isspace(*ll))
							break;
				}
				for (lll = ll; *lll; lll++)
					if (isspace(*lll))
						break;
				/* Fall-through */

			case SMB_TYPE_SERVER:
				add_bytes_to_string(page, line, ll - line);
				add_to_string(page, "");
				add_bytes_to_string(page, ll, lll - ll);
				add_to_string(page, "");
				add_to_string(page, lll);
				break;

			case SMB_TYPE_NONE:
				goto print_as_is;
			}

		} else if (si->list_type == SMB_LIST_DIR) {
			if (strstr(line, "NT_STATUS")) {
				/* Error, stop after message. */
				stop = 1;
				goto print_as_is;
			}

			if (line_end - line_start >= 5
			    && line_start[0] == ' '
			    && line_start[1] == ' '
			    && line_start[2] != ' ') {
				int dir = 0;
				int may_be_dir = 0;
				unsigned char *p = line_end;
				unsigned char *url = line_start + 2;

				/* smbclient list parser
				 * The boring thing is that output is
				 * ambiguous in many ways:
				 * filenames with more than one space,
				 * etc...
				 * This bloated code tries to do a not
				 * so bad job. --Zas */

/* directory                      D        0  Fri May  7 11:23:18 2004 */
/* filename                             2444  Thu Feb 19 15:52:46 2004 */

				/* Skip end of line */
				while (p > url && !isdigit(*p)) p--;
				if (p == url) goto print_as_is;

				/* FIXME: Use parse_date()? */
				/* year */
				while (p > url && isdigit(*p)) p--;
				if (p == url || !isspace(*p)) goto print_as_is;
				while (p > url && isspace(*p)) p--;

				/* seconds */
				while (p > url && isdigit(*p)) p--;
				if (p == url || *p != ':') goto print_as_is;
				p--;

				/* minutes */
				while (p > url && isdigit(*p)) p--;
				if (p == url || *p != ':') goto print_as_is;
				p--;

				/* hours */
				while (p > url && isdigit(*p)) p--;
				if (p == url || !isspace(*p)) goto print_as_is;
				p--;

				/* day as number */
				while (p > url && isdigit(*p)) p--;
				while (p > url && isspace(*p)) p--;
				if (p == url) goto print_as_is;

				/* month */
				while (p > url && !isspace(*p)) p--;
				if (p == url || !isspace(*p)) goto print_as_is;
				p--;

				/* day name */
				while (p > url && !isspace(*p)) p--;
				if (p == url || !isspace(*p)) goto print_as_is;
				while (p > url && isspace(*p)) p--;

				/* file size */
				if (p == url || !isdigit(*p)) goto print_as_is;

				if (*p == '0' && isspace(*(p - 1))) may_be_dir = 1;

				while (p > url && isdigit(*p)) p--;
				if (p == url) goto print_as_is;

				/* Magic to determine if we have a
				 * filename or a dirname. Thanks to
				 * smbclient ambiguous output. */
				{
					unsigned char *pp = p;

					while (pp > url && isspace(*pp)) pp--;

					if (p - pp <= 8) {
						while (pp > url
						       && (*pp == 'D'
							  || *pp == 'H'
							  || *pp == 'A'
							  || *pp == 'S'
						          || *pp == 'R'
							  || *pp == 'V')) {
						        if (*pp == 'D' && may_be_dir)
								dir = 1;
							pp--;
						}
					}
					while (pp > url && isspace(*pp)) pp--;
					p = pp;
				}

				/* Don't display '.' directory */
				if (p == url && *url == '.') goto ignored;
				p++;

				add_to_string(page, "  ");
				add_bytes_to_string(page, url, p - url);
				add_to_string(page, "");
				add_bytes_to_string(page, p, line_end - p);

			} else {
				goto print_as_is;
			}

		} else {
print_as_is:
			add_bytes_to_string(page, line_start, line_len);
		}

		add_char_to_string(page, ASCII_LF);
ignored:
		line_start = line_end + start_offset + 1;
	}

	add_to_string(page, "
"); } static void end_smb_connection(struct connection *conn) { struct smb_connection_info *si = conn->info; struct uri *uri; enum connection_state state = S_OK; if (smb_get_cache(conn)) return; if (conn->from) { normalize_cache_entry(conn->cached, conn->from); goto bye; } /* Ensure termination by LF + NUL chars, memory for this * was reserved by smb_read_text(). */ if (si->textlen && si->text[si->textlen - 1] != ASCII_LF) si->text[si->textlen++] = ASCII_LF; si->text[si->textlen] = '\0'; uri = conn->uri; if (uri->datalen && uri->data[uri->datalen - 1] != '/' && uri->data[uri->datalen - 1] != '\\' && (strstr(si->text, "NT_STATUS_FILE_IS_A_DIRECTORY") || strstr(si->text, "NT_STATUS_ACCESS_DENIED") || strstr(si->text, "ERRbadfile"))) { redirect_cache(conn->cached, "/", 1, 0); } else { struct string page; if (!init_string(&page)) { state = S_OUT_OF_MEM; goto bye; } parse_smbclient_output(uri, si, &page); add_fragment(conn->cached, 0, page.source, page.length); conn->from += page.length; normalize_cache_entry(conn->cached, page.length); done_string(&page); mem_free_set(&conn->cached->content_type, stracpy("text/html")); } bye: close_socket(conn->socket); close_socket(conn->data_socket); abort_connection(conn, state); } void smb_protocol_handler(struct connection *conn) { int out_pipe[2] = { -1, -1 }; int err_pipe[2] = { -1, -1 }; unsigned char *share, *dir; unsigned char *p; pid_t cpid; int dirlen; struct smb_connection_info *si; struct uri *uri; si = mem_calloc(1, sizeof(*si) + 2); if (!si) { abort_connection(conn, S_OUT_OF_MEM); return; } conn->info = si; uri = conn->uri; p = strchr(uri->data, '/'); if (p && p - uri->data < uri->datalen) { share = memacpy(uri->data, p - uri->data); dir = p + 1; /* FIXME: ensure @dir do not contain dangerous chars. --Zas */ } else if (uri->datalen) { if (smb_get_cache(conn)) return; redirect_cache(conn->cached, "/", 1, 0); abort_connection(conn, S_OK); return; } else { share = stracpy(""); dir = ""; } if (!share) { abort_connection(conn, S_OUT_OF_MEM); return; } dirlen = strlen(dir); if (!*share) { si->list_type = SMB_LIST_SHARES; } else if (!dirlen || dir[dirlen - 1] == '/' || dir[dirlen - 1] == '\\') { si->list_type = SMB_LIST_DIR; } if (c_pipe(out_pipe) || c_pipe(err_pipe)) { int s_errno = errno; if (out_pipe[0] >= 0) close(out_pipe[0]); if (out_pipe[1] >= 0) close(out_pipe[1]); mem_free(share); abort_connection(conn, -s_errno); return; } conn->from = 0; cpid = fork(); if (cpid == -1) { int s_errno = errno; close(out_pipe[0]); close(out_pipe[1]); close(err_pipe[0]); close(err_pipe[1]); mem_free(share); retry_connection(conn, -s_errno); return; } if (!cpid) { #define SMBCLIENT "smbclient" #define MAX_SMBCLIENT_ARGS 32 int n = 0; unsigned char *v[MAX_SMBCLIENT_ARGS]; unsigned char *optstr; close(1); dup2(out_pipe[1], 1); close(2); dup2(err_pipe[1], 2); close(0); dup2(open("/dev/null", O_RDONLY), 0); close_all_non_term_fd(); close(out_pipe[0]); close(err_pipe[0]); /* Usage: smbclient service [options] */ v[n++] = SMBCLIENT; /* FIXME: handle alloc failures. */ /* At this point, we are the child process. * Maybe we just don't care if the child kills itself * dereferencing a NULL pointer... -- Miciah */ /* Leaving random core files after itself is not what a nice * program does. Also, the user might also want to know, why * the hell does he see nothing on the screen. --pasky */ if (*share) { /* Construct service path. */ asprintf((char **) &v[n++], "//%.*s/%s", uri->hostlen, uri->host, share); /* Add password if any. */ if (uri->passwordlen && !uri->userlen) { v[n++] = memacpy(uri->password, uri->passwordlen); } } else { /* Get a list of shares available on a host. */ v[n++] = "-L"; v[n++] = memacpy(uri->host, uri->hostlen); } v[n++] = "-N"; /* Don't ask for a password. */ v[n++] = "-E"; /* Write messages to stderr instead of stdout. */ v[n++] = "-d 0"; /* Disable debug mode. */ if (uri->portlen) { /* Connect to the specified port. */ v[n++] = "-p"; v[n++] = memacpy(uri->port, uri->portlen); } if (uri->userlen) { /* Set the network username. */ v[n++] = "-U"; if (!uri->passwordlen) { /* No password. */ v[n++] = memacpy(uri->user, uri->userlen); } else { /* With password. */ asprintf((char **) &v[n++], "%.*s%%%.*s", uri->userlen, uri->user, uri->passwordlen, uri->password); } } if (*share) { /* FIXME: use si->list_type here ?? --Zas */ if (!dirlen || dir[dirlen - 1] == '/' || dir[dirlen - 1] == '\\') { if (dirlen) { /* Initial directory. */ v[n++] = "-D"; v[n++] = dir; } v[n++] = "-c"; /* Execute semicolon separated commands. */ v[n++] = "ls"; /* List files. */ } else { /* Copy remote file to stdout. */ unsigned char *s = straconcat("get \"", dir, "\" -", NULL); unsigned char *ss = s; v[n++] = "-c"; /* Execute semicolon separated commands. */ while ((ss = strchr(ss, '/'))) *ss = '\\'; /* Escape '/' */ v[n++] = s; } } /* Optionally add SMB credentials file. */ optstr = get_opt_str("protocol.smb.credentials"); if (optstr[0]) { v[n++] = "-A"; v[n++] = optstr; } v[n++] = NULL; /* End of arguments list. */ assert(n < MAX_SMBCLIENT_ARGS); execvp(SMBCLIENT, (char **) v); /* FIXME: this message will never be displayed, since execvp() * failed. */ fprintf(stderr, SMBCLIENT " not found in $PATH"); _exit(1); #undef MAX_SMBCLIENT_ARGS #undef SMBCLIENT } mem_free(share); conn->data_socket->fd = out_pipe[0]; conn->socket->fd = err_pipe[0]; close(out_pipe[1]); close(err_pipe[1]); set_handlers(out_pipe[0], (select_handler_T) smb_got_data, NULL, NULL, conn); set_handlers(err_pipe[0], (select_handler_T) smb_got_text, NULL, NULL, conn); set_connection_state(conn, S_CONN); }