/***************************************************************************** * smb2.c: SMB2 access plug-in ***************************************************************************** * Copyright © 2018 VLC authors, VideoLAN and VideoLabs * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 2.1 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA. *****************************************************************************/ #ifdef HAVE_CONFIG_H # include "config.h" #endif #include #include #include #include #include #include #include #ifdef HAVE_POLL # include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef HAVE_DSM # include #if BDSM_VERSION_CURRENT >= 5 static void netbios_ns_interrupt_callback(void *data) { netbios_ns_abort(data); } static inline void netbios_ns_interrupt_register(netbios_ns *ns) { vlc_interrupt_register(netbios_ns_interrupt_callback, ns); } static inline int netbios_ns_interrupt_unregister(void) { return vlc_interrupt_unregister(); } #else static inline void netbios_ns_interrupt_register(netbios_ns *ns) { (void) ns; } static inline int netbios_ns_interrupt_unregister(void) { return 0; } #endif #endif #ifdef HAVE_ARPA_INET_H # include #endif #include "smb_common.h" static int Open(vlc_object_t *); static void Close(vlc_object_t *); vlc_module_begin() set_shortname("smb2") set_description("SMB2 / SMB3 input") set_help("Samba (Windows network shares) input via libsmb2") set_capability("access", 21) set_category(CAT_INPUT) set_subcategory(SUBCAT_INPUT_ACCESS) add_string("smb-user", NULL, SMB_USER_TEXT, SMB_USER_LONGTEXT, false) add_password("smb-pwd", NULL, SMB_PASS_TEXT, SMB_PASS_LONGTEXT, false) add_string("smb-domain", NULL, SMB_DOMAIN_TEXT, SMB_DOMAIN_LONGTEXT, false) add_shortcut("smb", "smb2") set_callbacks(Open, Close) vlc_module_end() struct access_sys { struct smb2_context * smb2; struct smb2fh * smb2fh; struct smb2dir * smb2dir; struct srvsvc_netshareenumall_rep *share_enum; uint64_t smb2_size; vlc_url_t encoded_url; bool eof; bool smb2_connected; }; struct vlc_smb2_op { vlc_object_t *log; struct smb2_context *smb2; struct smb2_context **smb2p; int error_status; bool res_done; union { struct { size_t len; } read; void *data; } res; }; #define VLC_SMB2_OP(access, smb2p_) { \ .log = access ? VLC_OBJECT(access) : NULL, \ .smb2p = smb2p_, \ .smb2 = (assert(*smb2p_ != NULL), *smb2p_), \ .error_status = 0, \ .res_done = false, \ }; static inline void vlc_smb2_op_reset(struct vlc_smb2_op *op, struct smb2_context **smb2p) { op->res_done = false; op->smb2p = smb2p; op->smb2 = *smb2p; op->error_status = 0; } static int smb2_check_status(struct vlc_smb2_op *op, const char *psz_func, int status) { if (status < 0) { const char *psz_error = smb2_get_error(op->smb2); if (op->log) msg_Warn(op->log, "%s failed: %d, '%s'", psz_func, status, psz_error); op->error_status = status; return -1; } else { op->res_done = true; return 0; } } static void smb2_set_error(struct vlc_smb2_op *op, const char *psz_func, int err) { if (op->log && err != -EINTR) msg_Err(op->log, "%s failed: %d, %s", psz_func, err, smb2_get_error(op->smb2)); /* Don't override if set via smb2_check_status */ if (op->error_status == 0) op->error_status = err; smb2_destroy_context(op->smb2); op->smb2 = NULL; *op->smb2p = NULL; } #define VLC_SMB2_CHECK_STATUS(op, status) \ smb2_check_status(op, __func__, status) #define VLC_SMB2_SET_ERROR(op, func, err) \ smb2_set_error(op, func, err) #define VLC_SMB2_STATUS_DENIED(x) (x == -ECONNREFUSED || x == -EACCES) #if defined (__ELF__) || defined (__MACH__) /* weak support */ /* There is no way to know if libsmb2 has these new symbols and we don't want * to increase the version requirement on VLC 3.0, therefore implement a weak * compat version. */ const t_socket * smb2_get_fds(struct smb2_context *smb2, size_t *fd_count, int *timeout); int smb2_service_fd(struct smb2_context *smb2, int fd, int revents); __attribute__((weak)) const t_socket * smb2_get_fds(struct smb2_context *smb2, size_t *fd_count, int *timeout) { (void) timeout; static thread_local t_socket fd; *fd_count = 1; fd = smb2_get_fd(smb2); return &fd; } __attribute__((weak)) int smb2_service_fd(struct smb2_context *smb2, int fd, int revents) { (void) fd; return smb2_service(smb2, revents); } #endif static int vlc_smb2_mainloop(struct vlc_smb2_op *op) { while (op->error_status == 0 && !op->res_done) { int ret, smb2_timeout; size_t fd_count; const t_socket *fds = smb2_get_fds(op->smb2, &fd_count, &smb2_timeout); int events = smb2_which_events(op->smb2); struct pollfd p_fds[fd_count]; for (size_t i = 0; i < fd_count; ++i) { p_fds[i].events = events; p_fds[i].fd = fds[i]; } if (fds == NULL || (ret = vlc_poll_i11e(p_fds, fd_count, smb2_timeout)) < 0) { if (op->log && errno == EINTR) msg_Warn(op->log, "vlc_poll_i11e interrupted"); VLC_SMB2_SET_ERROR(op, "poll", -errno); } else if (ret == 0) { if (smb2_service_fd(op->smb2, -1, 0) < 0) VLC_SMB2_SET_ERROR(op, "smb2_service", -EINVAL); } else { for (size_t i = 0; i < fd_count; ++i) { if (p_fds[i].revents && smb2_service_fd(op->smb2, p_fds[i].fd, p_fds[i].revents) < 0) VLC_SMB2_SET_ERROR(op, "smb2_service", -EINVAL); } } } if (op->error_status != 0 && op->smb2 != NULL) { /* An error was signalled from a smb2 cb. Destroy the smb2 context now * since this call might still trigger callbacks using the current op * (that is allocated on the stack). */ smb2_destroy_context(op->smb2); op->smb2 = NULL; *op->smb2p = NULL; } return op->error_status; } #define VLC_SMB2_GENERIC_CB() \ struct vlc_smb2_op *op = private_data; \ assert(op->smb2 == smb2); (void) smb2; \ if (VLC_SMB2_CHECK_STATUS(op, status)) \ return static void smb2_generic_cb(struct smb2_context *smb2, int status, void *data, void *private_data) { VLC_UNUSED(data); VLC_SMB2_GENERIC_CB(); } static void smb2_read_cb(struct smb2_context *smb2, int status, void *data, void *private_data) { VLC_UNUSED(data); VLC_SMB2_GENERIC_CB(); op->res.read.len = status; } static ssize_t FileRead(stream_t *access, void *buf, size_t len) { struct access_sys *sys = access->p_sys; if (sys->eof || sys->smb2 == NULL) return 0; /* Limit the read size since smb2_read_async() will complete only after * reading the whole requested data and not when whatever data is available * (high read size means a faster I/O but a higher latency). */ if (len > 262144) len = 262144; struct vlc_smb2_op op = VLC_SMB2_OP(access, &sys->smb2); op.res.read.len = 0; int err = smb2_read_async(sys->smb2, sys->smb2fh, buf, len, smb2_read_cb, &op); if (err < 0) { VLC_SMB2_SET_ERROR(&op, "smb2_read_async", err); return 0; } if (vlc_smb2_mainloop(&op) < 0) return 0; if (op.res.read.len == 0) sys->eof = true; return op.res.read.len; } static int FileSeek(stream_t *access, uint64_t i_pos) { struct access_sys *sys = access->p_sys; if (sys->smb2 == NULL) return VLC_EGENERIC; if (i_pos > INT64_MAX) { msg_Err(access, "can't seek past INT64_MAX (requested: %"PRIu64")\n", i_pos); return VLC_EGENERIC; } struct vlc_smb2_op op = VLC_SMB2_OP(access, &sys->smb2); int64_t err = smb2_lseek(op.smb2, sys->smb2fh, i_pos, SEEK_SET, NULL); if (err < 0) { VLC_SMB2_SET_ERROR(&op, "smb2_lseek", err); return err; } sys->eof = false; return VLC_SUCCESS; } static int FileControl(stream_t *access, int i_query, va_list args) { struct access_sys *sys = access->p_sys; switch (i_query) { case STREAM_CAN_SEEK: *va_arg(args, bool *) = true; break; case STREAM_CAN_FASTSEEK: *va_arg(args, bool *) = false; break; case STREAM_CAN_PAUSE: case STREAM_CAN_CONTROL_PACE: *va_arg(args, bool *) = true; break; case STREAM_GET_SIZE: { *va_arg(args, uint64_t *) = sys->smb2_size; break; } case STREAM_GET_PTS_DELAY: *va_arg( args, int64_t * ) = INT64_C(1000) * var_InheritInteger( access, "network-caching" ); break; case STREAM_SET_PAUSE_STATE: break; default: return VLC_EGENERIC; } return VLC_SUCCESS; } static char * vlc_smb2_get_url(vlc_url_t *url, const char *file) { /* smb2://? */ struct vlc_memstream buf; vlc_memstream_open(&buf); vlc_memstream_printf(&buf, "smb://%s", url->psz_host); if (url->i_port != 0) vlc_memstream_printf(&buf, ":%d", url->i_port); if (url->psz_path != NULL) { vlc_memstream_puts(&buf, url->psz_path); if (url->psz_path[0] != '\0' && url->psz_path[strlen(url->psz_path) - 1] != '/') vlc_memstream_putc(&buf, '/'); } else vlc_memstream_putc(&buf, '/'); vlc_memstream_puts(&buf, file); if (url->psz_option) vlc_memstream_printf(&buf, "?%s", url->psz_option); if (vlc_memstream_close(&buf)) return NULL; return buf.ptr; } static int AddItem(stream_t *access, struct vlc_readdir_helper *rdh, const char *name, int i_type) { struct access_sys *sys = access->p_sys; char *name_encoded = vlc_uri_encode(name); if (name_encoded == NULL) return VLC_ENOMEM; char *url = vlc_smb2_get_url(&sys->encoded_url, name_encoded); free(name_encoded); if (url == NULL) return VLC_ENOMEM; int ret = vlc_readdir_helper_additem(rdh, url, NULL, name, i_type, ITEM_NET); free(url); return ret; } static int DirRead(stream_t *access, input_item_node_t *p_node) { struct access_sys *sys = access->p_sys; struct smb2dirent *smb2dirent; int ret = VLC_SUCCESS; assert(sys->smb2dir); struct vlc_readdir_helper rdh; vlc_readdir_helper_init(&rdh, access, p_node); while (ret == VLC_SUCCESS && (smb2dirent = smb2_readdir(sys->smb2, sys->smb2dir)) != NULL) { int i_type; switch (smb2dirent->st.smb2_type) { case SMB2_TYPE_FILE: i_type = ITEM_TYPE_FILE; break; case SMB2_TYPE_DIRECTORY: i_type = ITEM_TYPE_DIRECTORY; break; default: i_type = ITEM_TYPE_UNKNOWN; break; } ret = AddItem(access, &rdh, smb2dirent->name, i_type); } vlc_readdir_helper_finish(&rdh, ret == VLC_SUCCESS); return ret; } static int ShareEnum(stream_t *access, input_item_node_t *p_node) { struct access_sys *sys = access->p_sys; assert(sys->share_enum != NULL); int ret = VLC_SUCCESS; struct vlc_readdir_helper rdh; vlc_readdir_helper_init(&rdh, access, p_node); struct srvsvc_netsharectr *ctr = sys->share_enum->ctr; for (uint32_t iinfo = 0; iinfo < ctr->ctr1.count && ret == VLC_SUCCESS; ++iinfo) { struct srvsvc_netshareinfo1 *info = &ctr->ctr1.array[iinfo]; if (info->type & SHARE_TYPE_HIDDEN) continue; switch (info->type & 0x3) { case SHARE_TYPE_DISKTREE: ret = AddItem(access, &rdh, info->name, ITEM_TYPE_DIRECTORY); break; } } vlc_readdir_helper_finish(&rdh, ret == VLC_SUCCESS); return 0; } static int vlc_smb2_close_fh(stream_t *access, struct smb2_context **smb2p, struct smb2fh *smb2fh) { struct vlc_smb2_op op = VLC_SMB2_OP(access, smb2p); int err = smb2_close_async(op.smb2, smb2fh, smb2_generic_cb, &op); if (err < 0) { VLC_SMB2_SET_ERROR(&op, "smb2_close_async", err); return -1; } return vlc_smb2_mainloop(&op); } static int vlc_smb2_disconnect_share(stream_t *access, struct smb2_context **smb2p) { struct vlc_smb2_op op = VLC_SMB2_OP(access, smb2p); int err = smb2_disconnect_share_async(op.smb2, smb2_generic_cb, &op); if (err < 0) { VLC_SMB2_SET_ERROR(&op, "smb2_connect_share_async", err); return -1; } return vlc_smb2_mainloop(&op); } static void smb2_open_cb(struct smb2_context *smb2, int status, void *data, void *private_data) { VLC_SMB2_GENERIC_CB(); op->res.data = data; } static void vlc_smb2_print_addr(stream_t *access) { struct access_sys *sys = access->p_sys; struct sockaddr_storage addr; if (getsockname(smb2_get_fd(sys->smb2), (struct sockaddr *)&addr, &(socklen_t){ sizeof(addr) }) != 0) return; void *sin_addr; switch (addr.ss_family) { case AF_INET6: sin_addr = &((struct sockaddr_in6 *)&addr)->sin6_addr; break; case AF_INET: sin_addr = &((struct sockaddr_in *)&addr)->sin_addr; break; default: return; } char ip[INET6_ADDRSTRLEN]; if (inet_ntop(addr.ss_family, sin_addr, ip, sizeof(ip)) == NULL) return; if (strcmp(ip, sys->encoded_url.psz_host) == 0) return; msg_Dbg(access, "%s: connected from %s\n", sys->encoded_url.psz_host, ip); } static int vlc_smb2_open_share(stream_t *access, struct smb2_context **smb2p, struct smb2_url *smb2_url, bool do_enum) { struct access_sys *sys = access->p_sys; struct smb2_stat_64 smb2_stat; struct vlc_smb2_op op = VLC_SMB2_OP(access, smb2p); int ret; if (do_enum) ret = smb2_share_enum_async(op.smb2, smb2_open_cb, &op); else { ret = smb2_stat_async(op.smb2, smb2_url->path, &smb2_stat, smb2_generic_cb, &op); if (ret < 0) { VLC_SMB2_SET_ERROR(&op, "smb2_stat_async", ret); goto error; } if (vlc_smb2_mainloop(&op) != 0) goto error; if (smb2_stat.smb2_type == SMB2_TYPE_FILE) { vlc_smb2_op_reset(&op, smb2p); sys->smb2_size = smb2_stat.smb2_size; ret = smb2_open_async(op.smb2, smb2_url->path, O_RDONLY, smb2_open_cb, &op); } else if (smb2_stat.smb2_type == SMB2_TYPE_DIRECTORY) { vlc_smb2_op_reset(&op, smb2p); ret = smb2_opendir_async(op.smb2, smb2_url->path, smb2_open_cb, &op); } else { msg_Err(access, "smb2_stat_cb: file type not handled"); ret = -ENOENT; } } if (ret < 0) { VLC_SMB2_SET_ERROR(&op, "smb2_open*_async", ret); goto error; } if (vlc_smb2_mainloop(&op) != 0) goto error; if (do_enum) sys->share_enum = op.res.data; else if (smb2_stat.smb2_type == SMB2_TYPE_FILE) sys->smb2fh = op.res.data; else if (smb2_stat.smb2_type == SMB2_TYPE_DIRECTORY) sys->smb2dir = op.res.data; else vlc_assert_unreachable(); return 0; error: return op.error_status; } static int vlc_smb2_connect_open_share(stream_t *access, const char *url, const vlc_credential *credential, bool guest_with_valid_passwd) { struct access_sys *sys = access->p_sys; struct smb2_url *smb2_url = NULL; sys->smb2 = smb2_init_context(); if (sys->smb2 == NULL) { msg_Err(access, "smb2_init_context failed"); return -ENOMEM; } smb2_url = smb2_parse_url(sys->smb2, url); if (!smb2_url || !smb2_url->share || !smb2_url->server) { msg_Err(access, "smb2_parse_url failed"); goto error; } const bool do_enum = smb2_url->share[0] == '\0'; const char *username = credential->psz_username; const char *password = credential->psz_password; const char *domain = credential->psz_realm; const char *share = do_enum ? "IPC$" : smb2_url->share; if (!username) { username = "Guest"; /* A NULL password enable ntlmssp anonymous login */ password = guest_with_valid_passwd ? "" : NULL; } smb2_set_security_mode(sys->smb2, SMB2_NEGOTIATE_SIGNING_ENABLED); smb2_set_password(sys->smb2, password); smb2_set_domain(sys->smb2, domain ? domain : ""); struct vlc_smb2_op op = VLC_SMB2_OP(access, &sys->smb2); int err = smb2_connect_share_async(sys->smb2, smb2_url->server, share, username, smb2_generic_cb, &op); if (err < 0) { VLC_SMB2_SET_ERROR(&op, "smb2_connect_share_async", err); goto error; } if (vlc_smb2_mainloop(&op) != 0) goto error; sys->smb2_connected = true; vlc_smb2_print_addr(access); err = vlc_smb2_open_share(access, &sys->smb2, smb2_url, do_enum); if (err < 0) { op.error_status = err; goto error; } smb2_destroy_url(smb2_url); return 0; error: if (smb2_url != NULL) smb2_destroy_url(smb2_url); if (sys->smb2 != NULL) { if (sys->smb2_connected) { vlc_smb2_disconnect_share(access, &sys->smb2); sys->smb2_connected = false; } if (sys->smb2 != NULL) { smb2_destroy_context(sys->smb2); sys->smb2 = NULL; } } return op.error_status; } static int vlc_smb2_resolve(stream_t *access, const char *host, unsigned port, char **out_host) { (void) access; if (!host) return -ENOENT; #ifdef HAVE_DSM /* Test if the host is an IP */ struct in_addr addr; if (inet_pton(AF_INET, host, &addr) == 1) return -ENOENT; /* Test if the host can be resolved */ struct addrinfo *info = NULL; if (vlc_getaddrinfo_i11e(host, port, NULL, &info) == 0) { freeaddrinfo(info); /* Let smb2 resolve it */ return -ENOENT; } /* Test if the host is a netbios name */ netbios_ns *ns = netbios_ns_new(); if (!ns) return -ENOMEM; int ret = -ENOENT; netbios_ns_interrupt_register(ns); uint32_t ip4_addr; if (netbios_ns_resolve(ns, host, NETBIOS_FILESERVER, &ip4_addr) == 0) { char ip[INET_ADDRSTRLEN]; if (inet_ntop(AF_INET, &ip4_addr, ip, sizeof(ip))) { *out_host = strdup(ip); ret = 0; } } if (netbios_ns_interrupt_unregister() == EINTR) { if (unlikely(ret == 0)) free(*out_host); netbios_ns_destroy(ns); return -EINTR; } netbios_ns_destroy(ns); return ret; #else (void) port; return -ENOENT; #endif } static int Open(vlc_object_t *p_obj) { stream_t *access = (stream_t *)p_obj; struct access_sys *sys = vlc_obj_calloc(p_obj, 1, sizeof (*sys)); char *var_domain = NULL; int ret; if (unlikely(sys == NULL)) return VLC_ENOMEM; access->p_sys = sys; /* Parse the encoded URL */ if (vlc_UrlParseFixup(&sys->encoded_url, access->psz_url) != 0) return VLC_ENOMEM; if (sys->encoded_url.psz_path == NULL) sys->encoded_url.psz_path = (char *) "/"; char *resolved_host = NULL; ret = vlc_smb2_resolve(access, sys->encoded_url.psz_host, sys->encoded_url.i_port, &resolved_host); /* smb2_* functions need a decoded url. Re compose the url from the * modified sys->encoded_url (with the resolved host). */ char *url; if (ret == -EINTR) goto error; else if (ret == 0) { vlc_url_t resolved_url = sys->encoded_url; resolved_url.psz_host = resolved_host; url = vlc_uri_compose(&resolved_url); } else { url = vlc_uri_compose(&sys->encoded_url); } if (!vlc_uri_decode(url)) { free(url); free(resolved_host); ret = -ENOMEM; goto error; } vlc_credential credential; vlc_credential_init(&credential, &sys->encoded_url); var_domain = var_InheritString(access, "smb-domain"); credential.psz_realm = var_domain; /* First, try Guest login or using "smb-" options (without * keystore/user interaction) */ vlc_credential_get(&credential, access, "smb-user", "smb-pwd", NULL, NULL); ret = vlc_smb2_connect_open_share(access, url, &credential, false); if (ret == -EINVAL && credential.psz_username == NULL) { /* Since last Windows 11 update (KB5026436), Windows SMB servers need a * valid Auth (user + password) even for a guest/anonymous login. The * server will return 'STATUS_INVALID_PARAMETER' (so, libsmb2 will * return '-EINVAL') if the password is invalid. Therefore, try to * connect again with a valid password in that case. * * We don't try to connect with a valid password on the first try since * it seems to break anonymous login with other samba servers (but * samba.c doesn't have this problem so this might be libsmb2 issue). * */ ret = vlc_smb2_connect_open_share(access, url, &credential, true); } while (VLC_SMB2_STATUS_DENIED(ret) && vlc_credential_get(&credential, access, "smb-user", "smb-pwd", SMB_LOGIN_DIALOG_TITLE, SMB_LOGIN_DIALOG_TEXT, sys->encoded_url.psz_host)) ret = vlc_smb2_connect_open_share(access, url, &credential, false); free(resolved_host); free(url); if (ret == 0) vlc_credential_store(&credential, access); vlc_credential_clean(&credential); if (ret != 0) { const char *error = smb2_get_error(sys->smb2); if (error && *error) vlc_dialog_display_error(access, "SMB2 operation failed", "%s", error); if (credential.i_get_order == GET_FROM_DIALOG) { /* Tell other smb modules (likely dsm) that we already requested * credential to the users and that it it useless to try again. * This avoid to show 2 login dialogs for the same access. */ var_Create(access, "smb-dialog-failed", VLC_VAR_VOID); } goto error; } if (sys->smb2fh != NULL) { access->pf_read = FileRead; access->pf_seek = FileSeek; access->pf_control = FileControl; } else if (sys->smb2dir != NULL) { access->pf_readdir = DirRead; access->pf_seek = NULL; access->pf_control = access_vaDirectoryControlHelper; } else if (sys->share_enum != NULL) { access->pf_readdir = ShareEnum; access->pf_seek = NULL; access->pf_control = access_vaDirectoryControlHelper; } else vlc_assert_unreachable(); free(var_domain); return VLC_SUCCESS; error: vlc_UrlClean(&sys->encoded_url); free(var_domain); /* Returning VLC_ETIMEOUT will stop the module probe and prevent to load * the next smb module. The smb2 module can return this specific error in * case of network error (EIO) or when the user asked to cancel it * (vlc_killed()). Indeed, in these cases, it is useless to try next smb * modules. */ return ret == -EINTR || ret == -EIO || vlc_killed() ? VLC_ETIMEOUT : VLC_EGENERIC; } static void Close(vlc_object_t *p_obj) { stream_t *access = (stream_t *)p_obj; struct access_sys *sys = access->p_sys; if (sys->smb2fh != NULL) { if (sys->smb2) vlc_smb2_close_fh(access, &sys->smb2, sys->smb2fh); } else if (sys->smb2dir != NULL) smb2_closedir(sys->smb2, sys->smb2dir); else if (sys->share_enum != NULL) smb2_free_data(sys->smb2, sys->share_enum); else vlc_assert_unreachable(); assert(sys->smb2_connected); if (sys->smb2 != NULL) { vlc_smb2_disconnect_share(access, &sys->smb2); if (sys->smb2 != NULL) smb2_destroy_context(sys->smb2); } vlc_UrlClean(&sys->encoded_url); }