413 lines
13 KiB
JavaScript
413 lines
13 KiB
JavaScript
import {App} from './app.js';
|
|
import {ImportExport} from './import-export.js';
|
|
import {Pattern} from './pattern.js';
|
|
import {Flag} from './flag.js';
|
|
import {Color} from './color.js';
|
|
import {PAC} from './pac.js';
|
|
import {Toggle} from './toggle.js';
|
|
import {Tester} from './tester.js';
|
|
import {Log} from './log.js';
|
|
import {Nav} from './nav.js';
|
|
|
|
export class Proxies {
|
|
|
|
static {
|
|
this.proxyDiv = document.querySelector('div.proxy-div');
|
|
[this.proxyTemplate, this.patternTemplate] =
|
|
document.querySelector('.proxy-section template').content.children;
|
|
|
|
// firefox only, disabling Tab Proxy in the template for chrome
|
|
!App.firefox && (this.patternTemplate.children[1].lastElementChild.disabled = true);
|
|
|
|
// --- buttons
|
|
document.querySelector('.proxy-top button[data-i18n="add"]').addEventListener('click', e => {
|
|
// this.addProxy(null, e.ctrlKey)
|
|
const [pxy, title] = this.addProxy();
|
|
e.ctrlKey ? this.proxyDiv.prepend(pxy) : this.proxyDiv.append(pxy);
|
|
pxy.open = true;
|
|
pxy.draggable = false;
|
|
title.focus();
|
|
});
|
|
|
|
// used to find proxy
|
|
this.proxyCache = {};
|
|
// used to get the details for the log
|
|
Log.proxyCache = this.proxyCache;
|
|
// this.process();
|
|
|
|
// import pattern from log.js
|
|
window.addEventListener('importPatternCustom', (e) => this.importPatternCustom(e));
|
|
}
|
|
|
|
// pref references the same object in the memory and its value gets updated
|
|
static process(pref) {
|
|
Log.mode = pref.mode;
|
|
// reset
|
|
this.proxyDiv.textContent = '';
|
|
const docFrag = document.createDocumentFragment();
|
|
pref.data.forEach(i => docFrag.append(this.addProxy(i)));
|
|
this.proxyDiv.append(docFrag);
|
|
}
|
|
|
|
static addProxy(item, modifier) {
|
|
// --- details: make a blank proxy with all event listeners
|
|
const pxy = this.proxyTemplate.cloneNode(true);
|
|
const summary = pxy.children[0];
|
|
const proxyBox = pxy.children[1].children[0];
|
|
const patternBox = pxy.children[1].children[2];
|
|
|
|
// disable draggable when details is open
|
|
summary.addEventListener('click', () => pxy.draggable = pxy.open);
|
|
|
|
// --- summary
|
|
const [flag, sumTitle, active, dup, del, up, down] = summary.children;
|
|
dup.addEventListener('click', () => this.duplicateProxy(pxy));
|
|
del.addEventListener('click', () => confirm(browser.i18n.getMessage('deleteConfirm')) && pxy.remove());
|
|
up.addEventListener('click', () => pxy.previousElementSibling?.before(pxy));
|
|
down.addEventListener('click', () => pxy.nextElementSibling?.after(pxy));
|
|
|
|
// proxy data
|
|
const [title, hostname, type, port, cc, username, city, passwordSpan,
|
|
colorSpan, pacSpan, proxyDNS] = [...proxyBox.children].filter((e, i) => i % 2);
|
|
title.addEventListener('change', e => sumTitle.textContent = e.target.value);
|
|
|
|
const [pac, storeLocallyLabel, view] = pacSpan.children;
|
|
|
|
type.addEventListener('change', e => {
|
|
// use for show/hide elements
|
|
pxy.dataset.type = e.target.value;
|
|
|
|
const elem = e.target.selectedOptions[0];
|
|
const data = elem.dataset;
|
|
// --- server auto-fill helpers
|
|
switch (true) {
|
|
case ['flag', 'hostname', 'port'].some(i => data[i]):
|
|
sumTitle.textContent = elem.textContent;
|
|
title.value = elem.textContent;
|
|
data.flag && (flag.textContent = data.flag);
|
|
data.hostname && (hostname.value = data.hostname);
|
|
data.port && (port.value = data.port);
|
|
break;
|
|
|
|
default:
|
|
flag.textContent = Flag.get(cc.value);
|
|
}
|
|
});
|
|
|
|
cc.addEventListener('change', () => flag.textContent = Flag.get(cc.value));
|
|
Toggle.password(passwordSpan.children[1]);
|
|
|
|
// random color
|
|
const color = item?.color || Color.getRandom();
|
|
summary.style.borderLeftColor = color;
|
|
const [colorInput, colorButton] = colorSpan.children;
|
|
colorInput.value = color;
|
|
colorInput.addEventListener('change', e => summary.style.borderLeftColor = e.target.value);
|
|
|
|
colorButton.addEventListener('click', e => {
|
|
e.target.previousElementSibling.value = Color.getRandom();
|
|
summary.style.borderLeftColor = e.target.previousElementSibling.value;
|
|
});
|
|
|
|
pac.addEventListener('change', e => {
|
|
const {hostname: h, port: p} = App.parseURL(e.target.value);
|
|
if (!h) {
|
|
e.target.classList.add('invalid');
|
|
return;
|
|
}
|
|
hostname.value = h;
|
|
port.value = p;
|
|
type.value = 'pac';
|
|
title.value ||= 'PAC';
|
|
sumTitle.textContent ||= 'PAC';
|
|
});
|
|
|
|
// ---patterns
|
|
pxy.querySelector('button[data-i18n="add|title"]').addEventListener('click', e => {
|
|
const elem = this.addPattern();
|
|
e.ctrlKey ? patternBox.prepend(elem) : patternBox.append(elem);
|
|
elem.children[4].focus();
|
|
});
|
|
pxy.querySelector('input[type="file"]').addEventListener('change', e => this.importPattern(e, patternBox));
|
|
pxy.querySelector('button[data-i18n^="export"]').addEventListener('click', () =>
|
|
this.exportPattern(patternBox, title.value.trim() || hostname.value.trim()));
|
|
|
|
// --- from add button
|
|
if (!item) {
|
|
return [pxy, title];
|
|
}
|
|
|
|
// show/hide elements
|
|
pxy.dataset.type = item.type;
|
|
|
|
const id = item.type === 'pac' ? item.pac : `${item.hostname}:${item.port}`;
|
|
// cache to find later
|
|
this.proxyCache[id] = item;
|
|
|
|
// --- populate with data
|
|
const pxyTitle = item.title || id;
|
|
|
|
// --- summary
|
|
flag.textContent = Flag.show(item);
|
|
sumTitle.textContent = pxyTitle;
|
|
active.checked = item.active;
|
|
|
|
// proxy details
|
|
title.value = item.title;
|
|
hostname.value = item.hostname;
|
|
type.value = item.type;
|
|
port.value = item.port;
|
|
cc.value = item.cc;
|
|
// used in "Get Location"
|
|
cc.dataset.hostname = item.hostname;
|
|
username.value = item.username;
|
|
city.value = item.city;
|
|
// used in "Get Location"
|
|
city.dataset.hostname = item.hostname;
|
|
passwordSpan.children[0].value = item.password;
|
|
|
|
pac.value = item.pac;
|
|
storeLocallyLabel.children[0].checked = !!item.pacString;
|
|
view.addEventListener('click', () => PAC.view(pac.value.trim()));
|
|
|
|
proxyDNS.checked = item.proxyDNS;
|
|
|
|
// --- patterns
|
|
patternBox.dataset.host = `${item.hostname}:${item.port}`;
|
|
item.tabProxy ||= [];
|
|
item.include.forEach(i => patternBox.append(this.addPattern(i, 'include')));
|
|
item.exclude.forEach(i => patternBox.append(this.addPattern(i, 'exclude')));
|
|
item.tabProxy.forEach(i => patternBox.append(this.addPattern(i, 'tabProxy')));
|
|
return pxy;
|
|
}
|
|
|
|
static addPattern(item, inc) {
|
|
// --- make a blank pattern with all event listeners
|
|
const div = this.patternTemplate.cloneNode(true);
|
|
const [quickAdd, include, type, title, pattern, active, test, del] = div.children;
|
|
|
|
quickAdd.addEventListener('change', e => {
|
|
const opt = e.target.selectedOptions[0];
|
|
type.value = opt.dataset.type;
|
|
title.value = opt.textContent;
|
|
pattern.value = opt.value;
|
|
// reset select option
|
|
e.target.selectedIndex = 0;
|
|
});
|
|
|
|
test.addEventListener('click', () => {
|
|
Tester.select.value = type.value;
|
|
Tester.pattern.value = pattern.value;
|
|
Tester.target = pattern;
|
|
// reset pre DOM
|
|
Tester.pre.textContent = Tester.pre.textContent.trim();
|
|
// navigate to Tester tab
|
|
Nav.get('tester');
|
|
});
|
|
|
|
// del.addEventListener('click', () => confirm(browser.i18n.getMessage('deleteConfirm')) && div.remove());
|
|
del.addEventListener('click', () => div.remove());
|
|
|
|
if (item) {
|
|
include.value = inc;
|
|
type.value = item.type;
|
|
title.value = item.title;
|
|
pattern.value = item.pattern;
|
|
active.checked = item.active;
|
|
}
|
|
|
|
return div;
|
|
}
|
|
|
|
static async duplicateProxy(item) {
|
|
// generating a new proxy as cloneNode() removes event listeners
|
|
const pxy = await this.getProxyDetails(item);
|
|
if (!pxy) { return; }
|
|
|
|
item.after(this.addProxy(pxy));
|
|
// close orig proxy
|
|
item.open = false;
|
|
// open duplicated proxy
|
|
item.nextElementSibling.open = true;
|
|
}
|
|
|
|
// import pattern from log.js
|
|
static importPatternCustom(e) {
|
|
const {host, data} = e.detail;
|
|
if (!host || !data) { return; }
|
|
|
|
const patternBox = document.querySelector(`.pattern-box[data-host="${host}"]`);
|
|
if (!patternBox) { return; }
|
|
|
|
data.forEach(i => patternBox.append(this.addPattern(i, i.include)));
|
|
patternBox.closest('details').open = true;
|
|
patternBox.lastElementChild.children[4].focus();
|
|
}
|
|
|
|
static importPattern(e, patternBox) {
|
|
const file = e.target.files[0];
|
|
switch (true) {
|
|
case !file: App.notify(browser.i18n.getMessage('error'));
|
|
return;
|
|
// check file MIME type
|
|
case !['text/plain', 'application/json'].includes(file.type):
|
|
App.notify(browser.i18n.getMessage('fileTypeError'));
|
|
return;
|
|
}
|
|
|
|
ImportExport.fileReader(file, data => {
|
|
try { data = JSON.parse(data); }
|
|
catch {
|
|
// display the error
|
|
App.notify(browser.i18n.getMessage('fileParseError'));
|
|
return;
|
|
}
|
|
|
|
Array.isArray(data) && data.forEach(i => patternBox.append(this.addPattern(i, i.include)));
|
|
});
|
|
}
|
|
|
|
static exportPattern(patternBox, title = '') {
|
|
const arr = [...patternBox.children].map(item => {
|
|
const elem = item.children;
|
|
return {
|
|
include: elem[1].value,
|
|
type: elem[2].value,
|
|
title: elem[3].value.trim(),
|
|
pattern: elem[4].value.trim(),
|
|
active: elem[5].checked,
|
|
};
|
|
});
|
|
|
|
// no patterns to export
|
|
if (!arr[0]) { return; }
|
|
|
|
title &&= '_' + title;
|
|
const data = JSON.stringify(arr, null, 2);
|
|
const filename = `${browser.i18n.getMessage('pattern')}${title}_${new Date().toISOString().substring(0, 10)}.json`;
|
|
ImportExport.saveFile({data, filename, type: 'application/json'});
|
|
}
|
|
|
|
static async getProxyDetails(elem) {
|
|
// proxy template
|
|
const obj = {
|
|
active: true,
|
|
title: '',
|
|
type: 'http',
|
|
hostname: '',
|
|
port: '',
|
|
username: '',
|
|
password: '',
|
|
cc: '',
|
|
city: '',
|
|
color: '',
|
|
pac: '',
|
|
pacString: '',
|
|
proxyDNS: true,
|
|
include: [],
|
|
exclude: [],
|
|
tabProxy: [],
|
|
};
|
|
|
|
// --- populate values
|
|
elem.querySelectorAll('[data-id]').forEach(i => {
|
|
// reset
|
|
i.classList.remove('invalid');
|
|
Object.hasOwn(obj, i.dataset.id) && (obj[i.dataset.id] = i.type === 'checkbox' ? i.checked : i.value.trim());
|
|
});
|
|
|
|
// --- check type: http | https | socks4 | socks5 | quic | pac | direct
|
|
switch (true) {
|
|
// DIRECT
|
|
case obj.type === 'direct':
|
|
obj.hostname = 'DIRECT';
|
|
break;
|
|
|
|
// PAC
|
|
case obj.type === 'pac':
|
|
const {hostname, port} = App.parseURL(obj.pac);
|
|
if (!hostname) {
|
|
this.setInvalid(elem, 'pac');
|
|
// alert(browser.i18n.getMessage('pacUrlError'));
|
|
return;
|
|
}
|
|
obj.hostname = hostname;
|
|
obj.port = port;
|
|
break;
|
|
|
|
// http | https | socks4 | socks5 | quic
|
|
case !obj.hostname:
|
|
this.setInvalid(elem, 'hostname');
|
|
alert(browser.i18n.getMessage('hostnamePortError'));
|
|
return;
|
|
|
|
case !obj.port:
|
|
this.setInvalid(elem, 'port');
|
|
alert(browser.i18n.getMessage('hostnamePortError'));
|
|
return;
|
|
}
|
|
|
|
// --- title check
|
|
if (!obj.title) {
|
|
const id = obj.type === 'pac' ? obj.pac : `${obj.hostname}:${obj.port}`;
|
|
elem.children[0].children[1].textContent = id;
|
|
}
|
|
|
|
// --- check store locally for active PAC
|
|
if (obj.active && obj.pac) {
|
|
const storeLocally = elem.querySelector('.pac input[type="checkbox"]');
|
|
if (storeLocally.checked) {
|
|
const str = await PAC.get(obj.pac);
|
|
/function\s+FindProxyForURL\s*\(/.test(str) && (obj.pacString = str.trim());
|
|
}
|
|
}
|
|
|
|
// --- check & build patterns
|
|
const cache = [];
|
|
for (const item of elem.querySelectorAll('.pattern-box .pattern-row')) {
|
|
const [, inc, type, title, pattern, active] = item.children;
|
|
// reset
|
|
pattern.classList.remove('invalid');
|
|
const pat = {
|
|
type: type.value,
|
|
title: title.value.trim(),
|
|
pattern: pattern.value.trim(),
|
|
active: active.checked,
|
|
};
|
|
|
|
// --- test pattern
|
|
// blank pattern
|
|
if (!pat.pattern) { continue; }
|
|
|
|
if (!Pattern.validate(pat.pattern, pat.type, true)) {
|
|
// show Proxy tab
|
|
Nav.get('proxies');
|
|
const details = item.closest('details');
|
|
// open proxy
|
|
details.open = true;
|
|
pattern.classList.add('invalid');
|
|
pattern.scrollIntoView({behavior: 'smooth'});
|
|
return;
|
|
}
|
|
|
|
// check for duplicate
|
|
if (cache.includes(pat.pattern)) {
|
|
item.remove();
|
|
continue;
|
|
}
|
|
|
|
// cache to check for duplicates
|
|
cache.push(pat.pattern);
|
|
obj[inc.value].push(pat);
|
|
}
|
|
|
|
return obj;
|
|
}
|
|
|
|
static setInvalid(parent, id) {
|
|
parent.open = true;
|
|
const elem = parent.querySelector(`[data-id="${id}"]`);
|
|
elem.classList.add('invalid');
|
|
parent.scrollIntoView({behavior: 'smooth'});
|
|
}
|
|
} |