/* fhandler_netdrive.cc: fhandler for // and //MACHINE handling This file is part of Cygwin. This software is a copyrighted work licensed under the terms of the Cygwin license. Please consult the file "CYGWIN_LICENSE" for details. */ #include "winsup.h" #include "cygerrno.h" #include "security.h" #include "path.h" #include "fhandler.h" #include "dtable.h" #include "cygheap.h" #include "cygthread.h" #include "tls_pbuf.h" #define USE_SYS_TYPES_FD_SET #include #include #include #include #include #include #include /* SMBv1 is deprectated and not even installed by default anymore on Windows 10 and 11 machines or their servers. As a result, neither WNetOpenEnumW() nor NetServerEnum() work as expected anymore. So this fhandler class now uses Network Discovery to enumerate the "//" directory, which, unfortunately, requires to use the shell API. */ /* There's something REALLY fishy going on in Windows. If the NFS enumeration via WNet functions is called *before* the share enumeration via Shell function, the Shell functions will enumerate the NFS shares instead of the SMB shares. Un-be-lie-va-ble! FWIW, we reverted the SMB share enumeration using WNet. */ /* Define the required GUIDs here to avoid linking with libuuid.a */ const GUID FOLDERID_NetworkFolder = { 0xd20beec4, 0x5ca8, 0x4905, { 0xae, 0x3b, 0xbf, 0x25, 0x1e, 0xa0, 0x9b, 0x53 } }; const GUID BHID_StorageEnum = { 0x4621a4e3, 0xf0d6, 0x4773, { 0x8a, 0x9c, 0x46, 0xe7, 0x7b, 0x17, 0x48, 0x40 } }; const GUID BHID_EnumItems = { 0x94f60519, 0x2850, 0x4924, { 0xaa, 0x5a, 0xd1, 0x5e, 0x84, 0x86, 0x80, 0x39 } }; class dir_cache { size_t max_entries; size_t num_entries; wchar_t **entry; public: dir_cache () : max_entries (0), num_entries (0), entry (NULL) {} ~dir_cache () { while (num_entries > 0) free (entry[--num_entries]); free (entry); } size_t count () const { return num_entries; } void add (wchar_t *str, bool downcase = false) { if (num_entries >= max_entries) { wchar_t **newentry; newentry = (wchar_t **) realloc (entry, (max_entries + 10) * sizeof (wchar_t *)); if (!newentry) return; entry = newentry; max_entries += 10; } entry[num_entries] = wcsdup (str); if (entry[num_entries]) { if (downcase) for (wchar_t *p = entry[num_entries]; (*p = towlower (*p)); ++p) ; ++num_entries; } } inline wchar_t *operator [](size_t idx) const { if (idx < num_entries) return entry[idx]; return NULL; } }; #define DIR_cache (*reinterpret_cast (dir->__handle)) struct netdriveinf { DIR *dir; int err; DWORD provider; HANDLE sem; }; static inline int hresult_to_errno (HRESULT wres) { if (SUCCEEDED (wres)) return 0; if (((ULONG) wres & 0xffff0000) == (ULONG) MAKE_HRESULT(SEVERITY_ERROR, FACILITY_WIN32, 0)) return geterrno_from_win_error ((ULONG) wres & 0xffff); return EACCES; } /* Workaround incompatible definitions. */ #define u_long __ms_u_long #define WS_FIONBIO _IOW('f', 126, u_long) #define WS_POLLOUT 0x10 static bool server_is_running_nfs (const wchar_t *servername) { /* Hack alarm: Only test TCP port 2049 */ struct addrinfoW hints = { 0 }; struct addrinfoW *ai = NULL, *aip; bool ret = false; INT wres; hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; /* The services contains "nfs" only as UDP service... sigh. */ wres = GetAddrInfoW (servername, L"2049", &hints, &ai); if (wres) return false; for (aip = ai; !ret && aip; aip = aip->ai_next) { SOCKET sock = ::socket (aip->ai_family, aip->ai_socktype, aip->ai_flags); if (sock != INVALID_SOCKET) { __ms_u_long nonblocking = 1; ::ioctlsocket (sock, WS_FIONBIO, &nonblocking); wres = ::connect (sock, aip->ai_addr, aip->ai_addrlen); if (wres == 0) ret = true; else if (WSAGetLastError () == WSAEWOULDBLOCK) { WSAPOLLFD fds = { .fd = sock, .events = WS_POLLOUT }; wres = WSAPoll (&fds, 1, 1500L); if (wres > 0 && fds.revents == WS_POLLOUT) ret = true; } ::closesocket (sock); } } FreeAddrInfoW (ai); return ret; } /* Use only to enumerate the Network top level. */ static DWORD thread_netdrive_wsd (void *arg) { netdriveinf *ndi = (netdriveinf *) arg; DIR *dir = ndi->dir; IEnumShellItems *netitem_enum; IShellItem *netparent; HRESULT wres; ReleaseSemaphore (ndi->sem, 1, NULL); wres = CoInitialize (NULL); if (FAILED (wres)) { ndi->err = hresult_to_errno (wres); goto out; } wres = SHGetKnownFolderItem (FOLDERID_NetworkFolder, KF_FLAG_DEFAULT, NULL, IID_PPV_ARGS (&netparent)); if (FAILED (wres)) { ndi->err = hresult_to_errno (wres); goto out; } wres = netparent->BindToHandler (NULL, BHID_StorageEnum, IID_PPV_ARGS (&netitem_enum)); if (FAILED (wres)) { ndi->err = hresult_to_errno (wres); netparent->Release (); goto out; } netitem_enum->Reset (); /* Don't look at me! Network discovery is very unreliable and the list of machines returned is just fly-by-night, if the enumerator doesn't have enough time. The fact that you see *most* (but not necessarily *all*) machines on the network in Windows Explorer is a result of the enumeration running in a loop. You can observe this when rebooting a remote machine and it disappears and reappears in the Explorer Network list. However, this is no option for the command line. We need to be able to enumerate in a single go, since we can't just linger during readdir() and reset the enumeration multiple times until we have a supposedly full list. This makes the following Sleep necessary. Sleeping ~3secs after Reset fills the enumeration with high probability with almost all available machines. */ Sleep (3000L); do { IShellItem *netitem[10] = { 0 }; LPWSTR item_name = NULL; ULONG count; wres = netitem_enum->Next (10, netitem, &count); if (SUCCEEDED (wres) && count > 0) { for (ULONG idx = 0; idx < count; ++idx) { if (netitem[idx]->GetDisplayName (SIGDN_PARENTRELATIVEPARSING, &item_name) == S_OK) { /* Skip "\\" on server names and downcase */ DIR_cache.add (item_name + 2, true); CoTaskMemFree (item_name); } netitem[idx]->Release (); } } } while (wres == S_OK); netitem_enum->Release (); netparent->Release (); ndi->err = 0; out: CoUninitialize (); ReleaseSemaphore (ndi->sem, 1, NULL); return 0; } static DWORD thread_netdrive_wnet (void *arg) { netdriveinf *ndi = (netdriveinf *) arg; DIR *dir = ndi->dir; DWORD wres; size_t entry_cache_size = DIR_cache.count (); WCHAR provider[256], *dummy = NULL; wchar_t srv_name[CYG_MAX_PATH]; NETRESOURCEW nri = { 0 }; LPNETRESOURCEW nro; tmp_pathbuf tp; HANDLE dom = NULL; DWORD cnt, size; ReleaseSemaphore (ndi->sem, 1, NULL); wres = WNetGetProviderNameW (ndi->provider, provider, (size = 256, &size)); if (wres != NO_ERROR) { ndi->err = geterrno_from_win_error (wres); goto out; } sys_mbstowcs (srv_name, CYG_MAX_PATH, dir->__d_dirname); srv_name[0] = L'\\'; srv_name[1] = L'\\'; if (ndi->provider == WNNC_NET_MS_NFS && !server_is_running_nfs (srv_name + 2)) { ndi->err = ENOENT; goto out; } nri.lpRemoteName = srv_name; nri.lpProvider = provider; nri.dwType = RESOURCETYPE_DISK; nro = (LPNETRESOURCEW) tp.c_get (); wres = WNetGetResourceInformationW (&nri, nro, (size = NT_MAX_PATH, &size), &dummy); if (wres != NO_ERROR) { ndi->err = geterrno_from_win_error (wres); goto out; } wres = WNetOpenEnumW (RESOURCE_GLOBALNET, RESOURCETYPE_DISK, RESOURCEUSAGE_ALL, nro, &dom); if (wres != NO_ERROR) { ndi->err = geterrno_from_win_error (wres); goto out; } while ((wres = WNetEnumResourceW (dom, (cnt = 1, &cnt), nro, (size = NT_MAX_PATH, &size))) == NO_ERROR) { /* Skip server name and trailing backslash */ wchar_t *name = nro->lpRemoteName + wcslen (srv_name) + 1; size_t cache_idx; if (ndi->provider == WNNC_NET_MS_NFS) { wchar_t *nm = name; /* Convert from "ANSI embedded in widechar" to multibyte and convert back to widechar. */ char mbname[wcslen (name) + 1]; char *mb = mbname; while ((*mb++ = *nm++)) ; sys_mbstowcs_alloc (&name, HEAP_NOTHEAP, mbname); if (!name) { ndi->err = ENOMEM; goto out; } /* NFS has deep links so convert embedded '\\' to '/' here */ for (wchar_t *bs = name; (bs = wcschr (bs, L'\\')); *bs++ = L'/') ; } /* If we already collected shares, drop duplicates. */ for (cache_idx = 0; cache_idx < entry_cache_size; ++ cache_idx) if (!wcscmp (name, DIR_cache[cache_idx])) // wcscasecmp? break; if (cache_idx >= entry_cache_size) DIR_cache.add (name); if (ndi->provider == WNNC_NET_MS_NFS) free (name); } out: if (dom) WNetCloseEnum (dom); ReleaseSemaphore (ndi->sem, 1, NULL); return 0; } static DWORD create_thread_and_wait (DIR *dir) { netdriveinf ndi = { dir, 0, 0, NULL }; cygthread *thr; /* For the Network root, fetch WSD info. */ if (strlen (dir->__d_dirname) == 2) { ndi.provider = WNNC_NET_SMB; ndi.sem = CreateSemaphore (&sec_none_nih, 0, 2, NULL); thr = new cygthread (thread_netdrive_wsd, &ndi, "netdrive_wsd"); if (thr->detach (ndi.sem)) ndi.err = EINTR; CloseHandle (ndi.sem); goto out; } /* For shares, use WNet functions. */ /* Try NFS first, if the name contains a dot (i. e., supposedly is a FQDN as used in NFS server enumeration). */ if (strchr (dir->__d_dirname, '.')) { ndi.provider = WNNC_NET_MS_NFS; ndi.sem = CreateSemaphore (&sec_none_nih, 0, 2, NULL); thr = new cygthread (thread_netdrive_wnet, &ndi, "netdrive_nfs"); if (thr->detach (ndi.sem)) ndi.err = EINTR; CloseHandle (ndi.sem); if (ndi.err == EINTR) goto out; } /* Eventually, try SMB via WNet for share enumeration. */ if (!strcmp (dir->__d_dirname + 2, "tsclient")) ndi.provider = WNNC_NET_TERMSRV; else ndi.provider = WNNC_NET_SMB; ndi.sem = CreateSemaphore (&sec_none_nih, 0, 2, NULL); thr = new cygthread (thread_netdrive_wnet, &ndi, "netdrive_smb"); if (thr->detach (ndi.sem)) ndi.err = EINTR; CloseHandle (ndi.sem); out: return DIR_cache.count() > 0 ? 0 : ndi.err; } virtual_ftype_t fhandler_netdrive::exists () { if (strlen (get_name ()) == 2) return virt_rootdir; wchar_t name[MAX_PATH]; struct addrinfoW *ai; INT ret; /* Hopefully we are allowed to assume an IP network with existing name resolution these days. Therefore, just try to resolve the name into IP addresses. This may take up to about 3 secs if the name doesn't exist, or about 8 secs if DNS is unavailable. Don't ask for "tsclient", it doesn't resolve. Just assume it exists. */ if (!strcmp (get_name () + 2, "tsclient")) return virt_directory; sys_mbstowcs (name, CYG_MAX_PATH, get_name ()); ret = GetAddrInfoW (name + 2, NULL, NULL, &ai); if (!ret) FreeAddrInfoW (ai); return ret ? virt_none : virt_directory; } fhandler_netdrive::fhandler_netdrive (): fhandler_virtual () { } int fhandler_netdrive::fstat (struct stat *buf) { const char *path = get_name (); debug_printf ("fstat (%s)", path); fhandler_base::fstat (buf); buf->st_mode = S_IFDIR | STD_RBITS | STD_XBITS; buf->st_ino = get_ino (); return 0; } DIR * fhandler_netdrive::opendir (int fd) { DIR *dir; int ret; dir = fhandler_virtual::opendir (fd); dir->__handle = (char *) new dir_cache (); if (dir && (ret = create_thread_and_wait (dir))) { free (dir->__d_dirname); free (dir->__d_dirent); free (dir); dir = NULL; set_errno (ret); syscall_printf ("%p = opendir (%s)", dir, get_name ()); } return dir; } int fhandler_netdrive::readdir (DIR *dir, dirent *de) { int ret; if (!DIR_cache[dir->__d_position]) { ret = ENMFILE; goto out; } sys_wcstombs (de->d_name, sizeof de->d_name, DIR_cache[dir->__d_position]); if (strlen (dir->__d_dirname) == 2) de->d_ino = hash_path_name (get_ino (), de->d_name); else { char full[2 * CYG_MAX_PATH]; char *s; s = stpcpy (full, dir->__d_dirname); *s++ = '/'; stpcpy (s, de->d_name); de->d_ino = readdir_get_ino (full, false); } dir->__d_position++; de->d_type = DT_DIR; ret = 0; out: syscall_printf ("%d = readdir(%p, %p)", ret, dir, de); return ret; } void fhandler_netdrive::seekdir (DIR *dir, long pos) { ::rewinddir (dir); if (pos < 0) return; while (dir->__d_position < pos) if (readdir (dir, dir->__d_dirent)) break; } void fhandler_netdrive::rewinddir (DIR *dir) { dir->__d_position = 0; } int fhandler_netdrive::closedir (DIR *dir) { if (dir->__handle != INVALID_HANDLE_VALUE) delete &DIR_cache; return fhandler_virtual::closedir (dir); } int fhandler_netdrive::open (int flags, mode_t mode) { if ((flags & (O_CREAT | O_EXCL)) == (O_CREAT | O_EXCL)) { set_errno (EEXIST); return 0; } if (flags & O_WRONLY) { set_errno (EISDIR); return 0; } /* Open a fake handle to \\Device\\Null */ return open_null (flags); } int fhandler_netdrive::close () { /* Skip fhandler_virtual::close, which is a no-op. */ return fhandler_base::close (); }