T-469: review round 24 (the inspector, for Codex): webOS's store gives roots only, the CA question only for a page
Codex's owed review of Tier 78, round 22 and Tier 83, run by the Cavekit inspector (the owner).
Tier 83 (REQUEST CHANGES):
- P1: Trust Always puts a site's own certificate in Certificate Manager's store, and a router's or
NAS box's (openssl req -x509) also says cA=TRUE, so R4 would have made it a root for every site
at the next start. R4 now takes only roots: self-issued, keyUsage allowing keyCertSign, no
subjectAltName or extendedKeyUsage. The daemon tells the component (jihad-platform-cert) about
every certificate its SSL dialog installs, and R4 never takes one (its SHA-256 kept in
jihad.security.webos_certs.accepted). An intermediate is no longer taken either.
- P2: a hidden blob: frame reached the engine's CA import and raised the question over the page.
The question now needs a one-use offer made by a CA certificate opened as the page itself (HTTP
in _caLoad, data:/file: in the content policy), or it refuses without a word.
- P3: only frames (subdocument, object) are cancelled, not a script's fetch; the question also
refuses a site's certificate that says cA=TRUE; the proxy-login marker is "[proxy] ", which no
site's host can produce; webOS's proxy setting keeps its last plan (jihad.proxy.webos.last) when a
read fails instead of going direct; card-selftest marks hook lines at or after the screenshot's
second, across midnight. Matching removed roots by Subject+SPKI was tried and dropped: Mozilla
removed old issues of roots it still trusts with the same name and key (GTS R1-R4, GlobalSign,
Firmaprofesional).
Round 22 (APPROVE): card-picture updates have their own observer topic (a Persona id of "thumbs"
was ambiguous); the cost of making them on the engine's thread is written down.
Tier 78 (APPROVE): the gallery offers the bundled set too, so one removed there stays removed;
link-style buttons and the toast's Undo are 44 px tall; the Personas test server reads Carbon
Light's picture from its package.
Device (CE 3.1.0, all three on daemon 06e75a68): R4 trusted the same 5 roots; a router certificate
with an IP SAN skipped as siteLike, one without keyUsage as noCertSign, a CA with keyCertSign
trusted, then removed; Trust Once logged the site certificate as never to be taken and the session
serial was swept; a blob: CA frame refused with no dialog; the test CA link still asked ("Offered by
192.168.1.245:8896"); a router certificate link refused; proxy logins read "The proxy ..." (Enyo,
Mochi) and "[proxy] ..." (Mojo's sheet).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LcrVQFRMC2ANGDfVaRG9Ed
This commit is contained in:
1 parent
cb7bd40cd4
commit
cfbfbcf293
17 files changed
+279
-105
No files matched your search
@@ -116,9 +116,9 @@ enyo.kind({
|
||||
this.$.prompt.show();
|
||||
},
|
||||
showLogin: function(msg) {
|
||||
// A proxy's login (a 407) comes as "proxy <host:port> - <realm>" (cavekit-proxies-certs.md R5).
|
||||
var proxy = /^proxy /.test(String(msg || ""));
|
||||
this.$.loginMsg.setContent(msg ? ((proxy ? "The proxy " + String(msg).slice(6) : "The server " + msg) +
|
||||
// A proxy's login (a 407) comes as "[proxy] <host:port> - <realm>" (cavekit-proxies-certs.md R5).
|
||||
var proxy = /^\[proxy\] /.test(String(msg || ""));
|
||||
this.$.loginMsg.setContent(msg ? ((proxy ? "The proxy " + String(msg).slice(8) : "The server " + msg) +
|
||||
" requires a username and password") : "Authentication required");
|
||||
this.$.userInput.setValue("");
|
||||
this.$.passInput.setValue("");
|
||||
|
||||
@@ -358,11 +358,11 @@ enyo.kind({
|
||||
},
|
||||
showUserPasswordDialog: function(inSender, inMsg) {
|
||||
this.$.loginDialog.validateComponents();
|
||||
// A proxy's login (a 407) comes as "proxy <host:port> - <realm>" (cavekit-proxies-certs.md R5).
|
||||
var proxy = /^proxy /.test(String(inMsg || ""));
|
||||
// A proxy's login (a 407) comes as "[proxy] <host:port> - <realm>" (cavekit-proxies-certs.md R5).
|
||||
var proxy = /^\[proxy\] /.test(String(inMsg || ""));
|
||||
var msg = proxy ? $L("The proxy {$serverName} requires a username and password")
|
||||
: $L("The server {$serverName} requires a username and password");
|
||||
msg = enyo.macroize(msg, {serverName: proxy ? String(inMsg).slice(6) : inMsg});
|
||||
msg = enyo.macroize(msg, {serverName: proxy ? String(inMsg).slice(8) : inMsg});
|
||||
this.$.loginMessage.setContent(msg);
|
||||
this.showPopup(this.$.loginDialog);
|
||||
},
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
# card-selftest.sh [mochi|enyo|mojo] run <scenario[:arg]> [target-url] [seconds] [shot.jpg]
|
||||
# close the card, launch it on target-url with that
|
||||
# scenario, wait, print the "[JIHAD-TEST]" lines (UTC
|
||||
# times; those logged after the screenshot are marked)
|
||||
# times; those logged at or after the screenshot are marked)
|
||||
# and save a screenshot (portrait, already rotated); a
|
||||
# target-url of "-" launches with none (the start page).
|
||||
# The screenshot is taken `seconds` after the launch,
|
||||
@@ -86,9 +86,10 @@ SHOT_AT=\$(date -u +%H:%M:%S)
|
||||
luna-send -n 1 palm://com.palm.systemmanager/takeScreenShot '{"file":"/tmp/st.png"}' >/dev/null 2>&1; sleep 2
|
||||
# /var/log/messages rotates within seconds on this device, so an offset taken before the launch
|
||||
# is not reliable: take every line of THIS scenario from both generations and keep what follows
|
||||
# its last "start". Each line keeps its UTC time, and a line logged after the screenshot says so:
|
||||
# a picture taken before a step ran is not that step's result (T-465 was such a picture).
|
||||
( zcat /var/log/messages.1.gz /var/log/messages.0.gz 2>/dev/null; cat /var/log/messages.0 /var/log/messages 2>/dev/null ) | grep 'JIHAD-TEST\] $SC: ' | sed 's/^[0-9-]*T\([0-9:]*\)[^ ]* .*\(JIHAD-[A-Z]*\]\)/\1 \2/; s/, file:.*//' | awk -v shot="\$SHOT_AT" '/: start\$/{buf=""} {m=""; if (substr(\$0,1,8) > shot) m="(after the screenshot) "; buf=buf m \$0 "\n"} END{printf "%s", buf}' | cut -c1-900
|
||||
# its last "start". Each line keeps its UTC time, and a line logged in the screenshot's second or
|
||||
# later says so (the capture itself takes about 2 s; midnight is allowed for): a picture taken
|
||||
# before a step ran is not that step's result (T-465 was such a picture).
|
||||
( zcat /var/log/messages.1.gz /var/log/messages.0.gz 2>/dev/null; cat /var/log/messages.0 /var/log/messages 2>/dev/null ) | grep 'JIHAD-TEST\] $SC: ' | sed 's/^[0-9-]*T\([0-9:]*\)[^ ]* .*\(JIHAD-[A-Z]*\]\)/\1 \2/; s/, file:.*//' | awk -v shot="\$SHOT_AT" 'function sod(t) { return substr(t,1,2)*3600 + substr(t,4,2)*60 + substr(t,7,2) } BEGIN{s=sod(shot)} /: start\$/{buf=""} {m=""; t=sod(\$0); if (t < s - 43200) t += 86400; if (t > s + 43200) t -= 86400; if (t >= s) m="(at or after the screenshot) "; buf=buf m \$0 "\n"} END{printf "%s", buf}' | cut -c1-900
|
||||
echo "JIHAD-SHOT] screenshot taken at \$SHOT_AT UTC"
|
||||
DEVEOF
|
||||
put "$TMP/sf.sh" /tmp/mochi-sf.sh
|
||||
|
||||
@@ -227,11 +227,13 @@ menulist .menulist-dropmarker .dropmarker-icon {
|
||||
border-top: 7px solid currentColor;
|
||||
}
|
||||
|
||||
/* A link look, but finger-sized: "More", "Undo" and "Restart now" are buttons of this class, and
|
||||
"More" is the way into an add-on's page (inspector round 24). */
|
||||
button.button-link {
|
||||
-moz-appearance: none;
|
||||
min-height: 0;
|
||||
min-height: 44px;
|
||||
margin: 0 4px;
|
||||
padding: 0;
|
||||
padding: 0 8px;
|
||||
color: #8cc7ff;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
|
||||
@@ -74,5 +74,5 @@
|
||||
#files button.file { display: block; width: 100%; margin: 0 0 8px 0; text-align: left; }
|
||||
.status { display: flex; align-items: center; }
|
||||
.status #status-text { flex: 1; }
|
||||
.status #status-undo { flex: 0 0 auto; margin-left: 12px; min-height: 40px; }
|
||||
.status #status-undo { flex: 0 0 auto; margin-left: 12px; min-height: 44px; }
|
||||
.status[hidden] { display: none !important; }
|
||||
@@ -211,9 +211,13 @@ function setupMine() {
|
||||
box.hidden = !box.hidden;
|
||||
}, false);
|
||||
// A removal finishes in the add-on manager a moment later; card pictures are made in the background.
|
||||
let watcher = {observe: function (subject, topic, data) { if (data === "thumbs") { updatePictures(); } else { afterChange(); } }};
|
||||
let watcher = {observe: function (subject, topic) { if (topic === "jihad-personas-thumbs") { updatePictures(); } else { afterChange(); } }};
|
||||
Services.obs.addObserver(watcher, "jihad-personas-mine", false);
|
||||
window.addEventListener("unload", function () { Services.obs.removeObserver(watcher, "jihad-personas-mine"); }, false);
|
||||
Services.obs.addObserver(watcher, "jihad-personas-thumbs", false);
|
||||
window.addEventListener("unload", function () {
|
||||
Services.obs.removeObserver(watcher, "jihad-personas-mine");
|
||||
Services.obs.removeObserver(watcher, "jihad-personas-thumbs");
|
||||
}, false);
|
||||
}
|
||||
|
||||
// The Personas that come with Jihad and are not installed (one removed can be had again here); the
|
||||
|
||||
@@ -1070,7 +1070,7 @@ const PROXY_PREFS = ["network.proxy.type", "network.proxy.http", "network.proxy.
|
||||
"network.proxy.ssl_port", "network.proxy.socks", "network.proxy.socks_port", "network.proxy.socks_version",
|
||||
"network.proxy.socks_remote_dns", "network.proxy.no_proxies_on", "network.proxy.autoconfig_url",
|
||||
"network.proxy.share_proxy_settings", "jihad.proxy.mode", "jihad.proxy.webone.host", "jihad.proxy.webone.port",
|
||||
"jihad.proxy.relay.trust_ca"];
|
||||
"jihad.proxy.relay.trust_ca", "jihad.proxy.webos.last"];
|
||||
const PROXY_HOST_RE = /^(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$|^\d{1,3}(?:\.\d{1,3}){3}$|^[0-9A-Fa-f]*:[0-9A-Fa-f:.]+$/;
|
||||
|
||||
function pInt(name, dflt) { try { return Services.prefs.getIntPref(name); } catch (e) { return dflt; } }
|
||||
@@ -1452,7 +1452,12 @@ function buildCertsSection(group) {
|
||||
if (k.stock) { parts.push(k.stock + " of HP's original roots"); }
|
||||
if (k.removed) { parts.push(k.removed + " Mozilla removed"); }
|
||||
if (k.expired) { parts.push(k.expired + " expired"); }
|
||||
if (k.notCA) { parts.push(k.notCA + " site certificates (Jihad asks about those per site)"); }
|
||||
let sites = (k.notCA || 0) + (k.siteLike || 0) + (k.accepted || 0);
|
||||
if (sites) { parts.push(sites + " site certificates (Jihad asks about those per site)"); }
|
||||
if (k.notRoot) { parts.push(k.notRoot + " intermediate certificates (only roots are taken)"); }
|
||||
if (k.noCertSign) {
|
||||
parts.push(k.noCertSign + " that do not say they sign certificates (to add such a CA, open its file in Jihad)");
|
||||
}
|
||||
if (k.already) { parts.push(k.already + " you added here as well"); }
|
||||
if (k.relay) { parts.push("the Riverstone Relay's (see Advanced → Proxy)"); }
|
||||
if (k.unreadable) { parts.push(k.unreadable + " unreadable files"); }
|
||||
|
||||
@@ -2000,11 +2000,16 @@ std::string BrowserPageGoanna::emitLocationAndTitle() {
|
||||
// Write-through to the platform store so the rest of the device sees
|
||||
// the same decision. Absent or failing store = Goanna-only trust; it
|
||||
// must never turn an accepted certificate into a failed load.
|
||||
if (!certFile.empty()) {
|
||||
jihad::certstore::Install(certFile.c_str(),
|
||||
permanent ? jihad::certstore::kTrustPermanent
|
||||
: jihad::certstore::kTrustSession,
|
||||
this);
|
||||
if (!certFile.empty() &&
|
||||
jihad::certstore::Install(certFile.c_str(),
|
||||
permanent ? jihad::certstore::kTrustPermanent
|
||||
: jihad::certstore::kTrustSession,
|
||||
this)) {
|
||||
// The store now holds a site's own certificate, where webOS's other trusted ones
|
||||
// are. components/jihadCerts.js reads that store for CAs (cavekit-proxies-certs R4)
|
||||
// and must never take this one as a root (the inspector, round 24).
|
||||
jihad::NotifyChromeGlobal("jihad-platform-cert",
|
||||
jihad::certstore::PemOf(der.data(), der.size()).c_str());
|
||||
}
|
||||
// Retry the load that FAILED, not the current document. CurrentUri() is the last
|
||||
// page that committed — when the certificate error came from a navigation away from
|
||||
|
||||
@@ -197,14 +197,16 @@ JihadPrompter::PromptAuth(nsIChannel* aChannel, uint32_t, nsIAuthInformation* aA
|
||||
if (flags & nsIAuthInformation::AUTH_PROXY) {
|
||||
// A 407: the login is the proxy's, not the site's, and the channel's URI is the site
|
||||
// (cavekit-proxies-certs.md R5). Name the proxy, so a site never seems to ask for it. The
|
||||
// text starts "proxy ": the Enyo and Mochi shells then say "The proxy … requires a username
|
||||
// and password" instead of "The server …"; Mojo's dialog is the system framework's own.
|
||||
// text starts "[proxy] ", which no site's host:port can (a site named "proxy" could pass for
|
||||
// "proxy "; the inspector, round 24): the Enyo and Mochi shells then say "The proxy …
|
||||
// requires a username and password" instead of "The server …"; Mojo's dialog is the system
|
||||
// framework's own and shows the text as it is.
|
||||
nsCOMPtr<nsIProxiedChannel> proxied = do_QueryInterface(aChannel);
|
||||
nsCOMPtr<nsIProxyInfo> pi;
|
||||
if (proxied) proxied->GetProxyInfo(getter_AddRefs(pi));
|
||||
nsAutoCString phost; int32_t pport = -1;
|
||||
if (pi) { pi->GetHost(phost); pi->GetPort(&pport); }
|
||||
text.AppendLiteral("proxy ");
|
||||
text.AppendLiteral("[proxy] ");
|
||||
if (!phost.IsEmpty()) {
|
||||
hostPort = phost;
|
||||
if (pport > 0) { hostPort.Append(':'); hostPort.AppendInt(pport); }
|
||||
|
||||
@@ -231,6 +231,11 @@ void ForgetCertPem(const std::string& path) {
|
||||
if (!path.empty()) unlink(path.c_str());
|
||||
}
|
||||
|
||||
std::string PemOf(const void* der, size_t derLen) {
|
||||
if (!der || !derLen) return std::string();
|
||||
return armourPem(static_cast<const unsigned char*>(der), derLen);
|
||||
}
|
||||
|
||||
bool Install(const char* pemPath, Trust trust, const void* owner) {
|
||||
if (!pemPath || !*pemPath) return false;
|
||||
if (!loadApi()) return false;
|
||||
|
||||
@@ -65,6 +65,9 @@ std::string WriteCertPem(const void* der, size_t derLen);
|
||||
// Delete a PEM written by WriteCertPem (no-op for an empty path).
|
||||
void ForgetCertPem(const std::string& path);
|
||||
|
||||
// `der` armoured as PEM text, as WriteCertPem writes it ("" for no bytes).
|
||||
std::string PemOf(const void* der, size_t derLen);
|
||||
|
||||
// What is wrong with the certificate, as the engine can describe it. `haveFlags`
|
||||
// false means nothing but the nsresult is known.
|
||||
struct Problem {
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
* (or webOS CE) brought, and the CAs a user trusted in Certificate Manager. They are
|
||||
* NSS temporary certificates: nothing is written to the profile, the stores are read again
|
||||
* at every start, and the switch "jihad.security.webos_certs" takes the trust away at once.
|
||||
* A site's own certificate found there is not taken: the engine never uses one as a root.
|
||||
* Only roots are taken (rootShape): never a site's own certificate, which Trust Always puts
|
||||
* in the same store, and never one Jihad's own SSL dialog put there.
|
||||
*
|
||||
* R2 The Riverstone Relay's CA, carried here and pinned by SHA-256. Trusted, the same way, only
|
||||
* while "jihad.proxy.relay.trust_ca" is on AND the Relay is the proxy in use. Found in a
|
||||
@@ -40,6 +41,10 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
const PREF_WEBOS = "jihad.security.webos_certs";
|
||||
// The SHA-256s of the sites' own certificates Jihad's SSL dialog put into webOS's store (Trust
|
||||
// Always and Trust Once install there too): R4 never takes one. Kept to the last MAX_ACCEPTED.
|
||||
const PREF_ACCEPTED = "jihad.security.webos_certs.accepted";
|
||||
const MAX_ACCEPTED = 200;
|
||||
const PREF_RELAY_CA = "jihad.proxy.relay.trust_ca";
|
||||
const RELAY_HOST = "proxy.riverstonerelay.org";
|
||||
// The Riverstone Relay's CA (certs/riverstone-relay-ca.pem), pinned HERE, not only in the lists, so
|
||||
@@ -146,12 +151,19 @@ function certsIn(file) {
|
||||
return out;
|
||||
}
|
||||
|
||||
// Is this certificate (its DER bytes) a v3 certificate whose basicConstraints says cA=TRUE?
|
||||
// The engine's own test (CERT_IsCACert) also counts any self-signed v1 certificate as a CA, and
|
||||
// mozilla::pkix accepts a v1 trust anchor, so a router's or a NAS box's own v1 certificate that a
|
||||
// user once accepted in webOS would become a root able to vouch for any site (the inspector,
|
||||
// 2026-10-13). Only the certificate's own word that it is a CA counts here.
|
||||
function derIsV3CA(b) {
|
||||
// What a certificate (its DER bytes) says about itself:
|
||||
// ca a v3 certificate whose basicConstraints says cA=TRUE. The engine's own test
|
||||
// (CERT_IsCACert) also counts any self-signed v1 certificate as a CA, and mozilla::pkix
|
||||
// accepts a v1 trust anchor, so only the certificate's own word counts (the inspector,
|
||||
// 2026-10-13).
|
||||
// keyCertSign keyUsage is present and allows signing certificates.
|
||||
// san, eku it names sites (subjectAltName) or limits its key to uses (extendedKeyUsage):
|
||||
// a site's own certificate does; a root does not (the CA/Browser Forum's rules).
|
||||
// self its issuer and subject are the same name: a root, not an intermediate.
|
||||
// Malformed or v1/v2 DER says nothing: every fact false.
|
||||
function derFacts(b) {
|
||||
const NONE = {ca: false, keyCertSign: false, san: false, eku: false, self: false};
|
||||
let f = {ca: false, keyCertSign: false, san: false, eku: false, self: false};
|
||||
function tlv(i) {
|
||||
if (i + 2 > b.length) { return null; }
|
||||
let tag = b[i], len = b[i + 1], p = i + 2;
|
||||
@@ -164,42 +176,76 @@ function derIsV3CA(b) {
|
||||
if (p + len > b.length) { return null; }
|
||||
return {tag: tag, start: p, end: p + len};
|
||||
}
|
||||
function basicConstraintsCA(exts) {
|
||||
let seq = tlv(exts.start);
|
||||
if (!seq || seq.tag !== 0x30) { return false; }
|
||||
for (let j = seq.start; j < seq.end;) {
|
||||
let ext = tlv(j);
|
||||
if (!ext || ext.tag !== 0x30) { return false; }
|
||||
let oid = tlv(ext.start);
|
||||
if (!oid || oid.tag !== 0x06) { return false; }
|
||||
if (oid.end - oid.start === 3 && b[oid.start] === 0x55 && b[oid.start + 1] === 0x1d && b[oid.start + 2] === 0x13) {
|
||||
let v = tlv(oid.end);
|
||||
if (v && v.tag === 0x01) { v = tlv(v.end); } // critical
|
||||
if (!v || v.tag !== 0x04) { return false; } // extnValue
|
||||
let bc = tlv(v.start);
|
||||
if (!bc || bc.tag !== 0x30 || bc.start === bc.end) { return false; } // cA absent: FALSE
|
||||
let ca = tlv(bc.start);
|
||||
return !!ca && ca.tag === 0x01 && ca.end - ca.start === 1 && b[ca.start] !== 0;
|
||||
}
|
||||
j = ext.end;
|
||||
}
|
||||
return false;
|
||||
function extIs(oid, last) { // 2.5.29.<last>: 55 1d <last>
|
||||
return oid.end - oid.start === 3 && b[oid.start] === 0x55 && b[oid.start + 1] === 0x1d && b[oid.start + 2] === last;
|
||||
}
|
||||
let cert = tlv(0);
|
||||
if (!cert || cert.tag !== 0x30) { return false; }
|
||||
if (!cert || cert.tag !== 0x30) { return NONE; }
|
||||
let tbs = tlv(cert.start);
|
||||
if (!tbs || tbs.tag !== 0x30) { return false; }
|
||||
if (!tbs || tbs.tag !== 0x30) { return NONE; }
|
||||
let ver = tlv(tbs.start);
|
||||
if (!ver || ver.tag !== 0xa0) { return false; } // v1: no version field
|
||||
if (!ver || ver.tag !== 0xa0) { return NONE; } // v1: no version field
|
||||
let n = tlv(ver.start);
|
||||
if (!n || n.tag !== 0x02 || n.end - n.start !== 1 || b[n.start] !== 2) { return false; }
|
||||
if (!n || n.tag !== 0x02 || n.end - n.start !== 1 || b[n.start] !== 2) { return NONE; }
|
||||
// serial, signature, issuer, validity, subject, key, then [1] [2] [3] if present
|
||||
let fields = [];
|
||||
for (let i = ver.end; i < tbs.end;) {
|
||||
let t = tlv(i);
|
||||
if (!t) { return false; }
|
||||
if (t.tag === 0xa3) { return basicConstraintsCA(t); }
|
||||
if (!t) { return NONE; }
|
||||
fields.push(t);
|
||||
i = t.end;
|
||||
}
|
||||
return false;
|
||||
if (fields.length < 6 || fields[2].tag !== 0x30 || fields[4].tag !== 0x30) { return NONE; }
|
||||
let is = fields[2], su = fields[4];
|
||||
f.self = is.end - is.start === su.end - su.start;
|
||||
for (let k = 0; f.self && k < is.end - is.start; k++) { f.self = b[is.start + k] === b[su.start + k]; }
|
||||
let exts = null;
|
||||
fields.forEach(function (t) { if (t.tag === 0xa3) { exts = t; } });
|
||||
if (!exts) { return f; }
|
||||
let seq = tlv(exts.start);
|
||||
if (!seq || seq.tag !== 0x30) { return NONE; }
|
||||
for (let j = seq.start; j < seq.end;) {
|
||||
let ext = tlv(j);
|
||||
if (!ext || ext.tag !== 0x30) { return NONE; }
|
||||
let oid = tlv(ext.start);
|
||||
if (!oid || oid.tag !== 0x06) { return NONE; }
|
||||
let v = tlv(oid.end);
|
||||
if (v && v.tag === 0x01) { v = tlv(v.end); } // critical
|
||||
if (!v || v.tag !== 0x04) { return NONE; } // extnValue
|
||||
if (extIs(oid, 0x13)) { // basicConstraints
|
||||
let bc = tlv(v.start);
|
||||
if (!bc || bc.tag !== 0x30) { return NONE; }
|
||||
let ca = bc.start < bc.end ? tlv(bc.start) : null; // cA absent: FALSE
|
||||
f.ca = !!ca && ca.tag === 0x01 && ca.end - ca.start === 1 && b[ca.start] !== 0;
|
||||
} else if (extIs(oid, 0x0f)) { // keyUsage: BIT STRING, keyCertSign is bit 5
|
||||
let ku = tlv(v.start);
|
||||
f.keyCertSign = !!ku && ku.tag === 0x03 && ku.end - ku.start >= 2 && (b[ku.start + 1] & 0x04) !== 0;
|
||||
} else if (extIs(oid, 0x11)) {
|
||||
f.san = true;
|
||||
} else if (extIs(oid, 0x25)) {
|
||||
f.eku = true;
|
||||
}
|
||||
j = ext.end;
|
||||
}
|
||||
return f;
|
||||
}
|
||||
function derIsV3CA(b) { return derFacts(b).ca; }
|
||||
// Removed roots are matched by their own fingerprint, not by name and key: Mozilla removed old
|
||||
// issues of roots it still trusts under the same name and key (GTS Root R1-R4, GlobalSign,
|
||||
// Firmaprofesional; measured against CCADB and webOS CE's store, round 24), so that match would
|
||||
// refuse current roots. A CA under a removed root is no root, and is refused as notRoot.
|
||||
// What R4 may trust without asking: a root that says its key signs certificates and names no site.
|
||||
// A site's own certificate a user accepted with Trust Always (here, in webOS's browser or in an app)
|
||||
// goes into Certificate Manager's store, and the usual router or NAS one (openssl req -x509) also says
|
||||
// cA=TRUE: taken as a root, its key could vouch for any site (the inspector, round 24).
|
||||
function rootShape(f) {
|
||||
if (!f.ca) { return "notCA"; }
|
||||
if (!f.self) { return "notRoot"; }
|
||||
if (f.san || f.eku) { return "siteLike"; }
|
||||
// A CA made without keyUsage (openssl's default) cannot be told from a router's certificate:
|
||||
// not taken silently. Opened in Jihad, it is asked about (R6).
|
||||
if (!f.keyCertSign) { return "noCertSign"; }
|
||||
return "";
|
||||
}
|
||||
|
||||
function rawDER(cert) {
|
||||
@@ -264,6 +310,7 @@ function JihadCerts() {
|
||||
this._offer = null; // the last top-level CA certificate load: {host, at}
|
||||
this._unreadLogged = {};
|
||||
Services.obs.addObserver(this, "jihad-nss-ready", false);
|
||||
Services.obs.addObserver(this, "jihad-platform-cert", false);
|
||||
// A CA certificate opened as a page is the R6 question even when the server marks it as a
|
||||
// download: WebOne sends its CA with "Content-Disposition: attachment", and an attachment
|
||||
// skips every content listener, the engine's certificate import among them (nsURILoader's
|
||||
@@ -287,6 +334,8 @@ JihadCerts.prototype = {
|
||||
if (topic === "http-on-examine-response" || topic === "http-on-examine-cached-response" ||
|
||||
topic === "http-on-examine-merged-response") {
|
||||
this._caLoad(subject);
|
||||
} else if (topic === "jihad-platform-cert") {
|
||||
this._platformCert(String(data || ""));
|
||||
} else if (topic === "jihad-nss-ready") {
|
||||
this._ready = true;
|
||||
this._applyWebos();
|
||||
@@ -300,24 +349,26 @@ JihadCerts.prototype = {
|
||||
}
|
||||
},
|
||||
|
||||
// A CA certificate over HTTP. In a frame (or any load that is not the page itself) it is stopped:
|
||||
// a hidden frame must never put "Trust ...?" over the page the user is looking at. The page
|
||||
// itself: the offering host is remembered for the question, and a download marking is taken off.
|
||||
// A CA certificate over HTTP. In a frame it is stopped: a hidden frame must never put "Trust ...?"
|
||||
// over the page the user is looking at. A script's fetch or an image never reaches the question,
|
||||
// so those are left alone (a router's page may fetch its CA to show it). The page itself: it may
|
||||
// be asked about once (offer), and a download marking is taken off.
|
||||
_caLoad: function (subject) {
|
||||
let ch = subject.QueryInterface(Ci.nsIHttpChannel);
|
||||
let type = "";
|
||||
try { type = String(ch.contentType || "").toLowerCase(); } catch (e) { return; }
|
||||
if (type !== "application/x-x509-ca-cert") { return; }
|
||||
let top = false;
|
||||
try { top = ch.loadInfo.externalContentPolicyType === Ci.nsIContentPolicy.TYPE_DOCUMENT; } catch (e) {}
|
||||
if (!top) {
|
||||
let kind = -1;
|
||||
try { kind = ch.loadInfo.externalContentPolicyType; } catch (e) {}
|
||||
if (kind === Ci.nsIContentPolicy.TYPE_SUBDOCUMENT || kind === Ci.nsIContentPolicy.TYPE_OBJECT) {
|
||||
try { ch.cancel(Components.results.NS_BINDING_ABORTED); } catch (e) {}
|
||||
log("a CA certificate in a frame or a sub-load was stopped (" + ch.URI.prePath.slice(0, 80) + ")");
|
||||
log("a CA certificate in a frame was stopped (" + ch.URI.prePath.slice(0, 80) + ")");
|
||||
return;
|
||||
}
|
||||
if (kind !== Ci.nsIContentPolicy.TYPE_DOCUMENT) { return; }
|
||||
let host = "";
|
||||
try { host = ch.URI.hostPort; } catch (e) {}
|
||||
this._offer = {host: host, at: Date.now()};
|
||||
this.offer(host);
|
||||
let disp = "";
|
||||
try { disp = ch.getResponseHeader("Content-Disposition"); } catch (e) { return; } // none: already inline
|
||||
if (!/^\s*attachment/i.test(disp)) { return; }
|
||||
@@ -325,12 +376,18 @@ JihadCerts.prototype = {
|
||||
log("a CA certificate sent as a download is opened, so its question is asked (" + ch.URI.prePath.slice(0, 80) + ")");
|
||||
},
|
||||
|
||||
// Who offered the certificate now being asked about: the page's host if one was opened in the
|
||||
// last minute, otherwise a file or page on this device.
|
||||
offeredBy: function () {
|
||||
// The question may be asked only for a CA certificate opened as the page itself, once: an HTTP one
|
||||
// offers here from _caLoad (with its host), a data: or file: one from JihadCertPolicy (no host).
|
||||
// Anything else that reaches the engine's certificate import (a blob: frame, which no address
|
||||
// check can see the type of) finds no offer and is refused (the inspector, round 24).
|
||||
offer: function (host) {
|
||||
this._offer = {host: String(host || ""), at: Date.now()};
|
||||
},
|
||||
// The offer, used up: null when there is none from the last 20 seconds.
|
||||
takeOffer: function () {
|
||||
let o = this._offer;
|
||||
this._offer = null;
|
||||
return (o && Date.now() - o.at < 60000 && o.host) ? o.host : "";
|
||||
return (o && Date.now() - o.at < 20000) ? o : null;
|
||||
},
|
||||
|
||||
lists: function () {
|
||||
@@ -359,6 +416,30 @@ JihadCerts.prototype = {
|
||||
},
|
||||
|
||||
// --- R4 ---
|
||||
// The daemon's SSL dialog put a site's own certificate (PEM) into webOS's store: remember it, and
|
||||
// if it is trusted from there now, stop at once.
|
||||
_platformCert: function (pem) {
|
||||
PEM_RE.lastIndex = 0;
|
||||
let m = PEM_RE.exec(pem);
|
||||
if (!m) { return; }
|
||||
let fp = sha256Hex(base64Bytes(m[1].replace(/[^A-Za-z0-9+/=]/g, "")));
|
||||
let list = this.acceptedList().filter(function (h) { return h !== fp; });
|
||||
list.push(fp);
|
||||
try { Services.prefs.setCharPref(PREF_ACCEPTED, list.slice(-MAX_ACCEPTED).join(",")); } catch (e) {}
|
||||
let db = certDB(), was = this._webos.length;
|
||||
this._webos = this._webos.filter(function (e) {
|
||||
if (e.sha256 !== fp) { return true; }
|
||||
try { db.setCertTrust(e.cert, Ci.nsIX509Cert.CA_CERT, 0); } catch (x) {}
|
||||
return false;
|
||||
});
|
||||
if (this._webos.length !== was) { dropSessions(); }
|
||||
log("a site's certificate went into webOS's store from the SSL dialog; never taken as a root (" + fp.slice(0, 16) + "...)");
|
||||
},
|
||||
|
||||
acceptedList: function () {
|
||||
return charPref(PREF_ACCEPTED).split(",").filter(function (h) { return /^[0-9a-f]{64}$/.test(h); });
|
||||
},
|
||||
|
||||
_applyWebos: function () {
|
||||
let on = boolPref(PREF_WEBOS, true);
|
||||
if (!on) { this._dropWebos(); return; }
|
||||
@@ -388,7 +469,8 @@ JihadCerts.prototype = {
|
||||
|
||||
_readWebos: function () {
|
||||
let lists = this.lists();
|
||||
let skipped = {stock: 0, removed: 0, builtin: 0, expired: 0, notCA: 0, already: 0, relay: 0, unreadable: 0};
|
||||
let skipped = {stock: 0, removed: 0, builtin: 0, expired: 0, notCA: 0, notRoot: 0, siteLike: 0, noCertSign: 0,
|
||||
accepted: 0, already: 0, relay: 0, unreadable: 0};
|
||||
let stores = [], seen = {}, found = [];
|
||||
if (lists.missing) { this._skipped = skipped; this._stores = stores; return; }
|
||||
|
||||
@@ -443,18 +525,24 @@ JihadCerts.prototype = {
|
||||
});
|
||||
}, this);
|
||||
|
||||
let db = certDB(), nowUs = Date.now() * 1000, trusted = [];
|
||||
let db = certDB(), nowUs = Date.now() * 1000, trusted = [], accepted = {};
|
||||
this.acceptedList().forEach(function (h) { accepted[h] = true; });
|
||||
found.forEach(function (c) {
|
||||
if (c.sha256 === RELAY_SHA256) { skipped.relay++; return; }
|
||||
if (lists.stock[c.sha256]) { skipped.stock++; return; }
|
||||
if (lists.removed[c.sha256]) { skipped.removed++; return; }
|
||||
if (!derIsV3CA(c.der)) { skipped.notCA++; return; }
|
||||
if (accepted[c.sha256]) { skipped.accepted++; return; }
|
||||
let cert;
|
||||
try { cert = db.constructX509FromBase64(c.b64); } catch (e) { skipped.unreadable++; return; }
|
||||
try {
|
||||
if (isRelayCert(cert) || isSquidWebosCert(cert)) { skipped.relay++; return; }
|
||||
if (cert.isBuiltInRoot) { skipped.builtin++; return; }
|
||||
if (cert.validity.notAfter < nowUs) { skipped.expired++; return; }
|
||||
// Only a root: a v3 CA, its own issuer, whose key signs certificates and which names no
|
||||
// site. An intermediate (a CA under a root Mozilla removed, say) or a site's own
|
||||
// certificate is never made an anchor here.
|
||||
let shape = rootShape(derFacts(c.der));
|
||||
if (shape) { skipped[shape]++; return; }
|
||||
if (cert.certType !== Ci.nsIX509Cert.CA_CERT) { skipped.notCA++; return; }
|
||||
// A CA the user already trusts in Jihad's own database (R6) is theirs; leave it.
|
||||
if (trustedNow(db, cert)) { skipped.already++; return; }
|
||||
@@ -592,18 +680,27 @@ JihadCertDialogs.prototype = {
|
||||
try { svc = Cc["@riverstonerelay.net/jihad/certs;1"].getService().wrappedJSObject; } catch (e) {}
|
||||
let fp = normFp(cert.sha256Fingerprint);
|
||||
let name = shownName(cert.commonName || cert.organization || cert.subjectName);
|
||||
let from = svc ? svc.offeredBy() : "";
|
||||
let offer = svc ? svc.takeOffer() : null;
|
||||
if (!offer) {
|
||||
// Not opened as a page: a frame or something else a page did. Nothing is asked or shown.
|
||||
log("refused a CA certificate that was not opened as a page (" + fp.slice(0, 16) + "...)");
|
||||
return false;
|
||||
}
|
||||
let from = offer.host;
|
||||
if (isRelayCert(cert)) {
|
||||
p.alert("Certificate", "This is the Riverstone Relay's certificate. Jihad trusts it only through " +
|
||||
"Settings \u2192 Advanced \u2192 Proxy, while the Relay is in use, and only if you turn that on.");
|
||||
log("refused the Relay's certificate offered by a link");
|
||||
return false;
|
||||
}
|
||||
// The engine also lets a self-signed v1 site certificate through as a "CA" (CERT_IsCACert).
|
||||
if (!derIsV3CA(rawDER(cert))) {
|
||||
// The engine also lets a self-signed v1 site certificate through as a "CA" (CERT_IsCACert); and a
|
||||
// site's own certificate that also says cA=TRUE (a router's, from openssl req -x509) names the
|
||||
// site it is for. Neither is a certificate authority's.
|
||||
let facts = derFacts(rawDER(cert));
|
||||
if (!facts.ca || facts.san || facts.eku) {
|
||||
p.alert("Certificate", "\"" + name + "\" is not a certificate authority's certificate, so Jihad does not " +
|
||||
"take it as one. A site's own certificate is accepted when the site is opened.");
|
||||
log("refused a certificate that is not a v3 CA (" + fp.slice(0, 16) + "...)");
|
||||
log("refused a certificate that is not a CA's (" + fp.slice(0, 16) + "...)");
|
||||
return false;
|
||||
}
|
||||
let lists = svc ? svc.lists() : null;
|
||||
@@ -638,10 +735,11 @@ JihadCertDialogs.prototype = {
|
||||
viewCert: function (ctx, cert) {}
|
||||
};
|
||||
|
||||
// --- R6: a certificate file in a frame never reaches the question -----------------------------------
|
||||
// An HTTP one is stopped by JihadCerts._caLoad. A data: or file: frame is not an HTTP load, so it is
|
||||
// refused here, by its address, before it loads (a page could otherwise raise the question from a
|
||||
// hidden frame with a data: address carrying a CA certificate).
|
||||
// --- R6: which certificate files may be asked about -------------------------------------------------
|
||||
// An HTTP one is sorted by JihadCerts._caLoad. A data: or file: one is not an HTTP load, so it is
|
||||
// sorted here, by its address, before it loads: in a frame it is refused (a page could otherwise
|
||||
// raise the question from a hidden frame with a data: address carrying a CA certificate); opened as
|
||||
// the page itself it may be asked about once. Anything else (a blob: frame) never has an offer.
|
||||
function JihadCertPolicy() {}
|
||||
|
||||
JihadCertPolicy.prototype = {
|
||||
@@ -649,19 +747,25 @@ JihadCertPolicy.prototype = {
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentPolicy]),
|
||||
|
||||
shouldLoad: function (type, location) {
|
||||
if (type !== Ci.nsIContentPolicy.TYPE_SUBDOCUMENT || !location) { return Ci.nsIContentPolicy.ACCEPT; }
|
||||
let refuse = false;
|
||||
let frame = type === Ci.nsIContentPolicy.TYPE_SUBDOCUMENT, page = type === Ci.nsIContentPolicy.TYPE_DOCUMENT;
|
||||
if ((!frame && !page) || !location) { return Ci.nsIContentPolicy.ACCEPT; }
|
||||
let certFile = false;
|
||||
try {
|
||||
if (location.schemeIs("data")) {
|
||||
refuse = /^data:\s*application\/(x-x509-ca-cert|pkix-cert)/i.test(location.spec);
|
||||
certFile = /^data:\s*application\/(x-x509-ca-cert|pkix-cert)/i.test(location.spec);
|
||||
} else if (location.schemeIs("file")) {
|
||||
let ext = "";
|
||||
try { ext = location.QueryInterface(Ci.nsIURL).fileExtension; } catch (e) {}
|
||||
refuse = /^(crt|cer|der|pem)$/i.test(ext);
|
||||
certFile = /^(crt|cer|der|pem)$/i.test(ext);
|
||||
}
|
||||
} catch (e) {}
|
||||
if (refuse) { log("a certificate file in a frame was not loaded (" + location.scheme + ":)"); }
|
||||
return refuse ? Ci.nsIContentPolicy.REJECT_REQUEST : Ci.nsIContentPolicy.ACCEPT;
|
||||
if (!certFile) { return Ci.nsIContentPolicy.ACCEPT; }
|
||||
if (page) {
|
||||
try { Cc["@riverstonerelay.net/jihad/certs;1"].getService().wrappedJSObject.offer(""); } catch (e) {}
|
||||
return Ci.nsIContentPolicy.ACCEPT;
|
||||
}
|
||||
log("a certificate file in a frame was not loaded (" + location.scheme + ":)");
|
||||
return Ci.nsIContentPolicy.REJECT_REQUEST;
|
||||
},
|
||||
|
||||
shouldProcess: function () { return Ci.nsIContentPolicy.ACCEPT; }
|
||||
|
||||
@@ -344,8 +344,11 @@ function thumbFor(t, keep) {
|
||||
}
|
||||
|
||||
// Copies are made one at a time, THUMB_GAP_MS apart, so the gallery stays responsive while the first
|
||||
// openings make them; when the queue is done, an open gallery puts them on the cards ("thumbs": only
|
||||
// the pictures change, no card is drawn again).
|
||||
// openings make them; when the queue is done, an open gallery puts them on the cards
|
||||
// ("jihad-personas-thumbs": only the pictures change, no card is drawn again). Each copy is a decode
|
||||
// and an encode on the engine's own thread (200-600 ms), so every page waits that long once per
|
||||
// copy while the queue runs: once per Persona, the first time after it is installed or the bundle
|
||||
// changes (the inspector, round 24).
|
||||
const THUMB_GAP_MS = 700;
|
||||
let thumbQueue = [], thumbTimer = null, thumbsMade = 0;
|
||||
function queueThumb(f, name) {
|
||||
@@ -365,7 +368,9 @@ function nextThumb() {
|
||||
}
|
||||
if (!thumbQueue.length && thumbsMade) {
|
||||
thumbsMade = 0;
|
||||
try { Services.obs.notifyObservers(null, "jihad-personas-mine", "thumbs"); } catch (e) {}
|
||||
// Its own topic: a Persona's id comes from the site that offered it, so "jihad-personas-mine"
|
||||
// with data "thumbs" could have been a Persona of that id being removed (the inspector, round 24).
|
||||
try { Services.obs.notifyObservers(null, "jihad-personas-thumbs", ""); } catch (e) {}
|
||||
}
|
||||
}
|
||||
function makeThumb(f, name) {
|
||||
@@ -865,10 +870,10 @@ function handle(req) {
|
||||
// should be shown for all personas", "default version 1.0");
|
||||
// - a picture made as a wallpaper (taller than a toolbar by far) is fitted, not cropped to its
|
||||
// corner (owner: "blue nebula looks like it isnt working properly");
|
||||
// - a link to the gallery (about:personas), and Install from file — the .xpi files already on the
|
||||
// device (R4);
|
||||
// - the Personas that come with Jihad (a Frutiger Aero set, owner 2026-10-08: "add fruiter
|
||||
// aero style themes") join the list once, as ordinary rows;
|
||||
// - a link to the gallery (about:personas), where Install from file (the .xpi files already on the
|
||||
// device, R4) and the online galleries are since Tier 78;
|
||||
// - the Personas that come with Jihad (render/goanna/personas/: Free Palestine and Aurora Silk
|
||||
// since Tier 78) join the list once, as ordinary rows;
|
||||
// - a few sentences on what a persona is when there are none yet (R5).
|
||||
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
||||
// The gallery link opens the browser's own gallery (about:personas), unless the pref Basilisk uses
|
||||
@@ -1138,8 +1143,9 @@ function xpiFilesOnDevice() {
|
||||
return out;
|
||||
}
|
||||
|
||||
// The Personas that come with the browser (render/goanna/personas/: a Frutiger Aero set drawn
|
||||
// for this project), shipped in the bundle's personas/ folder beside components/. Wearing one
|
||||
// The Personas that come with the browser (render/goanna/personas/: Free Palestine, from a CC BY
|
||||
// 2.0 photograph, and Aurora Silk, drawn for this project; credited there), shipped in the bundle's
|
||||
// personas/ folder beside components/. Wearing one
|
||||
// installs it exactly as a downloaded one: its picture copied into the profile, named by its hash.
|
||||
let bundledCache = null;
|
||||
function bundledPersonas() {
|
||||
@@ -1741,6 +1747,12 @@ function galleryRequest(bag) {
|
||||
// The installed Personas' pictures are resource://jihad-themes/ addresses, which resolve only once
|
||||
// that root is set: the gallery opened first after a start showed them blank (the owner, 2026-10-12).
|
||||
try { ensureThemesResource(); } catch (e) {}
|
||||
// The bundled set is offered (and a new edition's names remembered) here too, not only when
|
||||
// about:addons opens: Settings → Browse Personas never opens it, so a bundled Persona removed in
|
||||
// the gallery came back at the next about:addons (the inspector, round 24).
|
||||
if (action === "list" || action === "mine") {
|
||||
try { offerBundledOnce(); } catch (e) { log("bundled Personas: " + e); }
|
||||
}
|
||||
let get = function(k) { try { return bag.getPropertyAsAString(k); } catch (e) { return ""; } };
|
||||
let reply = function(v) { bag.setPropertyAsAString("result", JSON.stringify(v)); };
|
||||
let LWTM = manager(), cur = null, used = [];
|
||||
|
||||
@@ -32,6 +32,11 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
const PREF_MODE = "jihad.proxy.mode";
|
||||
// The last proxy webOS's setting was followed to (JSON). When a read fails, Jihad keeps following it:
|
||||
// the daemon cannot ask until it is on the bus, which can take minutes after a start, and going
|
||||
// direct meanwhile sent the pages of someone who chose webOS's proxy past it (the inspector,
|
||||
// round 24). With nothing followed yet, a failed read goes direct as before.
|
||||
const PREF_LAST = "jihad.proxy.webos.last";
|
||||
|
||||
function log(s) {
|
||||
try { Services.console.logStringMessage("proxy: " + s); } catch (e) {}
|
||||
@@ -110,13 +115,26 @@ JihadProxy.prototype = {
|
||||
apply: function () {
|
||||
let a = this._answer;
|
||||
let p;
|
||||
if (!a) {
|
||||
p = {type: 0, said: "", error: "webOS's proxy setting has not been read yet."};
|
||||
} else if (a.returnValue === false) {
|
||||
p = {type: 0, error: "webOS's proxy setting could not be read (" + String(a.errorText || "no reason given") + "), so Jihad goes direct."};
|
||||
if (!a || a.returnValue === false) {
|
||||
let why = !a ? "webOS's proxy setting has not been read yet" :
|
||||
"webOS's proxy setting could not be read (" + String(a.errorText || "no reason given") + ")";
|
||||
let last = null;
|
||||
try { last = JSON.parse(charPref(PREF_LAST) || "null"); } catch (e) { last = null; }
|
||||
if (last && (last.type === 0 || last.type === 1 || last.type === 2 || last.type === 4)) {
|
||||
p = {type: last.type, host: last.host, port: last.port, pac: last.pac, said: last.said || "",
|
||||
error: why + "; Jihad keeps what it last followed."};
|
||||
} else {
|
||||
p = {type: 0, said: "", error: why + ", so Jihad goes direct."};
|
||||
}
|
||||
} else {
|
||||
p = plan(pickEntry(a.proxyInfoList));
|
||||
if (p.error) { p.type = 0; p.error += " Jihad goes direct."; }
|
||||
else {
|
||||
try {
|
||||
Services.prefs.setCharPref(PREF_LAST, JSON.stringify({type: p.type, host: p.host, port: p.port, pac: p.pac,
|
||||
said: p.said}));
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
this._said = p.said || "";
|
||||
this._error = p.error || "";
|
||||
|
||||
@@ -135,6 +135,16 @@
|
||||
if (cfOpen) { cf.acceptClick(); } // the popup's own Accept button handler
|
||||
else if (alOpen) { br.viewCall("acceptDialog", []); }
|
||||
} catch (e) { log(name, "answering THREW " + e); }
|
||||
} else if (st === "D") { // report the engine dialogs that are open, answering none
|
||||
var db0 = app.$.browser, dal = db0.$.dialog, dcf = db0.$.confirmDialog, dss = db0.$.sslDialog;
|
||||
// (the messages belong to the Browser, which declares them, not to their popups)
|
||||
log(name, "dialogs: alert open=" + !!(dal && dal.isOpen) + (dal && dal.isOpen && db0.$.dialogMessage ? " [" + String(db0.$.dialogMessage.getContent()).slice(0, 140) + "]" : "") +
|
||||
" confirm open=" + !!(dcf && dcf.isOpen) + (dcf && dcf.isOpen && db0.$.confirmMessage ? " [" + String(db0.$.confirmMessage.getContent()).slice(0, 160) + "]" : "") +
|
||||
" ssl open=" + !!(dss && dss.isOpen));
|
||||
} else if (st.charAt(0) === "V") { // VN: answer the SSL dialog as its button N does (1 Trust Always, 2 Trust Once, 0 Don't Trust)
|
||||
var vs = app.$.browser.$.sslDialog;
|
||||
log(name, "ssl dialog open=" + !!(vs && vs.isOpen) + "; answering " + st.slice(1));
|
||||
if (vs && vs.isOpen) { vs.response = st.slice(1); vs.close(); }
|
||||
} else if (st.charAt(0) === "W") { // WUSER:PASS: fill the open login dialog (a 401/407) and press OK,
|
||||
// through the card's own fields and handler — what typing them and tapping OK does
|
||||
var wu = st.slice(1).split(":"), wb = app.$.browser;
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
- `carbon_light-2.0.xpi` — **Carbon Light by Jaxivo** as addons.mozilla.org serves it
|
||||
(addons.mozilla.org/firefox/addon/carbon-light/, CC BY-SA 3.0; sha256
|
||||
`01f5dcc11c0d0ce7383cc6563e345530919185da2443ff6a962af5c8b319c1d5`), for the static-theme
|
||||
conversion path (`components/jihadPersonas.js`). Its picture is one of the Personas Jihad
|
||||
bundles (`render/goanna/personas/carbon-light.png`); credits in `CREDITS.md`.
|
||||
conversion path (`components/jihadPersonas.js`). `echo-server.py` serves its `header.png` straight
|
||||
out of this package (Jihad no longer bundles Carbon Light); its credit is the line above.
|
||||
- `persona-header.png` — drawn for the tests by this project.
|
||||
- `echo-server.py` — the host test server: `/persona?v=carbon` serves a theme site that installs
|
||||
Carbon Light as a lightweight theme (and `?v=1,2,…` test Personas with `persona-header.png`);
|
||||
|
||||
@@ -33,8 +33,11 @@ class H(http.server.BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
p = urllib.parse.urlparse(self.path).path
|
||||
if p == "/carbon-light-header.png":
|
||||
# Carbon Light by Jaxivo, CC BY-SA 3.0 — one of the Personas Jihad bundles (render/goanna/personas/).
|
||||
data = open("../../personas/carbon-light.png","rb").read()
|
||||
# Carbon Light by Jaxivo, CC BY-SA 3.0, its picture read out of the theme's own package beside
|
||||
# this script (the bundle no longer carries it; the inspector, round 24).
|
||||
import os, zipfile
|
||||
with zipfile.ZipFile(os.path.join(os.path.dirname(os.path.abspath(__file__)), "carbon_light-2.0.xpi")) as z:
|
||||
data = z.read("header.png")
|
||||
self.send_response(200); self.send_header("Content-Type", "image/png"); self.send_header("Content-Length", str(len(data))); self.end_headers(); self.wfile.write(data); return
|
||||
if p.startswith("/themes/") and p.endswith(".xpi"):
|
||||
# AMO static themes for the README gallery (downloaded to the scratch themes dir).
|
||||
|
||||
Reference in new issue
Block a user