Support same_origin in webapps.

This commit is contained in:
John Preston
2026-05-28 13:04:04 +04:00
parent 7e0a5e7a80
commit d7ea6ed174
7 changed files with 145 additions and 40 deletions
@@ -79,6 +79,7 @@ enum class NativeMessageSource {
struct NativeMessage {
NativeMessageSource source = NativeMessageSource::LegacyWebApp;
QString origin;
QString command;
QJsonObject arguments;
};
@@ -115,6 +116,48 @@ struct NativeMessage {
&& url.fragment().isEmpty();
}
[[nodiscard]] int EffectivePort(const QUrl &url) {
const auto explicitPort = url.port(-1);
if (explicitPort >= 0) {
return explicitPort;
}
const auto scheme = url.scheme().toLower();
if (scheme == u"http"_q) {
return 80;
} else if (scheme == u"https"_q) {
return 443;
}
return -1;
}
[[nodiscard]] QString OriginFromUrl(const QString &url) {
const auto parsed = QUrl(url);
if (!parsed.isValid()) {
return {};
}
const auto scheme = parsed.scheme().toLower();
auto host = parsed.host().toLower();
const auto port = EffectivePort(parsed);
if (scheme.isEmpty() || host.isEmpty() || port < 0) {
return {};
}
if (host.contains(':') && !host.startsWith('[')) {
host = u"["_q + host + u"]"_q;
}
return u"%1://%2:%3"_q.arg(scheme, host, QString::number(port));
}
[[nodiscard]] bool OriginsMatch(const QString &a, const QString &b) {
if (a.isEmpty() || b.isEmpty()) {
return false;
}
const auto normalizedA = OriginFromUrl(a);
const auto normalizedB = OriginFromUrl(b);
return !normalizedA.isEmpty()
&& !normalizedB.isEmpty()
&& normalizedA == normalizedB;
}
[[nodiscard]] RectPart ParsePosition(const QString &position) {
if (position == u"left"_q) {
return RectPart::Left;
@@ -203,6 +246,7 @@ void LogNativeMessageRejected(
[[nodiscard]] std::optional<NativeMessage> ParseNativeMessage(
const QByteArray &bytes,
const QString &sourceUrl,
bool externalShell,
const QString &shellToken) {
const auto byteCount = quint64(bytes.size());
@@ -248,8 +292,8 @@ void LogNativeMessageRejected(
|| token.toString() != shellToken) {
return reject(u"bad external token"_q);
}
const auto origin = object.value(u"origin"_q);
if (source == NativeMessageSource::ExternalShell) {
const auto origin = object.value(u"origin"_q);
if (!origin.isString()
|| !IsExternalShellOrigin(origin.toString())) {
return reject(u"bad shell origin"_q);
@@ -273,6 +317,9 @@ void LogNativeMessageRejected(
}
return NativeMessage{
.source = source,
.origin = (source == NativeMessageSource::ExternalWebApp)
? origin.toString()
: QString(),
.command = command,
.arguments = arguments,
};
@@ -306,6 +353,7 @@ void LogNativeMessageRejected(
}
return NativeMessage{
.source = NativeMessageSource::LegacyWebApp,
.origin = OriginFromUrl(sourceUrl),
.command = command,
.arguments = arguments,
};
@@ -1137,7 +1185,8 @@ Panel::Panel(Args &&args)
? std::make_unique<StandaloneLayerStack>()
: nullptr)
, _fullscreen(args.fullscreen)
, _allowClipboardRead(args.allowClipboardRead) {
, _allowClipboardRead(args.allowClipboardRead)
, _sameOrigin(args.sameOrigin) {
if (_externalShell) {
_widget->setAttribute(Qt::WA_DontShowOnScreen);
_externalLayer->boxAdded(
@@ -1501,6 +1550,9 @@ void Panel::hideWebviewProgress() {
bool Panel::showWebview(Args &&args, const Webview::ThemeParams &params) {
_bottomText = std::move(args.bottom);
_externalUrl = args.url;
_sameOrigin = args.sameOrigin;
_initialOrigin = OriginFromUrl(args.url);
_currentOrigin = _initialOrigin;
if (_externalShell && !_webview) {
resetExternalShellIdentity();
}
@@ -1661,6 +1713,8 @@ void Panel::sendExternalShellBootstrap() {
const auto params = _delegate->botThemeParams();
sendExternalShellMethod("bootstrap", {
{ u"url"_q, _externalUrl },
{ u"sameOrigin"_q, _sameOrigin },
{ u"initialOrigin"_q, _initialOrigin },
{ u"title"_q, _externalTitle },
{ u"metrics"_q, LinuxShell::Metrics() },
{ u"colors"_q, LinuxShell::ColorPayload(externalShellColors(params)) },
@@ -2189,23 +2243,33 @@ bool Panel::createWebview(const Webview::ThemeParams &params) {
}, _webview->lifetime);
}
raw->setMessageHandler([=](std::string text) {
if (text.size() > size_t(kMaxNativeMessageBytes)) {
raw->setMessageHandler([=](Webview::Message message) {
if (message.text.size() > size_t(kMaxNativeMessageBytes)) {
LogNativeMessageRejected(
u"payload too large"_q,
quint64(text.size()));
quint64(message.text.size()));
return;
}
const auto bytes = QByteArray::fromRawData(
text.data(),
int(text.size()));
message.text.data(),
int(message.text.size()));
const auto parsed = ParseNativeMessage(
bytes,
QString::fromStdString(message.sourceUrl),
_externalShell,
_externalShellToken);
if (!parsed) {
return;
}
if (_sameOrigin
&& parsed->source != NativeMessageSource::ExternalShell
&& !OriginsMatch(parsed->origin, _initialOrigin)) {
LogNativeMessageRejected(
u"bad webapp origin"_q,
quint64(message.text.size()),
parsed->command);
return;
}
const auto &command = parsed->command;
const auto &arguments = parsed->arguments;
if (parsed->source == NativeMessageSource::ExternalShell) {
@@ -2372,6 +2436,7 @@ bool Panel::createWebview(const Webview::ThemeParams &params) {
} else if (newWindow) {
return true;
}
_currentOrigin = OriginFromUrl(uri);
showWebviewProgress();
return true;
});
@@ -3644,6 +3709,9 @@ void Panel::postEvent(const QString &event, EventData data) {
}
return;
}
if (_sameOrigin && !OriginsMatch(_currentOrigin, _initialOrigin)) {
return;
}
auto written = v::is<QString>(data)
? v::get<QString>(data).toUtf8()
: QJsonDocument(