1 /*
2     SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
3     SPDX-FileCopyrightText: 2020 Harald Sitter <sitter@kde.org>
4 */
5 
6 #include <QCoreApplication>
7 #include <QUrlQuery>
8 #include <QScopeGuard>
9 
10 #include "smbcdiscoverer.h"
11 
12 static QEvent::Type LoopEvent = QEvent::User;
13 
14 class SMBCServerDiscovery : public SMBCDiscovery
15 {
16 public:
SMBCServerDiscovery(const UDSEntry & entry)17     SMBCServerDiscovery(const UDSEntry &entry)
18         : SMBCDiscovery(entry)
19     {
20         m_entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR);
21         m_entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, (S_IRUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH));
22         m_entry.fastInsert(KIO::UDSEntry::UDS_URL, url());
23         m_entry.fastInsert(KIO::UDSEntry::UDS_MIME_TYPE, QStringLiteral("application/x-smb-server"));
24         m_entry.fastInsert(KIO::UDSEntry::UDS_ICON_NAME, QStringLiteral("network-server"));
25     }
26 
url()27     QString url()
28     {
29         QUrl u("smb://");
30         u.setHost(udsName());
31         return u.url();
32     }
33 };
34 
35 class SMBCShareDiscovery : public SMBCDiscovery
36 {
37 public:
SMBCShareDiscovery(const UDSEntry & entry)38     SMBCShareDiscovery(const UDSEntry &entry)
39         : SMBCDiscovery(entry)
40     {
41         m_entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR);
42         m_entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, (S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH));
43     }
44 };
45 
46 class SMBCWorkgroupDiscovery : public SMBCDiscovery
47 {
48 public:
SMBCWorkgroupDiscovery(const UDSEntry & entry)49     SMBCWorkgroupDiscovery(const UDSEntry &entry)
50         : SMBCDiscovery(entry)
51     {
52         m_entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR);
53         m_entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, (S_IRUSR | S_IRGRP | S_IROTH | S_IXUSR | S_IXGRP | S_IXOTH));
54         m_entry.fastInsert(KIO::UDSEntry::UDS_MIME_TYPE, QStringLiteral("application/x-smb-workgroup"));
55         m_entry.fastInsert(KIO::UDSEntry::UDS_URL, url());
56     }
57 
url()58     QString url()
59     {
60         QUrl u("smb://");
61         u.setHost(udsName());
62         if (!u.isValid()) {
63             // In the event that the workgroup contains bad characters, put it in a query instead.
64             // This is transparently handled by SMBUrl when we get this as input again.
65             // Also see documentation there.
66             // https://bugs.kde.org/show_bug.cgi?id=204423
67             u.setHost(QString());
68             QUrlQuery q;
69             q.addQueryItem("kio-workgroup", udsName());
70             u.setQuery(q);
71         }
72         return u.url();
73     }
74 };
75 
SMBCDiscovery(const UDSEntry & entry)76 SMBCDiscovery::SMBCDiscovery(const UDSEntry &entry)
77     : m_entry(entry)
78       // cache the name, it may get accessed more than once
79     , m_name(entry.stringValue(KIO::UDSEntry::UDS_NAME))
80 {
81 }
82 
udsName() const83 QString SMBCDiscovery::udsName() const
84 {
85     return m_name;
86 }
87 
toEntry() const88 KIO::UDSEntry SMBCDiscovery::toEntry() const
89 {
90     return m_entry;
91 }
92 
SMBCDiscoverer(const SMBUrl & url,QEventLoop * loop,SMBSlave * slave)93 SMBCDiscoverer::SMBCDiscoverer(const SMBUrl &url, QEventLoop *loop, SMBSlave *slave)
94     : m_url(url)
95     , m_loop(loop)
96     , m_slave(slave)
97 {
98 }
99 
~SMBCDiscoverer()100 SMBCDiscoverer::~SMBCDiscoverer()
101 {
102     if (m_dirFd > 0) {
103         smbc_closedir(m_dirFd);
104     }
105 }
106 
start()107 void SMBCDiscoverer::start()
108 {
109     queue();
110 }
111 
112 
discoverNextFileInfo()113 bool SMBCDiscoverer::discoverNextFileInfo()
114 {
115 #ifdef HAVE_READDIRPLUS2
116     // Readdirplus2 dir/file listing. Becomes noop when at end of data associated with dirfd.
117     // If readdirplus2 isn't available the regular dirent listing is done.
118     // readdirplus2 improves performance by giving us a stat without separate call (Samba>=4.12)
119     struct stat st;
120     const struct libsmb_file_info *fileInfo = smbc_readdirplus2(m_dirFd, &st);
121     if (fileInfo) {
122         const QString name = QString::fromUtf8(fileInfo->name);
123         qCDebug(KIO_SMB_LOG) << "fileInfo" << "name:" << name;
124         if (name == ".") {
125             return true;
126         } else if (name == "..") {
127             m_dirWasRoot = false;
128             return true;
129         }
130         UDSEntry entry;
131         entry.reserve(5); // Minimal size. stat will set at least 4 fields.
132         entry.fastInsert(KIO::UDSEntry::UDS_NAME, name);
133 
134         m_url.addPath(name);
135         m_slave->statToUDSEntry(m_url, st, entry); // won't produce useful error
136         Q_EMIT newDiscovery(Discovery::Ptr(new SMBCDiscovery(entry)));
137         m_url.cdUp();
138         return true;
139     }
140 #endif // HAVE_READDIRPLUS2
141     return false;
142 }
143 
discoverNext()144 void SMBCDiscoverer::discoverNext()
145 {
146     // Poor man's concurrency. smbc isn't thread safe so we'd hold up other
147     // discoverers until we are done. While that will likely happen anyway
148     // because smbc_opendir (usually?) blocks until it actually has all
149     // the data to loop on, meaning the actual looping after open is fairly
150     // fast. Even so, there's benefit in letting other discoverers do
151     // their work in the meantime because they may do more atomic
152     // requests that are async and can take a while due to network latency.
153     // To get somewhat reasonable behavior we simulate an async smbc discovery
154     // by posting loop events to the eventloop and each loop run we process
155     // a single dirent.
156     // This effectively unblocks the eventloop between iterations.
157     // Once we are out of entries this discoverer is considered finished.
158 
159     // Always queue a new iteration when returning so we don't forget to.
160     auto autoQueue = qScopeGuard([this] {
161         queue();
162     });
163 
164     if (m_dirFd == -1) {
165         init();
166         Q_ASSERT(m_dirFd || m_finished);
167         return;
168     }
169 
170     if (discoverNextFileInfo()) {
171         return;
172     }
173 
174     qCDebug(KIO_SMB_LOG) << "smbc_readdir ";
175     struct smbc_dirent *dirp = smbc_readdir(m_dirFd);
176     if (dirp == nullptr) {
177         qCDebug(KIO_SMB_LOG) << "done with smbc";
178         stop();
179         return;
180     }
181 
182     const QString name = QString::fromUtf8(dirp->name);
183     // We cannot trust dirp->commentlen has it might be with or without the NUL character
184     // See KDE bug #111430 and Samba bug #3030
185     const QString comment = QString::fromUtf8(dirp->comment);
186 
187     qCDebug(KIO_SMB_LOG) << "dirent "
188                          << "name:" << name
189                          << "comment:" << comment
190                          << "type:" << dirp->smbc_type;
191 
192     UDSEntry entry;
193     // Minimal potential size. The actual size depends on this function,
194     // possibly the stat function, and lastly the Discovery objects themselves.
195     // The smallest will be a ShareDiscovery with 5 fields.
196     entry.reserve(5);
197     entry.fastInsert(KIO::UDSEntry::UDS_NAME, name);
198     entry.fastInsert(KIO::UDSEntry::UDS_COMMENT, comment);
199     // Ensure system shares are marked hidden.
200     if (name.endsWith(QLatin1Char('$'))) {
201         entry.fastInsert(KIO::UDSEntry::UDS_HIDDEN, 1);
202     }
203 
204 #if !defined(HAVE_READDIRPLUS2)
205     // . and .. are always of the dir type so they are of no consequence outside
206     // actual dir listing and that'd be done by readdirplus2 already
207     if (name == ".") {
208         // Skip the "." entry
209         // Mind the way m_currentUrl is handled in the loop
210     } else if (name == "..") {
211         m_dirWasRoot = false;
212     } else if (dirp->smbc_type == SMBC_FILE || dirp->smbc_type == SMBC_DIR) {
213         // Set stat information
214         m_url.addPath(name);
215         const int statErr = m_slave->browse_stat_path(m_url, entry);
216         if (statErr != 0) {
217             // The entry can disappear in the time span between
218             // listing and the stat call. There's nothing we or the user
219             // can do about it. Log the incident and move on with listing.
220             qCWarning(KIO_SMB_LOG) << "Failed to stat" << m_url << statErr;
221         } else {
222             Q_EMIT newDiscovery(Discovery::Ptr(new SMBCDiscovery(entry)));
223         }
224         m_url.cdUp();
225     }
226 #endif // HAVE_READDIRPLUS2
227 
228     if (dirp->smbc_type == SMBC_SERVER) {
229         Q_EMIT newDiscovery(Discovery::Ptr(new SMBCServerDiscovery(entry)));
230     } else if (dirp->smbc_type == SMBC_FILE_SHARE) {
231         Q_EMIT newDiscovery(Discovery::Ptr(new SMBCShareDiscovery(entry)));
232     } else if (dirp->smbc_type == SMBC_WORKGROUP) {
233         Q_EMIT newDiscovery(Discovery::Ptr(new SMBCWorkgroupDiscovery(entry)));
234     } else {
235         qCDebug(KIO_SMB_LOG) << "SMBC_UNKNOWN :" << name;
236     }
237 }
238 
customEvent(QEvent * event)239 void SMBCDiscoverer::customEvent(QEvent *event)
240 {
241     if (event->type() == LoopEvent) {
242         if (!m_finished) {
243             discoverNext();
244         }
245         return;
246     }
247     QObject::customEvent(event);
248 }
249 
stop()250 void SMBCDiscoverer::stop()
251 {
252     m_finished = true;
253     Q_EMIT finished();
254 }
255 
isFinished() const256 bool SMBCDiscoverer::isFinished() const
257 {
258     return m_finished;
259 }
260 
dirWasRoot() const261 bool SMBCDiscoverer::dirWasRoot() const
262 {
263     return m_dirWasRoot;
264 }
265 
error() const266 int SMBCDiscoverer::error() const
267 {
268     return m_error;
269 }
270 
init()271 void SMBCDiscoverer::init()
272 {
273     Q_ASSERT(m_dirFd < 0);
274 
275     m_dirFd = smbc_opendir(m_url.toSmbcUrl());
276     if (m_dirFd >= 0) {
277         m_error = 0;
278     } else {
279         m_error = errno;
280         stop();
281     }
282 
283     qCDebug(KIO_SMB_LOG) << "open" << m_url.toSmbcUrl()
284                          << "url-type:" << m_url.getType()
285                          << "dirfd:" << m_dirFd
286                          << "errNum:" << m_error;
287 
288     return;
289 }
290 
queue()291 void SMBCDiscoverer::queue()
292 {
293     if (m_finished) {
294         return;
295     }
296 
297     // Queue low priority events. For server discovery (that is: other discoverers run as well)
298     // we want the modern discoverers to be peferred. For other discoveries only
299     // SMBC is running on the loop and so the priority has no negative impact.
300     QCoreApplication::postEvent(this, new QEvent(LoopEvent), Qt::LowEventPriority);
301 }
302