mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-29 23:15:13 +08:00
⛄ Sync 2026-09-26 23:11:03
This commit is contained in:
1 parent
a9ef1dfba0
commit
8b5f6dab51
19 files changed
+1326
-1077
No files matched your search
+2
-2
@@ -6,8 +6,8 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=gowebdav
|
||||
PKG_VERSION:=5.16.0
|
||||
PKG_RELEASE:=22
|
||||
PKG_VERSION:=5.16.1
|
||||
PKG_RELEASE:=23
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://codeload.github.com/hacdias/webdav/tar.gz/v$(PKG_VERSION)?
|
||||
|
||||
@@ -23,7 +23,7 @@ LUCI_DEPENDS:= \
|
||||
|
||||
PKG_NAME:=luci-app-homeproxy
|
||||
PKG_VERSION:=27.919.1.14
|
||||
PKG_RELEASE:=17
|
||||
PKG_RELEASE:=18
|
||||
|
||||
define Package/luci-app-homeproxy/conffiles
|
||||
/etc/config/homeproxy
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<div align="center">
|
||||
|
||||
## 新增本仓库“架构重构”试验性产品luci-app-homeproxy-pro
|
||||
(https://github.com/szwjp/luci-app-homeproxy-pro)
|
||||
|
||||
# luci-app-homeproxy
|
||||
|
||||
**The modern ImmortalWrt proxy platform for ARM64 / AMD64**
|
||||
|
||||
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-passwall
|
||||
PKG_VERSION:=26.9.26
|
||||
PKG_RELEASE:=293
|
||||
PKG_RELEASE:=294
|
||||
PKG_PO_VERSION:=$(PKG_VERSION)
|
||||
|
||||
PKG_CONFIG_DEPENDS:= \
|
||||
|
||||
@@ -649,6 +649,7 @@ local current_node = map:get(section)
|
||||
params += opt.query("congestion_control", dom_prefix + "tuic_congestion_control");
|
||||
params += opt.query("udp_relay_mode", dom_prefix + "tuic_udp_relay_mode");
|
||||
params += opt.query("allowinsecure", dom_prefix + "tls_allowInsecure");
|
||||
params += opt.query("pcs", dom_prefix + "tls_pinSHA256");
|
||||
|
||||
params += "#" + encodeURI(v_alias.value);
|
||||
if (params[0] == "&") {
|
||||
@@ -679,6 +680,7 @@ local current_node = map:get(section)
|
||||
params += opt.query("alpn", dom_prefix + "alpn");
|
||||
params += opt.query("sni", dom_prefix + "tls_serverName");
|
||||
params += opt.query("insecure", dom_prefix + "tls_allowInsecure");
|
||||
params += opt.query("pcs", dom_prefix + "tls_pinSHA256");
|
||||
}
|
||||
|
||||
params += "#" + encodeURI(v_alias.value);
|
||||
@@ -1705,6 +1707,7 @@ local current_node = map:get(section)
|
||||
dom_prefix + 'tls_allowInsecure',
|
||||
!((queryParam.allowinsecure ?? '0') === '0' && (queryParam.insecure ?? '0') === '0' && (queryParam.allow_insecure ?? '0') === '0')
|
||||
);
|
||||
opt.set(dom_prefix + 'tls_pinSHA256', queryParam.pcs || queryParam.pinSHA256 || '');
|
||||
if (hash) {
|
||||
opt.set('remarks', decodeURIComponent(hash.substr(1)));
|
||||
}
|
||||
@@ -1737,29 +1740,23 @@ local current_node = map:get(section)
|
||||
}
|
||||
queryParam.security = queryParam.security || "tls";
|
||||
if (queryParam.security) {
|
||||
opt.set(dom_prefix + 'tls', true);
|
||||
if (queryParam.security == "tls") {
|
||||
opt.set(dom_prefix + 'tls', true);
|
||||
opt.set(dom_prefix + 'reality', false);
|
||||
opt.set(dom_prefix + 'alpn', queryParam.alpn || 'default');
|
||||
opt.set(dom_prefix + 'tls_serverName', queryParam.sni || queryParam.peer || '');
|
||||
opt.set(dom_prefix + 'tls_allowInsecure', !((queryParam.allowinsecure ?? '0') === '0' && (queryParam.insecure ?? '0') === '0'));
|
||||
if (queryParam.fp && queryParam.fp.trim() != "") {
|
||||
opt.set(dom_prefix + 'utls', true);
|
||||
opt.set(dom_prefix + 'fingerprint', queryParam.fp);
|
||||
}
|
||||
}
|
||||
if (queryParam.security == "reality") {
|
||||
opt.set(dom_prefix + 'tls', true);
|
||||
opt.set(dom_prefix + 'reality', true);
|
||||
opt.set(dom_prefix + 'alpn', queryParam.alpn || 'default');
|
||||
opt.set(dom_prefix + 'tls_serverName', queryParam.sni || queryParam.peer || '');
|
||||
if (queryParam.fp && queryParam.fp.trim() != "") {
|
||||
opt.set(dom_prefix + 'utls', true);
|
||||
opt.set(dom_prefix + 'fingerprint', queryParam.fp);
|
||||
}
|
||||
opt.set(dom_prefix + 'reality_publicKey', queryParam.pbk || '');
|
||||
opt.set(dom_prefix + 'reality_shortId', queryParam.sid || '');
|
||||
}
|
||||
opt.set(dom_prefix + 'alpn', queryParam.alpn || 'default');
|
||||
opt.set(dom_prefix + 'tls_serverName', queryParam.sni || queryParam.peer || '');
|
||||
if (queryParam.fp && queryParam.fp.trim() != "") {
|
||||
opt.set(dom_prefix + 'utls', true);
|
||||
opt.set(dom_prefix + 'fingerprint', queryParam.fp);
|
||||
}
|
||||
opt.set(dom_prefix + 'tls_allowInsecure', !((queryParam.allowinsecure ?? '0') === '0' && (queryParam.insecure ?? '0') === '0'));
|
||||
opt.set(dom_prefix + 'tls_pinSHA256', queryParam.pcs || queryParam.pinSHA256 || '');
|
||||
}
|
||||
if (m.hash) {
|
||||
opt.set('remarks', decodeURIComponent(m.hash.substr(1)));
|
||||
|
||||
@@ -448,6 +448,7 @@ local function encode_tuic(node)
|
||||
if node["disable-sni"] then table.insert(p, "disable_sni=1") end
|
||||
if node["skip-cert-verify"] then table.insert(p, "allowInsecure=1") end
|
||||
if node["udp-relay-mode"] then table.insert(p, "udp_relay_mode=" .. node["udp-relay-mode"]) end
|
||||
if node["fingerprint"] then table.insert(p, "pcs=" .. urlencode(node["fingerprint"])) end
|
||||
|
||||
if #p > 0 then
|
||||
link = link .. "?" .. table.concat(p, "&")
|
||||
@@ -483,11 +484,8 @@ local function encode_anytls(node)
|
||||
if o.tls.alpn then table.insert(p, "alpn=" .. urlencode(o.tls.alpn)) end
|
||||
if o.tls.fp then table.insert(p, "fp=" .. urlencode(o.tls.fp)) end
|
||||
if o.tls.ech then table.insert(p, "ech=" .. urlencode(o.tls.ech)) end
|
||||
if o.tls.pcs then
|
||||
table.insert(p, "insecure=1")
|
||||
else
|
||||
table.insert(p, "insecure=" .. (o.tls.insecure and "1" or "0"))
|
||||
end
|
||||
if o.tls.pcs then table.insert(p, "pcs=" .. urlencode(o.tls.pcs)) end
|
||||
table.insert(p, "insecure=" .. (o.tls.insecure and "1" or "0"))
|
||||
|
||||
if #p > 0 then
|
||||
link = link .. "?" .. table.concat(p, "&")
|
||||
|
||||
@@ -1560,6 +1560,7 @@ local function processData(szType, content, add_mode, group, sub_cfg)
|
||||
end
|
||||
result.tls_serverName = params.sni
|
||||
result.tls_disable_sni = params.disable_sni
|
||||
result.tls_pinSHA256 = params.pcs or params.pinsha256
|
||||
result.tuic_alpn = params.alpn or "h3"
|
||||
result.tuic_congestion_control = params.congestion_control or "cubic"
|
||||
result.tuic_udp_relay_mode = params.udp_relay_mode or "native"
|
||||
@@ -1615,6 +1616,7 @@ local function processData(szType, content, add_mode, group, sub_cfg)
|
||||
if params.security == "tls" or params.security == "reality" then
|
||||
result.tls = "1"
|
||||
result.tls_serverName = params.sni or params.peer
|
||||
result.tls_pinSHA256 = params.pcs or params.pinsha256
|
||||
result.alpn = params.alpn
|
||||
if params.fp and params.fp ~= "" then
|
||||
result.utls = "1"
|
||||
|
||||
@@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-qosmate
|
||||
PKG_VERSION:=1.0.14
|
||||
PKG_RELEASE:=4
|
||||
PKG_RELEASE:=5
|
||||
|
||||
PKG_MAINTAINER:=Markus Hütter <mh@hudra.net>
|
||||
PKG_LICENSE:=GPL-3.0-or-later
|
||||
|
||||
@@ -253,7 +253,7 @@ return view.extend({
|
||||
if (headerRow) {
|
||||
var headers = headerRow.querySelectorAll('th');
|
||||
headers.forEach(function(th, index) {
|
||||
if (th.textContent.includes('Activity')) {
|
||||
if (th.textContent.includes(_('Activity'))) {
|
||||
activityColumnIndex = index;
|
||||
|
||||
}
|
||||
|
||||
@@ -253,6 +253,52 @@ var TimeSeriesChart = {
|
||||
}
|
||||
};
|
||||
|
||||
// Statistics tables and charts are rendered with translated headings, so they
|
||||
// have to be grouped into the Egress/Ingress tabs by comparing against the
|
||||
// translations as well. A plain English keyword match such as
|
||||
// title.includes('Egress') dropped every table and chart in a non-English UI.
|
||||
var qosmateEgressHeadings = [
|
||||
'CAKE Egress Statistics',
|
||||
'CAKE Statistics for Default Class (Egress)',
|
||||
'Egress Bytes Sent by Class',
|
||||
'Egress Bytes Sent by Tin',
|
||||
'Egress Dropped Packets by Class',
|
||||
'Egress Dropped Packets by Tin',
|
||||
'Egress Packets Sent by Class',
|
||||
'Egress Packets Sent by Tin',
|
||||
'HFSC Egress Class Statistics',
|
||||
'HTB Egress Class Statistics',
|
||||
'Hybrid Egress Class Statistics'
|
||||
];
|
||||
|
||||
var qosmateIngressHeadings = [
|
||||
'CAKE Ingress Statistics',
|
||||
'CAKE Statistics for Default Class (Ingress)',
|
||||
'HFSC Ingress Class Statistics',
|
||||
'HTB Ingress Class Statistics',
|
||||
'Hybrid Ingress Class Statistics',
|
||||
'Ingress Bytes Sent by Class',
|
||||
'Ingress Bytes Sent by Tin',
|
||||
'Ingress Dropped Packets by Class',
|
||||
'Ingress Dropped Packets by Tin',
|
||||
'Ingress Packets Sent by Class',
|
||||
'Ingress Packets Sent by Tin'
|
||||
];
|
||||
|
||||
var isEgressHeading = function(heading) {
|
||||
for (var i = 0; i < qosmateEgressHeadings.length; i++)
|
||||
if (heading === _(qosmateEgressHeadings[i]))
|
||||
return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
var isIngressHeading = function(heading) {
|
||||
for (var i = 0; i < qosmateIngressHeadings.length; i++)
|
||||
if (heading === _(qosmateIngressHeadings[i]))
|
||||
return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
// Main view
|
||||
return view.extend({
|
||||
pollInterval: 5,
|
||||
@@ -440,7 +486,7 @@ return view.extend({
|
||||
// Create table for CAKE egress tins
|
||||
if (egressRows.length > 0) {
|
||||
result.tables.push(self.createStatsTable(
|
||||
_('CAKE Egress Statistics - eth1'),
|
||||
_('CAKE Egress Statistics'),
|
||||
[_('Tin'), _('Threshold'), _('Target'), _('Interval'), _('Peak Delay'), _('Avg Delay'), _('Sparse Delay'), _('Bytes'), _('Packets'), _('Dropped'), _('ECN Marked')],
|
||||
egressRows
|
||||
));
|
||||
@@ -511,7 +557,7 @@ return view.extend({
|
||||
// Create table for CAKE ingress tins
|
||||
if (ingressRows.length > 0) {
|
||||
result.tables.push(self.createStatsTable(
|
||||
_('CAKE Ingress Statistics - eth1'),
|
||||
_('CAKE Ingress Statistics'),
|
||||
[_('Tin'), _('Threshold'), _('Target'), _('Interval'), _('Peak Delay'), _('Avg Delay'), _('Sparse Delay'), _('Bytes'), _('Packets'), _('Dropped'), _('ECN Marked')],
|
||||
ingressRows
|
||||
));
|
||||
@@ -1621,9 +1667,9 @@ return view.extend({
|
||||
var titleElement = cakeResults.tables[i].querySelector('h3, .table-title');
|
||||
if (titleElement) {
|
||||
var title = titleElement.textContent || titleElement.innerText;
|
||||
if (title.includes('Egress')) {
|
||||
if (isEgressHeading(title)) {
|
||||
egressTable = cakeResults.tables[i];
|
||||
} else if (title.includes('Ingress')) {
|
||||
} else if (isIngressHeading(title)) {
|
||||
ingressTable = cakeResults.tables[i];
|
||||
}
|
||||
}
|
||||
@@ -1649,9 +1695,9 @@ return view.extend({
|
||||
var chartTitle = cakeResults.charts[i].querySelector('h3');
|
||||
var titleText = chartTitle ? (chartTitle.textContent || chartTitle.innerText) : '';
|
||||
|
||||
if (titleText.includes('Egress')) {
|
||||
if (isEgressHeading(titleText)) {
|
||||
egressCharts.push(cakeResults.charts[i]);
|
||||
} else if (titleText.includes('Ingress')) {
|
||||
} else if (isIngressHeading(titleText)) {
|
||||
ingressCharts.push(cakeResults.charts[i]);
|
||||
}
|
||||
}
|
||||
@@ -1701,9 +1747,9 @@ return view.extend({
|
||||
var titleElement = hfscResults.tables[i].querySelector('h3, .table-title');
|
||||
if (titleElement) {
|
||||
var title = titleElement.textContent || titleElement.innerText;
|
||||
if (title.includes('Egress')) {
|
||||
if (isEgressHeading(title)) {
|
||||
egressTable = hfscResults.tables[i];
|
||||
} else if (title.includes('Ingress')) {
|
||||
} else if (isIngressHeading(title)) {
|
||||
ingressTable = hfscResults.tables[i];
|
||||
}
|
||||
}
|
||||
@@ -1729,9 +1775,9 @@ return view.extend({
|
||||
var chartTitle = hfscResults.charts[i].querySelector('h3');
|
||||
var titleText = chartTitle ? (chartTitle.textContent || chartTitle.innerText) : '';
|
||||
|
||||
if (titleText.includes('Egress')) {
|
||||
if (isEgressHeading(titleText)) {
|
||||
egressCharts.push(hfscResults.charts[i]);
|
||||
} else if (titleText.includes('Ingress')) {
|
||||
} else if (isIngressHeading(titleText)) {
|
||||
ingressCharts.push(hfscResults.charts[i]);
|
||||
}
|
||||
}
|
||||
@@ -1783,13 +1829,13 @@ return view.extend({
|
||||
var titleElement = hybridResults.tables[i].querySelector('h3, .table-title');
|
||||
if (titleElement) {
|
||||
var title = titleElement.textContent || titleElement.innerText;
|
||||
if (title.includes('Hybrid Egress Class')) {
|
||||
if (title === _('Hybrid Egress Class Statistics')) {
|
||||
egressClassTable = hybridResults.tables[i];
|
||||
} else if (title.includes('Hybrid Ingress Class')) {
|
||||
} else if (title === _('Hybrid Ingress Class Statistics')) {
|
||||
ingressClassTable = hybridResults.tables[i];
|
||||
} else if (title.includes('CAKE') && title.includes('Egress')) {
|
||||
} else if (title === _('CAKE Statistics for Default Class (Egress)')) {
|
||||
cakeEgressTable = hybridResults.tables[i];
|
||||
} else if (title.includes('CAKE') && title.includes('Ingress')) {
|
||||
} else if (title === _('CAKE Statistics for Default Class (Ingress)')) {
|
||||
cakeIngressTable = hybridResults.tables[i];
|
||||
}
|
||||
}
|
||||
@@ -1820,9 +1866,9 @@ return view.extend({
|
||||
var chartTitle = hybridResults.charts[i].querySelector('h3');
|
||||
var titleText = chartTitle ? (chartTitle.textContent || chartTitle.innerText) : '';
|
||||
|
||||
if (titleText.includes('Egress')) {
|
||||
if (isEgressHeading(titleText)) {
|
||||
egressCharts.push(hybridResults.charts[i]);
|
||||
} else if (titleText.includes('Ingress')) {
|
||||
} else if (isIngressHeading(titleText)) {
|
||||
ingressCharts.push(hybridResults.charts[i]);
|
||||
}
|
||||
}
|
||||
@@ -1872,9 +1918,9 @@ return view.extend({
|
||||
var titleElement = htbResults.tables[i].querySelector('h3, .table-title');
|
||||
if (titleElement) {
|
||||
var title = titleElement.textContent || titleElement.innerText;
|
||||
if (title.includes('HTB Egress Class')) {
|
||||
if (title === _('HTB Egress Class Statistics')) {
|
||||
egressClassTable = htbResults.tables[i];
|
||||
} else if (title.includes('HTB Ingress Class')) {
|
||||
} else if (title === _('HTB Ingress Class Statistics')) {
|
||||
ingressClassTable = htbResults.tables[i];
|
||||
}
|
||||
}
|
||||
@@ -1898,9 +1944,9 @@ return view.extend({
|
||||
var chartTitle = htbResults.charts[i].querySelector('h3');
|
||||
var titleText = chartTitle ? (chartTitle.textContent || chartTitle.innerText) : '';
|
||||
|
||||
if (titleText.includes('Egress')) {
|
||||
if (isEgressHeading(titleText)) {
|
||||
egressCharts.push(htbResults.charts[i]);
|
||||
} else if (titleText.includes('Ingress')) {
|
||||
} else if (isIngressHeading(titleText)) {
|
||||
ingressCharts.push(htbResults.charts[i]);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -161,9 +161,11 @@
|
||||
text-align: end !important;
|
||||
}
|
||||
|
||||
/* meter rows — any stock .table row with a .cbi-progressbar. docs/css.md above. */
|
||||
:where(#view)[data-page="admin-status-overview"] .cbi-section .table:not(:has(.tr.table-titles)):has(.cbi-progressbar) { display: block; }
|
||||
:where(#view)[data-page="admin-status-overview"] .cbi-section .table:not(:has(.tr.table-titles)) .tr:has(.cbi-progressbar) {
|
||||
/* meter rows — a key/value-shaped .table row with a .cbi-progressbar (same shape test as
|
||||
* :218 below and 30-tables.css's key/value rule); a data table's own bar stays untouched.
|
||||
* docs/css.md above. */
|
||||
:where(#view)[data-page="admin-status-overview"] .cbi-section .table:not(:has(.th)):not(:has(.td:nth-child(3))):has(.cbi-progressbar) { display: block; }
|
||||
:where(#view)[data-page="admin-status-overview"] .cbi-section .table:not(:has(.th)):not(:has(.td:nth-child(3))) .tr:has(.cbi-progressbar) {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: var(--fs-space-1) var(--fs-space-2-5);
|
||||
@@ -172,8 +174,8 @@
|
||||
border: 0;
|
||||
position: relative;
|
||||
}
|
||||
:where(#view)[data-page="admin-status-overview"] .cbi-section .table:not(:has(.tr.table-titles)) .tr:has(.cbi-progressbar):last-child { padding-bottom: var(--fs-space-0-5); }
|
||||
:where(#view)[data-page="admin-status-overview"] .cbi-section .table:not(:has(.tr.table-titles)) .tr:has(.cbi-progressbar) .td {
|
||||
:where(#view)[data-page="admin-status-overview"] .cbi-section .table:not(:has(.th)):not(:has(.td:nth-child(3))) .tr:has(.cbi-progressbar):last-child { padding-bottom: var(--fs-space-0-5); }
|
||||
:where(#view)[data-page="admin-status-overview"] .cbi-section .table:not(:has(.th)):not(:has(.td:nth-child(3))) .tr:has(.cbi-progressbar) .td {
|
||||
display: block;
|
||||
/* beats base's .td[width="33%"] with no flag: that sets a width ATTRIBUTE, a later layer wins */
|
||||
width: auto;
|
||||
@@ -182,7 +184,7 @@
|
||||
}
|
||||
/* label — undoes key/value nowrap: a mount point is one unbreakable line past the card.
|
||||
* padding-inline-end is the gutter for the out-of-flow value (::after over the bar's edge). */
|
||||
:where(#view)[data-page="admin-status-overview"] .cbi-section .table:not(:has(.tr.table-titles)) .tr:has(.cbi-progressbar) .td:first-child {
|
||||
:where(#view)[data-page="admin-status-overview"] .cbi-section .table:not(:has(.th)):not(:has(.td:nth-child(3))) .tr:has(.cbi-progressbar) .td:first-child {
|
||||
grid-column: 1; grid-row: 1;
|
||||
align-self: center;
|
||||
min-width: 0;
|
||||
@@ -191,32 +193,40 @@
|
||||
font-size: var(--fs-type);
|
||||
}
|
||||
|
||||
/* value rides the label's own line at desktop (::after floats over the row's top edge) —
|
||||
* opt out of the bare-meter default reserve. <=560px below restores it (own line, no room). */
|
||||
:where(#view)[data-page="admin-status-overview"] .cbi-section .table:not(:has(.th)):not(:has(.td:nth-child(3))) .tr:has(.cbi-progressbar) .cbi-progressbar {
|
||||
margin-block-start: 0;
|
||||
}
|
||||
|
||||
/* narrow card: the value takes its own line, the reserve above cannot fit it — docs/css.md
|
||||
* above has the 218px/190px measurement. */
|
||||
@container fs-view (max-width: 560px) {
|
||||
:where(#view)[data-page="admin-status-overview"] .cbi-section .table:not(:has(.tr.table-titles)) .tr:has(.cbi-progressbar) .td:first-child {
|
||||
:where(#view)[data-page="admin-status-overview"] .cbi-section .table:not(:has(.th)):not(:has(.td:nth-child(3))) .tr:has(.cbi-progressbar) .td:first-child {
|
||||
padding-inline-end: 0;
|
||||
}
|
||||
:where(#view)[data-page="admin-status-overview"] .cbi-section .table:not(:has(.tr.table-titles)) .tr:has(.cbi-progressbar) .cbi-progressbar {
|
||||
:where(#view)[data-page="admin-status-overview"] .cbi-section .table:not(:has(.th)):not(:has(.td:nth-child(3))) .tr:has(.cbi-progressbar) .cbi-progressbar {
|
||||
margin-block-start: calc(var(--fs-type-xs) * var(--fs-leading));
|
||||
}
|
||||
}
|
||||
:where(#view)[data-page="admin-status-overview"] .cbi-section .table:not(:has(.tr.table-titles)) .tr:has(.cbi-progressbar) .td:nth-child(2) {
|
||||
:where(#view)[data-page="admin-status-overview"] .cbi-section .table:not(:has(.th)):not(:has(.td:nth-child(3))) .tr:has(.cbi-progressbar) .td:nth-child(2) {
|
||||
grid-column: 1 / -1; grid-row: 2;
|
||||
}
|
||||
|
||||
/* per-row dividers dropped, keyed on table SHAPE (no header row) not on the three named cards —
|
||||
* docs/css.md above. A DATA table (.tr.table-titles present) keeps its dividers. */
|
||||
:where(#view)[data-page="admin-status-overview"] .cbi-section .table:not(:has(.tr.table-titles)) .td,
|
||||
:where(#view)[data-page="admin-status-overview"] .cbi-section .table:not(:has(.tr.table-titles)) .th,
|
||||
:where(#view)[data-page="admin-status-overview"] .cbi-section .table:not(:has(.tr.table-titles)) .tr { border-bottom: 0; }
|
||||
/* per-row dividers dropped, keyed on table SHAPE (no .th, no 3rd .td — same test as
|
||||
* 30-tables.css's key/value rule, docs/css.md "The key/value discriminator") not on the
|
||||
* three named cards. attendedsysupgrade's 11_upgrades.js (a stock header row) keeps its
|
||||
* dividers under this test. */
|
||||
:where(#view)[data-page="admin-status-overview"] .cbi-section .table:not(:has(.th)):not(:has(.td:nth-child(3))) .td,
|
||||
:where(#view)[data-page="admin-status-overview"] .cbi-section .table:not(:has(.th)):not(:has(.td:nth-child(3))) .th,
|
||||
:where(#view)[data-page="admin-status-overview"] .cbi-section .table:not(:has(.th)):not(:has(.td:nth-child(3))) .tr { border-bottom: 0; }
|
||||
/* own rule, not a 4th line above: .cbi-value carries no :has(), and a selector list is not
|
||||
* forgiving below the browser floor (npm run css-floor) */
|
||||
:where(#view)[data-page="admin-status-overview"] .cbi-section .cbi-value { border-bottom: 0; }
|
||||
|
||||
/* key/value name is the QUIET half: --fs-dim at normal weight, matching what the System table's
|
||||
* first column already resolved to. Data tables excluded by the same header-row test above. */
|
||||
:where(#view)[data-page="admin-status-overview"] .cbi-section .table:not(:has(.tr.table-titles)) .td:first-child {
|
||||
* first column already resolved to. Data tables excluded by the same shape test above. */
|
||||
:where(#view)[data-page="admin-status-overview"] .cbi-section .table:not(:has(.th)):not(:has(.td:nth-child(3))) .td:first-child {
|
||||
font-weight: var(--fs-weight-normal);
|
||||
color: var(--fs-dim);
|
||||
}
|
||||
@@ -240,16 +250,19 @@
|
||||
/* progressbar's attr(title) was --fs-type-xs/--fs-dim beside a now-demoted label; give Memory's
|
||||
* bars something at full strength. Overview only — on Software the value sits beside its own
|
||||
* field label. */
|
||||
:where(#view)[data-page="admin-status-overview"] .cbi-section .table:not(:has(.tr.table-titles)) .cbi-progressbar::after {
|
||||
:where(#view)[data-page="admin-status-overview"] .cbi-section .table:not(:has(.th)):not(:has(.td:nth-child(3))) .cbi-progressbar::after {
|
||||
font-size: var(--fs-type);
|
||||
color: var(--fs-text);
|
||||
}
|
||||
|
||||
/* row height in DATA tables (DHCP leases, station list): theme/30-tables.css pads for a page
|
||||
* that IS a table, here it's a card read at a glance — 8 leases measured 740px against a 440px
|
||||
* System card. One step down the space ladder, only here. */
|
||||
:where(#view)[data-page="admin-status-overview"] .cbi-section .table:has(.tr.table-titles) .td,
|
||||
:where(#view)[data-page="admin-status-overview"] .cbi-section .table:has(.tr.table-titles) .th {
|
||||
/* row height in DATA tables (DHCP leases, station list, attendedsysupgrade's 11_upgrades.js):
|
||||
* theme/30-tables.css pads for a page that IS a table, here it's a card read at a glance — 8
|
||||
* leases measured 740px against a 440px System card. Positive test is the complement of the
|
||||
* key/value shape above (:has(.th) or a 3rd .td), not .tr.table-titles — a stock header .th
|
||||
* emitted through initFromMarkup never carries that class. One step down the space ladder,
|
||||
* only here. */
|
||||
:where(#view)[data-page="admin-status-overview"] .cbi-section .table:is(:has(.th), :has(.td:nth-child(3))) .td,
|
||||
:where(#view)[data-page="admin-status-overview"] .cbi-section .table:is(:has(.th), :has(.td:nth-child(3))) .th {
|
||||
padding-block: var(--fs-space-1-5);
|
||||
}
|
||||
|
||||
|
||||
@@ -31,4 +31,9 @@
|
||||
:where(#view)[data-page="admin-system-package-manager"] label:has(+ .cbi-progressbar) {
|
||||
display: block; margin: 0; font-size: var(--fs-type); font-weight: var(--fs-weight); color: var(--fs-dim);
|
||||
}
|
||||
/* value rides the "Disk space:" label's line at both widths — one unconditional opt-out of
|
||||
* the bare-meter reserve; theme/90-responsive.css's phone rule only adds margin-bottom. */
|
||||
:where(#view)[data-page="admin-system-package-manager"] label + .cbi-progressbar {
|
||||
margin-block-start: 0;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
@layer theme {
|
||||
/* One thin meter for every .cbi-progressbar: track + accent fill, title value floated above
|
||||
* the right edge instead of stuffed inside a fat pill. */
|
||||
* the right edge instead of stuffed inside a fat pill. Default reserves that value's own line
|
||||
* in flow (bootstrap's margin:1.4em model) — an abspos value with zero reserve spilled 5-6px
|
||||
* UP across the divider of the row above, off the overview page (key/value .table, no header
|
||||
* row). Contexts that put the value BESIDE the bar or on a neighbour's line opt out with
|
||||
* margin-block-start: 0. */
|
||||
.cbi-progressbar {
|
||||
position: relative;
|
||||
height: 10px;
|
||||
@@ -10,6 +14,7 @@
|
||||
overflow: visible;
|
||||
min-width: 120px;
|
||||
margin: 0;
|
||||
margin-block-start: calc(var(--fs-type-xs) * var(--fs-leading));
|
||||
}
|
||||
.cbi-progressbar > div {
|
||||
height: 100%;
|
||||
@@ -39,6 +44,7 @@
|
||||
.cbi-value-field > .cbi-progressbar {
|
||||
/* @mirror meter/beside */
|
||||
margin-inline-end: var(--fs-meter-value);
|
||||
margin-block-start: 0;
|
||||
/* @endmirror */
|
||||
--fs-meter-value: calc(var(--fs-space-10) * 2);
|
||||
}
|
||||
@@ -51,6 +57,7 @@
|
||||
.cbi-section .table:has(.tr.table-titles) .td .cbi-progressbar {
|
||||
/* @mirror meter/beside */
|
||||
margin-inline-end: var(--fs-meter-value);
|
||||
margin-block-start: 0;
|
||||
/* @endmirror */
|
||||
--fs-meter-value: var(--fs-space-10);
|
||||
min-width: 60px; /* narrower and it stops reading as a meter; the column gives up the rest */
|
||||
|
||||
@@ -151,34 +151,36 @@
|
||||
}
|
||||
|
||||
/* --- KEY/VALUE: "no id" is not enough — GridSection also renders no-id .cbi-section-table,
|
||||
* excluded along with .fs-dt and the legend's inline table-layout. */
|
||||
.cbi-section .table:not([id]):not(.cbi-section-table):not(.fs-dt):not(:has(.tr.table-titles)):not([style*="table-layout"]) .td:first-child {
|
||||
* excluded along with .fs-dt and the legend's inline table-layout. Shape, not a class, says
|
||||
* data table: any .th or a 3rd .td rules a table out — a real key/value row is exactly 2 .td
|
||||
* and no .th. docs/css.md, "The key/value discriminator". */
|
||||
.cbi-section .table:not([id]):not(.cbi-section-table):not(.fs-dt):not(:has(.th)):not(:has(.td:nth-child(3))):not([style*="table-layout"]) .td:first-child {
|
||||
color: var(--fs-dim); font-weight: var(--fs-weight); width: 40%; white-space: nowrap;
|
||||
}
|
||||
.cbi-section .table:not([id]):not(.cbi-section-table):not(.fs-dt):not(:has(.tr.table-titles)):not([style*="table-layout"]) .td:last-child:not(:first-child) {
|
||||
.cbi-section .table:not([id]):not(.cbi-section-table):not(.fs-dt):not(:has(.th)):not(:has(.td:nth-child(3))):not([style*="table-layout"]) .td:last-child:not(:first-child) {
|
||||
font-family: var(--fs-font-mono); color: var(--fs-text); text-align: end;
|
||||
}
|
||||
/* below ~560px the 40%/nowrap column starves the value — stacked below (560, not the config
|
||||
* table's 960px). :first-child:first-child doubles the pseudo-class ON PURPOSE, to outrank. */
|
||||
@container fs-content (max-width: 560px) {
|
||||
/* the row's own .td/.th padding above spent a FULL padding per half; moved to the ROW */
|
||||
.cbi-section .table:not([id]):not(.cbi-section-table):not(.fs-dt):not(:has(.tr.table-titles)):not([style*="table-layout"]) .tr {
|
||||
.cbi-section .table:not([id]):not(.cbi-section-table):not(.fs-dt):not(:has(.th)):not(:has(.td:nth-child(3))):not([style*="table-layout"]) .tr {
|
||||
display: block;
|
||||
padding: var(--fs-space-1) 0 var(--fs-space-1-5);
|
||||
border: 0;
|
||||
border-bottom: var(--fs-hairline);
|
||||
}
|
||||
.cbi-section .table:not([id]):not(.cbi-section-table):not(.fs-dt):not(:has(.tr.table-titles)):not([style*="table-layout"]) .tr:last-child {
|
||||
.cbi-section .table:not([id]):not(.cbi-section-table):not(.fs-dt):not(:has(.th)):not(:has(.td:nth-child(3))):not([style*="table-layout"]) .tr:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
.cbi-section .table:not([id]):not(.cbi-section-table):not(.fs-dt):not(:has(.tr.table-titles)):not([style*="table-layout"]) .td:first-child:first-child {
|
||||
.cbi-section .table:not([id]):not(.cbi-section-table):not(.fs-dt):not(:has(.th)):not(:has(.td:nth-child(3))):not([style*="table-layout"]) .td:first-child:first-child {
|
||||
display: block; width: auto; white-space: normal;
|
||||
padding: 0; border: 0;
|
||||
font-size: var(--fs-type-xs); font-weight: var(--fs-eyebrow-weight);
|
||||
letter-spacing: var(--fs-eyebrow-tracking); color: var(--fs-eyebrow-color);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.cbi-section .table:not([id]):not(.cbi-section-table):not(.fs-dt):not(:has(.tr.table-titles)):not([style*="table-layout"]) .td:last-child:last-child:not(:first-child) {
|
||||
.cbi-section .table:not([id]):not(.cbi-section-table):not(.fs-dt):not(:has(.th)):not(:has(.td:nth-child(3))):not([style*="table-layout"]) .td:last-child:last-child:not(:first-child) {
|
||||
display: block; width: auto; text-align: start;
|
||||
padding: 0; border: 0;
|
||||
}
|
||||
|
||||
+16
-13
@@ -13,12 +13,12 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=wwand-ipa
|
||||
PKG_VERSION:=0.1.0
|
||||
PKG_RELEASE:=1
|
||||
PKG_VERSION:=0.1.0_p1
|
||||
PKG_RELEASE:=2
|
||||
|
||||
PKG_SOURCE_PROTO:=git
|
||||
PKG_SOURCE_URL:=https://github.com/ddimension/wwand-ipa.git
|
||||
PKG_SOURCE_VERSION:=fc0fad051213f23d10864f26ebc7205ae5c0a451
|
||||
PKG_SOURCE_VERSION:=f84625427503041472ba10f32df4969091a86f17
|
||||
PKG_MIRROR_HASH:=skip
|
||||
|
||||
PKG_LICENSE:=GPL-2.0-only
|
||||
@@ -34,21 +34,24 @@ define Package/wwand-ipa
|
||||
CATEGORY:=Network
|
||||
SUBMENU:=WWAN
|
||||
TITLE:=eSIM fleet management (SGP.32 eIM) for wwand
|
||||
# wwand-esim: its bridge carries the assistant's APDUs; wwand-ipad: the
|
||||
# assistant itself (onomondo-ipa with the stdio card backend)
|
||||
# wwand-esim: its bridge carries the assistant's APDUs and runs direct
|
||||
# downloads; wwand-ipad: the assistant itself (ipad, SGP.32 v1.3)
|
||||
DEPENDS:=+wwand-esim +wwand-ipad
|
||||
PKGARCH:=all
|
||||
endef
|
||||
|
||||
define Package/wwand-ipa/description
|
||||
Lets an eIM manage the modem's eSIM: wwand polls it (after the connection
|
||||
comes up, then every ipa_interval seconds) with the onomondo-ipa assistant,
|
||||
relays the assistant's card traffic over the modem's APDU channel, and after
|
||||
a profile change resets the SIM (or the modem) and waits for the session on
|
||||
the new profile before the result is reported. Runs against an SGP.22
|
||||
consumer eUICC (the assistant's IoT eUICC emulation); the eIM must accept
|
||||
that mode. Enable per modem with `wwandctl ipa [modem] eim <file>`. While
|
||||
enabled, manual profile changes on that card are refused unless forced.
|
||||
Lets an eIM manage the modem's eSIM (GSMA SGP.32): wwand polls it (after
|
||||
the connection comes up, then every ipa_interval seconds) with the ipad
|
||||
assistant, relays the assistant's card traffic over the modem's APDU
|
||||
channel, resets the SIM (or the modem) after a profile change and waits for
|
||||
the session on the new profile, runs direct downloads through lpac, and
|
||||
writes the enabled profile's APN into a wwand_sim section for its ICCID.
|
||||
Drives an IoT eUICC, or an SGP.22 consumer eUICC through ipad's emulation,
|
||||
whose results are signed with a device key the eIM imports
|
||||
(`wwandctl ipa export`). Enable per modem with `wwandctl ipa [modem] eim
|
||||
<file>`. While enabled, manual profile changes on that card are refused
|
||||
unless forced.
|
||||
endef
|
||||
|
||||
define Package/luci-app-wwand-ipa
|
||||
|
||||
+48
-116
@@ -1,154 +1,86 @@
|
||||
# onomondo-ipa, the GSMA SGP.32 IoT Profile Assistant, built for wwand-ipa.
|
||||
# ipad, the GSMA SGP.32 v1.3 IoT Profile Assistant, built for wwand-ipa.
|
||||
#
|
||||
# The assistant polls an eIM (the fleet server) for eUICC packages and runs
|
||||
# them on the card. Upstream reaches the card through PC/SC; patch 100 adds a
|
||||
# card backend that speaks lpac's stdio APDU protocol instead, so wwand's
|
||||
# esim_bridge relays its APDUs over the modem's own channel (QMI UIM / MBIM
|
||||
# UICC / AT) exactly as it does for lpac, and adds -H: a profile change is
|
||||
# handed to the host, which resets the SIM and waits for the new session. The
|
||||
# patch carries a test for the card backend (tests/scard_stdio). The package
|
||||
# build does not run it (BUILD_TESTING=OFF, target ipa only); on a host:
|
||||
# cmake -S . -B build -DIPA_SCARD_STDIO=ON && cmake --build build && ctest --test-dir build
|
||||
# The -H wiring in main.c has no test of its own: exercising it takes an eIM
|
||||
# that sends a profile change.
|
||||
# It polls an eIM (the fleet server) for eUICC packages and runs them on the
|
||||
# card, which it reaches through lpac's stdio APDU protocol: wwand's
|
||||
# esim_bridge relays those APDUs over the modem's own channel (QMI UIM / MBIM
|
||||
# UICC / AT), exactly as for lpac. An IoT eUICC is driven as it is; an SGP.22
|
||||
# consumer eUICC through ipad's emulation of the SGP.32 functions, with its
|
||||
# results signed by a device key the eIM imports (`wwandctl ipa export`).
|
||||
#
|
||||
# TLS: the same minimal static wolfSSL + libcurl bundle as wwand-lpac, for the
|
||||
# same reason (OpenWrt's mbedtls lacks curves the GSMA PKI uses, and
|
||||
# libcurl+OpenSSL is ~6.4 MB). The eIM is reached over HTTPS with the system
|
||||
# CA bundle unless the eIM configuration says otherwise.
|
||||
#
|
||||
# Upstream implements SGP.32 v1.0; wwand always runs it in IoT eUICC emulation
|
||||
# (-E), i.e. against an SGP.22 consumer eUICC. See wwand's src-ucode/ipa.uc.
|
||||
# mbedTLS 3.6 LTS is built in statically: RSP needs brainpoolP256r1, which
|
||||
# OpenWrt's shared mbedtls does not enable by default. The project's tests
|
||||
# (unit, a scripted eIM, the real binary over the stdio protocol) are not run
|
||||
# by the package build; see the ipad README.
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=wwand-ipad
|
||||
PKG_VERSION:=0.0.1_git20260901
|
||||
PKG_RELEASE:=1
|
||||
PKG_VERSION:=0.1.0
|
||||
PKG_RELEASE:=2
|
||||
|
||||
PKG_SOURCE_PROTO:=git
|
||||
PKG_SOURCE_URL:=https://github.com/onomondo/onomondo-ipa.git
|
||||
PKG_SOURCE_VERSION:=6aaeb38a569e648c8305aa5bd4d300cc1ea6822a
|
||||
PKG_SOURCE_URL:=https://github.com/ddimension/ipad.git
|
||||
PKG_SOURCE_VERSION:=fbb175f4d272f0db45b3bc006392f172cd248aa5
|
||||
PKG_MIRROR_HASH:=skip
|
||||
|
||||
PKG_LICENSE:=AGPL-3.0-only curl GPL-3.0-or-later
|
||||
PKG_LICENSE:=GPL-2.0-only Apache-2.0
|
||||
PKG_LICENSE_FILES:=LICENSE
|
||||
PKG_MAINTAINER:=
|
||||
|
||||
# bundled, statically-linked dependencies (keep in step with wwand-lpac)
|
||||
WOLFSSL_VERSION:=5.9.2-stable
|
||||
CURL_VERSION:=8.21.0
|
||||
MBEDTLS_VERSION:=3.6.7
|
||||
|
||||
PKG_BUILD_PARALLEL:=1
|
||||
|
||||
# wolfSSL's SP math carries inline assembly that MIPS16 cannot encode
|
||||
PKG_BUILD_FLAGS:=no-mips16
|
||||
CMAKE_BINARY_SUBDIR:=openwrt-build
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
include $(INCLUDE_DIR)/cmake.mk
|
||||
|
||||
define Download/wolfssl
|
||||
FILE:=wolfssl-$(WOLFSSL_VERSION).tar.gz
|
||||
URL:=https://github.com/wolfSSL/wolfssl/archive/v$(WOLFSSL_VERSION)
|
||||
# the release tarball; its SHA-256 is the one mbedtls-3.6.7-sha256sum.txt on
|
||||
# the release page publishes
|
||||
define Download/mbedtls
|
||||
FILE:=mbedtls-$(MBEDTLS_VERSION).tar.bz2
|
||||
URL:=https://github.com/Mbed-TLS/mbedtls/releases/download/mbedtls-$(MBEDTLS_VERSION)
|
||||
HASH:=skip
|
||||
endef
|
||||
$(eval $(call Download,wolfssl))
|
||||
$(eval $(call Download,mbedtls))
|
||||
|
||||
define Download/curl
|
||||
FILE:=curl-$(CURL_VERSION).tar.xz
|
||||
URL:=https://github.com/curl/curl/releases/download/curl-8_21_0
|
||||
HASH:=skip
|
||||
define Build/Prepare
|
||||
$(call Build/Prepare/Default)
|
||||
$(TAR) -C $(PKG_BUILD_DIR) -xjf $(DL_DIR)/mbedtls-$(MBEDTLS_VERSION).tar.bz2
|
||||
endef
|
||||
$(eval $(call Download,curl))
|
||||
|
||||
CMAKE_OPTIONS += \
|
||||
-DIPAD_MBEDTLS_DIR=$(PKG_BUILD_DIR)/mbedtls-$(MBEDTLS_VERSION) \
|
||||
-DIPAD_WERROR=OFF \
|
||||
-DBUILD_TESTING=OFF
|
||||
|
||||
TARGET_CFLAGS += -ffunction-sections -fdata-sections
|
||||
TARGET_LDFLAGS += -Wl,--gc-sections
|
||||
|
||||
define Package/wwand-ipad
|
||||
SECTION:=net
|
||||
CATEGORY:=Network
|
||||
SUBMENU:=WWAN
|
||||
TITLE:=SGP.32 IoT Profile Assistant (onomondo-ipa) for wwand-ipa
|
||||
URL:=https://github.com/onomondo/onomondo-ipa
|
||||
# the eIM is an HTTPS server with an ordinary certificate chain
|
||||
TITLE:=SGP.32 v1.3 IoT Profile Assistant (ipad) for wwand-ipa
|
||||
URL:=https://github.com/ddimension/ipad
|
||||
# the eIM is an HTTPS server; without a pin in its configuration the
|
||||
# system CAs decide
|
||||
DEPENDS:=+ca-bundle
|
||||
endef
|
||||
|
||||
define Package/wwand-ipad/description
|
||||
onomondo-ipa, the GSMA SGP.32 IoT Profile Assistant, built with a card
|
||||
backend that reaches the eUICC through wwand (lpac's stdio APDU protocol)
|
||||
instead of PC/SC, and with a minimal static wolfSSL + libcurl linked in.
|
||||
Installed as /usr/lib/wwand/ipad; wwand-ipa runs it. Implements SGP.32 v1.0
|
||||
and is run in IoT eUICC emulation against an SGP.22 consumer eUICC.
|
||||
endef
|
||||
|
||||
PREFIX:=$(PKG_BUILD_DIR)/.bundle
|
||||
WOLFSSL_DIR:=$(PKG_BUILD_DIR)/wolfssl-$(WOLFSSL_VERSION)
|
||||
CURL_DIR:=$(PKG_BUILD_DIR)/curl-$(CURL_VERSION)
|
||||
IPA_BUILD:=$(PKG_BUILD_DIR)/.ipa-build
|
||||
|
||||
LTO_CFLAGS:=-flto=auto -ffat-lto-objects
|
||||
BUNDLE_AR:=AR="$(TARGET_CROSS)gcc-ar" RANLIB="$(TARGET_CROSS)gcc-ranlib"
|
||||
|
||||
# unpack + patch the assistant, then the bundled wolfSSL + curl beside it
|
||||
define Build/Prepare
|
||||
$(call Build/Prepare/Default)
|
||||
$(TAR) -C $(PKG_BUILD_DIR) -xzf $(DL_DIR)/wolfssl-$(WOLFSSL_VERSION).tar.gz
|
||||
$(TAR) -C $(PKG_BUILD_DIR) -xJf $(DL_DIR)/curl-$(CURL_VERSION).tar.xz
|
||||
endef
|
||||
|
||||
Build/Configure:=
|
||||
|
||||
define Build/Compile
|
||||
# 1) minimal static wolfSSL (the wwand-lpac recipe)
|
||||
( cd $(WOLFSSL_DIR) && ./autogen.sh )
|
||||
( cd $(WOLFSSL_DIR) && ./configure --host=$(GNU_TARGET_NAME) --prefix=$(PREFIX) \
|
||||
--enable-static --disable-shared \
|
||||
--enable-curl --enable-tls13 --enable-brainpool --enable-ecccustcurves \
|
||||
--enable-curve25519 --disable-dtls --disable-oldtls \
|
||||
--disable-examples --disable-crypttests \
|
||||
CC="$(TARGET_CC)" $(BUNDLE_AR) \
|
||||
CFLAGS="$(TARGET_CFLAGS) $(LTO_CFLAGS) -ffunction-sections -fdata-sections -DWOLFSSL_NO_ASN_STRICT" )
|
||||
+$(MAKE) -C $(WOLFSSL_DIR)
|
||||
$(MAKE) -C $(WOLFSSL_DIR) install
|
||||
|
||||
# 2) minimal static libcurl (HTTP/HTTPS only) against it. HTTP stays: an
|
||||
# eIM configuration may name a plain-HTTP lab eIM (the assistant's -S).
|
||||
( cd $(CURL_DIR) && \
|
||||
PKG_CONFIG_PATH="$(PREFIX)/lib/pkgconfig" PKG_CONFIG_LIBDIR="$(PREFIX)/lib/pkgconfig" \
|
||||
./configure --host=$(GNU_TARGET_NAME) --prefix=$(PREFIX) --with-wolfssl=$(PREFIX) \
|
||||
--enable-static --disable-shared \
|
||||
--disable-ftp --disable-file --disable-ldap --disable-ldaps --disable-rtsp \
|
||||
--disable-dict --disable-telnet --disable-tftp --disable-pop3 --disable-imap \
|
||||
--disable-smtp --disable-gopher --disable-mqtt --disable-smb --disable-ntlm \
|
||||
--disable-manual --disable-unix-sockets --disable-progress-meter \
|
||||
--without-libpsl --without-zlib --without-nghttp2 --without-brotli \
|
||||
--without-zstd --without-libidn2 --without-librtmp --without-ca-path \
|
||||
--with-ca-bundle=/etc/ssl/certs/ca-certificates.crt \
|
||||
CC="$(TARGET_CC)" $(BUNDLE_AR) \
|
||||
CFLAGS="$(TARGET_CFLAGS) $(LTO_CFLAGS) -ffunction-sections -fdata-sections -DSIZEOF_LONG_LONG=8" \
|
||||
CPPFLAGS="-I$(PREFIX)/include" LDFLAGS="-L$(PREFIX)/lib" LIBS="-lm" )
|
||||
+$(MAKE) -C $(CURL_DIR)
|
||||
$(MAKE) -C $(CURL_DIR) install
|
||||
|
||||
# 3) the assistant with the stdio card backend, statically against both.
|
||||
# AR/RANLIB go in as full paths: CMake took the bare name as a path relative
|
||||
# to its build directory and wrote `.ipa-build/aarch64-openwrt-linux-musl-gcc-ar`
|
||||
# into libasn's link.txt, which failed with "Error running link command: no
|
||||
# such file or directory" (SDK snapshot, aarch64, 2026-09-26).
|
||||
rm -rf $(IPA_BUILD)
|
||||
mkdir -p $(IPA_BUILD)
|
||||
( cd $(IPA_BUILD) && cmake $(PKG_BUILD_DIR) \
|
||||
-DCMAKE_SYSTEM_NAME=Linux -DCMAKE_SYSTEM_PROCESSOR=$(ARCH) \
|
||||
-DCMAKE_C_COMPILER="$(TARGET_CROSS)gcc" \
|
||||
-DCMAKE_AR="$(TOOLCHAIN_DIR)/bin/$(TARGET_CROSS)gcc-ar" \
|
||||
-DCMAKE_RANLIB="$(TOOLCHAIN_DIR)/bin/$(TARGET_CROSS)gcc-ranlib" \
|
||||
-DCMAKE_C_FLAGS="$(TARGET_CFLAGS) $(LTO_CFLAGS) -ffunction-sections -fdata-sections -I$(PREFIX)/include" \
|
||||
-DCMAKE_EXE_LINKER_FLAGS="$(TARGET_LDFLAGS) $(LTO_CFLAGS) -Wl,--gc-sections -L$(PREFIX)/lib" \
|
||||
-DCMAKE_C_STANDARD_LIBRARIES="-L$(PREFIX)/lib -lwolfssl -lm" \
|
||||
-DIPA_SCARD_STDIO=ON -DBUILD_TESTING=OFF )
|
||||
+$(MAKE) -C $(IPA_BUILD) ipa
|
||||
ipad, a GSMA SGP.32 v1.3 IoT Profile Assistant: eIM packages (every PSMO
|
||||
and eCO), profile rollback, IPA/eUICC data, direct and indirect profile
|
||||
download, notifications and the connectivity parameters of the enabled
|
||||
profile. Drives an IoT eUICC, or an SGP.22 consumer eUICC through an
|
||||
emulation that signs its results with a device key the eIM imports.
|
||||
Reaches the card through wwand (lpac's stdio APDU protocol); mbedTLS
|
||||
linked in statically. Installed as /usr/lib/wwand/ipad; wwand-ipa runs it.
|
||||
endef
|
||||
|
||||
define Package/wwand-ipad/install
|
||||
$(INSTALL_DIR) $(1)/usr/lib/wwand
|
||||
$(INSTALL_BIN) $(IPA_BUILD)/src/ipa/ipa $(1)/usr/lib/wwand/ipad
|
||||
$(INSTALL_BIN) $(PKG_BUILD_DIR)/$(CMAKE_BINARY_SUBDIR)/ipad $(1)/usr/lib/wwand/ipad
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,wwand-ipad))
|
||||
@@ -1,833 +0,0 @@
|
||||
diff --git a/include/onomondo/ipa/scard_stdio.h b/include/onomondo/ipa/scard_stdio.h
|
||||
new file mode 100644
|
||||
index 0000000..fa9ec54
|
||||
--- /dev/null
|
||||
+++ b/include/onomondo/ipa/scard_stdio.h
|
||||
@@ -0,0 +1,13 @@
|
||||
+/*
|
||||
+ * Copyright (c) 2026 André Valentin <avalentin@marcant.net>
|
||||
+ *
|
||||
+ * SPDX-License-Identifier: AGPL-3.0-only
|
||||
+ */
|
||||
+
|
||||
+#pragma once
|
||||
+
|
||||
+/*! Hand a profile change to the host and wait until it has restored the data
|
||||
+ * connection, then re-open the card channel the host's SIM reset closed.
|
||||
+ * \returns 1 when the host reports the connection back, 0 when it gave up
|
||||
+ * waiting, negative when the host is gone. */
|
||||
+int ipa_scard_stdio_await_online(void);
|
||||
diff --git a/src/ipa/CMakeLists.txt b/src/ipa/CMakeLists.txt
|
||||
index 2cf4a2b..b4ee0e5 100644
|
||||
--- a/src/ipa/CMakeLists.txt
|
||||
+++ b/src/ipa/CMakeLists.txt
|
||||
@@ -5,7 +5,15 @@ add_subdirectory(libasn)
|
||||
add_subdirectory(libipa)
|
||||
|
||||
add_library(http STATIC http.c)
|
||||
-add_library(scard STATIC scard.c)
|
||||
+option(IPA_SCARD_STDIO "Reach the eUICC through a host process (lpac stdio protocol) instead of PC/SC" OFF)
|
||||
+if (IPA_SCARD_STDIO)
|
||||
+ add_library(scard STATIC scard_stdio.c)
|
||||
+ target_compile_definitions(scard PUBLIC IPA_SCARD_STDIO)
|
||||
+ set(IPA_SCARD_LIBS "")
|
||||
+else()
|
||||
+ add_library(scard STATIC scard.c)
|
||||
+ set(IPA_SCARD_LIBS pcsclite)
|
||||
+endif()
|
||||
|
||||
target_include_directories(http PUBLIC ${CMAKE_SOURCE_DIR}/include)
|
||||
target_include_directories(scard PUBLIC ${CMAKE_SOURCE_DIR}/include)
|
||||
@@ -16,7 +24,7 @@ set_property(TARGET ipa PROPERTY C_STANDARD 99)
|
||||
target_compile_options(ipa PRIVATE -Wall)
|
||||
|
||||
target_include_directories(ipa PUBLIC ${CMAKE_SOURCE_DIR}/include)
|
||||
-target_link_libraries(ipa libipa http scard pcsclite curl)
|
||||
+target_link_libraries(ipa libipa http scard ${IPA_SCARD_LIBS} curl)
|
||||
|
||||
if (M32)
|
||||
set_target_properties(http PROPERTIES COMPILE_FLAGS "-m32" LINK_FLAGS "-m32")
|
||||
diff --git a/src/ipa/main.c b/src/ipa/main.c
|
||||
index d4316f2..5dfa703 100644
|
||||
--- a/src/ipa/main.c
|
||||
+++ b/src/ipa/main.c
|
||||
@@ -17,6 +17,9 @@
|
||||
#include <onomondo/ipa/utils.h>
|
||||
#include <onomondo/ipa/log.h>
|
||||
#include <onomondo/ipa/ipad.h>
|
||||
+#ifdef IPA_SCARD_STDIO
|
||||
+#include <onomondo/ipa/scard_stdio.h>
|
||||
+#endif
|
||||
|
||||
#define DEFAULT_READER_NUMBER 0
|
||||
#define DEFAULT_CHANNEL_NUMBER 1
|
||||
@@ -58,6 +61,9 @@ static void print_help(void)
|
||||
printf(" -I .................. disable SSL certificate verification (insecure)\n");
|
||||
printf(" -E .................. emulate IoT eUICC (compatibility mode to use consumer eUICCs)\n");
|
||||
printf(" -1 .................. force the IPAd to process only one eUICC package (debug, use with caution)\n");
|
||||
+#ifdef IPA_SCARD_STDIO
|
||||
+ printf(" -H .................. hand profile changes to the host and wait for it to restore connectivity\n");
|
||||
+#endif
|
||||
}
|
||||
|
||||
struct ipa_buf *load_ber_from_file(char *dir, char *file)
|
||||
@@ -134,6 +140,22 @@ static void sig_usr1(int signum)
|
||||
running = false;
|
||||
}
|
||||
|
||||
+#ifdef IPA_SCARD_STDIO
|
||||
+/* The host ends a run it has given up on with SIGTERM. Without a handler that
|
||||
+ * kills the process before ipa_free_ctx() hands back the nvstate, and in IoT
|
||||
+ * eUICC emulation the nvstate IS the eIM configuration and the replay counter.
|
||||
+ * No SA_RESTART: a read blocked on the host must return, so the card layer
|
||||
+ * fails the current exchange and main() reaches the save below. */
|
||||
+static void install_sigterm(void)
|
||||
+{
|
||||
+ struct sigaction sa = { 0 };
|
||||
+
|
||||
+ sa.sa_handler = sig_usr1;
|
||||
+ sigemptyset(&sa.sa_mask);
|
||||
+ sigaction(SIGTERM, &sa, NULL);
|
||||
+}
|
||||
+#endif
|
||||
+
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
struct ipa_config cfg = { 0 };
|
||||
@@ -146,8 +168,12 @@ int main(int argc, char **argv)
|
||||
struct ipa_buf *nvstate_load = NULL;
|
||||
struct ipa_buf *nvstate_save = NULL;
|
||||
bool getopt_one_euicc_pkg_only = false;
|
||||
+ bool getopt_host_online = false;
|
||||
|
||||
signal(SIGUSR1, sig_usr1);
|
||||
+#ifdef IPA_SCARD_STDIO
|
||||
+ install_sigterm();
|
||||
+#endif
|
||||
|
||||
printf("IPAd!\n");
|
||||
|
||||
@@ -159,7 +185,7 @@ int main(int argc, char **argv)
|
||||
|
||||
/* Overwrite configuration values with user defined parameters */
|
||||
while (1) {
|
||||
- opt = getopt(argc, argv, "ht:e:r:c:f:mn:C:SIEy:a1");
|
||||
+ opt = getopt(argc, argv, "ht:e:r:c:f:mn:C:SIEy:a1H");
|
||||
if (opt == -1)
|
||||
break;
|
||||
|
||||
@@ -210,6 +236,9 @@ int main(int argc, char **argv)
|
||||
case '1':
|
||||
getopt_one_euicc_pkg_only = true;
|
||||
break;
|
||||
+ case 'H':
|
||||
+ getopt_host_online = true;
|
||||
+ break;
|
||||
default:
|
||||
printf("unhandled option: %c!\n", opt);
|
||||
break;
|
||||
@@ -284,6 +313,19 @@ int main(int argc, char **argv)
|
||||
case IPA_POLL_AGAIN_WHEN_ONLINE:
|
||||
/* ipa_poll asks us to wait with the next poll cycle until we have a stable IP
|
||||
* connection. In this example we assume that IP connectivity is always available. */
|
||||
+#ifdef IPA_SCARD_STDIO
|
||||
+ /* On a router it is not: the modem keeps the old profile until the host resets
|
||||
+ * the SIM, and the data session has to come back on the new one. The process
|
||||
+ * stays alive meanwhile, because the pending result (and a rollback, should the
|
||||
+ * eIM stay unreachable) lives in the context, not in the nvstate. Whatever the
|
||||
+ * host answers, the next ipa_poll is the one that decides: it reports to the
|
||||
+ * eIM, or rolls the profile back when it cannot. */
|
||||
+ if (getopt_host_online && ipa_scard_stdio_await_online() < 0) {
|
||||
+ IPA_LOGP(SMAIN, LERROR, "host went away during a profile change\n");
|
||||
+ rc = -EIO;
|
||||
+ goto leave;
|
||||
+ }
|
||||
+#endif
|
||||
IPA_LOGP(SMAIN, LINFO, "poll cycle continues normally (profile change)\n");
|
||||
rc = 0;
|
||||
break;
|
||||
diff --git a/src/ipa/scard_stdio.c b/src/ipa/scard_stdio.c
|
||||
new file mode 100644
|
||||
index 0000000..5b58991
|
||||
--- /dev/null
|
||||
+++ b/src/ipa/scard_stdio.c
|
||||
@@ -0,0 +1,513 @@
|
||||
+/*
|
||||
+ * Copyright (c) 2026 André Valentin <avalentin@marcant.net>
|
||||
+ *
|
||||
+ * SPDX-License-Identifier: AGPL-3.0-only
|
||||
+ *
|
||||
+ * Smartcard backend for onomondo-ipa that reaches the eUICC through a HOST
|
||||
+ * PROCESS instead of PC/SC: every card operation is one JSON line on stdout,
|
||||
+ * and the answer is one JSON line on stdin. The wire format is lpac's stdio
|
||||
+ * APDU driver (lpac driver/apdu/stdio.c, v2.3.0) byte for byte, so the host
|
||||
+ * side that already bridges lpac -- wwand's esim_bridge, which answers from
|
||||
+ * the modem's own APDU channel (QMI UIM / MBIM UICC / AT) -- bridges this too:
|
||||
+ *
|
||||
+ * -> {"type":"apdu","payload":{"func":"logic_channel_open","param":"<AID>"}}
|
||||
+ * <- {"type":"apdu","payload":{"ecode":<channel or -1>}}
|
||||
+ * -> {"type":"apdu","payload":{"func":"transmit","param":"<C-APDU>"}}
|
||||
+ * <- {"type":"apdu","payload":{"ecode":0,"data":"<R-APDU incl. SW>"}}
|
||||
+ *
|
||||
+ * The two sides do not speak the same card dialect, and this file is where
|
||||
+ * that is reconciled:
|
||||
+ *
|
||||
+ * - libipa talks raw T=0 (libipa/euicc.c): TERMINAL CAPABILITY on the basic
|
||||
+ * channel, MANAGE CHANNEL to open one, SELECT ISD-R on it and then STORE
|
||||
+ * DATA / GET RESPONSE with the channel number in CLA. A modem's APDU
|
||||
+ * service does not let the host do any of that directly: QMI UIM opens a
|
||||
+ * logical channel BY AID (OPEN_LOGICAL_CHANNEL, which selects it in the
|
||||
+ * same step) and addresses the channel by id. So MANAGE CHANNEL is
|
||||
+ * answered here, the channel is really opened when the SELECT names the
|
||||
+ * AID, and CLA is rewritten to the channel the modem handed out.
|
||||
+ * - The modem completes GET RESPONSE itself and returns the whole answer with
|
||||
+ * 9000 (the QMI UIM long-response path in wwand's sim.uc reassembles even
|
||||
+ * the chunked ones). libipa insists on seeing 61xx after the last STORE
|
||||
+ * DATA block and fetching with GET RESPONSE, and it rejects a block whose
|
||||
+ * length differs from what 61xx announced (euicc.c recv_es10x_block). So a
|
||||
+ * response with data is held here and handed out again in 61xx/GET
|
||||
+ * RESPONSE portions of exactly the announced size.
|
||||
+ *
|
||||
+ * Logging goes to stderr (libipa's log.c already writes there), so stdout
|
||||
+ * carries only the protocol. A host may still merge the two, as wwand does
|
||||
+ * (2>&1, into its syslog): every log line starts with the space-padded
|
||||
+ * subsystem ("%8s %8s ", log.c:69), never with the '{' a protocol line starts
|
||||
+ * with, so the two stay apart.
|
||||
+ */
|
||||
+
|
||||
+#include <stdio.h>
|
||||
+#include <stdlib.h>
|
||||
+#include <stdbool.h>
|
||||
+#include <stdint.h>
|
||||
+#include <string.h>
|
||||
+#include <errno.h>
|
||||
+#include <assert.h>
|
||||
+#include <onomondo/ipa/utils.h>
|
||||
+#include <onomondo/ipa/scard.h>
|
||||
+#include <onomondo/ipa/scard_stdio.h>
|
||||
+#include <onomondo/ipa/log.h>
|
||||
+#include <onomondo/ipa/mem.h>
|
||||
+
|
||||
+/* ES10 answers (a profile list, certificates) reach several kB; one line
|
||||
+ * carries the hex of the largest plus the JSON around it */
|
||||
+#define RX_MAX_LEN 32768
|
||||
+#define LINE_MAX_LEN (2 * RX_MAX_LEN + 128)
|
||||
+#define AID_MAX_LEN 16
|
||||
+
|
||||
+struct scard_ctx {
|
||||
+ /* the AID libipa selected, kept so the channel can be re-opened after
|
||||
+ * the host has reset the card (see ipa_scard_stdio_await_online) */
|
||||
+ uint8_t aid[AID_MAX_LEN];
|
||||
+ size_t aid_len;
|
||||
+
|
||||
+ /* the channel the modem handed out; -1 while none is open */
|
||||
+ int channel;
|
||||
+
|
||||
+ /* a card answer being handed out in GET RESPONSE portions */
|
||||
+ uint8_t *pending;
|
||||
+ size_t pending_len;
|
||||
+ size_t pending_off;
|
||||
+};
|
||||
+
|
||||
+/* One process drives one card, and the connectivity hook below is called from
|
||||
+ * main.c, which only holds libipa's opaque context. */
|
||||
+static struct scard_ctx *g_ctx;
|
||||
+
|
||||
+/* Set once a read from the host failed (EOF, or a SIGTERM interrupting it).
|
||||
+ * Every later exchange fails at once instead of blocking again: the shutdown
|
||||
+ * path still closes the channel and disconnects, and waiting on a host that
|
||||
+ * has gone would keep main() from ever saving the nvstate. */
|
||||
+static bool host_gone;
|
||||
+
|
||||
+static const char hexdigits[] = "0123456789ABCDEF";
|
||||
+
|
||||
+static char *to_hex(const uint8_t *data, size_t len)
|
||||
+{
|
||||
+ char *s = malloc(len * 2 + 1);
|
||||
+ size_t i;
|
||||
+
|
||||
+ assert(s);
|
||||
+ for (i = 0; i < len; i++) {
|
||||
+ s[i * 2] = hexdigits[data[i] >> 4];
|
||||
+ s[i * 2 + 1] = hexdigits[data[i] & 0x0F];
|
||||
+ }
|
||||
+ s[len * 2] = '\0';
|
||||
+ return s;
|
||||
+}
|
||||
+
|
||||
+static int hexval(char c)
|
||||
+{
|
||||
+ if (c >= '0' && c <= '9')
|
||||
+ return c - '0';
|
||||
+ if (c >= 'a' && c <= 'f')
|
||||
+ return c - 'a' + 10;
|
||||
+ if (c >= 'A' && c <= 'F')
|
||||
+ return c - 'A' + 10;
|
||||
+ return -1;
|
||||
+}
|
||||
+
|
||||
+/* Read one line from stdin. Returns false on EOF: the host is gone, and every
|
||||
+ * caller turns that into a card error rather than waiting forever. */
|
||||
+static bool read_line(char *buf, size_t size)
|
||||
+{
|
||||
+ size_t n;
|
||||
+
|
||||
+ if (host_gone)
|
||||
+ return false;
|
||||
+
|
||||
+ if (!fgets(buf, size, stdin)) {
|
||||
+ host_gone = true;
|
||||
+ return false;
|
||||
+ }
|
||||
+
|
||||
+ n = strlen(buf);
|
||||
+
|
||||
+ /* A line longer than the buffer: the rest would be read as the NEXT
|
||||
+ * answer and every later exchange would be off by one. Drain it and
|
||||
+ * report the round as failed instead. */
|
||||
+ if (n == size - 1 && buf[n - 1] != '\n') {
|
||||
+ int c;
|
||||
+
|
||||
+ while ((c = getchar()) != EOF && c != '\n')
|
||||
+ ;
|
||||
+ buf[0] = '\0';
|
||||
+ IPA_LOGP(SSCARD, LERROR, "stdio: answer longer than %zu bytes dropped\n", size);
|
||||
+ return true;
|
||||
+ }
|
||||
+ while (n && (buf[n - 1] == '\n' || buf[n - 1] == '\r'))
|
||||
+ buf[--n] = '\0';
|
||||
+
|
||||
+ return true;
|
||||
+}
|
||||
+
|
||||
+/* The host's answers are produced by wwand (a fixed sprintf) or lpac-compatible
|
||||
+ * bridges, so a field lookup is enough; a full JSON parser would be the only
|
||||
+ * dependency this binary had besides libcurl. */
|
||||
+static const char *field(const char *line, const char *name)
|
||||
+{
|
||||
+ char key[32];
|
||||
+ const char *p;
|
||||
+
|
||||
+ snprintf(key, sizeof(key), "\"%s\"", name);
|
||||
+ p = strstr(line, key);
|
||||
+ if (!p)
|
||||
+ return NULL;
|
||||
+ p += strlen(key);
|
||||
+ while (*p == ' ' || *p == ':')
|
||||
+ p++;
|
||||
+ return p;
|
||||
+}
|
||||
+
|
||||
+/* One request/answer round. `data` receives the decoded "data" field (may be
|
||||
+ * empty); returns the ecode, or -1 on a protocol failure. */
|
||||
+static int roundtrip(const char *func, const uint8_t *param, size_t param_len, struct ipa_buf *data)
|
||||
+{
|
||||
+ static char line[LINE_MAX_LEN];
|
||||
+ char *hex = param_len ? to_hex(param, param_len) : NULL;
|
||||
+ const char *p;
|
||||
+ int ecode;
|
||||
+
|
||||
+ if (host_gone) {
|
||||
+ free(hex);
|
||||
+ return -1;
|
||||
+ }
|
||||
+
|
||||
+ fflush(stdout);
|
||||
+ printf("{\"type\":\"apdu\",\"payload\":{\"func\":\"%s\",\"param\":%s%s%s}}\n",
|
||||
+ func, hex ? "\"" : "", hex ? hex : "null", hex ? "\"" : "");
|
||||
+ fflush(stdout);
|
||||
+ free(hex);
|
||||
+
|
||||
+ if (!read_line(line, sizeof(line))) {
|
||||
+ IPA_LOGP(SSCARD, LERROR, "stdio: host closed the channel during %s\n", func);
|
||||
+ return -1;
|
||||
+ }
|
||||
+
|
||||
+ p = field(line, "ecode");
|
||||
+ if (!p) {
|
||||
+ IPA_LOGP(SSCARD, LERROR, "stdio: malformed answer to %s: %s\n", func, line);
|
||||
+ return -1;
|
||||
+ }
|
||||
+ ecode = atoi(p);
|
||||
+
|
||||
+ if (data) {
|
||||
+ data->len = 0;
|
||||
+ p = field(line, "data");
|
||||
+ if (p && *p == '"') {
|
||||
+ p++;
|
||||
+ while (hexval(p[0]) >= 0 && hexval(p[1]) >= 0) {
|
||||
+ if (data->len >= data->data_len) {
|
||||
+ IPA_LOGP(SSCARD, LERROR, "stdio: answer to %s exceeds %zu bytes\n", func,
|
||||
+ data->data_len);
|
||||
+ return -1;
|
||||
+ }
|
||||
+ data->data[data->len++] = (hexval(p[0]) << 4) | hexval(p[1]);
|
||||
+ p += 2;
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ return ecode;
|
||||
+}
|
||||
+
|
||||
+/* CLA with the channel number in it (ISO/IEC 7816-4 5.4.1). Channels 0-3 sit
|
||||
+ * in bits 1-2 of the first interindustry coding, 4-19 need the further
|
||||
+ * coding: bit 7 set and channel-4 in bits 1-4. Bit 8 (the proprietary class
|
||||
+ * STORE DATA uses) is kept either way. libipa sends no secure messaging, so
|
||||
+ * the SM bits are left clear. lpac writes the low nibble instead
|
||||
+ * (euicc/interface.c, v2.3.0), which is the same for 0-3 and wrong from 4 on;
|
||||
+ * a modem is free to hand out a channel above 3. */
|
||||
+static uint8_t cla_for_channel(uint8_t cla, int channel)
|
||||
+{
|
||||
+ if (channel <= 3)
|
||||
+ return (cla & 0x80) | (uint8_t) channel;
|
||||
+ return (cla & 0x80) | 0x40 | (uint8_t) (channel - 4);
|
||||
+}
|
||||
+
|
||||
+static void drop_pending(struct scard_ctx *ctx)
|
||||
+{
|
||||
+ free(ctx->pending);
|
||||
+ ctx->pending = NULL;
|
||||
+ ctx->pending_len = 0;
|
||||
+ ctx->pending_off = 0;
|
||||
+}
|
||||
+
|
||||
+static void put_sw(struct ipa_buf *res, uint16_t sw)
|
||||
+{
|
||||
+ res->data[res->len++] = sw >> 8;
|
||||
+ res->data[res->len++] = sw & 0xFF;
|
||||
+}
|
||||
+
|
||||
+/* 61xx for what is still held: xx is the size of the next portion, 00 meaning
|
||||
+ * 256 (ISO/IEC 7816-4 and ETSI TS 102 221 10.1.6, which libipa follows). */
|
||||
+static uint16_t pending_sw(const struct scard_ctx *ctx)
|
||||
+{
|
||||
+ size_t left = ctx->pending_len - ctx->pending_off;
|
||||
+
|
||||
+ if (!left)
|
||||
+ return 0x9000;
|
||||
+ return 0x6100 | (left >= 256 ? 0 : (uint8_t) left);
|
||||
+}
|
||||
+
|
||||
+static void close_channel(struct scard_ctx *ctx)
|
||||
+{
|
||||
+ uint8_t ch;
|
||||
+
|
||||
+ if (ctx->channel < 0)
|
||||
+ return;
|
||||
+ ch = (uint8_t) ctx->channel;
|
||||
+ roundtrip("logic_channel_close", &ch, 1, NULL);
|
||||
+ ctx->channel = -1;
|
||||
+}
|
||||
+
|
||||
+static int open_channel(struct scard_ctx *ctx)
|
||||
+{
|
||||
+ int ch = roundtrip("logic_channel_open", ctx->aid, ctx->aid_len, NULL);
|
||||
+
|
||||
+ /* channel 0 is the basic channel, never one the modem hands out for an
|
||||
+ * AID; lpac treats it as a failure the same way */
|
||||
+ if (ch <= 0) {
|
||||
+ IPA_LOGP(SSCARD, LERROR, "stdio: could not open a logical channel to %s (ecode %d)\n",
|
||||
+ ipa_hexdump(ctx->aid, ctx->aid_len), ch);
|
||||
+ ctx->channel = -1;
|
||||
+ return -EIO;
|
||||
+ }
|
||||
+
|
||||
+ ctx->channel = ch;
|
||||
+ IPA_LOGP(SSCARD, LINFO, "stdio: logical channel %d open to %s\n", ch, ipa_hexdump(ctx->aid, ctx->aid_len));
|
||||
+ return 0;
|
||||
+}
|
||||
+
|
||||
+void *ipa_scard_init(unsigned int reader_num)
|
||||
+{
|
||||
+ struct scard_ctx *ctx;
|
||||
+
|
||||
+ (void)reader_num; /* the host decides which modem and slot this is */
|
||||
+
|
||||
+ if (roundtrip("connect", NULL, 0, NULL) != 0) {
|
||||
+ IPA_LOGP(SSCARD, LERROR, "stdio: the host refused the connection\n");
|
||||
+ return NULL;
|
||||
+ }
|
||||
+
|
||||
+ ctx = IPA_ALLOC_ZERO(struct scard_ctx);
|
||||
+ assert(ctx);
|
||||
+ ctx->channel = -1;
|
||||
+ g_ctx = ctx;
|
||||
+ return ctx;
|
||||
+}
|
||||
+
|
||||
+int ipa_scard_transceive(void *scard_ctx, struct ipa_buf *res, const struct ipa_buf *req)
|
||||
+{
|
||||
+ struct scard_ctx *ctx = scard_ctx;
|
||||
+ struct ipa_buf *rx = NULL;
|
||||
+ uint8_t *tx = NULL;
|
||||
+ uint8_t cla, ins, p1;
|
||||
+ uint16_t sw;
|
||||
+ size_t n;
|
||||
+ int rc = 0;
|
||||
+
|
||||
+ assert(ctx);
|
||||
+ assert(res);
|
||||
+ assert(req);
|
||||
+ res->len = 0;
|
||||
+
|
||||
+ if (req->len < 4)
|
||||
+ return -EINVAL;
|
||||
+
|
||||
+ cla = req->data[0];
|
||||
+ ins = req->data[1];
|
||||
+ p1 = req->data[2];
|
||||
+
|
||||
+ IPA_LOGP(SSCARD, LDEBUG, "stdio TX: %s\n", ipa_buf_hexdump(req));
|
||||
+
|
||||
+ /* TERMINAL CAPABILITY belongs to the terminal, which is the modem: it
|
||||
+ * has sent its own to the card at power-up, and it is not something a
|
||||
+ * host can say on the modem's behalf through a logical channel. lpac
|
||||
+ * does not send it either. */
|
||||
+ if ((cla & 0xF0) == 0x80 && ins == 0xAA) {
|
||||
+ put_sw(res, 0x9000);
|
||||
+ goto out;
|
||||
+ }
|
||||
+
|
||||
+ /* MANAGE CHANNEL: the modem opens channels by AID, so opening waits for
|
||||
+ * the SELECT that names one; closing closes the real channel. */
|
||||
+ if (ins == 0x70) {
|
||||
+ if (p1 & 0x80)
|
||||
+ close_channel(ctx);
|
||||
+ put_sw(res, 0x9000);
|
||||
+ goto out;
|
||||
+ }
|
||||
+
|
||||
+ /* SELECT by DF name: this is where the channel is really opened. Any
|
||||
+ * channel still open is closed first so a re-select does not leak one
|
||||
+ * per poll. The select response is not handed on: libipa checks only
|
||||
+ * for 61xx here (euicc.c select_isd_r) and never fetches it. */
|
||||
+ if (ins == 0xA4 && p1 == 0x04 && req->len >= 5) {
|
||||
+ size_t lc = req->data[4];
|
||||
+
|
||||
+ if (lc == 0 || lc > AID_MAX_LEN || req->len < 5 + lc) {
|
||||
+ put_sw(res, 0x6A82);
|
||||
+ goto out;
|
||||
+ }
|
||||
+ close_channel(ctx);
|
||||
+ drop_pending(ctx);
|
||||
+ memcpy(ctx->aid, req->data + 5, lc);
|
||||
+ ctx->aid_len = lc;
|
||||
+ put_sw(res, open_channel(ctx) == 0 ? 0x6100 : 0x6A82);
|
||||
+ goto out;
|
||||
+ }
|
||||
+
|
||||
+ /* GET RESPONSE for an answer held here: hand out exactly the announced
|
||||
+ * portion (Le 00 = 256), then 61xx for the rest or 9000. */
|
||||
+ if (ins == 0xC0 && ctx->pending) {
|
||||
+ size_t le = req->len >= 5 ? req->data[4] : 0;
|
||||
+ size_t left = ctx->pending_len - ctx->pending_off;
|
||||
+
|
||||
+ if (le == 0)
|
||||
+ le = 256;
|
||||
+ n = le < left ? le : left;
|
||||
+ if (n + 2 > res->data_len)
|
||||
+ n = res->data_len - 2;
|
||||
+ memcpy(res->data, ctx->pending + ctx->pending_off, n);
|
||||
+ res->len = n;
|
||||
+ ctx->pending_off += n;
|
||||
+ sw = pending_sw(ctx);
|
||||
+ if (sw == 0x9000)
|
||||
+ drop_pending(ctx);
|
||||
+ put_sw(res, sw);
|
||||
+ goto out;
|
||||
+ }
|
||||
+
|
||||
+ if (ctx->channel < 0) {
|
||||
+ IPA_LOGP(SSCARD, LERROR, "stdio: APDU for a card channel that is not open\n");
|
||||
+ return -EIO;
|
||||
+ }
|
||||
+
|
||||
+ /* the channel number in CLA is the modem's, not the one libipa chose */
|
||||
+ tx = malloc(req->len);
|
||||
+ assert(tx);
|
||||
+ memcpy(tx, req->data, req->len);
|
||||
+ tx[0] = cla_for_channel(tx[0], ctx->channel);
|
||||
+
|
||||
+ drop_pending(ctx);
|
||||
+ rx = ipa_buf_alloc(RX_MAX_LEN);
|
||||
+ assert(rx);
|
||||
+
|
||||
+ if (roundtrip("transmit", tx, req->len, rx) != 0 || rx->len < 2) {
|
||||
+ IPA_LOGP(SSCARD, LERROR, "stdio: transmit failed\n");
|
||||
+ rc = -EIO;
|
||||
+ goto free;
|
||||
+ }
|
||||
+
|
||||
+ sw = (rx->data[rx->len - 2] << 8) | rx->data[rx->len - 1];
|
||||
+ n = rx->len - 2;
|
||||
+
|
||||
+ /* An answer with data and a normal ending is held and announced as 61xx.
|
||||
+ * 91xx is a normal ending too (a proactive command is pending, which the
|
||||
+ * modem fetches itself); libipa knows only 9000/61xx and would abort. */
|
||||
+ if (n && (sw == 0x9000 || (sw & 0xFF00) == 0x9100)) {
|
||||
+ ctx->pending = malloc(n);
|
||||
+ assert(ctx->pending);
|
||||
+ memcpy(ctx->pending, rx->data, n);
|
||||
+ ctx->pending_len = n;
|
||||
+ ctx->pending_off = 0;
|
||||
+ put_sw(res, pending_sw(ctx));
|
||||
+ goto free;
|
||||
+ }
|
||||
+
|
||||
+ if ((sw & 0xFF00) == 0x9100)
|
||||
+ sw = 0x9000;
|
||||
+
|
||||
+ /* anything else goes to libipa as the card said it, clipped to fit */
|
||||
+ if (n + 2 > res->data_len)
|
||||
+ n = res->data_len - 2;
|
||||
+ memcpy(res->data, rx->data, n);
|
||||
+ res->len = n;
|
||||
+ put_sw(res, sw);
|
||||
+
|
||||
+free:
|
||||
+ free(tx);
|
||||
+ ipa_buf_free(rx);
|
||||
+ if (rc < 0)
|
||||
+ return rc;
|
||||
+out:
|
||||
+ IPA_LOGP(SSCARD, LDEBUG, "stdio RX: %s\n", ipa_buf_hexdump(res));
|
||||
+ return 0;
|
||||
+}
|
||||
+
|
||||
+int ipa_scard_reset(void *scard_ctx)
|
||||
+{
|
||||
+ /* a card reset is the host's to do (it owns the modem and knows what a
|
||||
+ * reset costs the data session); from here it is a re-open */
|
||||
+ struct scard_ctx *ctx = scard_ctx;
|
||||
+
|
||||
+ assert(ctx);
|
||||
+ drop_pending(ctx);
|
||||
+ close_channel(ctx);
|
||||
+ return ctx->aid_len ? open_channel(ctx) : 0;
|
||||
+}
|
||||
+
|
||||
+int ipa_scard_atr(void *scard_ctx, struct ipa_buf *atr)
|
||||
+{
|
||||
+ /* the modem does not hand the ATR to the host over this channel */
|
||||
+ (void)scard_ctx;
|
||||
+ atr->len = 0;
|
||||
+ return -EIO;
|
||||
+}
|
||||
+
|
||||
+int ipa_scard_free(void *scard_ctx)
|
||||
+{
|
||||
+ struct scard_ctx *ctx = scard_ctx;
|
||||
+
|
||||
+ if (!ctx)
|
||||
+ return 0;
|
||||
+
|
||||
+ drop_pending(ctx);
|
||||
+ close_channel(ctx);
|
||||
+ roundtrip("disconnect", NULL, 0, NULL);
|
||||
+ if (g_ctx == ctx)
|
||||
+ g_ctx = NULL;
|
||||
+ IPA_FREE(ctx);
|
||||
+ return 0;
|
||||
+}
|
||||
+
|
||||
+int ipa_scard_stdio_await_online(void)
|
||||
+{
|
||||
+ static char line[LINE_MAX_LEN];
|
||||
+ const char *p;
|
||||
+ int online;
|
||||
+
|
||||
+ /* The profile just changed. The host has to make the modem use it (a SIM
|
||||
+ * power cycle: without REFRESH the modem keeps running the old profile)
|
||||
+ * and wait until the data session is back; only then can the result reach
|
||||
+ * the eIM. That power cycle closes every logical channel, so ours is
|
||||
+ * re-opened here before libipa talks to the card again. */
|
||||
+ fflush(stdout);
|
||||
+ printf("{\"type\":\"event\",\"payload\":{\"event\":\"profile_changed\"}}\n");
|
||||
+ fflush(stdout);
|
||||
+
|
||||
+ if (!read_line(line, sizeof(line))) {
|
||||
+ IPA_LOGP(SSCARD, LERROR, "stdio: host closed the channel while restoring connectivity\n");
|
||||
+ return -1;
|
||||
+ }
|
||||
+
|
||||
+ p = field(line, "online");
|
||||
+ online = p && strncmp(p, "true", 4) == 0;
|
||||
+ IPA_LOGP(SSCARD, LINFO, "stdio: host reports the connection %s\n", online ? "back" : "NOT back");
|
||||
+
|
||||
+ if (g_ctx) {
|
||||
+ /* no close first: the card reset already closed it, and the modem
|
||||
+ * may have handed that id to someone else since */
|
||||
+ drop_pending(g_ctx);
|
||||
+ if (g_ctx->aid_len && open_channel(g_ctx) < 0)
|
||||
+ return -1;
|
||||
+ }
|
||||
+
|
||||
+ return online;
|
||||
+}
|
||||
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
|
||||
index a3e0ef8..de33415 100644
|
||||
--- a/tests/CMakeLists.txt
|
||||
+++ b/tests/CMakeLists.txt
|
||||
@@ -6,3 +6,6 @@ enable_testing()
|
||||
add_subdirectory(activation_code)
|
||||
add_subdirectory(utils)
|
||||
add_subdirectory(bpp_segments)
|
||||
+if (IPA_SCARD_STDIO)
|
||||
+ add_subdirectory(scard_stdio)
|
||||
+endif()
|
||||
diff --git a/tests/scard_stdio/CMakeLists.txt b/tests/scard_stdio/CMakeLists.txt
|
||||
new file mode 100644
|
||||
index 0000000..639a9c7
|
||||
--- /dev/null
|
||||
+++ b/tests/scard_stdio/CMakeLists.txt
|
||||
@@ -0,0 +1,11 @@
|
||||
+# Copyright (c) 2026 André Valentin <avalentin@marcant.net>
|
||||
+# SPDX-License-Identifier: AGPL-3.0-only
|
||||
+
|
||||
+add_executable(scard_stdio_test scard_stdio_test.c)
|
||||
+set_property(TARGET scard_stdio_test PROPERTY C_STANDARD 99)
|
||||
+target_compile_options(scard_stdio_test PRIVATE -Wall)
|
||||
+target_include_directories(scard_stdio_test PRIVATE ${PROJECT_SOURCE_DIR}/include)
|
||||
+target_link_libraries(scard_stdio_test scard libipa)
|
||||
+
|
||||
+add_test(NAME scard_stdio_test
|
||||
+ COMMAND sh -c "$<TARGET_FILE:scard_stdio_test> < ${CMAKE_CURRENT_SOURCE_DIR}/scard_stdio_test.in > scard_stdio_test.out && cmp scard_stdio_test.out ${CMAKE_CURRENT_SOURCE_DIR}/scard_stdio_test.ok")
|
||||
diff --git a/tests/scard_stdio/scard_stdio_test.c b/tests/scard_stdio/scard_stdio_test.c
|
||||
new file mode 100644
|
||||
index 0000000..831320d
|
||||
--- /dev/null
|
||||
+++ b/tests/scard_stdio/scard_stdio_test.c
|
||||
@@ -0,0 +1,95 @@
|
||||
+/*
|
||||
+ * Copyright (c) 2026 André Valentin <avalentin@marcant.net>
|
||||
+ *
|
||||
+ * SPDX-License-Identifier: AGPL-3.0-only
|
||||
+ *
|
||||
+ * The stdio card backend against a scripted host: stdin carries the host's
|
||||
+ * answers (scard_stdio_test.in), stdout the requests, which the test compares
|
||||
+ * with scard_stdio_test.ok. The card answers the way a modem's APDU service
|
||||
+ * does -- the whole answer at once with 9000 -- and the checks here are what
|
||||
+ * libipa relies on seeing instead (libipa/euicc.c).
|
||||
+ */
|
||||
+
|
||||
+#include <stdio.h>
|
||||
+#include <string.h>
|
||||
+#include <assert.h>
|
||||
+#include <onomondo/ipa/utils.h>
|
||||
+#include <onomondo/ipa/scard.h>
|
||||
+#include <onomondo/ipa/scard_stdio.h>
|
||||
+
|
||||
+static void *card;
|
||||
+
|
||||
+/* send one C-APDU (hex) and return the R-APDU as a buffer */
|
||||
+static struct ipa_buf *xfer(const char *capdu_hex)
|
||||
+{
|
||||
+ struct ipa_buf *req = ipa_buf_alloc(300);
|
||||
+ struct ipa_buf *res = ipa_buf_alloc(258);
|
||||
+
|
||||
+ req->len = ipa_binary_from_hexstr(req->data, req->data_len, capdu_hex);
|
||||
+ assert(ipa_scard_transceive(card, res, req) == 0);
|
||||
+ ipa_buf_free(req);
|
||||
+ return res;
|
||||
+}
|
||||
+
|
||||
+static unsigned sw_of(const struct ipa_buf *r)
|
||||
+{
|
||||
+ assert(r->len >= 2);
|
||||
+ return (r->data[r->len - 2] << 8) | r->data[r->len - 1];
|
||||
+}
|
||||
+
|
||||
+static void expect_sw(const char *capdu_hex, unsigned sw, size_t data_len)
|
||||
+{
|
||||
+ struct ipa_buf *r = xfer(capdu_hex);
|
||||
+
|
||||
+ if (sw_of(r) != sw || r->len - 2 != data_len) {
|
||||
+ fprintf(stderr, "%s: got sw %04X with %zu bytes, want %04X with %zu\n", capdu_hex, sw_of(r),
|
||||
+ r->len - 2, sw, data_len);
|
||||
+ assert(0);
|
||||
+ }
|
||||
+ ipa_buf_free(r);
|
||||
+}
|
||||
+
|
||||
+int main(void)
|
||||
+{
|
||||
+ card = ipa_scard_init(0);
|
||||
+ assert(card);
|
||||
+
|
||||
+ /* TERMINAL CAPABILITY and MANAGE CHANNEL never reach the host */
|
||||
+ expect_sw("80AA000005A903830107", 0x9000, 0);
|
||||
+ expect_sw("0070000100", 0x9000, 0);
|
||||
+
|
||||
+ /* SELECT ISD-R opens the real channel (the host hands out 3); libipa
|
||||
+ * accepts only 61xx here */
|
||||
+ expect_sw("01A4040410A0000005591010FFFFFFFF8900000100", 0x6100, 0);
|
||||
+
|
||||
+ /* A 300-byte answer with 9000 is announced as 61 00 and handed out as
|
||||
+ * 256 + 44, each portion exactly the size announced. CLA goes out with
|
||||
+ * the host's channel (83), not libipa's (81). */
|
||||
+ expect_sw("81E2910003BF2200", 0x6100, 0);
|
||||
+ expect_sw("01C0000000", 0x612C, 256);
|
||||
+ expect_sw("01C000002C", 0x9000, 44);
|
||||
+
|
||||
+ /* The host resets the card for a profile change: the channel it closed
|
||||
+ * is re-opened (the host now hands out 2) and later APDUs use it */
|
||||
+ assert(ipa_scard_stdio_await_online() == 1);
|
||||
+ expect_sw("81E2910003BF2B00", 0x6A88, 0);
|
||||
+
|
||||
+ /* 91xx is a normal ending too: held and announced like 9000 */
|
||||
+ expect_sw("81E2910003BF2D00", 0x6102, 0);
|
||||
+ expect_sw("01C0000002", 0x9000, 2);
|
||||
+
|
||||
+ /* the host reports it could not get the connection back, and re-opens
|
||||
+ * on channel 4: from there CLA takes the further coding, 0x40 | ch-4
|
||||
+ * (ISO/IEC 7816-4 5.4.1), so STORE DATA goes out as C0, not 84 */
|
||||
+ assert(ipa_scard_stdio_await_online() == 0);
|
||||
+ expect_sw("81E2910003BF2B00", 0x9000, 0);
|
||||
+
|
||||
+ assert(ipa_scard_free(card) == 0);
|
||||
+
|
||||
+ /* the host is gone: every exchange fails at once instead of blocking */
|
||||
+ card = ipa_scard_init(0);
|
||||
+ assert(card == NULL);
|
||||
+
|
||||
+ fprintf(stderr, "scard_stdio_test: ok\n");
|
||||
+ return 0;
|
||||
+}
|
||||
diff --git a/tests/scard_stdio/scard_stdio_test.in b/tests/scard_stdio/scard_stdio_test.in
|
||||
new file mode 100644
|
||||
index 0000000..50d2f24
|
||||
--- /dev/null
|
||||
+++ b/tests/scard_stdio/scard_stdio_test.in
|
||||
@@ -0,0 +1,12 @@
|
||||
+{"type":"apdu","payload":{"ecode":0}}
|
||||
+{"type":"apdu","payload":{"ecode":3}}
|
||||
+{"type":"apdu","payload":{"ecode":0,"data":"A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A59000"}}
|
||||
+{"type":"event","payload":{"online":true}}
|
||||
+{"type":"apdu","payload":{"ecode":2}}
|
||||
+{"type":"apdu","payload":{"ecode":0,"data":"6A88"}}
|
||||
+{"type":"apdu","payload":{"ecode":0,"data":"AABB9110"}}
|
||||
+{"type":"event","payload":{"online":false}}
|
||||
+{"type":"apdu","payload":{"ecode":4}}
|
||||
+{"type":"apdu","payload":{"ecode":0,"data":"9000"}}
|
||||
+{"type":"apdu","payload":{"ecode":0}}
|
||||
+{"type":"apdu","payload":{"ecode":0}}
|
||||
diff --git a/tests/scard_stdio/scard_stdio_test.ok b/tests/scard_stdio/scard_stdio_test.ok
|
||||
new file mode 100644
|
||||
index 0000000..2e653d9
|
||||
--- /dev/null
|
||||
+++ b/tests/scard_stdio/scard_stdio_test.ok
|
||||
@@ -0,0 +1,13 @@
|
||||
+{"type":"apdu","payload":{"func":"connect","param":null}}
|
||||
+{"type":"apdu","payload":{"func":"logic_channel_open","param":"A0000005591010FFFFFFFF8900000100"}}
|
||||
+{"type":"apdu","payload":{"func":"transmit","param":"83E2910003BF2200"}}
|
||||
+{"type":"event","payload":{"event":"profile_changed"}}
|
||||
+{"type":"apdu","payload":{"func":"logic_channel_open","param":"A0000005591010FFFFFFFF8900000100"}}
|
||||
+{"type":"apdu","payload":{"func":"transmit","param":"82E2910003BF2B00"}}
|
||||
+{"type":"apdu","payload":{"func":"transmit","param":"82E2910003BF2D00"}}
|
||||
+{"type":"event","payload":{"event":"profile_changed"}}
|
||||
+{"type":"apdu","payload":{"func":"logic_channel_open","param":"A0000005591010FFFFFFFF8900000100"}}
|
||||
+{"type":"apdu","payload":{"func":"transmit","param":"C0E2910003BF2B00"}}
|
||||
+{"type":"apdu","payload":{"func":"logic_channel_close","param":"04"}}
|
||||
+{"type":"apdu","payload":{"func":"disconnect","param":null}}
|
||||
+{"type":"apdu","payload":{"func":"connect","param":null}}
|
||||
+3
-3
@@ -50,12 +50,12 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=wwand
|
||||
PKG_VERSION:=1.6.8_p23
|
||||
PKG_RELEASE:=57
|
||||
PKG_VERSION:=1.6.8_p25
|
||||
PKG_RELEASE:=58
|
||||
|
||||
PKG_SOURCE_PROTO:=git
|
||||
PKG_SOURCE_URL:=https://github.com/ddimension/wwand.git
|
||||
PKG_SOURCE_VERSION:=cf8d5cf1cc5767d58824a02c61de4d9d2ff58cf6
|
||||
PKG_SOURCE_VERSION:=cd55f6dd5c0b81d47f7dd6f8a07ab69b58bd1061
|
||||
PKG_MIRROR_HASH:=skip
|
||||
|
||||
PKG_LICENSE:=GPL-2.0-only
|
||||
|
||||
Reference in new issue
Block a user