mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-07-27 10:31:38 +08:00
🌴 Sync 2026-06-17 21:17:50
This commit is contained in:
parent
613096ac36
commit
bd9e30ea67
@ -6,8 +6,8 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=headscale
|
||||
PKG_VERSION:=0.28.0
|
||||
PKG_RELEASE:=2
|
||||
PKG_VERSION:=0.29.0
|
||||
PKG_RELEASE:=3
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||
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_VERSION:=1.13.0
|
||||
PKG_RELEASE:=12
|
||||
PKG_RELEASE:=13
|
||||
PKG_MAINTAINER:=kenzok8
|
||||
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/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_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
|
||||
|
||||
@ -98,6 +98,7 @@ function parseAirportSection(section) {
|
||||
name: String(section.name || ''),
|
||||
source_hash: String(section.source_hash || ''),
|
||||
group_id: String(section.group_id || ''),
|
||||
subscription_id: String(section.subscription_id || ''),
|
||||
node_ids: list(section.node_id)
|
||||
};
|
||||
}
|
||||
@ -109,6 +110,7 @@ function airportSectionValues(record) {
|
||||
name: String(record.name || ''),
|
||||
source_hash: String(record.sourceHash || record.source_hash || ''),
|
||||
group_id: String(record.groupId || record.group_id || ''),
|
||||
subscription_id: String(record.subscriptionId || record.subscription_id || ''),
|
||||
node_id: list(record.nodeIds || record.node_ids)
|
||||
};
|
||||
}
|
||||
|
||||
@ -12,6 +12,7 @@
|
||||
|
||||
const FETCHER = '/usr/share/luci-app-daede/fetch-clash-yaml.sh';
|
||||
const GENERATOR = '/usr/share/luci-app-daede/gen-dae-config.sh';
|
||||
const SUB_STAGE = '/tmp/daede-sub.txt';
|
||||
const FETCH_CHUNK_BYTES = 16384;
|
||||
|
||||
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() {
|
||||
return uci.save().then(function() {
|
||||
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 values = airportSync.airportSectionValues(record);
|
||||
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;
|
||||
}
|
||||
@ -485,114 +480,125 @@ return view.extend({
|
||||
const existingAirport = currentAirport();
|
||||
const airportId = existingAirport ? existingAirport.id : airportSync.makeAirportId();
|
||||
const groupName = airportSync.backendGroupName(airportName.value, 'dae', airportId);
|
||||
const batchId = Date.now().toString(36);
|
||||
const foundGroupSection = existingAirport && existingAirport.group_id ? uciSection('dae', existingAirport.group_id) : null;
|
||||
const foundOldGroup = foundGroupSection && foundGroupSection.name === existingAirport.name ? foundGroupSection : null;
|
||||
// the group sources this tag; gen-dae-config slugs tag and source the
|
||||
// same way, so subtag() resolves even for CJK/hyphen names.
|
||||
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 usedTags = {};
|
||||
const nodesByLink = {};
|
||||
(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 : '';
|
||||
const oldNodeIds = existingAirport ? existingAirport.node_ids : [];
|
||||
let subCreated = 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) {
|
||||
created.forEach(function(sid) { uci.remove('dae', sid); });
|
||||
if (groupCreated) {
|
||||
uci.remove('dae', groupSid);
|
||||
} else if (oldGroupSection) {
|
||||
const current = uciSection('dae', groupSid) || {};
|
||||
Object.keys(current).forEach(function(key) {
|
||||
if (key.charAt(0) !== '.' && !Object.prototype.hasOwnProperty.call(oldGroupSection, key))
|
||||
uci.unset('dae', groupSid, key);
|
||||
});
|
||||
Object.keys(oldGroupSection).forEach(function(key) {
|
||||
if (key.charAt(0) !== '.')
|
||||
uci.set('dae', groupSid, key, oldGroupSection[key]);
|
||||
});
|
||||
const restoreSection = function(sid, snapshot) {
|
||||
const current = uciSection('dae', sid) || {};
|
||||
Object.keys(current).forEach(function(key) {
|
||||
if (key.charAt(0) !== '.' && !Object.prototype.hasOwnProperty.call(snapshot, key))
|
||||
uci.unset('dae', sid, key);
|
||||
});
|
||||
Object.keys(snapshot).forEach(function(key) {
|
||||
if (key.charAt(0) !== '.')
|
||||
uci.set('dae', sid, key, snapshot[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() {
|
||||
const referencedTags = {};
|
||||
(uci.sections('dae', 'group') || []).filter(function(group) {
|
||||
return group['.name'] !== groupSid;
|
||||
}).forEach(function(group) {
|
||||
const source = Array.isArray(group.source) ? group.source : (group.source ? [ group.source ] : []);
|
||||
source.forEach(function(tag) { referencedTags[tag] = true; });
|
||||
// 4. drop this airport's legacy converted_N manual nodes
|
||||
oldNodeIds.forEach(function(sid) {
|
||||
if (uciSection('dae', sid)) uci.remove('dae', sid);
|
||||
});
|
||||
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, {
|
||||
id: airportId,
|
||||
backend: 'dae',
|
||||
name: airportName.value.trim(),
|
||||
sourceHash: state.sourceHash,
|
||||
groupId: groupSid,
|
||||
nodeIds: owned
|
||||
subscriptionId: subSid,
|
||||
nodeIds: []
|
||||
});
|
||||
return applyUciChanges().then(runGenerator);
|
||||
}).then(function() {
|
||||
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) {
|
||||
let credentials;
|
||||
let token = '';
|
||||
let existingAirport = currentAirport();
|
||||
const existingAirport = currentAirport();
|
||||
const airportId = existingAirport ? existingAirport.id : airportSync.makeAirportId();
|
||||
const name = airportName.value.trim();
|
||||
const managedName = airportSync.backendId(airportId);
|
||||
const groupName = airportSync.backendGroupName(name, 'daed', airportId);
|
||||
const batchTag = managedName + Date.now().toString(36);
|
||||
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;
|
||||
const createdIds = [];
|
||||
let newSubId = '';
|
||||
let createdGroupId = '';
|
||||
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;
|
||||
return graphQL(endpoint,
|
||||
'query Login($username:String!,$password:String!){token(username:$username,password:$password)}',
|
||||
@ -601,61 +607,33 @@ return view.extend({
|
||||
token = login.token;
|
||||
credentials.password = '';
|
||||
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) {
|
||||
before = dataValue;
|
||||
const existing = {};
|
||||
(before.nodes.edges || []).forEach(function(node) {
|
||||
existing[clashConverter.normalizeLink(node.link)] = node;
|
||||
});
|
||||
const fresh = items.filter(function(item) {
|
||||
const duplicate = existing[clashConverter.normalizeLink(item.link)];
|
||||
if (duplicate) {
|
||||
item.duplicate = true;
|
||||
}
|
||||
return !duplicate;
|
||||
});
|
||||
if (!fresh.length)
|
||||
return { importNodes: [], fresh: fresh, existing: existing };
|
||||
// drop this airport's previous subscription first: the new one
|
||||
// reuses the same tag (group name) and daed enforces unique tags.
|
||||
if (!oldSubId)
|
||||
return null;
|
||||
return graphQL(endpoint, 'mutation RmSub($ids:[ID!]!){removeSubscriptions(ids:$ids)}', { ids: [ oldSubId ] }, token).catch(function() {});
|
||||
}).then(function() {
|
||||
// import the whole converted batch as one subscription
|
||||
return graphQL(endpoint,
|
||||
'mutation ImportNodes($args:[ImportArgument!]!){importNodes(rollbackError:false,args:$args){link error node{id}}}',
|
||||
{ args: fresh.map(function(item, index) { return { link: item.link, tag: batchTag + (index + 1) }; }) }, token)
|
||||
.then(function(value) { value.fresh = fresh; value.existing = existing; return value; });
|
||||
'mutation Import($a:ImportArgument!){importSubscription(rollbackError:false,arg:$a){sub{id} nodeImportResult{error}}}',
|
||||
{ a: { link: subUrl, tag: groupName } }, token);
|
||||
}).then(function(result) {
|
||||
const rows = result.importNodes || [];
|
||||
const nodeIds = [];
|
||||
const ownedIds = [];
|
||||
const rowByLink = {};
|
||||
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('; ');
|
||||
const sub = result.importSubscription && result.importSubscription.sub;
|
||||
if (!sub || !sub.id) {
|
||||
const rows = (result.importSubscription && result.importSubscription.nodeImportResult) || [];
|
||||
const details = rows.map(function(r) { return r.error; }).filter(Boolean).join('; ');
|
||||
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 group = oldGroup;
|
||||
const ensureGroup = group
|
||||
? Promise.resolve(group.id)
|
||||
const ensureGroup = oldGroup
|
||||
? Promise.resolve(oldGroup.id)
|
||||
: graphQL(endpoint,
|
||||
'mutation CreateGroup($name:String!,$policy:Policy!){createGroup(name:$name,policy:$policy){id}}',
|
||||
{ name: groupName, policy: 'min_moving_avg' }, token).then(function(value) {
|
||||
@ -663,64 +641,65 @@ return view.extend({
|
||||
return createdGroupId;
|
||||
});
|
||||
return ensureGroup.then(function(groupId) {
|
||||
const rename = group && group.name !== groupName
|
||||
? graphQL(endpoint, 'mutation RenameGroup($id:ID!,$name:String!){renameGroup(id:$id,name:$name)}', { id: groupId, name: groupName }, token)
|
||||
const rename = oldGroup && oldGroup.name !== groupName
|
||||
? graphQL(endpoint, 'mutation Rename($id:ID!,$name:String!){renameGroup(id:$id,name:$name)}', { id: groupId, name: groupName }, token)
|
||||
: Promise.resolve();
|
||||
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() {
|
||||
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() {
|
||||
groupReady = true;
|
||||
const oldMembers = group ? group.nodes.map(function(node) { return node.id; }) : [];
|
||||
const removedMembers = oldMembers.filter(function(id) { return nodeIds.indexOf(id) < 0; });
|
||||
return (removedMembers.length
|
||||
? graphQL(endpoint, 'mutation DelNodes($id:ID!,$nodeIDs:[ID!]!){groupDelNodes(id:$id,nodeIDs:$nodeIDs)}', { id: groupId, nodeIDs: removedMembers }, token)
|
||||
: Promise.resolve()).catch(function() {
|
||||
failed++;
|
||||
// scrub the group of its previous members: old manual nodes
|
||||
// (from the legacy importNodes flow) and any other subscription.
|
||||
const oldMemberNodes = oldGroup ? (oldGroup.nodes || []).map(function(n) { return n.id; }) : [];
|
||||
const oldGroupSubs = oldGroup
|
||||
? (oldGroup.subscriptions || []).map(function(s) { return s.subscription && s.subscription.id; }).filter(function(id) { return id && id !== newSubId; })
|
||||
: [];
|
||||
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() {
|
||||
const otherAirportNodes = airportRecords().filter(function(record) {
|
||||
return record.backend === 'daed' && (!existingAirport || record.id !== existingAirport.id);
|
||||
}).map(function(record) { return record.node_ids; });
|
||||
const otherGroupNodes = before.groups.filter(function(other) {
|
||||
return other.id !== groupId;
|
||||
}).map(function(other) { return other.nodes.map(function(node) { return node.id; }); });
|
||||
const stale = airportSync.safeOldNodeIds(existingAirport ? existingAirport.node_ids : [], ownedIds, otherAirportNodes, otherGroupNodes);
|
||||
return (stale.length
|
||||
? graphQL(endpoint, 'mutation RemoveNodes($ids:[ID!]!){removeNodes(ids:$ids)}', { ids: stale }, token)
|
||||
: Promise.resolve()).catch(function() {
|
||||
failed++;
|
||||
}).then(function() {
|
||||
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 };
|
||||
});
|
||||
writeAirportRecord(existingAirport, {
|
||||
id: airportId,
|
||||
backend: 'daed',
|
||||
name: name,
|
||||
sourceHash: state.sourceHash,
|
||||
groupId: groupId,
|
||||
subscriptionId: newSubId,
|
||||
nodeIds: []
|
||||
});
|
||||
return applyUciChanges().then(function() {
|
||||
items.forEach(function(item) { item.duplicate = true; item.selected = false; });
|
||||
return { added: items.length, duplicates: 0, failed: failed };
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}).catch(function(error) {
|
||||
if (groupReady || !token || (!createdIds.length && !createdGroupId))
|
||||
if (groupReady || !token || (!newSubId && !createdGroupId))
|
||||
throw error;
|
||||
const cleanup = [];
|
||||
if (createdIds.length)
|
||||
cleanup.push(graphQL(endpoint, 'mutation RemoveNodes($ids:[ID!]!){removeNodes(ids:$ids)}', { ids: createdIds }, token));
|
||||
if (newSubId)
|
||||
cleanup.push(graphQL(endpoint, 'mutation Rm($ids:[ID!]!){removeSubscriptions(ids:$ids)}', { ids: [ newSubId ] }, token));
|
||||
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; });
|
||||
}).finally(function() {
|
||||
if (credentials)
|
||||
credentials.password = '';
|
||||
credentials = null;
|
||||
token = '';
|
||||
return dropStage();
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@ -168,7 +168,7 @@ return view.extend({
|
||||
};
|
||||
|
||||
// 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 orig = btn.textContent;
|
||||
btn.disabled = true;
|
||||
@ -177,7 +177,7 @@ return view.extend({
|
||||
const poll = function() {
|
||||
return L.resolveDefault(fs.read_direct(logPath, 'text'), '').then(function(c) {
|
||||
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;
|
||||
return new Promise(function(r) { setTimeout(r, 2000); }).then(poll);
|
||||
});
|
||||
|
||||
@ -59,7 +59,7 @@ import)
|
||||
fi
|
||||
rm -f "$tmp" "$IMPORT_B64"
|
||||
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 &
|
||||
echo "started in background, see $LOG"
|
||||
;;
|
||||
|
||||
@ -21,6 +21,8 @@ CONFIG_DAE="/etc/dae/config.dae"
|
||||
TMP_GEN="/tmp/dae-gen.dae"
|
||||
DAE_BIN="/usr/bin/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.
|
||||
sanitize() {
|
||||
@ -378,10 +380,36 @@ do_import() {
|
||||
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 =====================
|
||||
|
||||
case "$1" in
|
||||
generate) generate ;;
|
||||
import) do_import ;;
|
||||
*) echo "usage: $0 {generate|import}" >&2; exit 2 ;;
|
||||
generate) generate ;;
|
||||
import) do_import ;;
|
||||
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
|
||||
|
||||
@ -70,7 +70,7 @@ fi
|
||||
echo "$(date '+%F %T') updated $DEST ($size bytes)"
|
||||
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 &
|
||||
|
||||
echo "started in background, see $LOG"
|
||||
|
||||
@ -85,7 +85,7 @@ fi
|
||||
exit 3
|
||||
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.
|
||||
if [ "$PKG" = "luci-app-daede" ] && [ "$rc" = "0" ]; then
|
||||
|
||||
@ -42,7 +42,12 @@
|
||||
"/etc/dae/config.dae": [ "write" ],
|
||||
"/tmp/dae-validate.dae": [ "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/gen-dae-config.sh write-sub *": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/gen-dae-config.sh delete-sub *": [ "exec" ],
|
||||
"/etc/config/daede": [ "write" ],
|
||||
"/var/log/dae/dae.log": [ "write" ],
|
||||
"/var/log/daed/daed.log": [ "write" ],
|
||||
|
||||
25
luci-app-daede/root/www/cgi-bin/daede-sub
Normal file
25
luci-app-daede/root/www/cgi-bin/daede-sub
Normal file
@ -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 = [
|
||||
['rematch', _('Rematch'), _('Rematching routing rules')],
|
||||
['direct', _('DIRECT') + ' - ' + _('TCP/UDP')],
|
||||
['http', _('HTTP') + ' - ' + _('TCP')],
|
||||
['socks5', _('SOCKS5') + ' - ' + _('TCP/UDP')],
|
||||
@ -281,6 +282,7 @@ const rules_type = [
|
||||
//['IN-TYPE'],
|
||||
//['IN-USER'],
|
||||
//['IN-NAME'],
|
||||
['REMATCH-NAME'],
|
||||
|
||||
['PROCESS-PATH'],
|
||||
['PROCESS-PATH-REGEX'],
|
||||
@ -300,7 +302,7 @@ const rules_type = [
|
||||
|
||||
const rules_type_allowparms = [
|
||||
// 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',
|
||||
'IP-ASN',
|
||||
'IP-CIDR',
|
||||
@ -1192,6 +1194,31 @@ function loadSubRuleGroup(preadds, 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) {
|
||||
const visible = isRunning && (isRunning.http || isRunning.https);
|
||||
|
||||
@ -1797,6 +1824,7 @@ return baseclass.extend({
|
||||
loadProviderLabel,
|
||||
loadRulesetLabel,
|
||||
loadSubRuleGroup,
|
||||
loadRematchName,
|
||||
// render
|
||||
renderStatus,
|
||||
updateStatus,
|
||||
|
||||
@ -602,6 +602,10 @@ function renderListeners(s, uciconfig, isClient) {
|
||||
|
||||
/* Extra fields */
|
||||
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'),
|
||||
_('Name of the Sub rule used for inbound matching.'));
|
||||
o.value.apply(o, ['', _('null')]);
|
||||
|
||||
@ -310,7 +310,7 @@ const parseRulesYaml = hm.parseYaml.extend({
|
||||
if (!entry)
|
||||
return null;
|
||||
|
||||
// key mapping // 2026/01/18
|
||||
// key mapping // 2026/06/12
|
||||
let config = {
|
||||
id: this.id,
|
||||
label: '%s %s'.format(this.id.slice(0,7), _('(Imported)')),
|
||||
@ -365,7 +365,7 @@ const parseRulesYaml = hm.parseYaml.extend({
|
||||
|
||||
ParseRule(tp, payload, target, params) {
|
||||
// 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
|
||||
let logical_payload, subrule;
|
||||
@ -427,7 +427,7 @@ const parseRulesYaml = hm.parseYaml.extend({
|
||||
|
||||
parseRules(line) {
|
||||
// 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);
|
||||
if (!target)
|
||||
return null; // error: format invalid
|
||||
@ -561,6 +561,14 @@ function renderPayload(s, total, uciconfig) {
|
||||
o.depends(Object.fromEntries([[prefix + 'type', /\bPROCESS\b/]]));
|
||||
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.datatype = 'uinteger';
|
||||
if (n === 0)
|
||||
@ -571,7 +579,7 @@ function renderPayload(s, total, uciconfig) {
|
||||
o = s.option(form.Value, prefix + 'ip', _('Factor') + ` ${n+1}`);
|
||||
o.datatype = 'cidr';
|
||||
if (n === 0) {
|
||||
o.depends({type: /\b(CIDR|CIDR6)\b/});
|
||||
o.depends({type: /\bCIDR6?\b/});
|
||||
o.depends({type: /\bIP-SUFFIX\b/});
|
||||
}
|
||||
o.depends(Object.fromEntries([[prefix + 'type', /\bCIDR6?\b/]]));
|
||||
@ -1386,6 +1394,9 @@ return view.extend({
|
||||
so.placeholder = '7853';
|
||||
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.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.'));
|
||||
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.datatype = 'port';
|
||||
so.placeholder = '9090';
|
||||
|
||||
@ -247,7 +247,7 @@ return view.extend({
|
||||
so.default = so.enabled;
|
||||
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];
|
||||
hm.outbound_type.forEach((res) => {
|
||||
so.value.apply(so, res);
|
||||
@ -256,12 +256,24 @@ return view.extend({
|
||||
so = ss.taboption('field_general', form.Value, 'server', _('Server address'));
|
||||
so.datatype = 'host';
|
||||
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.datatype = 'port';
|
||||
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 */
|
||||
/* hm.validateAuth */
|
||||
@ -579,6 +591,11 @@ return view.extend({
|
||||
so.depends('type', 'tuic');
|
||||
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'),
|
||||
_('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.'));
|
||||
@ -721,18 +738,38 @@ return view.extend({
|
||||
so.datatype = 'ip4addr(1)';
|
||||
so.placeholder = '172.16.0.2';
|
||||
so.rmempty = false;
|
||||
so.depends('type', 'masque');
|
||||
so.depends({type: 'masque', masque_network: /^(|h2)$/});
|
||||
so.modalonly = true;
|
||||
|
||||
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'));
|
||||
so.datatype = 'ip6addr(1)';
|
||||
so.depends('type', 'masque');
|
||||
so.depends({type: 'masque', masque_network: /^(|h2)$/});
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.taboption('field_general', form.Value, 'masque_mtu', _('MTU'));
|
||||
so.datatype = 'range(0,9000)';
|
||||
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.modalonly = true;
|
||||
|
||||
@ -889,7 +926,8 @@ return view.extend({
|
||||
hm.congestion_controller.forEach((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 = 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.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.modalonly = true;
|
||||
|
||||
@ -1038,7 +1076,7 @@ return view.extend({
|
||||
|
||||
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 = ss.taboption('field_tls', form.Flag, 'tls_disable_sni', _('Disable SNI'),
|
||||
@ -1081,9 +1119,6 @@ return view.extend({
|
||||
case 'vless':
|
||||
def_alpn = ['h3', 'h2', 'http/1.1'];
|
||||
break;
|
||||
case 'masque':
|
||||
def_alpn = ['h2'];
|
||||
break;
|
||||
case 'trusttunnel':
|
||||
def_alpn = ['h3', 'h2'];
|
||||
break;
|
||||
@ -1096,7 +1131,7 @@ return view.extend({
|
||||
|
||||
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.modalonly = true;
|
||||
|
||||
@ -1470,10 +1505,12 @@ return view.extend({
|
||||
/* Dial fields */
|
||||
so = ss.taboption('field_dial', form.Flag, 'tfo', _('TFO'));
|
||||
so.default = so.disabled;
|
||||
so.depends({type: /^(rematch)$/, '!reverse': true});
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.taboption('field_dial', form.Flag, 'mptcp', _('mpTCP'));
|
||||
so.default = so.disabled;
|
||||
so.depends({type: /^(rematch)$/, '!reverse': true});
|
||||
so.modalonly = true;
|
||||
|
||||
/* Features are implemented in proxy chain
|
||||
@ -1487,11 +1524,13 @@ return view.extend({
|
||||
_('Priority: Proxy Node > Global.'));
|
||||
so.multiple = false;
|
||||
so.noaliases = true;
|
||||
so.depends({type: /^(rematch)$/, '!reverse': 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.'));
|
||||
so.datatype = 'uinteger';
|
||||
so.depends({type: /^(rematch)$/, '!reverse': true});
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.taboption('field_dial', form.ListValue, 'ip_version', _('IP version'));
|
||||
@ -1499,6 +1538,7 @@ return view.extend({
|
||||
hm.ip_version.forEach((res) => {
|
||||
so.value.apply(so, res);
|
||||
})
|
||||
so.depends({type: /^(rematch)$/, '!reverse': true});
|
||||
so.modalonly = true;
|
||||
/* Proxy Node END */
|
||||
|
||||
@ -1874,7 +1914,7 @@ return view.extend({
|
||||
so.depends({type: 'inline', '!reverse': 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.'));
|
||||
so.datatype = 'uinteger';
|
||||
so.depends({type: 'inline', '!reverse': true});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -259,11 +259,13 @@ config.tls = {
|
||||
/* API START */
|
||||
const api_port = uci.get(uciconf, uciapi, 'external_controller_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 */
|
||||
config["external-controller-cors"] = {
|
||||
"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
|
||||
};
|
||||
config["external-controller-routing-mark"] = strToInt(api_routing_mark) || null;
|
||||
config["external-controller"] = api_port ? '[::]:' + api_port : null;
|
||||
config["external-controller-tls"] = api_tls_port ? '[::]:' + api_tls_port : null;
|
||||
config["external-doh-server"] = uci.get(uciconf, uciapi, 'external_doh_server');
|
||||
@ -386,6 +388,7 @@ config.dns = {
|
||||
enable: true,
|
||||
"prefer-h3": false,
|
||||
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,
|
||||
"enhanced-mode": 'redir-host',
|
||||
"use-hosts": true,
|
||||
@ -479,14 +482,16 @@ uci.foreach(uciconf, ucinode, (cfg) => {
|
||||
"routing-mark": strToInt(cfg.routing_mark) || null,
|
||||
"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,
|
||||
uuid: cfg.vmess_uuid || cfg.uuid,
|
||||
cipher: cfg.vmess_chipher || cfg.shadowsocks_chipher,
|
||||
password: cfg.shadowsocks_password || cfg.password,
|
||||
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,
|
||||
ipv6: cfg.masque_ipv6 || cfg.wireguard_ipv6,
|
||||
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-version": cfg.tuic_udp_over_stream_version,
|
||||
"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),
|
||||
"heartbeat-interval": strToInt(cfg.tuic_heartbeat) || null,
|
||||
"request-timeout": strToInt(cfg.tuic_request_timeout) || null,
|
||||
// @"fast-open": true,
|
||||
"max-open-streams": strToInt(cfg.tuic_max_open_streams) || null,
|
||||
|
||||
/* Trojan */
|
||||
@ -591,8 +596,11 @@ uci.foreach(uciconf, ucinode, (cfg) => {
|
||||
"packet-encoding": cfg.vmess_packet_encoding,
|
||||
encryption: cfg.vless_encryption === '1' ? cfg.vless_encryption_encryption : null,
|
||||
|
||||
/* Masque */
|
||||
network: cfg.masque_network || null,
|
||||
|
||||
/* 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),
|
||||
|
||||
/* WireGuard */
|
||||
@ -619,6 +627,7 @@ uci.foreach(uciconf, ucinode, (cfg) => {
|
||||
"udp-over-tcp": strToBool(cfg.uot),
|
||||
"udp-over-tcp-version": cfg.uot_version,
|
||||
|
||||
/* SSH / WireGuard / Masque */
|
||||
/* TLS fields */
|
||||
tls: (cfg.type in ['trojan', 'anytls', 'hysteria', 'hysteria2', 'tuic', 'trusttunnel']) ? null : strToBool(cfg.tls),
|
||||
"disable-sni": strToBool(cfg.tls_disable_sni),
|
||||
@ -627,7 +636,8 @@ uci.foreach(uciconf, ucinode, (cfg) => {
|
||||
alpn: cfg.tls_alpn, // Array
|
||||
"skip-cert-verify": strToBool(cfg.tls_skip_cert_verify),
|
||||
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,
|
||||
"ech-opts": cfg.tls_ech === '1' ? {
|
||||
enable: true,
|
||||
|
||||
@ -217,8 +217,9 @@ export function parseListener(cfg, isClient, label) {
|
||||
type: cfg.type,
|
||||
|
||||
listen: cfg.listen || '::',
|
||||
port: cfg.port,
|
||||
port: strToInt(cfg.port),
|
||||
...(isClient ? {
|
||||
"routing-mark": strToInt(cfg.routing_mark) || null,
|
||||
rule: cfg.rule,
|
||||
proxy: label,
|
||||
} : {}),
|
||||
|
||||
@ -7,10 +7,10 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-gecoosac
|
||||
PKG_VERSION:=2.2
|
||||
PKG_RELEASE:=8
|
||||
PKG_RELEASE:=9
|
||||
|
||||
LUCI_TITLE:=LuCI Support for gecoosac
|
||||
LUCI_DEPENDS:=+luci-compat +gecoosac
|
||||
LUCI_DEPENDS:=+gecoosac
|
||||
LUCI_PKGARCH:=all
|
||||
PKG_LICENSE:=AGPL-3.0-only
|
||||
|
||||
|
||||
233
luci-app-gecoosac/htdocs/luci-static/resources/view/gecoosac.js
Normal file
233
luci-app-gecoosac/htdocs/luci-static/resources/view/gecoosac.js
Normal file
@ -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."
|
||||
msgstr "当前 AC 版本 %s ,仅支持 AP 7.6 及以上版本。"
|
||||
|
||||
msgid "The AC program does not exist, please check."
|
||||
msgstr "AC 程序不存在,请检查。"
|
||||
|
||||
msgid "Global Settings"
|
||||
msgstr "全局设置"
|
||||
|
||||
@ -99,15 +96,18 @@ msgstr "集客AC控制器 未运行"
|
||||
msgid "Open the mgmt page"
|
||||
msgstr "打开管理页面"
|
||||
|
||||
msgid "Collecting data..."
|
||||
msgstr "收集数据..."
|
||||
|
||||
msgid "Clear Upload Directory"
|
||||
msgstr "清理上传目录"
|
||||
|
||||
msgid "Clear"
|
||||
msgstr "清理"
|
||||
|
||||
msgid "Only files under the configured Gecoos upload directory will be removed."
|
||||
msgstr "只会删除已配置的集客 AC 上传目录中的文件。"
|
||||
|
||||
msgid "Really clear the configured upload directory?"
|
||||
msgstr "确定要清理已配置的上传目录吗?"
|
||||
|
||||
msgid "Expecting an absolute path"
|
||||
msgstr "请输入绝对路径"
|
||||
|
||||
@ -120,8 +120,5 @@ msgstr "上传目录未清理"
|
||||
msgid "Upload directory cleared"
|
||||
msgstr "上传目录已清理"
|
||||
|
||||
msgid "Unable to read %s"
|
||||
msgstr "无法读取 %s"
|
||||
|
||||
msgid "Unable to remove %s"
|
||||
msgstr "无法删除 %s"
|
||||
msgid "Unable to remove upload directory contents"
|
||||
msgstr "无法删除上传目录内容"
|
||||
|
||||
91
luci-app-gecoosac/root/usr/libexec/rpcd/luci.gecoosac
Executable file
91
luci-app-gecoosac/root/usr/libexec/rpcd/luci.gecoosac
Executable file
@ -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": {
|
||||
"description": "Grant UCI access for luci-app-gecoosac",
|
||||
"description": "Grant access for luci-app-gecoosac",
|
||||
"read": {
|
||||
"ubus": {
|
||||
"service": [ "list" ]
|
||||
},
|
||||
"uci": [ "gecoosac" ]
|
||||
},
|
||||
"write": {
|
||||
"ubus": {
|
||||
"luci.gecoosac": [ "clear_upload" ]
|
||||
},
|
||||
"uci": [ "gecoosac" ]
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-run
|
||||
PKG_VERSION:=1.0.0
|
||||
PKG_RELEASE:=2
|
||||
PKG_RELEASE:=3
|
||||
|
||||
LUCI_TITLE:=LuCI support for running makeself .run installers
|
||||
LUCI_DESCRIPTION:=Upload and execute makeself-generated .run installers from LuCI.
|
||||
|
||||
@ -5,14 +5,11 @@
|
||||
'require poll';
|
||||
|
||||
// ====================== 纯JSON国际化 · 中英双语 ======================
|
||||
// 25.12 兼容版:唯一变量名 + 可靠语言检测
|
||||
const RUN_LANG = (function () {
|
||||
try {
|
||||
// 优先从 cookie 获取(LuCI 最常用方式)
|
||||
const m = document.cookie.match(/luci_lang=([a-zA-Z-]+)/);
|
||||
if (m) return m[1].substring(0, 2).toLowerCase();
|
||||
|
||||
// 备选:LuCI 环境变量
|
||||
if (window.L && L.env && L.env.lang)
|
||||
return L.env.lang.substring(0, 2).toLowerCase();
|
||||
|
||||
@ -25,15 +22,15 @@ const RUN_LANG = (function () {
|
||||
const I18N = {
|
||||
zh: {
|
||||
title: "Run安装器",
|
||||
desc: "在路由器上上传并执行 makeself 生成的 .run 安装包。",
|
||||
drop_tip: "拖入一个 makeself .run 文件,或从电脑选择。",
|
||||
choose_file: "选择 .run 文件",
|
||||
desc: "在路由器上上传并执行 .run 安装包或 .sh 脚本,注意架构务必匹配。",
|
||||
drop_tip: "拖入一个 .run 或 .sh 文件,或从电脑选择。",
|
||||
choose_file: "选择 .run 或 .sh 文件",
|
||||
execute: "执行",
|
||||
clean_up: "清理",
|
||||
upload_title: "上传 .run 安装包",
|
||||
upload_title: "上传安装脚本 (.run / .sh)",
|
||||
log_title: "执行日志",
|
||||
clean_done: "临时文件与日志已清理。",
|
||||
only_run: "仅支持 .run 文件。",
|
||||
only_run: "仅支持 .run 和 .sh 文件。",
|
||||
prepare_upload: "准备上传:%s (%s)",
|
||||
upload_failed: "上传失败。",
|
||||
uploading: "正在上传 %s:%d%%",
|
||||
@ -47,15 +44,15 @@ const I18N = {
|
||||
},
|
||||
en: {
|
||||
title: "Run Installer",
|
||||
desc: "Upload and execute a makeself-generated .run package on this router.",
|
||||
drop_tip: "Drop a makeself .run file here, or choose one from your computer.",
|
||||
choose_file: "Choose .run file",
|
||||
desc: "Upload and execute .run packages or .sh scripts on this router, ensuring architecture compatibility.",
|
||||
drop_tip: "Drop a .run or .sh file here, or choose one from your computer.",
|
||||
choose_file: "Choose .run or .sh file",
|
||||
execute: "Execute",
|
||||
clean_up: "Clean up",
|
||||
upload_title: "Upload .run installer",
|
||||
upload_title: "Upload installer script (.run / .sh)",
|
||||
log_title: "Execution log",
|
||||
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)",
|
||||
upload_failed: "Upload failed.",
|
||||
uploading: "Uploading %s: %d%%",
|
||||
@ -137,7 +134,6 @@ function bufferToBase64(buffer) {
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
// ====================== 底部按钮正确隐藏 ======================
|
||||
handleSave: null,
|
||||
handleReset: null,
|
||||
handleSaveApply: null,
|
||||
@ -156,7 +152,7 @@ return view.extend({
|
||||
|
||||
var fileInput = E('input', {
|
||||
'type': 'file',
|
||||
accept: '.run,application/x-sh,application/octet-stream',
|
||||
accept: '.run,.sh,application/x-shellscript,application/octet-stream',
|
||||
style: 'display:none'
|
||||
});
|
||||
|
||||
@ -259,8 +255,7 @@ return view.extend({
|
||||
},
|
||||
|
||||
applyStatus: function (status, state) {
|
||||
if (!status)
|
||||
return;
|
||||
if (!status) return;
|
||||
|
||||
if (status.running)
|
||||
state.textContent = _('running');
|
||||
@ -271,7 +266,8 @@ return view.extend({
|
||||
uploadFile: function (file, progress, state, runButton) {
|
||||
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');
|
||||
return Promise.reject();
|
||||
}
|
||||
@ -305,30 +301,20 @@ return view.extend({
|
||||
xhr.setRequestHeader('Content-Type', 'application/octet-stream');
|
||||
|
||||
xhr.upload.onprogress = function (ev) {
|
||||
if (!ev.lengthComputable)
|
||||
return;
|
||||
|
||||
if (!ev.lengthComputable) return;
|
||||
progress.value = Math.floor(ev.loaded * 100 / ev.total);
|
||||
state.textContent = _('uploading', file.name, progress.value);
|
||||
};
|
||||
|
||||
xhr.onerror = function () {
|
||||
// 强制启用按钮
|
||||
document.querySelector('.cbi-button-action').disabled = false;
|
||||
reject(new Error(_('upload_err')));
|
||||
};
|
||||
|
||||
xhr.onload = function () {
|
||||
// ==========================================
|
||||
// 【直接强制解锁按钮:永远生效】
|
||||
// ==========================================
|
||||
document.querySelector('.cbi-button-action').disabled = false;
|
||||
progress.value = 100;
|
||||
|
||||
// 忽略所有返回解析
|
||||
try { JSON.parse(xhr.responseText); } catch (e) { }
|
||||
|
||||
// 直接完成流程
|
||||
uploadFinish(session.id).then(function () {
|
||||
state.textContent = _('upload_done', file.name, formatBytes(file.size));
|
||||
}).catch(function () {
|
||||
@ -345,8 +331,7 @@ return view.extend({
|
||||
startRun: function (runButton, state) {
|
||||
var self = this;
|
||||
|
||||
if (!this.currentUploadId)
|
||||
return;
|
||||
if (!this.currentUploadId) return;
|
||||
|
||||
runButton.disabled = true;
|
||||
state.textContent = _('starting');
|
||||
@ -367,8 +352,7 @@ return view.extend({
|
||||
var self = this;
|
||||
|
||||
return readLog(this.logOffset).then(function (res) {
|
||||
if (!res || res.error)
|
||||
return;
|
||||
if (!res || res.error) return;
|
||||
|
||||
if (res.data) {
|
||||
log.textContent += res.data;
|
||||
|
||||
@ -223,7 +223,14 @@ run_installer() {
|
||||
[ -d "$dir" ] || { json_error "Unknown upload id"; return; }
|
||||
name="$(cat "$dir/name" 2>/dev/null)"
|
||||
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
|
||||
if is_running "$PID"; then
|
||||
|
||||
@ -72,8 +72,13 @@ saved_token="$(cat "$dir/token" 2>/dev/null)"
|
||||
|
||||
name="$(cat "$dir/name" 2>/dev/null)"
|
||||
case "$name" in
|
||||
*.run) ;;
|
||||
*) fail "Invalid filename" ;;
|
||||
*.run)
|
||||
;;
|
||||
*.sh)
|
||||
;;
|
||||
*)
|
||||
fail "Invalid filename"
|
||||
;;
|
||||
esac
|
||||
|
||||
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_PKGARCH:=all
|
||||
|
||||
PKG_VERSION:=0.2.0-r2
|
||||
PKG_VERSION:=0.2.0-r3
|
||||
# PKG_RELEASE MUST be empty for luci.mk
|
||||
PKG_RELEASE:=
|
||||
|
||||
ISTORE_UI_VERSION:=0.2.0
|
||||
ISTORE_UI_RELEASE:=1
|
||||
ISTORE_UI_RELEASE:=2
|
||||
PKG_HASH:=skip
|
||||
|
||||
PKG_SOURCE_URL_FILE:=v$(ISTORE_UI_VERSION)-$(ISTORE_UI_RELEASE).tar.gz
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=opera-proxy
|
||||
PKG_VERSION:=1.24.0
|
||||
PKG_RELEASE:=5
|
||||
PKG_VERSION:=1.25.0
|
||||
PKG_RELEASE:=6
|
||||
|
||||
PKG_MAINTAINER:=Konstantine Shevlakov <shevlakov@132lan.ru>
|
||||
PKG_LICENSE:=MIT
|
||||
|
||||
Loading…
Reference in New Issue
Block a user