mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-02 15:03:56 +08:00
🌴 Sync 2026-06-17 21:17:50
This commit is contained in:
1 parent
7e638c9c4a
commit
0ec700ea8c
36 files changed
+3856
-3564
No files matched your search
+2
-2
@@ -6,8 +6,8 @@
|
|||||||
include $(TOPDIR)/rules.mk
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
PKG_NAME:=headscale
|
PKG_NAME:=headscale
|
||||||
PKG_VERSION:=0.28.0
|
PKG_VERSION:=0.29.0
|
||||||
PKG_RELEASE:=2
|
PKG_RELEASE:=3
|
||||||
|
|
||||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||||
PKG_SOURCE_URL:=https://codeload.github.com/juanfont/headscale/tar.gz/v$(PKG_VERSION)?
|
PKG_SOURCE_URL:=https://codeload.github.com/juanfont/headscale/tar.gz/v$(PKG_VERSION)?
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk
|
|||||||
|
|
||||||
PKG_NAME:=luci-app-daede
|
PKG_NAME:=luci-app-daede
|
||||||
PKG_VERSION:=1.13.0
|
PKG_VERSION:=1.13.0
|
||||||
PKG_RELEASE:=12
|
PKG_RELEASE:=13
|
||||||
PKG_MAINTAINER:=kenzok8
|
PKG_MAINTAINER:=kenzok8
|
||||||
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)
|
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)
|
||||||
|
|
||||||
@@ -78,6 +78,9 @@ define Package/$(PKG_NAME)/install
|
|||||||
$(INSTALL_BIN) ./root/usr/share/luci-app-daede/proxy-check.sh $(1)/usr/share/luci-app-daede/proxy-check.sh
|
$(INSTALL_BIN) ./root/usr/share/luci-app-daede/proxy-check.sh $(1)/usr/share/luci-app-daede/proxy-check.sh
|
||||||
$(INSTALL_BIN) ./root/usr/share/luci-app-daede/fetch-clash-yaml.sh $(1)/usr/share/luci-app-daede/fetch-clash-yaml.sh
|
$(INSTALL_BIN) ./root/usr/share/luci-app-daede/fetch-clash-yaml.sh $(1)/usr/share/luci-app-daede/fetch-clash-yaml.sh
|
||||||
|
|
||||||
|
$(INSTALL_DIR) $(1)/www/cgi-bin
|
||||||
|
$(INSTALL_BIN) ./root/www/cgi-bin/daede-sub $(1)/www/cgi-bin/daede-sub
|
||||||
|
|
||||||
$(INSTALL_DIR) $(1)/www/luci-static/resources/view/daede
|
$(INSTALL_DIR) $(1)/www/luci-static/resources/view/daede
|
||||||
$(INSTALL_DATA) ./htdocs/luci-static/resources/view/daede/*.js $(1)/www/luci-static/resources/view/daede/
|
$(INSTALL_DATA) ./htdocs/luci-static/resources/view/daede/*.js $(1)/www/luci-static/resources/view/daede/
|
||||||
$(INSTALL_DIR) $(1)/www/luci-static/resources/view/daede/vendor
|
$(INSTALL_DIR) $(1)/www/luci-static/resources/view/daede/vendor
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ function parseAirportSection(section) {
|
|||||||
name: String(section.name || ''),
|
name: String(section.name || ''),
|
||||||
source_hash: String(section.source_hash || ''),
|
source_hash: String(section.source_hash || ''),
|
||||||
group_id: String(section.group_id || ''),
|
group_id: String(section.group_id || ''),
|
||||||
|
subscription_id: String(section.subscription_id || ''),
|
||||||
node_ids: list(section.node_id)
|
node_ids: list(section.node_id)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -109,6 +110,7 @@ function airportSectionValues(record) {
|
|||||||
name: String(record.name || ''),
|
name: String(record.name || ''),
|
||||||
source_hash: String(record.sourceHash || record.source_hash || ''),
|
source_hash: String(record.sourceHash || record.source_hash || ''),
|
||||||
group_id: String(record.groupId || record.group_id || ''),
|
group_id: String(record.groupId || record.group_id || ''),
|
||||||
|
subscription_id: String(record.subscriptionId || record.subscription_id || ''),
|
||||||
node_id: list(record.nodeIds || record.node_ids)
|
node_id: list(record.nodeIds || record.node_ids)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
|
|
||||||
const FETCHER = '/usr/share/luci-app-daede/fetch-clash-yaml.sh';
|
const FETCHER = '/usr/share/luci-app-daede/fetch-clash-yaml.sh';
|
||||||
const GENERATOR = '/usr/share/luci-app-daede/gen-dae-config.sh';
|
const GENERATOR = '/usr/share/luci-app-daede/gen-dae-config.sh';
|
||||||
|
const SUB_STAGE = '/tmp/daede-sub.txt';
|
||||||
const FETCH_CHUNK_BYTES = 16384;
|
const FETCH_CHUNK_BYTES = 16384;
|
||||||
|
|
||||||
function decodeHexChunks(chunks) {
|
function decodeHexChunks(chunks) {
|
||||||
@@ -130,17 +131,6 @@ function requestDaedCredentials() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function uniqueNodeTag(existing, index) {
|
|
||||||
let n = index + 1;
|
|
||||||
let tag = 'converted_' + n;
|
|
||||||
while (existing[tag]) {
|
|
||||||
n++;
|
|
||||||
tag = 'converted_' + n;
|
|
||||||
}
|
|
||||||
existing[tag] = true;
|
|
||||||
return tag;
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyUciChanges() {
|
function applyUciChanges() {
|
||||||
return uci.save().then(function() {
|
return uci.save().then(function() {
|
||||||
return uci.changes().then(function(changes) {
|
return uci.changes().then(function(changes) {
|
||||||
@@ -165,7 +155,12 @@ function writeAirportRecord(existing, record) {
|
|||||||
const sid = existing && existing.sid ? existing.sid : uci.add('daede', 'airport');
|
const sid = existing && existing.sid ? existing.sid : uci.add('daede', 'airport');
|
||||||
const values = airportSync.airportSectionValues(record);
|
const values = airportSync.airportSectionValues(record);
|
||||||
Object.keys(values).forEach(function(key) {
|
Object.keys(values).forEach(function(key) {
|
||||||
uci.set('daede', sid, key, values[key]);
|
const v = values[key];
|
||||||
|
// uci/set rejects an empty list value; clear the option instead.
|
||||||
|
if (Array.isArray(v) && v.length === 0)
|
||||||
|
uci.unset('daede', sid, key);
|
||||||
|
else
|
||||||
|
uci.set('daede', sid, key, v);
|
||||||
});
|
});
|
||||||
return sid;
|
return sid;
|
||||||
}
|
}
|
||||||
@@ -485,114 +480,125 @@ return view.extend({
|
|||||||
const existingAirport = currentAirport();
|
const existingAirport = currentAirport();
|
||||||
const airportId = existingAirport ? existingAirport.id : airportSync.makeAirportId();
|
const airportId = existingAirport ? existingAirport.id : airportSync.makeAirportId();
|
||||||
const groupName = airportSync.backendGroupName(airportName.value, 'dae', airportId);
|
const groupName = airportSync.backendGroupName(airportName.value, 'dae', airportId);
|
||||||
const batchId = Date.now().toString(36);
|
// the group sources this tag; gen-dae-config slugs tag and source the
|
||||||
const foundGroupSection = existingAirport && existingAirport.group_id ? uciSection('dae', existingAirport.group_id) : null;
|
// same way, so subtag() resolves even for CJK/hyphen names.
|
||||||
const foundOldGroup = foundGroupSection && foundGroupSection.name === existingAirport.name ? foundGroupSection : null;
|
const subTag = groupName;
|
||||||
|
const subUrl = 'file://subscriptions/' + airportId + '.sub';
|
||||||
|
const subContent = items.map(function(item) { return item.link; }).join('\n') + '\n';
|
||||||
|
|
||||||
|
let subSid = existingAirport && existingAirport.subscription_id ? existingAirport.subscription_id : '';
|
||||||
|
let groupSid = existingAirport && existingAirport.group_id ? existingAirport.group_id : '';
|
||||||
|
const foundOldSub = (subSid && uciSection('dae', subSid)) || null;
|
||||||
|
const foundOldGroup = (groupSid && uciSection('dae', groupSid)) || null;
|
||||||
|
const oldSubSection = foundOldSub ? JSON.parse(JSON.stringify(foundOldSub)) : null;
|
||||||
const oldGroupSection = foundOldGroup ? JSON.parse(JSON.stringify(foundOldGroup)) : null;
|
const oldGroupSection = foundOldGroup ? JSON.parse(JSON.stringify(foundOldGroup)) : null;
|
||||||
const usedTags = {};
|
const oldNodeIds = existingAirport ? existingAirport.node_ids : [];
|
||||||
const nodesByLink = {};
|
let subCreated = false;
|
||||||
(uci.sections('dae', 'node') || []).forEach(function(section) {
|
|
||||||
if (section.tag)
|
|
||||||
usedTags[section.tag] = true;
|
|
||||||
if (section.link)
|
|
||||||
nodesByLink[clashConverter.normalizeLink(section.link)] = section;
|
|
||||||
});
|
|
||||||
const oldOwned = existingAirport ? existingAirport.node_ids : [];
|
|
||||||
const owned = [];
|
|
||||||
const sources = [];
|
|
||||||
const created = [];
|
|
||||||
items.forEach(function(item, index) {
|
|
||||||
const existingNode = nodesByLink[clashConverter.normalizeLink(item.link)];
|
|
||||||
if (existingNode) {
|
|
||||||
sources.push(existingNode.tag);
|
|
||||||
if (oldOwned.indexOf(existingNode['.name']) >= 0)
|
|
||||||
owned.push(existingNode['.name']);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const sid = uci.add('dae', 'node', airportId + '_node_' + batchId + '_' + (index + 1));
|
|
||||||
const tag = uniqueNodeTag(usedTags, index);
|
|
||||||
uci.set('dae', sid, 'tag', tag);
|
|
||||||
uci.set('dae', sid, 'link', item.link);
|
|
||||||
uci.set('dae', sid, 'enabled', '1');
|
|
||||||
sources.push(tag);
|
|
||||||
owned.push(sid);
|
|
||||||
created.push(sid);
|
|
||||||
});
|
|
||||||
|
|
||||||
let groupSid = foundOldGroup ? existingAirport.group_id : '';
|
|
||||||
let groupCreated = false;
|
let groupCreated = false;
|
||||||
if (!groupSid) {
|
|
||||||
const groupBase = airportId + '_group';
|
|
||||||
groupSid = groupBase;
|
|
||||||
let suffix = 2;
|
|
||||||
while (uci.get('dae', groupSid))
|
|
||||||
groupSid = groupBase + '_' + suffix++;
|
|
||||||
uci.add('dae', 'group', groupSid);
|
|
||||||
groupCreated = true;
|
|
||||||
}
|
|
||||||
uci.set('dae', groupSid, 'name', groupName);
|
|
||||||
uci.set('dae', groupSid, 'policy', 'min_moving_avg');
|
|
||||||
uci.set('dae', groupSid, 'source', sources);
|
|
||||||
|
|
||||||
return applyUciChanges().then(runGenerator).catch(function(error) {
|
const restoreSection = function(sid, snapshot) {
|
||||||
created.forEach(function(sid) { uci.remove('dae', sid); });
|
const current = uciSection('dae', sid) || {};
|
||||||
if (groupCreated) {
|
Object.keys(current).forEach(function(key) {
|
||||||
uci.remove('dae', groupSid);
|
if (key.charAt(0) !== '.' && !Object.prototype.hasOwnProperty.call(snapshot, key))
|
||||||
} else if (oldGroupSection) {
|
uci.unset('dae', sid, key);
|
||||||
const current = uciSection('dae', groupSid) || {};
|
});
|
||||||
Object.keys(current).forEach(function(key) {
|
Object.keys(snapshot).forEach(function(key) {
|
||||||
if (key.charAt(0) !== '.' && !Object.prototype.hasOwnProperty.call(oldGroupSection, key))
|
if (key.charAt(0) !== '.')
|
||||||
uci.unset('dae', groupSid, key);
|
uci.set('dae', sid, key, snapshot[key]);
|
||||||
});
|
});
|
||||||
Object.keys(oldGroupSection).forEach(function(key) {
|
};
|
||||||
if (key.charAt(0) !== '.')
|
|
||||||
uci.set('dae', groupSid, key, oldGroupSection[key]);
|
// 1. stage the converted links and let the backend write the local
|
||||||
});
|
// .sub file (mkdir + chmod 0600 — dae rejects loose perms).
|
||||||
|
return fs.write(SUB_STAGE, subContent).then(function() {
|
||||||
|
return fs.exec(GENERATOR, [ 'write-sub', airportId ]);
|
||||||
|
}).then(function(res) {
|
||||||
|
if (res && res.code !== 0)
|
||||||
|
throw new Error((res && (res.stderr || res.stdout)) || _('Failed to write subscription file'));
|
||||||
|
|
||||||
|
// 2. one file:// subscription for the whole airport
|
||||||
|
if (!foundOldSub) {
|
||||||
|
subSid = uci.add('dae', 'subscription', airportId + '_sub');
|
||||||
|
subCreated = true;
|
||||||
}
|
}
|
||||||
return applyUciChanges().then(runGenerator).catch(function() {}).then(function() { throw error; });
|
uci.set('dae', subSid, 'tag', subTag);
|
||||||
|
uci.set('dae', subSid, 'url', subUrl);
|
||||||
|
uci.set('dae', subSid, 'enabled', '1');
|
||||||
|
|
||||||
|
// 3. group that sources the subscription
|
||||||
|
if (!foundOldGroup) {
|
||||||
|
const groupBase = airportId + '_group';
|
||||||
|
groupSid = groupBase;
|
||||||
|
let suffix = 2;
|
||||||
|
while (uci.get('dae', groupSid))
|
||||||
|
groupSid = groupBase + '_' + suffix++;
|
||||||
|
uci.add('dae', 'group', groupSid);
|
||||||
|
groupCreated = true;
|
||||||
|
}
|
||||||
|
uci.set('dae', groupSid, 'name', groupName);
|
||||||
|
uci.set('dae', groupSid, 'policy', 'min_moving_avg');
|
||||||
|
uci.set('dae', groupSid, 'source', [ subTag ]);
|
||||||
|
|
||||||
|
return applyUciChanges().then(runGenerator).catch(function(error) {
|
||||||
|
if (subCreated) uci.remove('dae', subSid);
|
||||||
|
else if (oldSubSection) restoreSection(subSid, oldSubSection);
|
||||||
|
if (groupCreated) uci.remove('dae', groupSid);
|
||||||
|
else if (oldGroupSection) restoreSection(groupSid, oldGroupSection);
|
||||||
|
return applyUciChanges().then(runGenerator).catch(function() {})
|
||||||
|
.then(function() {
|
||||||
|
if (subCreated) return fs.exec(GENERATOR, [ 'delete-sub', airportId ]).catch(function() {});
|
||||||
|
})
|
||||||
|
.then(function() { throw error; });
|
||||||
|
});
|
||||||
}).then(function() {
|
}).then(function() {
|
||||||
const referencedTags = {};
|
// 4. drop this airport's legacy converted_N manual nodes
|
||||||
(uci.sections('dae', 'group') || []).filter(function(group) {
|
oldNodeIds.forEach(function(sid) {
|
||||||
return group['.name'] !== groupSid;
|
if (uciSection('dae', sid)) uci.remove('dae', sid);
|
||||||
}).forEach(function(group) {
|
|
||||||
const source = Array.isArray(group.source) ? group.source : (group.source ? [ group.source ] : []);
|
|
||||||
source.forEach(function(tag) { referencedTags[tag] = true; });
|
|
||||||
});
|
});
|
||||||
const stale = oldOwned.filter(function(sid) {
|
|
||||||
const section = uciSection('dae', sid);
|
|
||||||
return owned.indexOf(sid) < 0 && !(section && referencedTags[section.tag]);
|
|
||||||
});
|
|
||||||
stale.forEach(function(sid) { uci.remove('dae', sid); });
|
|
||||||
writeAirportRecord(existingAirport, {
|
writeAirportRecord(existingAirport, {
|
||||||
id: airportId,
|
id: airportId,
|
||||||
backend: 'dae',
|
backend: 'dae',
|
||||||
name: airportName.value.trim(),
|
name: airportName.value.trim(),
|
||||||
sourceHash: state.sourceHash,
|
sourceHash: state.sourceHash,
|
||||||
groupId: groupSid,
|
groupId: groupSid,
|
||||||
nodeIds: owned
|
subscriptionId: subSid,
|
||||||
|
nodeIds: []
|
||||||
});
|
});
|
||||||
return applyUciChanges().then(runGenerator);
|
return applyUciChanges().then(runGenerator);
|
||||||
}).then(function() {
|
}).then(function() {
|
||||||
items.forEach(function(item) { item.duplicate = true; item.selected = false; });
|
items.forEach(function(item) { item.duplicate = true; item.selected = false; });
|
||||||
return { added: created.length, duplicates: items.length - created.length, failed: 0 };
|
return { added: items.length, duplicates: 0, failed: 0 };
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const importDaed = function(items) {
|
const importDaed = function(items) {
|
||||||
let credentials;
|
let credentials;
|
||||||
let token = '';
|
let token = '';
|
||||||
let existingAirport = currentAirport();
|
const existingAirport = currentAirport();
|
||||||
const airportId = existingAirport ? existingAirport.id : airportSync.makeAirportId();
|
const airportId = existingAirport ? existingAirport.id : airportSync.makeAirportId();
|
||||||
const name = airportName.value.trim();
|
const name = airportName.value.trim();
|
||||||
const managedName = airportSync.backendId(airportId);
|
|
||||||
const groupName = airportSync.backendGroupName(name, 'daed', airportId);
|
const groupName = airportSync.backendGroupName(name, 'daed', airportId);
|
||||||
const batchTag = managedName + Date.now().toString(36);
|
|
||||||
const endpoint = daedEndpoint();
|
const endpoint = daedEndpoint();
|
||||||
|
// daed can't read file:// — it HTTP-fetches a subscription link. Stage
|
||||||
|
// the converted links (base64) and let daed pull them from the
|
||||||
|
// loopback-only CGI, so the import lands as ONE subscription.
|
||||||
|
const subToken = airportSync.backendId(airportId) + Date.now().toString(36);
|
||||||
|
const stageFile = '/tmp/daede-daedsub-' + subToken;
|
||||||
|
const subUrl = 'http://127.0.0.1/cgi-bin/daede-sub?t=' + subToken;
|
||||||
|
const b64 = btoa(items.map(function(item) { return item.link; }).join('\n') + '\n');
|
||||||
|
|
||||||
let before;
|
let before;
|
||||||
const createdIds = [];
|
let newSubId = '';
|
||||||
let createdGroupId = '';
|
let createdGroupId = '';
|
||||||
let groupReady = false;
|
let groupReady = false;
|
||||||
return requestDaedCredentials().then(function(value) {
|
const oldSubId = existingAirport ? existingAirport.subscription_id : '';
|
||||||
|
const oldNodeIds = existingAirport ? existingAirport.node_ids : [];
|
||||||
|
|
||||||
|
const dropStage = function() { return fs.exec('/bin/rm', [ '-f', stageFile ]).catch(function() {}); };
|
||||||
|
|
||||||
|
return fs.write(stageFile, b64).then(function() {
|
||||||
|
return requestDaedCredentials();
|
||||||
|
}).then(function(value) {
|
||||||
credentials = value;
|
credentials = value;
|
||||||
return graphQL(endpoint,
|
return graphQL(endpoint,
|
||||||
'query Login($username:String!,$password:String!){token(username:$username,password:$password)}',
|
'query Login($username:String!,$password:String!){token(username:$username,password:$password)}',
|
||||||
@@ -601,61 +607,33 @@ return view.extend({
|
|||||||
token = login.token;
|
token = login.token;
|
||||||
credentials.password = '';
|
credentials.password = '';
|
||||||
credentials = null;
|
credentials = null;
|
||||||
return graphQL(endpoint, 'query AirportState{nodes(first:10000){edges{id link tag}} groups{id name nodes{id}}}', {}, token);
|
return graphQL(endpoint, 'query State{groups{id name nodes{id} subscriptions{subscription{id}}}}', {}, token);
|
||||||
}).then(function(dataValue) {
|
}).then(function(dataValue) {
|
||||||
before = dataValue;
|
before = dataValue;
|
||||||
const existing = {};
|
// drop this airport's previous subscription first: the new one
|
||||||
(before.nodes.edges || []).forEach(function(node) {
|
// reuses the same tag (group name) and daed enforces unique tags.
|
||||||
existing[clashConverter.normalizeLink(node.link)] = node;
|
if (!oldSubId)
|
||||||
});
|
return null;
|
||||||
const fresh = items.filter(function(item) {
|
return graphQL(endpoint, 'mutation RmSub($ids:[ID!]!){removeSubscriptions(ids:$ids)}', { ids: [ oldSubId ] }, token).catch(function() {});
|
||||||
const duplicate = existing[clashConverter.normalizeLink(item.link)];
|
}).then(function() {
|
||||||
if (duplicate) {
|
// import the whole converted batch as one subscription
|
||||||
item.duplicate = true;
|
|
||||||
}
|
|
||||||
return !duplicate;
|
|
||||||
});
|
|
||||||
if (!fresh.length)
|
|
||||||
return { importNodes: [], fresh: fresh, existing: existing };
|
|
||||||
return graphQL(endpoint,
|
return graphQL(endpoint,
|
||||||
'mutation ImportNodes($args:[ImportArgument!]!){importNodes(rollbackError:false,args:$args){link error node{id}}}',
|
'mutation Import($a:ImportArgument!){importSubscription(rollbackError:false,arg:$a){sub{id} nodeImportResult{error}}}',
|
||||||
{ args: fresh.map(function(item, index) { return { link: item.link, tag: batchTag + (index + 1) }; }) }, token)
|
{ a: { link: subUrl, tag: groupName } }, token);
|
||||||
.then(function(value) { value.fresh = fresh; value.existing = existing; return value; });
|
|
||||||
}).then(function(result) {
|
}).then(function(result) {
|
||||||
const rows = result.importNodes || [];
|
const sub = result.importSubscription && result.importSubscription.sub;
|
||||||
const nodeIds = [];
|
if (!sub || !sub.id) {
|
||||||
const ownedIds = [];
|
const rows = (result.importSubscription && result.importSubscription.nodeImportResult) || [];
|
||||||
const rowByLink = {};
|
const details = rows.map(function(r) { return r.error; }).filter(Boolean).join('; ');
|
||||||
rows.forEach(function(row) {
|
|
||||||
rowByLink[clashConverter.normalizeLink(row.link)] = row;
|
|
||||||
});
|
|
||||||
let failed = rows.filter(function(row) { return !!row.error && row.error !== 'node already exists'; }).length;
|
|
||||||
items.forEach(function(item) {
|
|
||||||
const key = clashConverter.normalizeLink(item.link);
|
|
||||||
const row = rowByLink[key];
|
|
||||||
const old = result.existing[key];
|
|
||||||
const id = row && row.node && row.node.id || old && old.id;
|
|
||||||
if (id)
|
|
||||||
nodeIds.push(id);
|
|
||||||
if (row && !row.error && row.node) {
|
|
||||||
ownedIds.push(row.node.id);
|
|
||||||
createdIds.push(row.node.id);
|
|
||||||
}
|
|
||||||
else if (existingAirport && existingAirport.node_ids.indexOf(id) >= 0)
|
|
||||||
ownedIds.push(id);
|
|
||||||
});
|
|
||||||
if (!nodeIds.length) {
|
|
||||||
const details = rows.filter(function(row) { return !!row.error; }).map(function(row) {
|
|
||||||
return row.error;
|
|
||||||
}).filter(function(error, index, errors) {
|
|
||||||
return errors.indexOf(error) === index;
|
|
||||||
}).join('; ');
|
|
||||||
throw new Error(details || _('No usable nodes were imported'));
|
throw new Error(details || _('No usable nodes were imported'));
|
||||||
}
|
}
|
||||||
|
newSubId = sub.id;
|
||||||
|
const rows = result.importSubscription.nodeImportResult || [];
|
||||||
|
const failed = rows.filter(function(r) { return !!r.error && r.error !== 'node already exists'; }).length;
|
||||||
|
|
||||||
const oldGroup = airportSync.findManagedGroup(before.groups, existingAirport);
|
const oldGroup = airportSync.findManagedGroup(before.groups, existingAirport);
|
||||||
const group = oldGroup;
|
const ensureGroup = oldGroup
|
||||||
const ensureGroup = group
|
? Promise.resolve(oldGroup.id)
|
||||||
? Promise.resolve(group.id)
|
|
||||||
: graphQL(endpoint,
|
: graphQL(endpoint,
|
||||||
'mutation CreateGroup($name:String!,$policy:Policy!){createGroup(name:$name,policy:$policy){id}}',
|
'mutation CreateGroup($name:String!,$policy:Policy!){createGroup(name:$name,policy:$policy){id}}',
|
||||||
{ name: groupName, policy: 'min_moving_avg' }, token).then(function(value) {
|
{ name: groupName, policy: 'min_moving_avg' }, token).then(function(value) {
|
||||||
@@ -663,64 +641,65 @@ return view.extend({
|
|||||||
return createdGroupId;
|
return createdGroupId;
|
||||||
});
|
});
|
||||||
return ensureGroup.then(function(groupId) {
|
return ensureGroup.then(function(groupId) {
|
||||||
const rename = group && group.name !== groupName
|
const rename = oldGroup && oldGroup.name !== groupName
|
||||||
? graphQL(endpoint, 'mutation RenameGroup($id:ID!,$name:String!){renameGroup(id:$id,name:$name)}', { id: groupId, name: groupName }, token)
|
? graphQL(endpoint, 'mutation Rename($id:ID!,$name:String!){renameGroup(id:$id,name:$name)}', { id: groupId, name: groupName }, token)
|
||||||
: Promise.resolve();
|
: Promise.resolve();
|
||||||
return rename.then(function() {
|
return rename.then(function() {
|
||||||
return graphQL(endpoint, 'mutation SetPolicy($id:ID!,$policy:Policy!){groupSetPolicy(id:$id,policy:$policy)}', { id: groupId, policy: 'min_moving_avg' }, token);
|
return graphQL(endpoint, 'mutation Pol($id:ID!,$policy:Policy!){groupSetPolicy(id:$id,policy:$policy)}', { id: groupId, policy: 'min_moving_avg' }, token);
|
||||||
}).then(function() {
|
}).then(function() {
|
||||||
return graphQL(endpoint, 'mutation AddNodes($id:ID!,$nodeIDs:[ID!]!){groupAddNodes(id:$id,nodeIDs:$nodeIDs)}', { id: groupId, nodeIDs: nodeIds }, token);
|
// attach the whole subscription to the group
|
||||||
|
return graphQL(endpoint, 'mutation AddSubs($id:ID!,$ids:[ID!]!){groupAddSubscriptions(id:$id,subscriptionIDs:$ids)}', { id: groupId, ids: [ newSubId ] }, token);
|
||||||
}).then(function() {
|
}).then(function() {
|
||||||
groupReady = true;
|
groupReady = true;
|
||||||
const oldMembers = group ? group.nodes.map(function(node) { return node.id; }) : [];
|
// scrub the group of its previous members: old manual nodes
|
||||||
const removedMembers = oldMembers.filter(function(id) { return nodeIds.indexOf(id) < 0; });
|
// (from the legacy importNodes flow) and any other subscription.
|
||||||
return (removedMembers.length
|
const oldMemberNodes = oldGroup ? (oldGroup.nodes || []).map(function(n) { return n.id; }) : [];
|
||||||
? graphQL(endpoint, 'mutation DelNodes($id:ID!,$nodeIDs:[ID!]!){groupDelNodes(id:$id,nodeIDs:$nodeIDs)}', { id: groupId, nodeIDs: removedMembers }, token)
|
const oldGroupSubs = oldGroup
|
||||||
: Promise.resolve()).catch(function() {
|
? (oldGroup.subscriptions || []).map(function(s) { return s.subscription && s.subscription.id; }).filter(function(id) { return id && id !== newSubId; })
|
||||||
failed++;
|
: [];
|
||||||
|
const scrub = [];
|
||||||
|
if (oldMemberNodes.length)
|
||||||
|
scrub.push(graphQL(endpoint, 'mutation DelNodes($id:ID!,$nodeIDs:[ID!]!){groupDelNodes(id:$id,nodeIDs:$nodeIDs)}', { id: groupId, nodeIDs: oldMemberNodes }, token).catch(function() {}));
|
||||||
|
if (oldGroupSubs.length)
|
||||||
|
scrub.push(graphQL(endpoint, 'mutation DelSubs($id:ID!,$ids:[ID!]!){groupDelSubscriptions(id:$id,subscriptionIDs:$ids)}', { id: groupId, ids: oldGroupSubs }, token).catch(function() {}));
|
||||||
|
return Promise.all(scrub).then(function() {
|
||||||
|
// delete this airport's orphaned legacy manual nodes (the old
|
||||||
|
// subscription was already removed before the import).
|
||||||
|
if (!oldNodeIds.length)
|
||||||
|
return null;
|
||||||
|
return graphQL(endpoint, 'mutation Rm($ids:[ID!]!){removeNodes(ids:$ids)}', { ids: oldNodeIds }, token).catch(function() {});
|
||||||
}).then(function() {
|
}).then(function() {
|
||||||
const otherAirportNodes = airportRecords().filter(function(record) {
|
writeAirportRecord(existingAirport, {
|
||||||
return record.backend === 'daed' && (!existingAirport || record.id !== existingAirport.id);
|
id: airportId,
|
||||||
}).map(function(record) { return record.node_ids; });
|
backend: 'daed',
|
||||||
const otherGroupNodes = before.groups.filter(function(other) {
|
name: name,
|
||||||
return other.id !== groupId;
|
sourceHash: state.sourceHash,
|
||||||
}).map(function(other) { return other.nodes.map(function(node) { return node.id; }); });
|
groupId: groupId,
|
||||||
const stale = airportSync.safeOldNodeIds(existingAirport ? existingAirport.node_ids : [], ownedIds, otherAirportNodes, otherGroupNodes);
|
subscriptionId: newSubId,
|
||||||
return (stale.length
|
nodeIds: []
|
||||||
? graphQL(endpoint, 'mutation RemoveNodes($ids:[ID!]!){removeNodes(ids:$ids)}', { ids: stale }, token)
|
});
|
||||||
: Promise.resolve()).catch(function() {
|
return applyUciChanges().then(function() {
|
||||||
failed++;
|
items.forEach(function(item) { item.duplicate = true; item.selected = false; });
|
||||||
}).then(function() {
|
return { added: items.length, duplicates: 0, failed: failed };
|
||||||
writeAirportRecord(existingAirport, {
|
|
||||||
id: airportId,
|
|
||||||
backend: 'daed',
|
|
||||||
name: name,
|
|
||||||
sourceHash: state.sourceHash,
|
|
||||||
groupId: groupId,
|
|
||||||
nodeIds: ownedIds
|
|
||||||
});
|
|
||||||
return applyUciChanges().then(function() {
|
|
||||||
items.forEach(function(item) { item.duplicate = true; item.selected = false; });
|
|
||||||
return { added: ownedIds.length, duplicates: items.length - ownedIds.length, failed: failed };
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}).catch(function(error) {
|
}).catch(function(error) {
|
||||||
if (groupReady || !token || (!createdIds.length && !createdGroupId))
|
if (groupReady || !token || (!newSubId && !createdGroupId))
|
||||||
throw error;
|
throw error;
|
||||||
const cleanup = [];
|
const cleanup = [];
|
||||||
if (createdIds.length)
|
if (newSubId)
|
||||||
cleanup.push(graphQL(endpoint, 'mutation RemoveNodes($ids:[ID!]!){removeNodes(ids:$ids)}', { ids: createdIds }, token));
|
cleanup.push(graphQL(endpoint, 'mutation Rm($ids:[ID!]!){removeSubscriptions(ids:$ids)}', { ids: [ newSubId ] }, token));
|
||||||
if (createdGroupId)
|
if (createdGroupId)
|
||||||
cleanup.push(graphQL(endpoint, 'mutation RemoveGroup($id:ID!){removeGroup(id:$id)}', { id: createdGroupId }, token));
|
cleanup.push(graphQL(endpoint, 'mutation RmG($id:ID!){removeGroup(id:$id)}', { id: createdGroupId }, token));
|
||||||
return Promise.all(cleanup).catch(function() {}).then(function() { throw error; });
|
return Promise.all(cleanup).catch(function() {}).then(function() { throw error; });
|
||||||
}).finally(function() {
|
}).finally(function() {
|
||||||
if (credentials)
|
if (credentials)
|
||||||
credentials.password = '';
|
credentials.password = '';
|
||||||
credentials = null;
|
credentials = null;
|
||||||
token = '';
|
token = '';
|
||||||
|
return dropStage();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ return view.extend({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// run a backgrounded script and stream its log into the log pane until it
|
// run a backgrounded script and stream its log into the log pane until it
|
||||||
// logs "done (rc=N)"; then refresh the badges. Result shows inline (no popup).
|
// logs a final ✓/✗ status line; then refresh the badges. Inline, no popup.
|
||||||
const runJob = function(script, arg, btn, logPath) {
|
const runJob = function(script, arg, btn, logPath) {
|
||||||
const orig = btn.textContent;
|
const orig = btn.textContent;
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
@@ -177,7 +177,7 @@ return view.extend({
|
|||||||
const poll = function() {
|
const poll = function() {
|
||||||
return L.resolveDefault(fs.read_direct(logPath, 'text'), '').then(function(c) {
|
return L.resolveDefault(fs.read_direct(logPath, 'text'), '').then(function(c) {
|
||||||
if (c) { logPane.textContent = c; logPane.classList.add('show'); }
|
if (c) { logPane.textContent = c; logPane.classList.add('show'); }
|
||||||
if (/done \(rc=\d+\)/.test(c)) { refresh(); return; }
|
if (/[✓✗]/.test(c)) { refresh(); return; }
|
||||||
if (tries++ > 90) return;
|
if (tries++ > 90) return;
|
||||||
return new Promise(function(r) { setTimeout(r, 2000); }).then(poll);
|
return new Promise(function(r) { setTimeout(r, 2000); }).then(poll);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ import)
|
|||||||
fi
|
fi
|
||||||
rm -f "$tmp" "$IMPORT_B64"
|
rm -f "$tmp" "$IMPORT_B64"
|
||||||
if [ "$rc" = 0 ]; then echo "result: config restored, backend restarted"; else echo "result: import failed"; fi
|
if [ "$rc" = 0 ]; then echo "result: config restored, backend restarted"; else echo "result: import failed"; fi
|
||||||
echo "$(date '+%F %T') done (rc=$rc)"
|
if [ "$rc" = 0 ]; then echo "$(date '+%F %T') ✓ 完成"; else echo "$(date '+%F %T') ✗ 失败 (rc=$rc)"; fi
|
||||||
) </dev/null >/dev/null 2>&1 &
|
) </dev/null >/dev/null 2>&1 &
|
||||||
echo "started in background, see $LOG"
|
echo "started in background, see $LOG"
|
||||||
;;
|
;;
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ CONFIG_DAE="/etc/dae/config.dae"
|
|||||||
TMP_GEN="/tmp/dae-gen.dae"
|
TMP_GEN="/tmp/dae-gen.dae"
|
||||||
DAE_BIN="/usr/bin/dae"
|
DAE_BIN="/usr/bin/dae"
|
||||||
DAE_INITD="/etc/init.d/dae"
|
DAE_INITD="/etc/init.d/dae"
|
||||||
|
SUBS_DIR="/etc/dae/subscriptions"
|
||||||
|
SUB_STAGE="/tmp/daede-sub.txt"
|
||||||
|
|
||||||
# dae single-quoted strings have no escaping; strip quotes/newlines defensively.
|
# dae single-quoted strings have no escaping; strip quotes/newlines defensively.
|
||||||
sanitize() {
|
sanitize() {
|
||||||
@@ -378,10 +380,36 @@ do_import() {
|
|||||||
echo "ok"
|
echo "ok"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Converter writes a batch of share links to a local file that dae reads as a
|
||||||
|
# file:// subscription, so a converted airport is one subscription instead of
|
||||||
|
# hundreds of manual nodes. $1=id (airport id), reads links from SUB_STAGE.
|
||||||
|
# id is restricted to [A-Za-z0-9_] to keep the path inside SUBS_DIR.
|
||||||
|
do_write_sub() {
|
||||||
|
local id="$1"
|
||||||
|
case "$id" in ''|*[!A-Za-z0-9_]*) echo "bad id" >&2; return 2 ;; esac
|
||||||
|
[ -f "$SUB_STAGE" ] || { echo "no staged data" >&2; return 1; }
|
||||||
|
mkdir -p "$SUBS_DIR"
|
||||||
|
# dae rejects subscription/config files readable by group/other (needs <=0640)
|
||||||
|
cp "$SUB_STAGE" "$SUBS_DIR/$id.sub.tmp" || { echo "write failed" >&2; return 1; }
|
||||||
|
chmod 0600 "$SUBS_DIR/$id.sub.tmp"
|
||||||
|
mv "$SUBS_DIR/$id.sub.tmp" "$SUBS_DIR/$id.sub"
|
||||||
|
rm -f "$SUB_STAGE"
|
||||||
|
echo "ok"
|
||||||
|
}
|
||||||
|
|
||||||
|
do_delete_sub() {
|
||||||
|
local id="$1"
|
||||||
|
case "$id" in ''|*[!A-Za-z0-9_]*) return 0 ;; esac
|
||||||
|
rm -f "$SUBS_DIR/$id.sub"
|
||||||
|
echo "ok"
|
||||||
|
}
|
||||||
|
|
||||||
# ===================== entry =====================
|
# ===================== entry =====================
|
||||||
|
|
||||||
case "$1" in
|
case "$1" in
|
||||||
generate) generate ;;
|
generate) generate ;;
|
||||||
import) do_import ;;
|
import) do_import ;;
|
||||||
*) echo "usage: $0 {generate|import}" >&2; exit 2 ;;
|
write-sub) do_write_sub "$2" ;;
|
||||||
|
delete-sub) do_delete_sub "$2" ;;
|
||||||
|
*) echo "usage: $0 {generate|import|write-sub <id>|delete-sub <id>}" >&2; exit 2 ;;
|
||||||
esac
|
esac
|
||||||
@@ -70,7 +70,7 @@ fi
|
|||||||
echo "$(date '+%F %T') updated $DEST ($size bytes)"
|
echo "$(date '+%F %T') updated $DEST ($size bytes)"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
echo "$(date '+%F %T') done (rc=$rc)"
|
if [ "$rc" = 0 ]; then echo "$(date '+%F %T') ✓ 完成"; else echo "$(date '+%F %T') ✗ 失败 (rc=$rc)"; fi
|
||||||
) </dev/null >/dev/null 2>&1 &
|
) </dev/null >/dev/null 2>&1 &
|
||||||
|
|
||||||
echo "started in background, see $LOG"
|
echo "started in background, see $LOG"
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ fi
|
|||||||
exit 3
|
exit 3
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "$(date '+%F %T') done (rc=$rc)"
|
if [ "$rc" = 0 ]; then echo "$(date '+%F %T') ✓ 完成"; else echo "$(date '+%F %T') ✗ 失败 (rc=$rc)"; fi
|
||||||
|
|
||||||
# luci-app-daede upgrade replaces ACL JSON — reload rpcd so changes apply.
|
# luci-app-daede upgrade replaces ACL JSON — reload rpcd so changes apply.
|
||||||
if [ "$PKG" = "luci-app-daede" ] && [ "$rc" = "0" ]; then
|
if [ "$PKG" = "luci-app-daede" ] && [ "$rc" = "0" ]; then
|
||||||
|
|||||||
@@ -42,7 +42,12 @@
|
|||||||
"/etc/dae/config.dae": [ "write" ],
|
"/etc/dae/config.dae": [ "write" ],
|
||||||
"/tmp/dae-validate.dae": [ "write" ],
|
"/tmp/dae-validate.dae": [ "write" ],
|
||||||
"/tmp/daede-import.b64": [ "write" ],
|
"/tmp/daede-import.b64": [ "write" ],
|
||||||
|
"/tmp/daede-sub.txt": [ "write" ],
|
||||||
|
"/tmp/daede-daedsub-*": [ "write" ],
|
||||||
|
"/bin/rm -f /tmp/daede-daedsub-*": [ "exec" ],
|
||||||
"/usr/share/luci-app-daede/config-backup.sh import": [ "exec" ],
|
"/usr/share/luci-app-daede/config-backup.sh import": [ "exec" ],
|
||||||
|
"/usr/share/luci-app-daede/gen-dae-config.sh write-sub *": [ "exec" ],
|
||||||
|
"/usr/share/luci-app-daede/gen-dae-config.sh delete-sub *": [ "exec" ],
|
||||||
"/etc/config/daede": [ "write" ],
|
"/etc/config/daede": [ "write" ],
|
||||||
"/var/log/dae/dae.log": [ "write" ],
|
"/var/log/dae/dae.log": [ "write" ],
|
||||||
"/var/log/daed/daed.log": [ "write" ],
|
"/var/log/daed/daed.log": [ "write" ],
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# daede-sub - serve a converter's staged node list to the local daed only.
|
||||||
|
#
|
||||||
|
# daed (dae-wing) can only build a subscription by HTTP-fetching a link, and it
|
||||||
|
# does not read file:// like dae core. So the converter stages the base64 share
|
||||||
|
# links in /tmp/daede-daedsub-<token> and points daed's importSubscription at
|
||||||
|
# http://127.0.0.1/cgi-bin/daede-sub?t=<token>. This CGI is the read side.
|
||||||
|
#
|
||||||
|
# Locked to loopback (REMOTE_ADDR), so even though uhttpd listens on the LAN no
|
||||||
|
# remote client can pull the links. The frontend deletes the staged file after
|
||||||
|
# the import (success or failure), so the exposure is one local fetch.
|
||||||
|
|
||||||
|
deny() { printf 'Status: 403 Forbidden\r\nContent-Type: text/plain\r\n\r\nforbidden\n'; exit 0; }
|
||||||
|
|
||||||
|
[ "$REMOTE_ADDR" = "127.0.0.1" ] || deny
|
||||||
|
|
||||||
|
# token: only [A-Za-z0-9] so the path can't escape /tmp
|
||||||
|
t=$(printf '%s' "$QUERY_STRING" | sed -n 's/^.*\bt=\([A-Za-z0-9]\{1,64\}\).*$/\1/p')
|
||||||
|
[ -n "$t" ] || deny
|
||||||
|
|
||||||
|
f="/tmp/daede-daedsub-$t"
|
||||||
|
[ -f "$f" ] || deny
|
||||||
|
|
||||||
|
printf 'Content-Type: text/plain\r\n\r\n'
|
||||||
|
cat "$f"
|
||||||
@@ -184,6 +184,7 @@ const load_balance_strategy = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const outbound_type = [
|
const outbound_type = [
|
||||||
|
['rematch', _('Rematch'), _('Rematching routing rules')],
|
||||||
['direct', _('DIRECT') + ' - ' + _('TCP/UDP')],
|
['direct', _('DIRECT') + ' - ' + _('TCP/UDP')],
|
||||||
['http', _('HTTP') + ' - ' + _('TCP')],
|
['http', _('HTTP') + ' - ' + _('TCP')],
|
||||||
['socks5', _('SOCKS5') + ' - ' + _('TCP/UDP')],
|
['socks5', _('SOCKS5') + ' - ' + _('TCP/UDP')],
|
||||||
@@ -281,6 +282,7 @@ const rules_type = [
|
|||||||
//['IN-TYPE'],
|
//['IN-TYPE'],
|
||||||
//['IN-USER'],
|
//['IN-USER'],
|
||||||
//['IN-NAME'],
|
//['IN-NAME'],
|
||||||
|
['REMATCH-NAME'],
|
||||||
|
|
||||||
['PROCESS-PATH'],
|
['PROCESS-PATH'],
|
||||||
['PROCESS-PATH-REGEX'],
|
['PROCESS-PATH-REGEX'],
|
||||||
@@ -300,7 +302,7 @@ const rules_type = [
|
|||||||
|
|
||||||
const rules_type_allowparms = [
|
const rules_type_allowparms = [
|
||||||
// params only available for types other than
|
// params only available for types other than
|
||||||
// https://github.com/muink/mihomo/blob/300eb8b12a75504c4bd4a6037d2f6503fd3b347f/rules/parser.go#L12
|
// https://github.com/muink/mihomo/blob/ea19cda0c9b666aa0fc1b0412ae6fbc0ea9d44e0/rules/parser.go#L12
|
||||||
'GEOIP',
|
'GEOIP',
|
||||||
'IP-ASN',
|
'IP-ASN',
|
||||||
'IP-CIDR',
|
'IP-CIDR',
|
||||||
@@ -1192,6 +1194,31 @@ function loadSubRuleGroup(preadds, section_id) {
|
|||||||
return this.super('load', section_id);
|
return this.super('load', section_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function loadRematchName(preadds, section_id) {
|
||||||
|
delete this.keylist;
|
||||||
|
delete this.vallist;
|
||||||
|
|
||||||
|
preadds?.forEach((arr) => {
|
||||||
|
this.value.apply(this, arr);
|
||||||
|
});
|
||||||
|
let names = {};
|
||||||
|
for (const section_type of ['rules', 'subrules'])
|
||||||
|
uci.sections(this.config, section_type, (res) => {
|
||||||
|
if (res.enabled !== '0')
|
||||||
|
try {
|
||||||
|
const obj = JSON.parse(res?.entry?.trim() || '{}');
|
||||||
|
for (const p of obj.payload || [])
|
||||||
|
if (p.type === 'REMATCH-NAME')
|
||||||
|
names[p.factor] = p.factor;
|
||||||
|
} catch {}
|
||||||
|
});
|
||||||
|
Object.keys(names).forEach((name) => {
|
||||||
|
this.value(name, name);
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.super('load', section_id);
|
||||||
|
}
|
||||||
|
|
||||||
function renderStatus(ElId, isRunning, instance, noGlobal) {
|
function renderStatus(ElId, isRunning, instance, noGlobal) {
|
||||||
const visible = isRunning && (isRunning.http || isRunning.https);
|
const visible = isRunning && (isRunning.http || isRunning.https);
|
||||||
|
|
||||||
@@ -1797,6 +1824,7 @@ return baseclass.extend({
|
|||||||
loadProviderLabel,
|
loadProviderLabel,
|
||||||
loadRulesetLabel,
|
loadRulesetLabel,
|
||||||
loadSubRuleGroup,
|
loadSubRuleGroup,
|
||||||
|
loadRematchName,
|
||||||
// render
|
// render
|
||||||
renderStatus,
|
renderStatus,
|
||||||
updateStatus,
|
updateStatus,
|
||||||
|
|||||||
@@ -602,6 +602,10 @@ function renderListeners(s, uciconfig, isClient) {
|
|||||||
|
|
||||||
/* Extra fields */
|
/* Extra fields */
|
||||||
if (isClient) {
|
if (isClient) {
|
||||||
|
o = s.taboption('field_general', form.Value, 'routing_mark', _('Routing mark (Fwmark)'));
|
||||||
|
o.datatype = 'uinteger';
|
||||||
|
o.editable = true;
|
||||||
|
|
||||||
o = s.taboption('field_general', hm.ListValue, 'rule', _('Sub rule'),
|
o = s.taboption('field_general', hm.ListValue, 'rule', _('Sub rule'),
|
||||||
_('Name of the Sub rule used for inbound matching.'));
|
_('Name of the Sub rule used for inbound matching.'));
|
||||||
o.value.apply(o, ['', _('null')]);
|
o.value.apply(o, ['', _('null')]);
|
||||||
|
|||||||
@@ -310,7 +310,7 @@ const parseRulesYaml = hm.parseYaml.extend({
|
|||||||
if (!entry)
|
if (!entry)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
// key mapping // 2026/01/18
|
// key mapping // 2026/06/12
|
||||||
let config = {
|
let config = {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
label: '%s %s'.format(this.id.slice(0,7), _('(Imported)')),
|
label: '%s %s'.format(this.id.slice(0,7), _('(Imported)')),
|
||||||
@@ -365,7 +365,7 @@ const parseRulesYaml = hm.parseYaml.extend({
|
|||||||
|
|
||||||
ParseRule(tp, payload, target, params) {
|
ParseRule(tp, payload, target, params) {
|
||||||
// parse rules
|
// parse rules
|
||||||
// https://github.com/muink/mihomo/blob/487de9b5482d838acc33b067045a0dc293e35d40/rules/parser.go#L12
|
// https://github.com/muink/mihomo/blob/ea19cda0c9b666aa0fc1b0412ae6fbc0ea9d44e0/rules/parser.go#L12
|
||||||
|
|
||||||
// nested ParseRule
|
// nested ParseRule
|
||||||
let logical_payload, subrule;
|
let logical_payload, subrule;
|
||||||
@@ -427,7 +427,7 @@ const parseRulesYaml = hm.parseYaml.extend({
|
|||||||
|
|
||||||
parseRules(line) {
|
parseRules(line) {
|
||||||
// parse rules
|
// parse rules
|
||||||
// https://github.com/muink/mihomo/blob/300eb8b12a75504c4bd4a6037d2f6503fd3b347f/config/config.go#L1038-L1062
|
// https://github.com/muink/mihomo/blob/ad730ae9744971dfaa75bda20cfc2ffe07eeb59d/config/config.go#L1090-L1118
|
||||||
let [tp, payload, target, params] = this.ParseRulePayload(line, true);
|
let [tp, payload, target, params] = this.ParseRulePayload(line, true);
|
||||||
if (!target)
|
if (!target)
|
||||||
return null; // error: format invalid
|
return null; // error: format invalid
|
||||||
@@ -561,6 +561,14 @@ function renderPayload(s, total, uciconfig) {
|
|||||||
o.depends(Object.fromEntries([[prefix + 'type', /\bPROCESS\b/]]));
|
o.depends(Object.fromEntries([[prefix + 'type', /\bPROCESS\b/]]));
|
||||||
initPayload(o, n, 'factor', uciconfig);
|
initPayload(o, n, 'factor', uciconfig);
|
||||||
|
|
||||||
|
o = s.option(form.Value, prefix + 'rematchname', _('Factor') + ` ${n+1}`);
|
||||||
|
o.value('rematch1');
|
||||||
|
o.validate = hm.validateAuthUsername;
|
||||||
|
if (n === 0)
|
||||||
|
o.depends('type', 'REMATCH-NAME');
|
||||||
|
o.depends(prefix + 'type', 'REMATCH-NAME');
|
||||||
|
initPayload(o, n, 'factor', uciconfig);
|
||||||
|
|
||||||
o = s.option(form.Value, prefix + 'uint', _('Factor') + ` ${n+1}`);
|
o = s.option(form.Value, prefix + 'uint', _('Factor') + ` ${n+1}`);
|
||||||
o.datatype = 'uinteger';
|
o.datatype = 'uinteger';
|
||||||
if (n === 0)
|
if (n === 0)
|
||||||
@@ -571,7 +579,7 @@ function renderPayload(s, total, uciconfig) {
|
|||||||
o = s.option(form.Value, prefix + 'ip', _('Factor') + ` ${n+1}`);
|
o = s.option(form.Value, prefix + 'ip', _('Factor') + ` ${n+1}`);
|
||||||
o.datatype = 'cidr';
|
o.datatype = 'cidr';
|
||||||
if (n === 0) {
|
if (n === 0) {
|
||||||
o.depends({type: /\b(CIDR|CIDR6)\b/});
|
o.depends({type: /\bCIDR6?\b/});
|
||||||
o.depends({type: /\bIP-SUFFIX\b/});
|
o.depends({type: /\bIP-SUFFIX\b/});
|
||||||
}
|
}
|
||||||
o.depends(Object.fromEntries([[prefix + 'type', /\bCIDR6?\b/]]));
|
o.depends(Object.fromEntries([[prefix + 'type', /\bCIDR6?\b/]]));
|
||||||
@@ -1386,6 +1394,9 @@ return view.extend({
|
|||||||
so.placeholder = '7853';
|
so.placeholder = '7853';
|
||||||
so.rmempty = false;
|
so.rmempty = false;
|
||||||
|
|
||||||
|
so = ss.option(form.Value, 'routing_mark', _('Listen routing mark (Fwmark)'));
|
||||||
|
so.datatype = 'uinteger'
|
||||||
|
|
||||||
so = ss.option(form.Flag, 'ipv6', _('IPv6 support'));
|
so = ss.option(form.Flag, 'ipv6', _('IPv6 support'));
|
||||||
so.default = so.enabled;
|
so.default = so.enabled;
|
||||||
|
|
||||||
|
|||||||
@@ -648,6 +648,9 @@ return view.extend({
|
|||||||
'To access the API on a private network from a public website, it must be enabled.'));
|
'To access the API on a private network from a public website, it must be enabled.'));
|
||||||
so.default = so.enabled;
|
so.default = so.enabled;
|
||||||
|
|
||||||
|
so = ss.option(form.Value, 'external_controller_routing_mark', _('API routing mark (Fwmark)'));
|
||||||
|
so.datatype = 'uinteger';
|
||||||
|
|
||||||
so = ss.option(form.Value, 'external_controller_port', _('API HTTP port'));
|
so = ss.option(form.Value, 'external_controller_port', _('API HTTP port'));
|
||||||
so.datatype = 'port';
|
so.datatype = 'port';
|
||||||
so.placeholder = '9090';
|
so.placeholder = '9090';
|
||||||
|
|||||||
@@ -247,7 +247,7 @@ return view.extend({
|
|||||||
so.default = so.enabled;
|
so.default = so.enabled;
|
||||||
so.editable = true;
|
so.editable = true;
|
||||||
|
|
||||||
so = ss.taboption('field_general', form.ListValue, 'type', _('Type'));
|
so = ss.taboption('field_general', form.RichListValue, 'type', _('Type'));
|
||||||
so.default = hm.outbound_type[0][0];
|
so.default = hm.outbound_type[0][0];
|
||||||
hm.outbound_type.forEach((res) => {
|
hm.outbound_type.forEach((res) => {
|
||||||
so.value.apply(so, res);
|
so.value.apply(so, res);
|
||||||
@@ -256,12 +256,24 @@ return view.extend({
|
|||||||
so = ss.taboption('field_general', form.Value, 'server', _('Server address'));
|
so = ss.taboption('field_general', form.Value, 'server', _('Server address'));
|
||||||
so.datatype = 'host';
|
so.datatype = 'host';
|
||||||
so.rmempty = false;
|
so.rmempty = false;
|
||||||
so.depends({type: 'direct', '!reverse': true});
|
so.depends({type: /^(rematch|direct)$/, '!reverse': true});
|
||||||
|
|
||||||
so = ss.taboption('field_general', form.Value, 'port', _('Port'));
|
so = ss.taboption('field_general', form.Value, 'port', _('Port'));
|
||||||
so.datatype = 'port';
|
so.datatype = 'port';
|
||||||
so.rmempty = false;
|
so.rmempty = false;
|
||||||
so.depends({type: /^(direct|mieru)$/, '!reverse': true});
|
so.depends({type: /^(rematch|direct|mieru)$/, '!reverse': true});
|
||||||
|
|
||||||
|
/* Rematch fields */
|
||||||
|
so = ss.taboption('field_general', form.ListValue, 'target_rematch_name', _('REMATCH-NAME marking'));
|
||||||
|
so.load = L.bind(hm.loadRematchName, so, [['', _('-- Please choose --')]]);
|
||||||
|
so.rmempty = false;
|
||||||
|
so.depends('type', 'rematch');
|
||||||
|
so.modalonly = true;
|
||||||
|
|
||||||
|
so = ss.taboption('field_general', form.ListValue, 'target_sub_rule', _('Use sub rule'));
|
||||||
|
so.load = L.bind(hm.loadSubRuleGroup, so, [['', _('-- Please choose --')]]);
|
||||||
|
so.depends('type', 'rematch');
|
||||||
|
so.modalonly = true;
|
||||||
|
|
||||||
/* HTTP / SOCKS fields */
|
/* HTTP / SOCKS fields */
|
||||||
/* hm.validateAuth */
|
/* hm.validateAuth */
|
||||||
@@ -579,6 +591,11 @@ return view.extend({
|
|||||||
so.depends('type', 'tuic');
|
so.depends('type', 'tuic');
|
||||||
so.modalonly = true;
|
so.modalonly = true;
|
||||||
|
|
||||||
|
so = ss.taboption('field_general', form.Flag, 'tuic_fast_open', _('Enable fast open'));
|
||||||
|
so.default = so.disabled;
|
||||||
|
so.depends('type', 'tuic');
|
||||||
|
so.modalonly = true;
|
||||||
|
|
||||||
so = ss.taboption('field_general', form.Flag, 'tuic_reduce_rtt', _('Enable 0-RTT handshake'),
|
so = ss.taboption('field_general', form.Flag, 'tuic_reduce_rtt', _('Enable 0-RTT handshake'),
|
||||||
_('Enable 0-RTT QUIC connection handshake on the client side. This is not impacting much on the performance, as the protocol is fully multiplexed.<br/>' +
|
_('Enable 0-RTT QUIC connection handshake on the client side. This is not impacting much on the performance, as the protocol is fully multiplexed.<br/>' +
|
||||||
'Disabling this is highly recommended, as it is vulnerable to replay attacks.'));
|
'Disabling this is highly recommended, as it is vulnerable to replay attacks.'));
|
||||||
@@ -721,18 +738,38 @@ return view.extend({
|
|||||||
so.datatype = 'ip4addr(1)';
|
so.datatype = 'ip4addr(1)';
|
||||||
so.placeholder = '172.16.0.2';
|
so.placeholder = '172.16.0.2';
|
||||||
so.rmempty = false;
|
so.rmempty = false;
|
||||||
so.depends('type', 'masque');
|
so.depends({type: 'masque', masque_network: /^(|h2)$/});
|
||||||
so.modalonly = true;
|
so.modalonly = true;
|
||||||
|
|
||||||
so = ss.taboption('field_general', form.Value, 'masque_ipv6', _('Local IPv6 address'),
|
so = ss.taboption('field_general', form.Value, 'masque_ipv6', _('Local IPv6 address'),
|
||||||
_('The %s address used by local machine in the Cloudflare WARP network.').format('IPv6'));
|
_('The %s address used by local machine in the Cloudflare WARP network.').format('IPv6'));
|
||||||
so.datatype = 'ip6addr(1)';
|
so.datatype = 'ip6addr(1)';
|
||||||
so.depends('type', 'masque');
|
so.depends({type: 'masque', masque_network: /^(|h2)$/});
|
||||||
so.modalonly = true;
|
so.modalonly = true;
|
||||||
|
|
||||||
so = ss.taboption('field_general', form.Value, 'masque_mtu', _('MTU'));
|
so = ss.taboption('field_general', form.Value, 'masque_mtu', _('MTU'));
|
||||||
so.datatype = 'range(0,9000)';
|
so.datatype = 'range(0,9000)';
|
||||||
so.placeholder = '1280';
|
so.placeholder = '1280';
|
||||||
|
so.depends({type: 'masque', masque_network: /^(|h2)$/});
|
||||||
|
so.modalonly = true;
|
||||||
|
|
||||||
|
so = ss.taboption('field_general', form.ListValue, 'masque_network', _('Network'));
|
||||||
|
so.default = '';
|
||||||
|
so.value('', _('h3'));
|
||||||
|
so.value('h3-l4proxy', _('h3-l4proxy'));
|
||||||
|
so.value('h2', _('h2'));
|
||||||
|
so.validate = function(section_id, value) {
|
||||||
|
const udp = this.section.getUIElement(section_id, 'udp').node.querySelector('input');
|
||||||
|
|
||||||
|
// Force disabled
|
||||||
|
if (value === 'h3-l4proxy') {
|
||||||
|
udp.checked = false;
|
||||||
|
udp.disabled = true;
|
||||||
|
} else
|
||||||
|
udp.removeAttribute('disabled');
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
so.depends('type', 'masque');
|
so.depends('type', 'masque');
|
||||||
so.modalonly = true;
|
so.modalonly = true;
|
||||||
|
|
||||||
@@ -889,7 +926,8 @@ return view.extend({
|
|||||||
hm.congestion_controller.forEach((res) => {
|
hm.congestion_controller.forEach((res) => {
|
||||||
so.value.apply(so, res);
|
so.value.apply(so, res);
|
||||||
})
|
})
|
||||||
so.depends({type: /^(tuic|masque|trusttunnel)$/});
|
so.depends({type: /^(tuic|trusttunnel)$/});
|
||||||
|
so.depends({type: 'masque', masque_network: /^(|h3-l4proxy)$/});
|
||||||
so.modalonly = true;
|
so.modalonly = true;
|
||||||
|
|
||||||
so = ss.taboption('field_general', form.ListValue, 'bbr_profile', _('BBR profile'));
|
so = ss.taboption('field_general', form.ListValue, 'bbr_profile', _('BBR profile'));
|
||||||
@@ -903,7 +941,7 @@ return view.extend({
|
|||||||
|
|
||||||
so = ss.taboption('field_general', form.Flag, 'udp', _('UDP'));
|
so = ss.taboption('field_general', form.Flag, 'udp', _('UDP'));
|
||||||
so.default = so.disabled;
|
so.default = so.disabled;
|
||||||
so.depends({type: /^(direct|socks5|ss|mieru|vmess|vless|trojan|anytls|trusttunnel|masque|wireguard)$/});
|
so.depends({type: /^(rematch|direct|socks5|ss|mieru|vmess|vless|trojan|anytls|trusttunnel|masque|wireguard)$/});
|
||||||
so.depends({type: 'snell', snell_version: /^(3|4|5)$/});
|
so.depends({type: 'snell', snell_version: /^(3|4|5)$/});
|
||||||
so.modalonly = true;
|
so.modalonly = true;
|
||||||
|
|
||||||
@@ -1038,7 +1076,7 @@ return view.extend({
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
so.depends({type: /^(http|socks5|vmess|vless|trojan|anytls|hysteria|hysteria2|tuic|masque|trusttunnel)$/});
|
so.depends({type: /^(http|socks5|vmess|vless|trojan|anytls|hysteria|hysteria2|tuic|trusttunnel)$/});
|
||||||
so.modalonly = true;
|
so.modalonly = true;
|
||||||
|
|
||||||
so = ss.taboption('field_tls', form.Flag, 'tls_disable_sni', _('Disable SNI'),
|
so = ss.taboption('field_tls', form.Flag, 'tls_disable_sni', _('Disable SNI'),
|
||||||
@@ -1081,9 +1119,6 @@ return view.extend({
|
|||||||
case 'vless':
|
case 'vless':
|
||||||
def_alpn = ['h3', 'h2', 'http/1.1'];
|
def_alpn = ['h3', 'h2', 'http/1.1'];
|
||||||
break;
|
break;
|
||||||
case 'masque':
|
|
||||||
def_alpn = ['h2'];
|
|
||||||
break;
|
|
||||||
case 'trusttunnel':
|
case 'trusttunnel':
|
||||||
def_alpn = ['h3', 'h2'];
|
def_alpn = ['h3', 'h2'];
|
||||||
break;
|
break;
|
||||||
@@ -1096,7 +1131,7 @@ return view.extend({
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
so.depends({tls: '1', type: /^(vmess|vless|trojan|anytls|hysteria|hysteria2|tuic|masque|trusttunnel)$/});
|
so.depends({tls: '1', type: /^(vmess|vless|trojan|anytls|hysteria|hysteria2|tuic|trusttunnel)$/});
|
||||||
so.depends({type: 'ss', plugin: 'shadow-tls'});
|
so.depends({type: 'ss', plugin: 'shadow-tls'});
|
||||||
so.modalonly = true;
|
so.modalonly = true;
|
||||||
|
|
||||||
@@ -1470,10 +1505,12 @@ return view.extend({
|
|||||||
/* Dial fields */
|
/* Dial fields */
|
||||||
so = ss.taboption('field_dial', form.Flag, 'tfo', _('TFO'));
|
so = ss.taboption('field_dial', form.Flag, 'tfo', _('TFO'));
|
||||||
so.default = so.disabled;
|
so.default = so.disabled;
|
||||||
|
so.depends({type: /^(rematch)$/, '!reverse': true});
|
||||||
so.modalonly = true;
|
so.modalonly = true;
|
||||||
|
|
||||||
so = ss.taboption('field_dial', form.Flag, 'mptcp', _('mpTCP'));
|
so = ss.taboption('field_dial', form.Flag, 'mptcp', _('mpTCP'));
|
||||||
so.default = so.disabled;
|
so.default = so.disabled;
|
||||||
|
so.depends({type: /^(rematch)$/, '!reverse': true});
|
||||||
so.modalonly = true;
|
so.modalonly = true;
|
||||||
|
|
||||||
/* Features are implemented in proxy chain
|
/* Features are implemented in proxy chain
|
||||||
@@ -1487,11 +1524,13 @@ return view.extend({
|
|||||||
_('Priority: Proxy Node > Global.'));
|
_('Priority: Proxy Node > Global.'));
|
||||||
so.multiple = false;
|
so.multiple = false;
|
||||||
so.noaliases = true;
|
so.noaliases = true;
|
||||||
|
so.depends({type: /^(rematch)$/, '!reverse': true});
|
||||||
so.modalonly = true;
|
so.modalonly = true;
|
||||||
|
|
||||||
so = ss.taboption('field_dial', form.Value, 'routing_mark', _('Routing mark'),
|
so = ss.taboption('field_dial', form.Value, 'routing_mark', _('Routing mark (Fwmark)'),
|
||||||
_('Priority: Proxy Node > Global.'));
|
_('Priority: Proxy Node > Global.'));
|
||||||
so.datatype = 'uinteger';
|
so.datatype = 'uinteger';
|
||||||
|
so.depends({type: /^(rematch)$/, '!reverse': true});
|
||||||
so.modalonly = true;
|
so.modalonly = true;
|
||||||
|
|
||||||
so = ss.taboption('field_dial', form.ListValue, 'ip_version', _('IP version'));
|
so = ss.taboption('field_dial', form.ListValue, 'ip_version', _('IP version'));
|
||||||
@@ -1499,6 +1538,7 @@ return view.extend({
|
|||||||
hm.ip_version.forEach((res) => {
|
hm.ip_version.forEach((res) => {
|
||||||
so.value.apply(so, res);
|
so.value.apply(so, res);
|
||||||
})
|
})
|
||||||
|
so.depends({type: /^(rematch)$/, '!reverse': true});
|
||||||
so.modalonly = true;
|
so.modalonly = true;
|
||||||
/* Proxy Node END */
|
/* Proxy Node END */
|
||||||
|
|
||||||
@@ -1874,7 +1914,7 @@ return view.extend({
|
|||||||
so.depends({type: 'inline', '!reverse': true});
|
so.depends({type: 'inline', '!reverse': true});
|
||||||
so.modalonly = true;
|
so.modalonly = true;
|
||||||
|
|
||||||
so = ss.taboption('field_override', form.Value, 'override_routing_mark', _('Routing mark'),
|
so = ss.taboption('field_override', form.Value, 'override_routing_mark', _('Routing mark (Fwmark)'),
|
||||||
_('Priority: Proxy Node > Global.'));
|
_('Priority: Proxy Node > Global.'));
|
||||||
so.datatype = 'uinteger';
|
so.datatype = 'uinteger';
|
||||||
so.depends({type: 'inline', '!reverse': true});
|
so.depends({type: 'inline', '!reverse': true});
|
||||||
|
|||||||
+1027
-986
File diff suppressed because it is too large.
Load diff
+1042
-986
File diff suppressed because it is too large.
Load diff
+1042
-986
File diff suppressed because it is too large.
Load diff
@@ -259,11 +259,13 @@ config.tls = {
|
|||||||
/* API START */
|
/* API START */
|
||||||
const api_port = uci.get(uciconf, uciapi, 'external_controller_port');
|
const api_port = uci.get(uciconf, uciapi, 'external_controller_port');
|
||||||
const api_tls_port = uci.get(uciconf, uciapi, 'external_controller_tls_port');
|
const api_tls_port = uci.get(uciconf, uciapi, 'external_controller_tls_port');
|
||||||
|
const api_routing_mark = uci.get(uciconf, uciapi, 'external_controller_routing_mark');
|
||||||
/* API settings */
|
/* API settings */
|
||||||
config["external-controller-cors"] = {
|
config["external-controller-cors"] = {
|
||||||
"allow-origins": uci.get(uciconf, uciapi, 'external_controller_cors_allow_origins') || ['*'],
|
"allow-origins": uci.get(uciconf, uciapi, 'external_controller_cors_allow_origins') || ['*'],
|
||||||
"allow-private-network" : (uci.get(uciconf, uciapi, 'external_controller_cors_allow_private_network') === '0') ? false : true
|
"allow-private-network" : (uci.get(uciconf, uciapi, 'external_controller_cors_allow_private_network') === '0') ? false : true
|
||||||
};
|
};
|
||||||
|
config["external-controller-routing-mark"] = strToInt(api_routing_mark) || null;
|
||||||
config["external-controller"] = api_port ? '[::]:' + api_port : null;
|
config["external-controller"] = api_port ? '[::]:' + api_port : null;
|
||||||
config["external-controller-tls"] = api_tls_port ? '[::]:' + api_tls_port : null;
|
config["external-controller-tls"] = api_tls_port ? '[::]:' + api_tls_port : null;
|
||||||
config["external-doh-server"] = uci.get(uciconf, uciapi, 'external_doh_server');
|
config["external-doh-server"] = uci.get(uciconf, uciapi, 'external_doh_server');
|
||||||
@@ -386,6 +388,7 @@ config.dns = {
|
|||||||
enable: true,
|
enable: true,
|
||||||
"prefer-h3": false,
|
"prefer-h3": false,
|
||||||
listen: '[::]:' + (uci.get(uciconf, ucidns, 'dns_port') || '7853'),
|
listen: '[::]:' + (uci.get(uciconf, ucidns, 'dns_port') || '7853'),
|
||||||
|
"listen-routing-mark": strToInt(uci.get(uciconf, ucidns, 'routing_mark')) || null,
|
||||||
ipv6: (uci.get(uciconf, ucidns, 'ipv6') === '0') ? false : true,
|
ipv6: (uci.get(uciconf, ucidns, 'ipv6') === '0') ? false : true,
|
||||||
"enhanced-mode": 'redir-host',
|
"enhanced-mode": 'redir-host',
|
||||||
"use-hosts": true,
|
"use-hosts": true,
|
||||||
@@ -479,14 +482,16 @@ uci.foreach(uciconf, ucinode, (cfg) => {
|
|||||||
"routing-mark": strToInt(cfg.routing_mark) || null,
|
"routing-mark": strToInt(cfg.routing_mark) || null,
|
||||||
"ip-version": cfg.ip_version,
|
"ip-version": cfg.ip_version,
|
||||||
|
|
||||||
/* HTTP / SOCKS / Shadowsocks / VMess / VLESS / Trojan / hysteria2 / TUIC / SSH / WireGuard / Masque */
|
/* Rematch */
|
||||||
|
"target-rematch-name": cfg.target_rematch_name,
|
||||||
|
"target-sub-rule": cfg.target_sub_rule,
|
||||||
|
|
||||||
|
/* HTTP / SOCKS / Shadowsocks / VMess / VLESS / Trojan / hysteria2 / TUIC / WireGuard / Masque */
|
||||||
username: cfg.username,
|
username: cfg.username,
|
||||||
uuid: cfg.vmess_uuid || cfg.uuid,
|
uuid: cfg.vmess_uuid || cfg.uuid,
|
||||||
cipher: cfg.vmess_chipher || cfg.shadowsocks_chipher,
|
cipher: cfg.vmess_chipher || cfg.shadowsocks_chipher,
|
||||||
password: cfg.shadowsocks_password || cfg.password,
|
password: cfg.shadowsocks_password || cfg.password,
|
||||||
headers: cfg.headers ? json(cfg.headers) : null,
|
headers: cfg.headers ? json(cfg.headers) : null,
|
||||||
"private-key": cfg.masque_private_key || cfg.wireguard_private_key || cfg.ssh_priv_key,
|
|
||||||
"public-key": cfg.masque_endpoint_public_key || cfg.wireguard_peer_public_key,
|
|
||||||
ip: cfg.masque_ip || cfg.wireguard_ip,
|
ip: cfg.masque_ip || cfg.wireguard_ip,
|
||||||
ipv6: cfg.masque_ipv6 || cfg.wireguard_ipv6,
|
ipv6: cfg.masque_ipv6 || cfg.wireguard_ipv6,
|
||||||
mtu: strToInt(cfg.masque_mtu ?? cfg.wireguard_mtu) || null,
|
mtu: strToInt(cfg.masque_mtu ?? cfg.wireguard_mtu) || null,
|
||||||
@@ -565,10 +570,10 @@ uci.foreach(uciconf, ucinode, (cfg) => {
|
|||||||
"udp-over-stream": strToBool(cfg.tuic_udp_over_stream),
|
"udp-over-stream": strToBool(cfg.tuic_udp_over_stream),
|
||||||
"udp-over-stream-version": cfg.tuic_udp_over_stream_version,
|
"udp-over-stream-version": cfg.tuic_udp_over_stream_version,
|
||||||
"max-udp-relay-packet-size": strToInt(cfg.tuic_max_udp_relay_packet_size) || null,
|
"max-udp-relay-packet-size": strToInt(cfg.tuic_max_udp_relay_packet_size) || null,
|
||||||
|
"fast-open": strToBool(cfg.tuic_fast_open),
|
||||||
"reduce-rtt": strToBool(cfg.tuic_reduce_rtt),
|
"reduce-rtt": strToBool(cfg.tuic_reduce_rtt),
|
||||||
"heartbeat-interval": strToInt(cfg.tuic_heartbeat) || null,
|
"heartbeat-interval": strToInt(cfg.tuic_heartbeat) || null,
|
||||||
"request-timeout": strToInt(cfg.tuic_request_timeout) || null,
|
"request-timeout": strToInt(cfg.tuic_request_timeout) || null,
|
||||||
// @"fast-open": true,
|
|
||||||
"max-open-streams": strToInt(cfg.tuic_max_open_streams) || null,
|
"max-open-streams": strToInt(cfg.tuic_max_open_streams) || null,
|
||||||
|
|
||||||
/* Trojan */
|
/* Trojan */
|
||||||
@@ -591,8 +596,11 @@ uci.foreach(uciconf, ucinode, (cfg) => {
|
|||||||
"packet-encoding": cfg.vmess_packet_encoding,
|
"packet-encoding": cfg.vmess_packet_encoding,
|
||||||
encryption: cfg.vless_encryption === '1' ? cfg.vless_encryption_encryption : null,
|
encryption: cfg.vless_encryption === '1' ? cfg.vless_encryption_encryption : null,
|
||||||
|
|
||||||
|
/* Masque */
|
||||||
|
network: cfg.masque_network || null,
|
||||||
|
|
||||||
/* TrustTunnel */
|
/* TrustTunnel */
|
||||||
"health-check": cfg.trusttunnel_health_check === '0' ? false : true,
|
"health-check": cfg.type === 'trusttunnel' ? (cfg.trusttunnel_health_check === '0' ? false : true) : null,
|
||||||
quic: strToBool(cfg.trusttunnel_quic),
|
quic: strToBool(cfg.trusttunnel_quic),
|
||||||
|
|
||||||
/* WireGuard */
|
/* WireGuard */
|
||||||
@@ -619,6 +627,7 @@ uci.foreach(uciconf, ucinode, (cfg) => {
|
|||||||
"udp-over-tcp": strToBool(cfg.uot),
|
"udp-over-tcp": strToBool(cfg.uot),
|
||||||
"udp-over-tcp-version": cfg.uot_version,
|
"udp-over-tcp-version": cfg.uot_version,
|
||||||
|
|
||||||
|
/* SSH / WireGuard / Masque */
|
||||||
/* TLS fields */
|
/* TLS fields */
|
||||||
tls: (cfg.type in ['trojan', 'anytls', 'hysteria', 'hysteria2', 'tuic', 'trusttunnel']) ? null : strToBool(cfg.tls),
|
tls: (cfg.type in ['trojan', 'anytls', 'hysteria', 'hysteria2', 'tuic', 'trusttunnel']) ? null : strToBool(cfg.tls),
|
||||||
"disable-sni": strToBool(cfg.tls_disable_sni),
|
"disable-sni": strToBool(cfg.tls_disable_sni),
|
||||||
@@ -627,7 +636,8 @@ uci.foreach(uciconf, ucinode, (cfg) => {
|
|||||||
alpn: cfg.tls_alpn, // Array
|
alpn: cfg.tls_alpn, // Array
|
||||||
"skip-cert-verify": strToBool(cfg.tls_skip_cert_verify),
|
"skip-cert-verify": strToBool(cfg.tls_skip_cert_verify),
|
||||||
certificate: cfg.tls_cert_path, // mTLS
|
certificate: cfg.tls_cert_path, // mTLS
|
||||||
"private-key": cfg.tls_key_path, // mTLS
|
"private-key": cfg.masque_private_key || cfg.wireguard_private_key || cfg.ssh_priv_key || cfg.tls_key_path, // mTLS/SSH/WireGuard/Masque
|
||||||
|
"public-key": cfg.masque_endpoint_public_key || cfg.wireguard_peer_public_key, // WireGuard/Masque
|
||||||
"client-fingerprint": cfg.tls_client_fingerprint,
|
"client-fingerprint": cfg.tls_client_fingerprint,
|
||||||
"ech-opts": cfg.tls_ech === '1' ? {
|
"ech-opts": cfg.tls_ech === '1' ? {
|
||||||
enable: true,
|
enable: true,
|
||||||
|
|||||||
@@ -217,8 +217,9 @@ export function parseListener(cfg, isClient, label) {
|
|||||||
type: cfg.type,
|
type: cfg.type,
|
||||||
|
|
||||||
listen: cfg.listen || '::',
|
listen: cfg.listen || '::',
|
||||||
port: cfg.port,
|
port: strToInt(cfg.port),
|
||||||
...(isClient ? {
|
...(isClient ? {
|
||||||
|
"routing-mark": strToInt(cfg.routing_mark) || null,
|
||||||
rule: cfg.rule,
|
rule: cfg.rule,
|
||||||
proxy: label,
|
proxy: label,
|
||||||
} : {}),
|
} : {}),
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ include $(TOPDIR)/rules.mk
|
|||||||
|
|
||||||
PKG_NAME:=luci-app-gecoosac
|
PKG_NAME:=luci-app-gecoosac
|
||||||
PKG_VERSION:=2.2
|
PKG_VERSION:=2.2
|
||||||
PKG_RELEASE:=8
|
PKG_RELEASE:=9
|
||||||
|
|
||||||
LUCI_TITLE:=LuCI Support for gecoosac
|
LUCI_TITLE:=LuCI Support for gecoosac
|
||||||
LUCI_DEPENDS:=+luci-compat +gecoosac
|
LUCI_DEPENDS:=+gecoosac
|
||||||
LUCI_PKGARCH:=all
|
LUCI_PKGARCH:=all
|
||||||
PKG_LICENSE:=AGPL-3.0-only
|
PKG_LICENSE:=AGPL-3.0-only
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,233 @@
|
|||||||
|
'use strict';
|
||||||
|
'require form';
|
||||||
|
'require poll';
|
||||||
|
'require rpc';
|
||||||
|
'require uci';
|
||||||
|
'require ui';
|
||||||
|
'require view';
|
||||||
|
|
||||||
|
const DEFAULT_UPLOAD_DIR = '/tmp/gecoosac/upload/';
|
||||||
|
const DEFAULT_DB_DIR = '/etc/gecoosac/';
|
||||||
|
const DEFAULT_CRT_FILE = '/etc/gecoosac/tls/gecoosac.crt';
|
||||||
|
const DEFAULT_KEY_FILE = '/etc/gecoosac/tls/gecoosac.key';
|
||||||
|
const DEFAULT_PID_DIR = '/var/run/';
|
||||||
|
|
||||||
|
const callServiceList = rpc.declare({
|
||||||
|
object: 'service',
|
||||||
|
method: 'list',
|
||||||
|
params: [ 'name' ],
|
||||||
|
expect: { '': {} }
|
||||||
|
});
|
||||||
|
|
||||||
|
const callClearUpload = rpc.declare({
|
||||||
|
object: 'luci.gecoosac',
|
||||||
|
method: 'clear_upload',
|
||||||
|
expect: { '': {} }
|
||||||
|
});
|
||||||
|
|
||||||
|
function validPort(value, defaultValue) {
|
||||||
|
const port = Number(value || defaultValue);
|
||||||
|
return Number.isInteger(port) && port >= 1 && port <= 65535 ? String(port) : defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
function serviceRunning(status) {
|
||||||
|
const service = status && status.gecoosac;
|
||||||
|
const instances = service && service.instances;
|
||||||
|
|
||||||
|
if (!instances)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
for (const name in instances)
|
||||||
|
if (instances[name] && instances[name].running)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clientHost() {
|
||||||
|
let host = window.location.hostname;
|
||||||
|
|
||||||
|
if (host.indexOf(':') !== -1 && host.charAt(0) !== '[')
|
||||||
|
host = '[' + host + ']';
|
||||||
|
|
||||||
|
return host;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clientUrl() {
|
||||||
|
const singlePort = uci.get('gecoosac', 'config', 'isonlyoneprot') !== '0';
|
||||||
|
const https = uci.get('gecoosac', 'config', 'https') === '1';
|
||||||
|
const port = singlePort
|
||||||
|
? validPort(uci.get('gecoosac', 'config', 'port'), '60650')
|
||||||
|
: validPort(uci.get('gecoosac', 'config', 'm_port'), '8080');
|
||||||
|
|
||||||
|
return (singlePort || !https ? 'http://' : 'https://') + clientHost() + ':' + port;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderStatusContent(status) {
|
||||||
|
const running = serviceRunning(status);
|
||||||
|
const text = running
|
||||||
|
? _('The GecoosAC service is running.')
|
||||||
|
: _('The GecoosAC service is not running.');
|
||||||
|
const state = E('span', { 'class': running ? 'gecoosac-running' : 'gecoosac-stopped' }, text);
|
||||||
|
|
||||||
|
if (!running)
|
||||||
|
return E('p', {}, state);
|
||||||
|
|
||||||
|
return E('p', {}, [
|
||||||
|
state,
|
||||||
|
E('button', {
|
||||||
|
'class': 'cbi-button cbi-button-reload',
|
||||||
|
'click': function() {
|
||||||
|
const client = window.open(clientUrl(), '_blank', 'noopener');
|
||||||
|
if (client)
|
||||||
|
client.opener = null;
|
||||||
|
}
|
||||||
|
}, _('Open the mgmt page'))
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStatus(status) {
|
||||||
|
const node = document.getElementById('gecoosac_status');
|
||||||
|
|
||||||
|
if (!node)
|
||||||
|
return;
|
||||||
|
|
||||||
|
while (node.firstChild)
|
||||||
|
node.removeChild(node.firstChild);
|
||||||
|
|
||||||
|
node.appendChild(renderStatusContent(status));
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearUploadError(res) {
|
||||||
|
return res && res.error ? _(res.error) : _('Upload directory was not cleared');
|
||||||
|
}
|
||||||
|
|
||||||
|
return view.extend({
|
||||||
|
load() {
|
||||||
|
return Promise.all([
|
||||||
|
uci.load('gecoosac'),
|
||||||
|
L.resolveDefault(callServiceList('gecoosac'), {})
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
render(data) {
|
||||||
|
let m, s, o;
|
||||||
|
|
||||||
|
m = new form.Map('gecoosac', _('Gecoos AC'),
|
||||||
|
_('Batch management Gecoos AP, Default password: admin') + '<br />' +
|
||||||
|
_('The current AC version %s, only supports AP 7.6 and above.').format('2.2'));
|
||||||
|
|
||||||
|
s = m.section(form.TypedSection, 'gecoosac');
|
||||||
|
s.anonymous = true;
|
||||||
|
s.render = function() {
|
||||||
|
poll.add(function() {
|
||||||
|
return L.resolveDefault(callServiceList('gecoosac'), {}).then(updateStatus);
|
||||||
|
}, 3);
|
||||||
|
|
||||||
|
return E('fieldset', { 'class': 'cbi-section' }, [
|
||||||
|
E('style', {}, [
|
||||||
|
'#gecoosac_status .gecoosac-running{color:green}',
|
||||||
|
'#gecoosac_status .gecoosac-stopped{color:red}',
|
||||||
|
'#gecoosac_status .cbi-button{margin-left:1em}'
|
||||||
|
].join('\n')),
|
||||||
|
E('div', { 'id': 'gecoosac_status' }, renderStatusContent(data[1]))
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
s = m.section(form.TypedSection, 'gecoosac', _('Global Settings'));
|
||||||
|
s.addremove = false;
|
||||||
|
s.anonymous = true;
|
||||||
|
|
||||||
|
o = s.option(form.Flag, 'enabled', _('Enabled AC'));
|
||||||
|
o.rmempty = false;
|
||||||
|
|
||||||
|
o = s.option(form.Value, 'port', _('Set interface port'));
|
||||||
|
o.placeholder = '60650';
|
||||||
|
o.default = '60650';
|
||||||
|
o.datatype = 'port';
|
||||||
|
o.rmempty = false;
|
||||||
|
|
||||||
|
o = s.option(form.Flag, 'isonlyoneprot', _('Single Port Mode'),
|
||||||
|
_('Do not enable the independent management port, only use one port for management.'));
|
||||||
|
o.default = '1';
|
||||||
|
o.rmempty = false;
|
||||||
|
|
||||||
|
o = s.option(form.Value, 'm_port', _('Set management port'));
|
||||||
|
o.placeholder = '8080';
|
||||||
|
o.default = '8080';
|
||||||
|
o.datatype = 'port';
|
||||||
|
o.depends('isonlyoneprot', '0');
|
||||||
|
|
||||||
|
o = s.option(form.Flag, 'https', _('Enable HTTPS service'),
|
||||||
|
_('Default certificate files are generated when HTTPS starts; custom paths must point to readable files.'));
|
||||||
|
o.default = '0';
|
||||||
|
o.depends('isonlyoneprot', '0');
|
||||||
|
|
||||||
|
o = s.option(form.Value, 'crt_file', _('Specify crt certificate file'));
|
||||||
|
o.placeholder = DEFAULT_CRT_FILE;
|
||||||
|
o.default = DEFAULT_CRT_FILE;
|
||||||
|
o.datatype = 'file';
|
||||||
|
o.depends('https', '1');
|
||||||
|
|
||||||
|
o = s.option(form.Value, 'key_file', _('Specify key certificate file'));
|
||||||
|
o.placeholder = DEFAULT_KEY_FILE;
|
||||||
|
o.default = DEFAULT_KEY_FILE;
|
||||||
|
o.datatype = 'file';
|
||||||
|
o.depends('https', '1');
|
||||||
|
|
||||||
|
o = s.option(form.Value, 'upload_dir', _('Upload dir path'), _('The path to upload AP upgrade firmware'));
|
||||||
|
o.placeholder = DEFAULT_UPLOAD_DIR;
|
||||||
|
o.default = DEFAULT_UPLOAD_DIR;
|
||||||
|
o.datatype = 'directory';
|
||||||
|
o.rmempty = false;
|
||||||
|
|
||||||
|
o = s.option(form.Value, 'db_dir', _('Database dir path'), _('The path to store the config database'));
|
||||||
|
o.placeholder = DEFAULT_DB_DIR;
|
||||||
|
o.default = DEFAULT_DB_DIR;
|
||||||
|
o.datatype = 'directory';
|
||||||
|
o.rmempty = false;
|
||||||
|
|
||||||
|
o = s.option(form.Value, 'piddir', _('PID dir path'), _('The path to store the AC program pid file'));
|
||||||
|
o.placeholder = DEFAULT_PID_DIR;
|
||||||
|
o.default = DEFAULT_PID_DIR;
|
||||||
|
o.datatype = 'directory';
|
||||||
|
o.rmempty = false;
|
||||||
|
|
||||||
|
o = s.option(form.ListValue, 'lang', _('Language'));
|
||||||
|
o.value('zh', _('Chinese'));
|
||||||
|
o.value('en', _('English'));
|
||||||
|
o.default = 'zh';
|
||||||
|
o.rmempty = false;
|
||||||
|
|
||||||
|
o = s.option(form.Flag, 'debug', _('Debug Mode'));
|
||||||
|
o.default = '0';
|
||||||
|
o.rmempty = false;
|
||||||
|
|
||||||
|
o = s.option(form.Flag, 'showtip', _('Show IP Tip'),
|
||||||
|
_('Show the IP 6.7.8.9 setup tip when it is not configured.'));
|
||||||
|
o.default = '0';
|
||||||
|
o.rmempty = false;
|
||||||
|
|
||||||
|
o = s.option(form.Flag, 'log', _('Enable Log'));
|
||||||
|
o.default = '0';
|
||||||
|
o.rmempty = false;
|
||||||
|
|
||||||
|
o = s.option(form.Button, '_clear_upload', _('Clear Upload Directory'),
|
||||||
|
_('Only files under the configured Gecoos upload directory will be removed.'));
|
||||||
|
o.inputstyle = 'remove';
|
||||||
|
o.inputtitle = _('Clear');
|
||||||
|
o.onclick = function() {
|
||||||
|
if (!confirm(_('Really clear the configured upload directory?')))
|
||||||
|
return Promise.resolve();
|
||||||
|
|
||||||
|
return callClearUpload().then(function(res) {
|
||||||
|
if (res && res.result === true)
|
||||||
|
ui.addNotification(null, E('p', {}, _('Upload directory cleared')));
|
||||||
|
else
|
||||||
|
ui.addNotification(null, E('p', {}, clearUploadError(res)), 'danger');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return m.render();
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
module("luci.controller.gecoosac", package.seeall)
|
|
||||||
|
|
||||||
local fs = require "nixio.fs"
|
|
||||||
local sys = require "luci.sys"
|
|
||||||
local uci = require "luci.model.uci"
|
|
||||||
|
|
||||||
local ACL_DEPENDS = { "luci-app-gecoosac" }
|
|
||||||
|
|
||||||
local function service_running()
|
|
||||||
if not fs.access("/etc/init.d/gecoosac") then
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
return sys.call("/etc/init.d/gecoosac status >/dev/null 2>&1") == 0
|
|
||||||
end
|
|
||||||
|
|
||||||
function index()
|
|
||||||
if not fs.access("/etc/config/gecoosac") then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
local page
|
|
||||||
page = entry({"admin", "services", "gecoosac"}, cbi("gecoosac"), _("Gecoos AC"), 100)
|
|
||||||
page.dependent = true
|
|
||||||
page.acl_depends = ACL_DEPENDS
|
|
||||||
page = entry({"admin", "services", "gecoosac", "status"}, call("act_status"))
|
|
||||||
page.leaf = true
|
|
||||||
page.acl_depends = ACL_DEPENDS
|
|
||||||
end
|
|
||||||
|
|
||||||
function act_status()
|
|
||||||
local cur = uci.cursor()
|
|
||||||
local enabled = cur:get("gecoosac", "config", "enabled") == "1"
|
|
||||||
local e = {
|
|
||||||
enabled = enabled,
|
|
||||||
running = enabled and service_running()
|
|
||||||
}
|
|
||||||
luci.http.prepare_content("application/json")
|
|
||||||
luci.http.write_json(e)
|
|
||||||
end
|
|
||||||
@@ -1,197 +0,0 @@
|
|||||||
local fs = require "nixio.fs"
|
|
||||||
|
|
||||||
local DEFAULT_UPLOAD_DIR = "/tmp/gecoosac/upload/"
|
|
||||||
local DEFAULT_DB_DIR = "/etc/gecoosac/"
|
|
||||||
local DEFAULT_CRT_FILE = "/etc/gecoosac/tls/gecoosac.crt"
|
|
||||||
local DEFAULT_KEY_FILE = "/etc/gecoosac/tls/gecoosac.key"
|
|
||||||
local DEFAULT_PID_DIR = "/var/run/"
|
|
||||||
|
|
||||||
local function trim(value)
|
|
||||||
return (value or ""):gsub("^%s+", ""):gsub("%s+$", "")
|
|
||||||
end
|
|
||||||
|
|
||||||
local function is_abs_path(value)
|
|
||||||
return type(value) == "string" and value:match("^/") and not value:find("[%z\r\n]")
|
|
||||||
end
|
|
||||||
|
|
||||||
local function validate_abs_path(self, value)
|
|
||||||
value = trim(value)
|
|
||||||
if is_abs_path(value) then
|
|
||||||
return value
|
|
||||||
end
|
|
||||||
|
|
||||||
return nil, translate("Expecting an absolute path")
|
|
||||||
end
|
|
||||||
|
|
||||||
local function is_upload_path(path)
|
|
||||||
path = trim(path):gsub("/+$", "")
|
|
||||||
return path == "/tmp/gecoosac/upload" or path:match("/gecoosac/upload$") ~= nil
|
|
||||||
end
|
|
||||||
|
|
||||||
local function remove_tree(path)
|
|
||||||
local stat = fs.lstat and fs.lstat(path) or fs.stat(path)
|
|
||||||
if not stat then
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
|
|
||||||
if stat.type == "dir" then
|
|
||||||
local iter = fs.dir(path)
|
|
||||||
if not iter then
|
|
||||||
return nil, translatef("Unable to read %s", path)
|
|
||||||
end
|
|
||||||
|
|
||||||
for entry in iter do
|
|
||||||
if entry ~= "." and entry ~= ".." then
|
|
||||||
local ok, err = remove_tree(path .. "/" .. entry)
|
|
||||||
if not ok then
|
|
||||||
return nil, err
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if not fs.rmdir(path) and fs.stat(path, "type") == "dir" then
|
|
||||||
return nil, translatef("Unable to remove %s", path)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
if not fs.unlink(path) and (fs.lstat and fs.lstat(path) or fs.stat(path)) then
|
|
||||||
return nil, translatef("Unable to remove %s", path)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
|
|
||||||
local function clear_upload_dir(path)
|
|
||||||
path = trim(path)
|
|
||||||
if path == "" then
|
|
||||||
path = DEFAULT_UPLOAD_DIR
|
|
||||||
end
|
|
||||||
|
|
||||||
path = path:gsub("/+$", "")
|
|
||||||
if not is_abs_path(path) then
|
|
||||||
return nil, translate("Expecting an absolute path")
|
|
||||||
end
|
|
||||||
|
|
||||||
local real = fs.realpath(path) or path
|
|
||||||
real = real:gsub("/+$", "")
|
|
||||||
if not is_upload_path(real) then
|
|
||||||
return nil, translate("Only Gecoos upload directories can be cleared")
|
|
||||||
end
|
|
||||||
|
|
||||||
if fs.stat(real, "type") ~= "dir" then
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
|
|
||||||
local iter = fs.dir(real)
|
|
||||||
if not iter then
|
|
||||||
return nil, translatef("Unable to read %s", real)
|
|
||||||
end
|
|
||||||
|
|
||||||
for entry in iter do
|
|
||||||
if entry ~= "." and entry ~= ".." then
|
|
||||||
local ok, err = remove_tree(real .. "/" .. entry)
|
|
||||||
if not ok then
|
|
||||||
return nil, err
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
|
|
||||||
if fs.access("/usr/bin/gecoosac") then
|
|
||||||
m = Map("gecoosac", translate("Gecoos AC"), translate("Batch management Gecoos AP, Default password: admin") .. "<br>" .. translatef("The current AC version %s, only supports AP 7.6 and above.","2.2"))
|
|
||||||
else
|
|
||||||
m = Map("gecoosac", translate("Gecoos AC"), translate("Batch management Gecoos AP, Default password: admin") .. "<br>" .. translate("The AC program does not exist, please check."))
|
|
||||||
end
|
|
||||||
|
|
||||||
m:section(SimpleSection).template = "gecoosac/gecoosac_status"
|
|
||||||
|
|
||||||
s = m:section(TypedSection, "gecoosac", translate("Global Settings"))
|
|
||||||
s.addremove = false
|
|
||||||
s.anonymous = true
|
|
||||||
|
|
||||||
enable = s:option(Flag, "enabled", translate("Enabled AC"))
|
|
||||||
enable.rmempty = false
|
|
||||||
|
|
||||||
o = s:option(Value, "port", translate("Set interface port"))
|
|
||||||
o.placeholder = 60650
|
|
||||||
o.default = 60650
|
|
||||||
o.datatype = "port"
|
|
||||||
o.rmempty = false
|
|
||||||
|
|
||||||
o = s:option(Flag, "isonlyoneprot", translate("Single Port Mode"), translate("Do not enable the independent management port, only use one port for management."))
|
|
||||||
o.default = 1
|
|
||||||
o.rmempty = false
|
|
||||||
|
|
||||||
o = s:option(Value, "m_port", translate("Set management port"))
|
|
||||||
o.placeholder = 8080
|
|
||||||
o.default = 8080
|
|
||||||
o.datatype = "port"
|
|
||||||
o:depends("isonlyoneprot", false)
|
|
||||||
|
|
||||||
o = s:option(Flag, "https", translate("Enable HTTPS service"), translate("Default certificate files are generated when HTTPS starts; custom paths must point to readable files."))
|
|
||||||
o.default = 0
|
|
||||||
o:depends("isonlyoneprot", false)
|
|
||||||
|
|
||||||
o = s:option(Value, "crt_file", translate("Specify crt certificate file"))
|
|
||||||
o.placeholder = DEFAULT_CRT_FILE
|
|
||||||
o.default = DEFAULT_CRT_FILE
|
|
||||||
o.validate = validate_abs_path
|
|
||||||
o:depends("https", true)
|
|
||||||
|
|
||||||
o = s:option(Value, "key_file", translate("Specify key certificate file"))
|
|
||||||
o.placeholder = DEFAULT_KEY_FILE
|
|
||||||
o.default = DEFAULT_KEY_FILE
|
|
||||||
o.validate = validate_abs_path
|
|
||||||
o:depends("https", true)
|
|
||||||
|
|
||||||
upload_dir = s:option(Value, "upload_dir", translate("Upload dir path"), translate("The path to upload AP upgrade firmware"))
|
|
||||||
upload_dir.placeholder = DEFAULT_UPLOAD_DIR
|
|
||||||
upload_dir.default = DEFAULT_UPLOAD_DIR
|
|
||||||
upload_dir.rmempty = false
|
|
||||||
upload_dir.validate = validate_abs_path
|
|
||||||
|
|
||||||
db_dir = s:option(Value, "db_dir", translate("Database dir path"), translate("The path to store the config database"))
|
|
||||||
db_dir.placeholder = DEFAULT_DB_DIR
|
|
||||||
db_dir.default = DEFAULT_DB_DIR
|
|
||||||
db_dir.rmempty = false
|
|
||||||
db_dir.validate = validate_abs_path
|
|
||||||
|
|
||||||
o = s:option(Value, "piddir", translate("PID dir path"), translate("The path to store the AC program pid file"))
|
|
||||||
o.placeholder = DEFAULT_PID_DIR
|
|
||||||
o.default = DEFAULT_PID_DIR
|
|
||||||
o.rmempty = false
|
|
||||||
o.validate = validate_abs_path
|
|
||||||
|
|
||||||
o = s:option(ListValue, "lang", translate("Language"))
|
|
||||||
o:value("zh", translate("Chinese"))
|
|
||||||
o:value("en", translate("English"))
|
|
||||||
o.default = "zh"
|
|
||||||
o.rmempty = false
|
|
||||||
|
|
||||||
debug = s:option(Flag, "debug", translate("Debug Mode"))
|
|
||||||
debug.default = 0
|
|
||||||
debug.rmempty = false
|
|
||||||
|
|
||||||
showtip = s:option(Flag, "showtip", translate("Show IP Tip"), translate("Show the IP 6.7.8.9 setup tip when it is not configured."))
|
|
||||||
showtip.default = 0
|
|
||||||
showtip.rmempty = false
|
|
||||||
|
|
||||||
log = s:option(Flag, "log", translate("Enable Log"))
|
|
||||||
log.default = 0
|
|
||||||
log.rmempty = false
|
|
||||||
|
|
||||||
clear_upload = s:option(Button, "clear_upload", translate("Clear Upload Directory"), translate("Only files under the configured Gecoos upload directory will be removed."))
|
|
||||||
clear_upload.inputstyle = "remove"
|
|
||||||
clear_upload.write = function(self, section)
|
|
||||||
local path = upload_dir:formvalue(section) or upload_dir:cfgvalue(section) or DEFAULT_UPLOAD_DIR
|
|
||||||
local ok, err = clear_upload_dir(path)
|
|
||||||
if not ok then
|
|
||||||
self.map.message = err or translate("Upload directory was not cleared")
|
|
||||||
else
|
|
||||||
self.map.message = translate("Upload directory cleared")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return m
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
<%
|
|
||||||
local uci=require"luci.model.uci".cursor()
|
|
||||||
local json=require"luci.jsonc"
|
|
||||||
|
|
||||||
local function valid_port(value, default)
|
|
||||||
value = tostring(value or default)
|
|
||||||
if value:match("^%d+$") then
|
|
||||||
local port = tonumber(value)
|
|
||||||
if port and port >= 1 and port <= 65535 then
|
|
||||||
return tostring(port)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return default
|
|
||||||
end
|
|
||||||
|
|
||||||
local isonlyoneprot = uci:get("gecoosac", "config", "isonlyoneprot") or "1"
|
|
||||||
local https = uci:get("gecoosac", "config", "https") or "0"
|
|
||||||
local port = valid_port(uci:get("gecoosac", "config", "port"), "60650")
|
|
||||||
local m_port = valid_port(uci:get("gecoosac", "config", "m_port"), "8080")
|
|
||||||
local http = "http://"
|
|
||||||
if isonlyoneprot == "0" then
|
|
||||||
port = m_port
|
|
||||||
if https == "1" then
|
|
||||||
http = "https://"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
-%>
|
|
||||||
<style>
|
|
||||||
#gecoosac_status .gecoosac-running { color: green; }
|
|
||||||
#gecoosac_status .gecoosac-stopped { color: red; }
|
|
||||||
#gecoosac_status .cbi-button { margin-left: 1em; }
|
|
||||||
.gecoosac-status-text { display: none; }
|
|
||||||
</style>
|
|
||||||
<fieldset class="cbi-section">
|
|
||||||
<p id="gecoosac_status">
|
|
||||||
<em><%:Collecting data...%></em>
|
|
||||||
</p>
|
|
||||||
<span class="gecoosac-status-text" id="gecoosac_text_running"><%:The GecoosAC service is running.%></span>
|
|
||||||
<span class="gecoosac-status-text" id="gecoosac_text_stopped"><%:The GecoosAC service is not running.%></span>
|
|
||||||
<span class="gecoosac-status-text" id="gecoosac_text_open"><%:Open the mgmt page%></span>
|
|
||||||
</fieldset>
|
|
||||||
<script type="text/javascript">//<![CDATA[
|
|
||||||
var Port = <%=json.stringify(port)%>;
|
|
||||||
var Http = <%=json.stringify(http)%>;
|
|
||||||
var StatusText = {
|
|
||||||
running: document.getElementById('gecoosac_text_running').textContent,
|
|
||||||
stopped: document.getElementById('gecoosac_text_stopped').textContent,
|
|
||||||
open: document.getElementById('gecoosac_text_open').textContent
|
|
||||||
};
|
|
||||||
|
|
||||||
function clearStatus(node) {
|
|
||||||
while (node.firstChild) {
|
|
||||||
node.removeChild(node.firstChild);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setStatus(node, running) {
|
|
||||||
var status = document.createElement('span');
|
|
||||||
status.className = running ? 'gecoosac-running' : 'gecoosac-stopped';
|
|
||||||
status.textContent = running ? StatusText.running : StatusText.stopped;
|
|
||||||
|
|
||||||
clearStatus(node);
|
|
||||||
node.appendChild(status);
|
|
||||||
|
|
||||||
if (running) {
|
|
||||||
var button = document.createElement('input');
|
|
||||||
button.className = 'cbi-button cbi-button-reload';
|
|
||||||
button.type = 'button';
|
|
||||||
button.value = StatusText.open;
|
|
||||||
button.onclick = openClient;
|
|
||||||
node.appendChild(button);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
XHR.poll(3, '<%=url([[admin]], [[services]], [[gecoosac]], [[status]])%>', null,
|
|
||||||
function(x, data) {
|
|
||||||
var tb = document.getElementById('gecoosac_status');
|
|
||||||
if (data && tb) {
|
|
||||||
setStatus(tb, data.running);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
function clientHost() {
|
|
||||||
var host = window.location.hostname;
|
|
||||||
if (host.indexOf(':') !== -1 && host.charAt(0) !== '[') {
|
|
||||||
host = '[' + host + ']';
|
|
||||||
}
|
|
||||||
return host;
|
|
||||||
}
|
|
||||||
|
|
||||||
function openClient() {
|
|
||||||
var url = Http + clientHost() + ":" + Port;
|
|
||||||
var client = window.open(url, '_blank', 'noopener');
|
|
||||||
if (client) {
|
|
||||||
client.opener = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//]]>
|
|
||||||
</script>
|
|
||||||
@@ -21,9 +21,6 @@ msgstr "批量集中管理集客 AP,默认密码:admin"
|
|||||||
msgid "The current AC version %s, only supports AP 7.6 and above."
|
msgid "The current AC version %s, only supports AP 7.6 and above."
|
||||||
msgstr "当前 AC 版本 %s ,仅支持 AP 7.6 及以上版本。"
|
msgstr "当前 AC 版本 %s ,仅支持 AP 7.6 及以上版本。"
|
||||||
|
|
||||||
msgid "The AC program does not exist, please check."
|
|
||||||
msgstr "AC 程序不存在,请检查。"
|
|
||||||
|
|
||||||
msgid "Global Settings"
|
msgid "Global Settings"
|
||||||
msgstr "全局设置"
|
msgstr "全局设置"
|
||||||
|
|
||||||
@@ -99,15 +96,18 @@ msgstr "集客AC控制器 未运行"
|
|||||||
msgid "Open the mgmt page"
|
msgid "Open the mgmt page"
|
||||||
msgstr "打开管理页面"
|
msgstr "打开管理页面"
|
||||||
|
|
||||||
msgid "Collecting data..."
|
|
||||||
msgstr "收集数据..."
|
|
||||||
|
|
||||||
msgid "Clear Upload Directory"
|
msgid "Clear Upload Directory"
|
||||||
msgstr "清理上传目录"
|
msgstr "清理上传目录"
|
||||||
|
|
||||||
|
msgid "Clear"
|
||||||
|
msgstr "清理"
|
||||||
|
|
||||||
msgid "Only files under the configured Gecoos upload directory will be removed."
|
msgid "Only files under the configured Gecoos upload directory will be removed."
|
||||||
msgstr "只会删除已配置的集客 AC 上传目录中的文件。"
|
msgstr "只会删除已配置的集客 AC 上传目录中的文件。"
|
||||||
|
|
||||||
|
msgid "Really clear the configured upload directory?"
|
||||||
|
msgstr "确定要清理已配置的上传目录吗?"
|
||||||
|
|
||||||
msgid "Expecting an absolute path"
|
msgid "Expecting an absolute path"
|
||||||
msgstr "请输入绝对路径"
|
msgstr "请输入绝对路径"
|
||||||
|
|
||||||
@@ -120,8 +120,5 @@ msgstr "上传目录未清理"
|
|||||||
msgid "Upload directory cleared"
|
msgid "Upload directory cleared"
|
||||||
msgstr "上传目录已清理"
|
msgstr "上传目录已清理"
|
||||||
|
|
||||||
msgid "Unable to read %s"
|
msgid "Unable to remove upload directory contents"
|
||||||
msgstr "无法读取 %s"
|
msgstr "无法删除上传目录内容"
|
||||||
|
|
||||||
msgid "Unable to remove %s"
|
|
||||||
msgstr "无法删除 %s"
|
|
||||||
+91
@@ -0,0 +1,91 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
. /usr/share/libubox/jshn.sh
|
||||||
|
|
||||||
|
DEFAULT_UPLOAD_DIR=/tmp/gecoosac/upload/
|
||||||
|
|
||||||
|
json_result() {
|
||||||
|
local ok="$1"
|
||||||
|
local message="$2"
|
||||||
|
local path="$3"
|
||||||
|
|
||||||
|
json_init
|
||||||
|
json_add_boolean result "$ok"
|
||||||
|
[ -n "$message" ] && json_add_string error "$message"
|
||||||
|
[ -n "$path" ] && json_add_string path "$path"
|
||||||
|
json_dump
|
||||||
|
json_cleanup
|
||||||
|
}
|
||||||
|
|
||||||
|
strip_trailing_slashes() {
|
||||||
|
local path="$1"
|
||||||
|
|
||||||
|
while [ "$path" != "/" ] && [ "${path%/}" != "$path" ]; do
|
||||||
|
path="${path%/}"
|
||||||
|
done
|
||||||
|
|
||||||
|
printf '%s\n' "$path"
|
||||||
|
}
|
||||||
|
|
||||||
|
safe_upload_path() {
|
||||||
|
local path
|
||||||
|
|
||||||
|
path="$(strip_trailing_slashes "$1")"
|
||||||
|
[ "$path" = "/tmp/gecoosac/upload" ] && return 0
|
||||||
|
|
||||||
|
case "$path" in
|
||||||
|
*/gecoosac/upload) return 0 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
clear_upload() {
|
||||||
|
local path real entry
|
||||||
|
|
||||||
|
path="$(uci -q get gecoosac.config.upload_dir)"
|
||||||
|
[ -n "$path" ] || path="$DEFAULT_UPLOAD_DIR"
|
||||||
|
|
||||||
|
case "$path" in
|
||||||
|
/*) ;;
|
||||||
|
*) json_result 0 "Expecting an absolute path"; return ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
path="$(strip_trailing_slashes "$path")"
|
||||||
|
real="$(readlink -f "$path" 2>/dev/null)"
|
||||||
|
[ -n "$real" ] || real="$path"
|
||||||
|
real="$(strip_trailing_slashes "$real")"
|
||||||
|
|
||||||
|
if ! safe_upload_path "$real"; then
|
||||||
|
json_result 0 "Only Gecoos upload directories can be cleared" "$real"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ -d "$real" ] || { json_result 1 "" "$real"; return; }
|
||||||
|
|
||||||
|
for entry in "$real"/* "$real"/.[!.]* "$real"/..?*; do
|
||||||
|
[ -e "$entry" ] || [ -L "$entry" ] || continue
|
||||||
|
|
||||||
|
if ! rm -rf "$entry"; then
|
||||||
|
json_result 0 "Unable to remove upload directory contents" "$real"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
json_result 1 "" "$real"
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
list)
|
||||||
|
printf '{ "clear_upload": {} }'
|
||||||
|
;;
|
||||||
|
call)
|
||||||
|
case "$2" in
|
||||||
|
clear_upload) clear_upload ;;
|
||||||
|
*) json_result 0 "Unknown method" ;;
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
json_result 0 "Usage: luci.gecoosac list|call <method>"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"admin/services/gecoosac": {
|
||||||
|
"title": "Gecoos AC",
|
||||||
|
"order": 100,
|
||||||
|
"action": {
|
||||||
|
"type": "view",
|
||||||
|
"path": "gecoosac"
|
||||||
|
},
|
||||||
|
"depends": {
|
||||||
|
"acl": [ "luci-app-gecoosac" ],
|
||||||
|
"uci": { "gecoosac": true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,16 @@
|
|||||||
{
|
{
|
||||||
"luci-app-gecoosac": {
|
"luci-app-gecoosac": {
|
||||||
"description": "Grant UCI access for luci-app-gecoosac",
|
"description": "Grant access for luci-app-gecoosac",
|
||||||
"read": {
|
"read": {
|
||||||
|
"ubus": {
|
||||||
|
"service": [ "list" ]
|
||||||
|
},
|
||||||
"uci": [ "gecoosac" ]
|
"uci": [ "gecoosac" ]
|
||||||
},
|
},
|
||||||
"write": {
|
"write": {
|
||||||
|
"ubus": {
|
||||||
|
"luci.gecoosac": [ "clear_upload" ]
|
||||||
|
},
|
||||||
"uci": [ "gecoosac" ]
|
"uci": [ "gecoosac" ]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
|
|||||||
|
|
||||||
PKG_NAME:=luci-app-run
|
PKG_NAME:=luci-app-run
|
||||||
PKG_VERSION:=1.0.0
|
PKG_VERSION:=1.0.0
|
||||||
PKG_RELEASE:=2
|
PKG_RELEASE:=3
|
||||||
|
|
||||||
LUCI_TITLE:=LuCI support for running makeself .run installers
|
LUCI_TITLE:=LuCI support for running makeself .run installers
|
||||||
LUCI_DESCRIPTION:=Upload and execute makeself-generated .run installers from LuCI.
|
LUCI_DESCRIPTION:=Upload and execute makeself-generated .run installers from LuCI.
|
||||||
|
|||||||
@@ -5,14 +5,11 @@
|
|||||||
'require poll';
|
'require poll';
|
||||||
|
|
||||||
// ====================== 纯JSON国际化 · 中英双语 ======================
|
// ====================== 纯JSON国际化 · 中英双语 ======================
|
||||||
// 25.12 兼容版:唯一变量名 + 可靠语言检测
|
|
||||||
const RUN_LANG = (function () {
|
const RUN_LANG = (function () {
|
||||||
try {
|
try {
|
||||||
// 优先从 cookie 获取(LuCI 最常用方式)
|
|
||||||
const m = document.cookie.match(/luci_lang=([a-zA-Z-]+)/);
|
const m = document.cookie.match(/luci_lang=([a-zA-Z-]+)/);
|
||||||
if (m) return m[1].substring(0, 2).toLowerCase();
|
if (m) return m[1].substring(0, 2).toLowerCase();
|
||||||
|
|
||||||
// 备选:LuCI 环境变量
|
|
||||||
if (window.L && L.env && L.env.lang)
|
if (window.L && L.env && L.env.lang)
|
||||||
return L.env.lang.substring(0, 2).toLowerCase();
|
return L.env.lang.substring(0, 2).toLowerCase();
|
||||||
|
|
||||||
@@ -25,15 +22,15 @@ const RUN_LANG = (function () {
|
|||||||
const I18N = {
|
const I18N = {
|
||||||
zh: {
|
zh: {
|
||||||
title: "Run安装器",
|
title: "Run安装器",
|
||||||
desc: "在路由器上上传并执行 makeself 生成的 .run 安装包。",
|
desc: "在路由器上上传并执行 .run 安装包或 .sh 脚本,注意架构务必匹配。",
|
||||||
drop_tip: "拖入一个 makeself .run 文件,或从电脑选择。",
|
drop_tip: "拖入一个 .run 或 .sh 文件,或从电脑选择。",
|
||||||
choose_file: "选择 .run 文件",
|
choose_file: "选择 .run 或 .sh 文件",
|
||||||
execute: "执行",
|
execute: "执行",
|
||||||
clean_up: "清理",
|
clean_up: "清理",
|
||||||
upload_title: "上传 .run 安装包",
|
upload_title: "上传安装脚本 (.run / .sh)",
|
||||||
log_title: "执行日志",
|
log_title: "执行日志",
|
||||||
clean_done: "临时文件与日志已清理。",
|
clean_done: "临时文件与日志已清理。",
|
||||||
only_run: "仅支持 .run 文件。",
|
only_run: "仅支持 .run 和 .sh 文件。",
|
||||||
prepare_upload: "准备上传:%s (%s)",
|
prepare_upload: "准备上传:%s (%s)",
|
||||||
upload_failed: "上传失败。",
|
upload_failed: "上传失败。",
|
||||||
uploading: "正在上传 %s:%d%%",
|
uploading: "正在上传 %s:%d%%",
|
||||||
@@ -47,15 +44,15 @@ const I18N = {
|
|||||||
},
|
},
|
||||||
en: {
|
en: {
|
||||||
title: "Run Installer",
|
title: "Run Installer",
|
||||||
desc: "Upload and execute a makeself-generated .run package on this router.",
|
desc: "Upload and execute .run packages or .sh scripts on this router, ensuring architecture compatibility.",
|
||||||
drop_tip: "Drop a makeself .run file here, or choose one from your computer.",
|
drop_tip: "Drop a .run or .sh file here, or choose one from your computer.",
|
||||||
choose_file: "Choose .run file",
|
choose_file: "Choose .run or .sh file",
|
||||||
execute: "Execute",
|
execute: "Execute",
|
||||||
clean_up: "Clean up",
|
clean_up: "Clean up",
|
||||||
upload_title: "Upload .run installer",
|
upload_title: "Upload installer script (.run / .sh)",
|
||||||
log_title: "Execution log",
|
log_title: "Execution log",
|
||||||
clean_done: "Temporary files and logs were removed.",
|
clean_done: "Temporary files and logs were removed.",
|
||||||
only_run: "Only .run files are accepted.",
|
only_run: "Only .run and .sh files are accepted.",
|
||||||
prepare_upload: "Preparing upload: %s (%s)",
|
prepare_upload: "Preparing upload: %s (%s)",
|
||||||
upload_failed: "Upload failed.",
|
upload_failed: "Upload failed.",
|
||||||
uploading: "Uploading %s: %d%%",
|
uploading: "Uploading %s: %d%%",
|
||||||
@@ -137,7 +134,6 @@ function bufferToBase64(buffer) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return view.extend({
|
return view.extend({
|
||||||
// ====================== 底部按钮正确隐藏 ======================
|
|
||||||
handleSave: null,
|
handleSave: null,
|
||||||
handleReset: null,
|
handleReset: null,
|
||||||
handleSaveApply: null,
|
handleSaveApply: null,
|
||||||
@@ -156,7 +152,7 @@ return view.extend({
|
|||||||
|
|
||||||
var fileInput = E('input', {
|
var fileInput = E('input', {
|
||||||
'type': 'file',
|
'type': 'file',
|
||||||
accept: '.run,application/x-sh,application/octet-stream',
|
accept: '.run,.sh,application/x-shellscript,application/octet-stream',
|
||||||
style: 'display:none'
|
style: 'display:none'
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -259,8 +255,7 @@ return view.extend({
|
|||||||
},
|
},
|
||||||
|
|
||||||
applyStatus: function (status, state) {
|
applyStatus: function (status, state) {
|
||||||
if (!status)
|
if (!status) return;
|
||||||
return;
|
|
||||||
|
|
||||||
if (status.running)
|
if (status.running)
|
||||||
state.textContent = _('running');
|
state.textContent = _('running');
|
||||||
@@ -271,7 +266,8 @@ return view.extend({
|
|||||||
uploadFile: function (file, progress, state, runButton) {
|
uploadFile: function (file, progress, state, runButton) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
if (!file.name.match(/\.run$/i)) {
|
// 支持 .run 和 .sh 文件
|
||||||
|
if (!file.name.match(/\.(run|sh)$/i)) {
|
||||||
ui.addNotification(null, E('p', [_('only_run')]), 'danger');
|
ui.addNotification(null, E('p', [_('only_run')]), 'danger');
|
||||||
return Promise.reject();
|
return Promise.reject();
|
||||||
}
|
}
|
||||||
@@ -305,30 +301,20 @@ return view.extend({
|
|||||||
xhr.setRequestHeader('Content-Type', 'application/octet-stream');
|
xhr.setRequestHeader('Content-Type', 'application/octet-stream');
|
||||||
|
|
||||||
xhr.upload.onprogress = function (ev) {
|
xhr.upload.onprogress = function (ev) {
|
||||||
if (!ev.lengthComputable)
|
if (!ev.lengthComputable) return;
|
||||||
return;
|
|
||||||
|
|
||||||
progress.value = Math.floor(ev.loaded * 100 / ev.total);
|
progress.value = Math.floor(ev.loaded * 100 / ev.total);
|
||||||
state.textContent = _('uploading', file.name, progress.value);
|
state.textContent = _('uploading', file.name, progress.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
xhr.onerror = function () {
|
xhr.onerror = function () {
|
||||||
// 强制启用按钮
|
|
||||||
document.querySelector('.cbi-button-action').disabled = false;
|
document.querySelector('.cbi-button-action').disabled = false;
|
||||||
reject(new Error(_('upload_err')));
|
reject(new Error(_('upload_err')));
|
||||||
};
|
};
|
||||||
|
|
||||||
xhr.onload = function () {
|
xhr.onload = function () {
|
||||||
// ==========================================
|
|
||||||
// 【直接强制解锁按钮:永远生效】
|
|
||||||
// ==========================================
|
|
||||||
document.querySelector('.cbi-button-action').disabled = false;
|
document.querySelector('.cbi-button-action').disabled = false;
|
||||||
progress.value = 100;
|
progress.value = 100;
|
||||||
|
|
||||||
// 忽略所有返回解析
|
|
||||||
try { JSON.parse(xhr.responseText); } catch (e) { }
|
|
||||||
|
|
||||||
// 直接完成流程
|
|
||||||
uploadFinish(session.id).then(function () {
|
uploadFinish(session.id).then(function () {
|
||||||
state.textContent = _('upload_done', file.name, formatBytes(file.size));
|
state.textContent = _('upload_done', file.name, formatBytes(file.size));
|
||||||
}).catch(function () {
|
}).catch(function () {
|
||||||
@@ -345,8 +331,7 @@ return view.extend({
|
|||||||
startRun: function (runButton, state) {
|
startRun: function (runButton, state) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
if (!this.currentUploadId)
|
if (!this.currentUploadId) return;
|
||||||
return;
|
|
||||||
|
|
||||||
runButton.disabled = true;
|
runButton.disabled = true;
|
||||||
state.textContent = _('starting');
|
state.textContent = _('starting');
|
||||||
@@ -367,8 +352,7 @@ return view.extend({
|
|||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
return readLog(this.logOffset).then(function (res) {
|
return readLog(this.logOffset).then(function (res) {
|
||||||
if (!res || res.error)
|
if (!res || res.error) return;
|
||||||
return;
|
|
||||||
|
|
||||||
if (res.data) {
|
if (res.data) {
|
||||||
log.textContent += res.data;
|
log.textContent += res.data;
|
||||||
|
|||||||
@@ -223,7 +223,14 @@ run_installer() {
|
|||||||
[ -d "$dir" ] || { json_error "Unknown upload id"; return; }
|
[ -d "$dir" ] || { json_error "Unknown upload id"; return; }
|
||||||
name="$(cat "$dir/name" 2>/dev/null)"
|
name="$(cat "$dir/name" 2>/dev/null)"
|
||||||
file="$dir/$name"
|
file="$dir/$name"
|
||||||
[ -x "$file" ] || { json_error "Uploaded file is not executable"; return; }
|
|
||||||
|
# 确保文件可执行(.run 和 .sh 都需要)
|
||||||
|
chmod 700 "$file" 2>/dev/null || true
|
||||||
|
|
||||||
|
[ -x "$file" ] || {
|
||||||
|
json_error "Uploaded file is not executable. Please check file permissions."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
read_state
|
read_state
|
||||||
if is_running "$PID"; then
|
if is_running "$PID"; then
|
||||||
|
|||||||
@@ -72,8 +72,13 @@ saved_token="$(cat "$dir/token" 2>/dev/null)"
|
|||||||
|
|
||||||
name="$(cat "$dir/name" 2>/dev/null)"
|
name="$(cat "$dir/name" 2>/dev/null)"
|
||||||
case "$name" in
|
case "$name" in
|
||||||
*.run) ;;
|
*.run)
|
||||||
*) fail "Invalid filename" ;;
|
;;
|
||||||
|
*.sh)
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
fail "Invalid filename"
|
||||||
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
length="${CONTENT_LENGTH:-0}"
|
length="${CONTENT_LENGTH:-0}"
|
||||||
|
|||||||
@@ -13,12 +13,12 @@ LUCI_DEPENDS+=$(if $(CONFIG_USE_APK),+apk +luci-compat,+opkg)
|
|||||||
LUCI_EXTRA_DEPENDS:=luci-lib-taskd (>=1.0.19)
|
LUCI_EXTRA_DEPENDS:=luci-lib-taskd (>=1.0.19)
|
||||||
LUCI_PKGARCH:=all
|
LUCI_PKGARCH:=all
|
||||||
|
|
||||||
PKG_VERSION:=0.2.0-r2
|
PKG_VERSION:=0.2.0-r3
|
||||||
# PKG_RELEASE MUST be empty for luci.mk
|
# PKG_RELEASE MUST be empty for luci.mk
|
||||||
PKG_RELEASE:=
|
PKG_RELEASE:=
|
||||||
|
|
||||||
ISTORE_UI_VERSION:=0.2.0
|
ISTORE_UI_VERSION:=0.2.0
|
||||||
ISTORE_UI_RELEASE:=1
|
ISTORE_UI_RELEASE:=2
|
||||||
PKG_HASH:=skip
|
PKG_HASH:=skip
|
||||||
|
|
||||||
PKG_SOURCE_URL_FILE:=v$(ISTORE_UI_VERSION)-$(ISTORE_UI_RELEASE).tar.gz
|
PKG_SOURCE_URL_FILE:=v$(ISTORE_UI_VERSION)-$(ISTORE_UI_RELEASE).tar.gz
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
include $(TOPDIR)/rules.mk
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
PKG_NAME:=opera-proxy
|
PKG_NAME:=opera-proxy
|
||||||
PKG_VERSION:=1.24.0
|
PKG_VERSION:=1.25.0
|
||||||
PKG_RELEASE:=5
|
PKG_RELEASE:=6
|
||||||
|
|
||||||
PKG_MAINTAINER:=Konstantine Shevlakov <shevlakov@132lan.ru>
|
PKG_MAINTAINER:=Konstantine Shevlakov <shevlakov@132lan.ru>
|
||||||
PKG_LICENSE:=MIT
|
PKG_LICENSE:=MIT
|
||||||
|
|||||||
Reference in new issue
Block a user