| | | 1 | | // SPDX-FileCopyrightText: 2026 Alper Çelik |
| | | 2 | | // SPDX-License-Identifier: MIT OR Apache-2.0 |
| | | 3 | | // |
| | | 4 | | // Custom URL schemes (`ipc://`, `tauri://`, `asset://`) for the Qt runtime. |
| | | 5 | | // |
| | | 6 | | // The runtime implements no protocol logic of its own: every request is |
| | | 7 | | // serialized into the `\n`-separated `k: v` form Tauri expects and handed to the |
| | | 8 | | // Rust callback `qt_scheme_request` (declared by the cxx bridge), whose |
| | | 9 | | // `QtSchemeResponse` is then written straight back onto the Qt job. |
| | | 10 | | |
| | | 11 | | #include "qt_scheme.hpp" |
| | | 12 | | |
| | | 13 | | #include <cstdio> |
| | | 14 | | |
| | | 15 | | |
| | | 16 | | // The cxx bridge declares the Rust callback `qt_scheme_request` and the |
| | | 17 | | // `::QtSchemeResponse` struct (with the correct linkage). Never hand-declare |
| | | 18 | | // them here; include the generated header instead. |
| | | 19 | | #include "tauri-runtime-qt/src/qt.rs.h" |
| | | 20 | | |
| | | 21 | | #include <cstdio> |
| | | 22 | | |
| | | 23 | | #include <QtCore/QBuffer> |
| | | 24 | | #include <QtCore/QByteArray> |
| | | 25 | | #include <QtCore/QDebug> |
| | | 26 | | #include <QtCore/QHash> |
| | | 27 | | #include <QtCore/QIODevice> |
| | | 28 | | #include <QtCore/QList> |
| | | 29 | | #include <QtCore/QMultiMap> |
| | | 30 | | #include <QtCore/QPair> |
| | | 31 | | #include <QtCore/QPointer> |
| | | 32 | | #include <QtCore/QUrl> |
| | | 33 | | #include <QtWebEngineCore/QWebEnginePage> |
| | | 34 | | #include <QtWebEngineCore/QWebEngineProfile> |
| | | 35 | | #include <QtWebEngineCore/QWebEngineUrlRequestJob> |
| | | 36 | | #include <QtWebEngineCore/QWebEngineUrlScheme> |
| | | 37 | | #include <QtWebEngineCore/QWebEngineUrlSchemeHandler> |
| | | 38 | | #include <QtWebEngineWidgets/QWebEngineView> |
| | | 39 | | |
| | | 40 | | namespace { |
| | | 41 | | |
| | | 42 | | // Scheme kinds shared with the Rust side: 0 = ipc, 1 = tauri, 2 = asset. |
| | | 43 | | enum SchemeKind : std::int32_t { |
| | | 44 | | SchemeIpc = 0, |
| | | 45 | | SchemeTauri = 1, |
| | | 46 | | SchemeAsset = 2, |
| | | 47 | | }; |
| | | 48 | | |
| | | 49 | | // webview id -> backing view (weak). |
| | 105 | 50 | | QHash<std::uint64_t, QPointer<QWebEngineView>>& webviewRegistry() { |
| | 105 | 51 | | static QHash<std::uint64_t, QPointer<QWebEngineView>> registry; |
| | 105 | 52 | | return registry; |
| | 105 | 53 | | } |
| | | 54 | | |
| | | 55 | | // Parses a `\n`-separated `k: v` header list. Splits on the first `:` and |
| | | 56 | | // strips surrounding whitespace and any trailing CR from each value. |
| | 1323 | 57 | | QList<QPair<QByteArray, QByteArray>> parseHeaderList(const QByteArray& blob) { |
| | 1323 | 58 | | QList<QPair<QByteArray, QByteArray>> headers; |
| | 1323 | 59 | | const QList<QByteArray> lines = blob.split('\n'); |
| | 7896 | 60 | | for (const QByteArray& raw : lines) { |
| | 7896 | 61 | | QByteArray line = raw; |
| | 7896 | 62 | | if (line.endsWith('\r')) { |
| | 0 | 63 | | line.chop(1); |
| | 0 | 64 | | } |
| | 7896 | 65 | | if (line.trimmed().isEmpty()) { |
| | 1323 | 66 | | continue; |
| | 1323 | 67 | | } |
| | 6573 | 68 | | const int colon = line.indexOf(':'); |
| | 6573 | 69 | | if (colon < 0) { |
| | 0 | 70 | | continue; |
| | 0 | 71 | | } |
| | 6573 | 72 | | const QByteArray key = line.left(colon).trimmed(); |
| | 6573 | 73 | | if (key.isEmpty()) { |
| | 0 | 74 | | continue; |
| | 0 | 75 | | } |
| | 6573 | 76 | | headers.append({key, line.mid(colon + 1).trimmed()}); |
| | 6573 | 77 | | } |
| | 1323 | 78 | | return headers; |
| | 1323 | 79 | | } |
| | | 80 | | |
| | | 81 | | // Serializes the job's request headers into the `\n`-separated `k: v` form Tauri |
| | | 82 | | // expects. Tauri's ipc parser hard-requires an `Origin` header, which |
| | | 83 | | // `requestHeaders()` may omit, so it is synthesized from the job's initiator |
| | | 84 | | // (the serialized origin of the content that started the request). |
| | 1323 | 85 | | QByteArray serializeRequestHeaders(QWebEngineUrlRequestJob* job) { |
| | 1323 | 86 | | QByteArray out; |
| | 1323 | 87 | | bool hasOrigin = false; |
| | 1323 | 88 | | const QMap<QByteArray, QByteArray> headers = job->requestHeaders(); |
| | 10521 | 89 | | for (auto it = headers.constBegin(); it != headers.constEnd(); ++it) { |
| | 9198 | 90 | | if (it.key().compare("Origin", Qt::CaseInsensitive) == 0) { |
| | 0 | 91 | | hasOrigin = true; |
| | 0 | 92 | | } |
| | 9198 | 93 | | out += it.key(); |
| | 9198 | 94 | | out += ": "; |
| | 9198 | 95 | | out += it.value(); |
| | 9198 | 96 | | out += '\n'; |
| | 9198 | 97 | | } |
| | 1323 | 98 | | if (!hasOrigin) { |
| | 1323 | 99 | | const QUrl initiator = job->initiator(); |
| | 1323 | 100 | | if (initiator.isValid() && !initiator.isEmpty()) { |
| | 1302 | 101 | | out += "Origin: "; |
| | 1302 | 102 | | out += initiator.toString(QUrl::FullyEncoded).toUtf8(); |
| | 1302 | 103 | | out += '\n'; |
| | 1302 | 104 | | } |
| | 1323 | 105 | | } |
| | 1323 | 106 | | return out; |
| | 1323 | 107 | | } |
| | | 108 | | |
| | | 109 | | // A request whose response Tauri will deliver asynchronously (a later call to |
| | | 110 | | // `qt_scheme_respond`). Keyed by the request id passed to `qt_scheme_request`. |
| | | 111 | | struct PendingRequest { |
| | | 112 | | QPointer<QWebEngineUrlRequestJob> job; |
| | | 113 | | std::uint64_t webview_id = 0; |
| | | 114 | | }; |
| | | 115 | | |
| | 105 | 116 | | QHash<std::uint64_t, PendingRequest>& pendingRequests() { |
| | 105 | 117 | | static QHash<std::uint64_t, PendingRequest> requests; |
| | 105 | 118 | | return requests; |
| | 105 | 119 | | } |
| | | 120 | | |
| | 1323 | 121 | | std::uint64_t nextRequestId() { |
| | 1323 | 122 | | static std::uint64_t next = 0; |
| | 1323 | 123 | | return ++next; |
| | 1323 | 124 | | } |
| | | 125 | | |
| | | 126 | | // Process-wide QObject on the GUI thread used as the receiver context for |
| | | 127 | | // marshalling async responses. Created on the GUI thread (first call is from |
| | | 128 | | // `requestStarted`). |
| | 1365 | 129 | | QObject* dispatchContext() { |
| | 1365 | 130 | | static QObject* context = nullptr; |
| | 1365 | 131 | | if (context == nullptr) { |
| | 21 | 132 | | context = new QObject(); |
| | 21 | 133 | | } |
| | 1365 | 134 | | return context; |
| | 1365 | 135 | | } |
| | | 136 | | |
| | | 137 | | // Applies a complete response to a job: every header through |
| | | 138 | | // `setAdditionalResponseHeaders`, `Content-Type` (or `application/json`) as the |
| | | 139 | | // reply mime, and the body through a `QBuffer` parented to the job so the web |
| | | 140 | | // engine's cross-thread reads stay valid and nothing leaks. |
| | | 141 | | void applyResponse(QWebEngineUrlRequestJob* job, std::int32_t status, |
| | 1323 | 142 | | const QByteArray& rawHeaders, const QByteArray& body) { |
| | 1323 | 143 | | if (job == nullptr) { |
| | 0 | 144 | | return; |
| | 0 | 145 | | } |
| | 1323 | 146 | | const QList<QPair<QByteArray, QByteArray>> headers = parseHeaderList(rawHeaders); |
| | | 147 | | |
| | 1323 | 148 | | QByteArray contentType; |
| | 1323 | 149 | | QByteArray location; |
| | 1323 | 150 | | bool hasExposeHeaders = false; |
| | 1323 | 151 | | QMultiMap<QByteArray, QByteArray> additional; |
| | | 152 | | |
| | 6573 | 153 | | for (const QPair<QByteArray, QByteArray>& header : headers) { |
| | 6573 | 154 | | if (header.first.compare("Content-Type", Qt::CaseInsensitive) == 0) { |
| | 1323 | 155 | | if (contentType.isEmpty()) { |
| | 1323 | 156 | | contentType = header.second; |
| | 1323 | 157 | | } |
| | 1323 | 158 | | continue; // passed to reply() instead |
| | 1323 | 159 | | } |
| | 5250 | 160 | | if (header.first.compare("Access-Control-Expose-Headers", Qt::CaseInsensitive) == 0) { |
| | 2625 | 161 | | hasExposeHeaders = true; |
| | 2625 | 162 | | } else if (header.first.compare("Location", Qt::CaseInsensitive) == 0) { |
| | 0 | 163 | | location = header.second; |
| | 0 | 164 | | } |
| | 5250 | 165 | | additional.insert(header.first, header.second); |
| | 5250 | 166 | | } |
| | | 167 | | |
| | | 168 | | // Qt's job API has no HTTP status setter; a redirect is the only |
| | | 169 | | // status-driven behaviour we can honour. Everything else (including Tauri's |
| | | 170 | | // 500 error responses) is delivered as a body + headers. |
| | 1323 | 171 | | if (status >= 300 && status < 400 && !location.isEmpty()) { |
| | 0 | 172 | | job->redirect(QUrl(QString::fromUtf8(location))); |
| | 0 | 173 | | return; |
| | 0 | 174 | | } |
| | | 175 | | |
| | | 176 | | // Tauri's JS reads `Tauri-Response` (and keys off `Content-Type`); both must |
| | | 177 | | // be exposed across the CORS boundary. |
| | 1323 | 178 | | if (!hasExposeHeaders) { |
| | 0 | 179 | | additional.insert(QByteArrayLiteral("Access-Control-Expose-Headers"), |
| | 0 | 180 | | QByteArrayLiteral("Tauri-Response, Content-Type")); |
| | 0 | 181 | | } |
| | | 182 | | |
| | 1323 | 183 | | auto* device = new QBuffer(job); // parented to the job: freed with it |
| | 1323 | 184 | | device->setData(body); |
| | 1323 | 185 | | device->open(QIODevice::ReadOnly); |
| | | 186 | | |
| | 1323 | 187 | | job->setAdditionalResponseHeaders(additional); |
| | 1323 | 188 | | job->reply(contentType.isEmpty() ? QByteArrayLiteral("application/json") : contentType, device); |
| | 1323 | 189 | | } |
| | | 190 | | |
| | | 191 | | // One `QWebEngineUrlSchemeHandler` per scheme; forwards every request to Rust. |
| | | 192 | | class TauriSchemeHandler final : public QWebEngineUrlSchemeHandler { |
| | | 193 | | public: |
| | | 194 | | TauriSchemeHandler(std::int32_t kind, std::uint64_t webview_id, QObject* parent = nullptr) |
| | 63 | 195 | | : QWebEngineUrlSchemeHandler(parent), kind_(kind), webview_id_(webview_id) {} |
| | | 196 | | |
| | | 197 | | void requestStarted(QWebEngineUrlRequestJob* job) override; |
| | | 198 | | |
| | | 199 | | private: |
| | | 200 | | std::int32_t kind_; |
| | | 201 | | std::uint64_t webview_id_; |
| | | 202 | | }; |
| | | 203 | | |
| | | 204 | | // Owns the per-request state and drives the reply. |
| | | 205 | | // |
| | | 206 | | // Created on the GUI thread from `requestStarted` and parented to the job, so |
| | | 207 | | // `job->destroyed` tears it down and any still-queued work becomes a no-op. The |
| | | 208 | | // reply `QIODevice` is likewise parented to the job (see `applyResponse`): the |
| | | 209 | | // web engine may read the device on another thread until the job is deleted, and |
| | | 210 | | // a job child is exactly as long-lived as the job. |
| | | 211 | | class JobContext final : public QObject { |
| | | 212 | | public: |
| | | 213 | | JobContext(QWebEngineUrlRequestJob* job, std::int32_t kind, std::uint64_t webview_id) |
| | 1323 | 214 | | : QObject(job), job_(job), kind_(kind), webview_id_(webview_id) { |
| | 1323 | 215 | | method_ = job->requestMethod(); |
| | 1323 | 216 | | url_ = job->requestUrl(); |
| | 1323 | 217 | | headers_ = serializeRequestHeaders(job); |
| | 1323 | 218 | | } |
| | | 219 | | |
| | | 220 | | // Collects the full request body before dispatching. `job->requestBody()` |
| | | 221 | | // is a sequential device whose data may arrive after `requestStarted`, so |
| | | 222 | | // the body is accumulated on `readyRead` and the dispatch is driven by |
| | | 223 | | // `readChannelFinished` (the reliable "body complete" signal), never with a |
| | | 224 | | // partial body. |
| | 1323 | 225 | | void start() { |
| | 1323 | 226 | | QIODevice* body = job_ ? job_->requestBody() : nullptr; |
| | | 227 | | // Only POST carries a body Tauri reads; every other method (the |
| | | 228 | | // `tauri://`/`asset://` GET loads, OPTIONS preflights) can be dispatched |
| | | 229 | | // right away. |
| | 1323 | 230 | | if (body == nullptr || method_ != QByteArrayLiteral("POST")) { |
| | 21 | 231 | | if (body != nullptr) { |
| | 21 | 232 | | bodyData_ += body->readAll(); |
| | 21 | 233 | | } |
| | 21 | 234 | | dispatch(); |
| | 21 | 235 | | return; |
| | 21 | 236 | | } |
| | | 237 | | |
| | 1302 | 238 | | body_ = body; |
| | 1302 | 239 | | bodyData_ += body_->readAll(); |
| | | 240 | | |
| | | 241 | | // Dump whatever arrived into the accumulator; never dispatch a partial |
| | | 242 | | // body here (only the end-of-stream cases below may). |
| | 1302 | 243 | | QObject::connect(body_, &QIODevice::readyRead, this, [this]() { |
| | 0 | 244 | | if (dispatched_ || body_ == nullptr) { |
| | 0 | 245 | | return; |
| | 0 | 246 | | } |
| | 0 | 247 | | bodyData_ += body_->readAll(); |
| | 0 | 248 | | if (body_->atEnd() || (body_->bytesAvailable() == 0 && !body_->isSequential())) { |
| | 0 | 249 | | dispatch(); |
| | 0 | 250 | | } |
| | 0 | 251 | | }); |
| | | 252 | | // Qt's "input stream closed" signal: for the custom-scheme POST body |
| | | 253 | | // device this fires once the whole body is available. |
| | 1302 | 254 | | QObject::connect(body_, &QIODevice::readChannelFinished, this, [this]() { |
| | 0 | 255 | | if (dispatched_ || body_ == nullptr) { |
| | 0 | 256 | | return; |
| | 0 | 257 | | } |
| | 0 | 258 | | bodyData_ += body_->readAll(); |
| | 0 | 259 | | dispatch(); |
| | 0 | 260 | | }); |
| | | 261 | | |
| | | 262 | | // Already fully buffered when we looked (Chromium usually delivers |
| | | 263 | | // custom-scheme POST bodies this way): dispatch right away. |
| | 1302 | 264 | | if (!body_->isSequential() && (body_->atEnd() || body_->bytesAvailable() == 0)) { |
| | 0 | 265 | | dispatch(); |
| | 0 | 266 | | return; |
| | 0 | 267 | | } |
| | | 268 | | |
| | | 269 | | // QtWebEngine 6.11 hands over an already-ended, empty device for |
| | | 270 | | // custom-scheme POSTs (the invoke payload travels in the |
| | | 271 | | // `X-Tauriqt-Payload` header instead), so an empty device dispatches at |
| | | 272 | | // once; non-empty devices are accumulated to completion. |
| | 1302 | 273 | | if (body_->atEnd() || (body_->bytesAvailable() == 0 && !body_->isSequential())) { |
| | 1302 | 274 | | dispatch(); |
| | 1302 | 275 | | return; |
| | 1302 | 276 | | } |
| | | 277 | | |
| | 0 | 278 | | QObject::connect(body_, &QIODevice::readyRead, this, [this]() { |
| | 0 | 279 | | if (dispatched_ || body_ == nullptr) { |
| | 0 | 280 | | return; |
| | 0 | 281 | | } |
| | 0 | 282 | | bodyData_ += body_->readAll(); |
| | 0 | 283 | | if (body_->atEnd() || (body_->bytesAvailable() == 0 && !body_->isSequential())) { |
| | 0 | 284 | | dispatch(); |
| | 0 | 285 | | } |
| | 0 | 286 | | }); |
| | 0 | 287 | | QObject::connect(body_, &QIODevice::readChannelFinished, this, [this]() { |
| | 0 | 288 | | if (dispatched_ || body_ == nullptr) { |
| | 0 | 289 | | return; |
| | 0 | 290 | | } |
| | 0 | 291 | | bodyData_ += body_->readAll(); |
| | 0 | 292 | | dispatch(); |
| | 0 | 293 | | }); |
| | | 294 | | // Safety net: never leave a request unanswered. |
| | 0 | 295 | | QMetaObject::invokeMethod(this, [this]() { |
| | 0 | 296 | | if (!dispatched_ && bodyData_.isEmpty()) { |
| | 0 | 297 | | dispatch(); |
| | 0 | 298 | | } |
| | 0 | 299 | | }, Qt::QueuedConnection); |
| | 0 | 300 | | } |
| | | 301 | | |
| | | 302 | | private: |
| | 1323 | 303 | | void dispatch() { |
| | 1323 | 304 | | if (dispatched_) { |
| | 0 | 305 | | return; |
| | 0 | 306 | | } |
| | 1323 | 307 | | dispatched_ = true; |
| | 1323 | 308 | | if (!job_) { |
| | 0 | 309 | | return; |
| | 0 | 310 | | } |
| | | 311 | | |
| | 1323 | 312 | | const std::uint64_t request_id = nextRequestId(); |
| | 1323 | 313 | | const QByteArray urlBytes = url_.toString(QUrl::FullyEncoded).toUtf8(); |
| | 1323 | 314 | | const ::QtSchemeResponse response = qt_scheme_request( |
| | 1323 | 315 | | webview_id_, |
| | 1323 | 316 | | request_id, |
| | 1323 | 317 | | kind_, |
| | 1323 | 318 | | rust::Str(method_.constData(), static_cast<std::size_t>(method_.size())), |
| | 1323 | 319 | | rust::Str(urlBytes.constData(), static_cast<std::size_t>(urlBytes.size())), |
| | 1323 | 320 | | rust::Str(headers_.constData(), static_cast<std::size_t>(headers_.size())), |
| | 1323 | 321 | | rust::Str(bodyData_.constData(), static_cast<std::size_t>(bodyData_.size()))); |
| | | 322 | | |
| | 1323 | 323 | | if (response.handled == 2) { |
| | | 324 | | // Tauri answered asynchronously: keep the job alive and wait for a |
| | | 325 | | // later `qt_scheme_respond(request_id, ...)`. Drop the entry if the |
| | | 326 | | // job is destroyed first (aborted request). |
| | 21 | 327 | | PendingRequest pending; |
| | 21 | 328 | | pending.job = job_; |
| | 21 | 329 | | pending.webview_id = webview_id_; |
| | 21 | 330 | | pendingRequests().insert(request_id, pending); |
| | 21 | 331 | | QObject::connect(job_, &QObject::destroyed, dispatchContext(), |
| | 21 | 332 | | [request_id]() { pendingRequests().remove(request_id); }); |
| | 21 | 333 | | return; |
| | 21 | 334 | | } |
| | 1302 | 335 | | if (response.handled != 1) { |
| | 0 | 336 | | job_->fail(QWebEngineUrlRequestJob::UrlNotFound); |
| | 0 | 337 | | return; |
| | 0 | 338 | | } |
| | 1302 | 339 | | applyResponse(job_, response.status, |
| | 1302 | 340 | | QByteArray(response.headers.data(), |
| | 1302 | 341 | | static_cast<int>(response.headers.size())), |
| | 1302 | 342 | | QByteArray(response.body.data(), static_cast<int>(response.body.size()))); |
| | 1302 | 343 | | } |
| | | 344 | | |
| | | 345 | | QPointer<QWebEngineUrlRequestJob> job_; |
| | | 346 | | std::int32_t kind_; |
| | | 347 | | std::uint64_t webview_id_; |
| | | 348 | | QByteArray method_; |
| | | 349 | | QUrl url_; |
| | | 350 | | QByteArray headers_; |
| | | 351 | | QByteArray bodyData_; |
| | | 352 | | QIODevice* body_ = nullptr; // owned by the job |
| | | 353 | | bool dispatched_ = false; |
| | | 354 | | }; |
| | | 355 | | |
| | 1323 | 356 | | void TauriSchemeHandler::requestStarted(QWebEngineUrlRequestJob* job) { |
| | 1323 | 357 | | if (job == nullptr) { |
| | 0 | 358 | | return; |
| | 0 | 359 | | } |
| | 1323 | 360 | | (void)dispatchContext(); // created here, on the GUI thread |
| | 1323 | 361 | | (new JobContext(job, kind_, webview_id_))->start(); |
| | 1323 | 362 | | } |
| | | 363 | | |
| | 42 | 364 | | void installOnProfile(QWebEngineProfile* profile, std::uint64_t webview_id) { |
| | 42 | 365 | | if (profile == nullptr) { |
| | 0 | 366 | | return; |
| | 0 | 367 | | } |
| | 42 | 368 | | if (profile->urlSchemeHandler(QByteArrayLiteral("ipc")) != nullptr) { |
| | 21 | 369 | | return; // already installed for this profile |
| | 21 | 370 | | } |
| | 21 | 371 | | profile->installUrlSchemeHandler(QByteArrayLiteral("ipc"), |
| | 21 | 372 | | new TauriSchemeHandler(SchemeIpc, webview_id, profile)); |
| | 21 | 373 | | profile->installUrlSchemeHandler(QByteArrayLiteral("tauri"), |
| | 21 | 374 | | new TauriSchemeHandler(SchemeTauri, webview_id, profile)); |
| | 21 | 375 | | profile->installUrlSchemeHandler(QByteArrayLiteral("asset"), |
| | 21 | 376 | | new TauriSchemeHandler(SchemeAsset, webview_id, profile)); |
| | 21 | 377 | | } |
| | | 378 | | |
| | | 379 | | } // namespace |
| | | 380 | | |
| | 21 | 381 | | void qt_scheme_register_schemes() { |
| | 21 | 382 | | static bool registered = false; |
| | 21 | 383 | | if (registered) { |
| | 0 | 384 | | return; |
| | 0 | 385 | | } |
| | 21 | 386 | | registered = true; |
| | | 387 | | |
| | | 388 | | // Tauri's URLs are `{scheme}://localhost/{path}`: host present, no port, so |
| | | 389 | | // Syntax::Host with PortUnspecified is the exact fit. SecureScheme makes the |
| | | 390 | | // origin trustworthy, CorsEnabled enables cross-origin loads from the page, |
| | | 391 | | // and FetchApiAllowed is required for `fetch()`/XHR with a body. |
| | 21 | 392 | | const QByteArray schemes[] = { |
| | 21 | 393 | | QByteArrayLiteral("ipc"), |
| | 21 | 394 | | QByteArrayLiteral("tauri"), |
| | 21 | 395 | | QByteArrayLiteral("asset"), |
| | 21 | 396 | | }; |
| | 63 | 397 | | for (const QByteArray& name : schemes) { |
| | 63 | 398 | | QWebEngineUrlScheme scheme(name); |
| | 63 | 399 | | scheme.setSyntax(QWebEngineUrlScheme::Syntax::Host); |
| | 63 | 400 | | scheme.setDefaultPort(QWebEngineUrlScheme::PortUnspecified); |
| | 63 | 401 | | scheme.setFlags(QWebEngineUrlScheme::SecureScheme |
| | 63 | 402 | | | QWebEngineUrlScheme::CorsEnabled |
| | 63 | 403 | | | QWebEngineUrlScheme::FetchApiAllowed); |
| | 63 | 404 | | QWebEngineUrlScheme::registerScheme(scheme); |
| | 63 | 405 | | } |
| | 21 | 406 | | } |
| | | 407 | | |
| | 21 | 408 | | void qt_scheme_bind_webview(std::uint64_t webview_id, QWebEngineView* view) { |
| | 21 | 409 | | webviewRegistry().insert(webview_id, QPointer<QWebEngineView>(view)); |
| | 21 | 410 | | } |
| | | 411 | | |
| | 42 | 412 | | void qt_scheme_install_handler(std::uint64_t webview_id) { |
| | 42 | 413 | | const auto it = webviewRegistry().constFind(webview_id); |
| | 42 | 414 | | QWebEngineView* view = (it != webviewRegistry().constEnd()) ? it.value().data() : nullptr; |
| | 42 | 415 | | if (view == nullptr) { |
| | 0 | 416 | | qWarning("tauri-runtime-qt: qt_scheme_install_handler(%llu): webview not bound; " |
| | 0 | 417 | | "call qt_scheme_bind_webview() after creating the view", |
| | 0 | 418 | | static_cast<unsigned long long>(webview_id)); |
| | 0 | 419 | | return; |
| | 0 | 420 | | } |
| | 42 | 421 | | QWebEnginePage* page = view->page(); |
| | 42 | 422 | | installOnProfile(page != nullptr ? page->profile() : nullptr, webview_id); |
| | 42 | 423 | | } |
| | | 424 | | |
| | | 425 | | void qt_scheme_respond(std::uint64_t request_id, std::int32_t status, |
| | 21 | 426 | | rust::Str headers, rust::Str body) { |
| | | 427 | | // Copy out of the Rust-borrowed strings before marshalling (the borrowed |
| | | 428 | | // memory is not valid once this call returns). |
| | 21 | 429 | | const QByteArray rawHeaders(headers.data(), static_cast<int>(headers.size())); |
| | 21 | 430 | | const QByteArray rawBody(body.data(), static_cast<int>(body.size())); |
| | | 431 | | |
| | | 432 | | // The responder may fire on any thread; always deliver on the GUI thread, |
| | | 433 | | // where the job and its reply device live. |
| | 21 | 434 | | QMetaObject::invokeMethod( |
| | 21 | 435 | | dispatchContext(), |
| | 21 | 436 | | [request_id, status, rawHeaders, rawBody]() { |
| | 21 | 437 | | const auto it = pendingRequests().find(request_id); |
| | 21 | 438 | | if (it == pendingRequests().end()) { |
| | 0 | 439 | | return; // already answered, or the job was destroyed |
| | 0 | 440 | | } |
| | 21 | 441 | | QWebEngineUrlRequestJob* job = it.value().job.data(); |
| | 21 | 442 | | pendingRequests().erase(it); |
| | 21 | 443 | | if (job == nullptr) { |
| | 0 | 444 | | return; |
| | 0 | 445 | | } |
| | 21 | 446 | | applyResponse(job, status, rawHeaders, rawBody); |
| | 21 | 447 | | }, |
| | 21 | 448 | | Qt::QueuedConnection); |
| | 21 | 449 | | } |