Added missing web folder

This commit is contained in:
Toby Chui
2023-05-23 01:29:59 +08:00
parent d51cb6efdf
commit ba347c5fa1
241 changed files with 150834 additions and 0 deletions

View File

@@ -0,0 +1,714 @@
<div class="standardContainer">
<div class="ui basic segment">
<h2>Blacklist</h2>
<p>Setup blacklist based on estimated IP geographic location or IP address</p>
</div>
<div class="ui divider"></div>
<div class="ui toggle checkbox">
<input type="checkbox" id="enableBlacklist">
<label>Enable Blacklist</label>
</div>
<div id="toggleSucc" style="float: right; display:none; color: #2abd4d;" >
<i class="ui green checkmark icon"></i> Setting Saved
</div>
<div class="ui message">
<i class="info circle icon"></i> Blacklist function require complex checking logic to validate each incoming request. Not recommend enabling this feature on servers with low end hardware.
</div>
<h4>Country Blacklist</h4>
<p><i class="yellow exclamation triangle icon"></i>
This will block all requests from the selected country. The requester's location is estimated from their IP address and may not be 100% accurate.</p>
<div class="ui form">
<div class="field">
<label>Select Country</label>
<div id="countrySelector" class="ui fluid search selection dropdown">
<input type="hidden" name="country">
<i class="dropdown icon"></i>
<div class="default text">Select Country</div>
<div class="menu">
<div class="item" data-value="af"><i class="af flag"></i>Afghanistan</div>
<div class="item" data-value="ax"><i class="ax flag"></i>Aland Islands</div>
<div class="item" data-value="al"><i class="al flag"></i>Albania</div>
<div class="item" data-value="dz"><i class="dz flag"></i>Algeria</div>
<div class="item" data-value="as"><i class="as flag"></i>American Samoa</div>
<div class="item" data-value="ad"><i class="ad flag"></i>Andorra</div>
<div class="item" data-value="ao"><i class="ao flag"></i>Angola</div>
<div class="item" data-value="ai"><i class="ai flag"></i>Anguilla</div>
<div class="item" data-value="ag"><i class="ag flag"></i>Antigua</div>
<div class="item" data-value="ar"><i class="ar flag"></i>Argentina</div>
<div class="item" data-value="am"><i class="am flag"></i>Armenia</div>
<div class="item" data-value="aw"><i class="aw flag"></i>Aruba</div>
<div class="item" data-value="au"><i class="au flag"></i>Australia</div>
<div class="item" data-value="at"><i class="at flag"></i>Austria</div>
<div class="item" data-value="az"><i class="az flag"></i>Azerbaijan</div>
<div class="item" data-value="bs"><i class="bs flag"></i>Bahamas</div>
<div class="item" data-value="bh"><i class="bh flag"></i>Bahrain</div>
<div class="item" data-value="bd"><i class="bd flag"></i>Bangladesh</div>
<div class="item" data-value="bb"><i class="bb flag"></i>Barbados</div>
<div class="item" data-value="by"><i class="by flag"></i>Belarus</div>
<div class="item" data-value="be"><i class="be flag"></i>Belgium</div>
<div class="item" data-value="bz"><i class="bz flag"></i>Belize</div>
<div class="item" data-value="bj"><i class="bj flag"></i>Benin</div>
<div class="item" data-value="bm"><i class="bm flag"></i>Bermuda</div>
<div class="item" data-value="bt"><i class="bt flag"></i>Bhutan</div>
<div class="item" data-value="bo"><i class="bo flag"></i>Bolivia</div>
<div class="item" data-value="ba"><i class="ba flag"></i>Bosnia</div>
<div class="item" data-value="bw"><i class="bw flag"></i>Botswana</div>
<div class="item" data-value="bv"><i class="bv flag"></i>Bouvet Island</div>
<div class="item" data-value="br"><i class="br flag"></i>Brazil</div>
<div class="item" data-value="vg"><i class="vg flag"></i>British Virgin Islands</div>
<div class="item" data-value="bn"><i class="bn flag"></i>Brunei</div>
<div class="item" data-value="bg"><i class="bg flag"></i>Bulgaria</div>
<div class="item" data-value="bf"><i class="bf flag"></i>Burkina Faso</div>
<div class="item" data-value="mm"><i class="mm flag"></i>Burma</div>
<div class="item" data-value="bi"><i class="bi flag"></i>Burundi</div>
<div class="item" data-value="tc"><i class="tc flag"></i>Caicos Islands</div>
<div class="item" data-value="kh"><i class="kh flag"></i>Cambodia</div>
<div class="item" data-value="cm"><i class="cm flag"></i>Cameroon</div>
<div class="item" data-value="ca"><i class="ca flag"></i>Canada</div>
<div class="item" data-value="cv"><i class="cv flag"></i>Cape Verde</div>
<div class="item" data-value="ky"><i class="ky flag"></i>Cayman Islands</div>
<div class="item" data-value="cf"><i class="cf flag"></i>Central African Republic</div>
<div class="item" data-value="td"><i class="td flag"></i>Chad</div>
<div class="item" data-value="cl"><i class="cl flag"></i>Chile</div>
<div class="item" data-value="cn"><i class="cn flag"></i>China</div>
<div class="item" data-value="cx"><i class="cx flag"></i>Christmas Island</div>
<div class="item" data-value="cc"><i class="cc flag"></i>Cocos Islands</div>
<div class="item" data-value="co"><i class="co flag"></i>Colombia</div>
<div class="item" data-value="km"><i class="km flag"></i>Comoros</div>
<div class="item" data-value="cg"><i class="cg flag"></i>Congo Brazzaville</div>
<div class="item" data-value="cd"><i class="cd flag"></i>Congo</div>
<div class="item" data-value="ck"><i class="ck flag"></i>Cook Islands</div>
<div class="item" data-value="cr"><i class="cr flag"></i>Costa Rica</div>
<div class="item" data-value="ci"><i class="ci flag"></i>Cote Divoire</div>
<div class="item" data-value="hr"><i class="hr flag"></i>Croatia</div>
<div class="item" data-value="cu"><i class="cu flag"></i>Cuba</div>
<div class="item" data-value="cy"><i class="cy flag"></i>Cyprus</div>
<div class="item" data-value="cz"><i class="cz flag"></i>Czech Republic</div>
<div class="item" data-value="dk"><i class="dk flag"></i>Denmark</div>
<div class="item" data-value="dj"><i class="dj flag"></i>Djibouti</div>
<div class="item" data-value="dm"><i class="dm flag"></i>Dominica</div>
<div class="item" data-value="do"><i class="do flag"></i>Dominican Republic</div>
<div class="item" data-value="ec"><i class="ec flag"></i>Ecuador</div>
<div class="item" data-value="eg"><i class="eg flag"></i>Egypt</div>
<div class="item" data-value="sv"><i class="sv flag"></i>El Salvador</div>
<div class="item" data-value="gb"><i class="gb flag"></i>England</div>
<div class="item" data-value="gq"><i class="gq flag"></i>Equatorial Guinea</div>
<div class="item" data-value="er"><i class="er flag"></i>Eritrea</div>
<div class="item" data-value="ee"><i class="ee flag"></i>Estonia</div>
<div class="item" data-value="et"><i class="et flag"></i>Ethiopia</div>
<div class="item" data-value="eu"><i class="eu flag"></i>European Union</div>
<div class="item" data-value="fk"><i class="fk flag"></i>Falkland Islands</div>
<div class="item" data-value="fo"><i class="fo flag"></i>Faroe Islands</div>
<div class="item" data-value="fj"><i class="fj flag"></i>Fiji</div>
<div class="item" data-value="fi"><i class="fi flag"></i>Finland</div>
<div class="item" data-value="fr"><i class="fr flag"></i>France</div>
<div class="item" data-value="gf"><i class="gf flag"></i>French Guiana</div>
<div class="item" data-value="pf"><i class="pf flag"></i>French Polynesia</div>
<div class="item" data-value="tf"><i class="tf flag"></i>French Territories</div>
<div class="item" data-value="ga"><i class="ga flag"></i>Gabon</div>
<div class="item" data-value="gm"><i class="gm flag"></i>Gambia</div>
<div class="item" data-value="ge"><i class="ge flag"></i>Georgia</div>
<div class="item" data-value="de"><i class="de flag"></i>Germany</div>
<div class="item" data-value="gh"><i class="gh flag"></i>Ghana</div>
<div class="item" data-value="gi"><i class="gi flag"></i>Gibraltar</div>
<div class="item" data-value="gr"><i class="gr flag"></i>Greece</div>
<div class="item" data-value="gl"><i class="gl flag"></i>Greenland</div>
<div class="item" data-value="gd"><i class="gd flag"></i>Grenada</div>
<div class="item" data-value="gp"><i class="gp flag"></i>Guadeloupe</div>
<div class="item" data-value="gu"><i class="gu flag"></i>Guam</div>
<div class="item" data-value="gt"><i class="gt flag"></i>Guatemala</div>
<div class="item" data-value="gw"><i class="gw flag"></i>Guinea-Bissau</div>
<div class="item" data-value="gn"><i class="gn flag"></i>Guinea</div>
<div class="item" data-value="gy"><i class="gy flag"></i>Guyana</div>
<div class="item" data-value="ht"><i class="ht flag"></i>Haiti</div>
<div class="item" data-value="hm"><i class="hm flag"></i>Heard Island</div>
<div class="item" data-value="hn"><i class="hn flag"></i>Honduras</div>
<div class="item" data-value="hk"><i class="hk flag"></i>Hong Kong</div>
<div class="item" data-value="hu"><i class="hu flag"></i>Hungary</div>
<div class="item" data-value="is"><i class="is flag"></i>Iceland</div>
<div class="item" data-value="in"><i class="in flag"></i>India</div>
<div class="item" data-value="io"><i class="io flag"></i>Indian Ocean Territory</div>
<div class="item" data-value="id"><i class="id flag"></i>Indonesia</div>
<div class="item" data-value="ir"><i class="ir flag"></i>Iran</div>
<div class="item" data-value="iq"><i class="iq flag"></i>Iraq</div>
<div class="item" data-value="ie"><i class="ie flag"></i>Ireland</div>
<div class="item" data-value="il"><i class="il flag"></i>Israel</div>
<div class="item" data-value="it"><i class="it flag"></i>Italy</div>
<div class="item" data-value="jm"><i class="jm flag"></i>Jamaica</div>
<div class="item" data-value="jp"><i class="jp flag"></i>Japan</div>
<div class="item" data-value="jo"><i class="jo flag"></i>Jordan</div>
<div class="item" data-value="kz"><i class="kz flag"></i>Kazakhstan</div>
<div class="item" data-value="ke"><i class="ke flag"></i>Kenya</div>
<div class="item" data-value="ki"><i class="ki flag"></i>Kiribati</div>
<div class="item" data-value="kw"><i class="kw flag"></i>Kuwait</div>
<div class="item" data-value="kg"><i class="kg flag"></i>Kyrgyzstan</div>
<div class="item" data-value="la"><i class="la flag"></i>Laos</div>
<div class="item" data-value="lv"><i class="lv flag"></i>Latvia</div>
<div class="item" data-value="lb"><i class="lb flag"></i>Lebanon</div>
<div class="item" data-value="ls"><i class="ls flag"></i>Lesotho</div>
<div class="item" data-value="lr"><i class="lr flag"></i>Liberia</div>
<div class="item" data-value="ly"><i class="ly flag"></i>Libya</div>
<div class="item" data-value="li"><i class="li flag"></i>Liechtenstein</div>
<div class="item" data-value="lt"><i class="lt flag"></i>Lithuania</div>
<div class="item" data-value="lu"><i class="lu flag"></i>Luxembourg</div>
<div class="item" data-value="mo"><i class="mo flag"></i>Macau</div>
<div class="item" data-value="mk"><i class="mk flag"></i>Macedonia</div>
<div class="item" data-value="mg"><i class="mg flag"></i>Madagascar</div>
<div class="item" data-value="mw"><i class="mw flag"></i>Malawi</div>
<div class="item" data-value="my"><i class="my flag"></i>Malaysia</div>
<div class="item" data-value="mv"><i class="mv flag"></i>Maldives</div>
<div class="item" data-value="ml"><i class="ml flag"></i>Mali</div>
<div class="item" data-value="mt"><i class="mt flag"></i>Malta</div>
<div class="item" data-value="mh"><i class="mh flag"></i>Marshall Islands</div>
<div class="item" data-value="mq"><i class="mq flag"></i>Martinique</div>
<div class="item" data-value="mr"><i class="mr flag"></i>Mauritania</div>
<div class="item" data-value="mu"><i class="mu flag"></i>Mauritius</div>
<div class="item" data-value="yt"><i class="yt flag"></i>Mayotte</div>
<div class="item" data-value="mx"><i class="mx flag"></i>Mexico</div>
<div class="item" data-value="fm"><i class="fm flag"></i>Micronesia</div>
<div class="item" data-value="md"><i class="md flag"></i>Moldova</div>
<div class="item" data-value="mc"><i class="mc flag"></i>Monaco</div>
<div class="item" data-value="mn"><i class="mn flag"></i>Mongolia</div>
<div class="item" data-value="me"><i class="me flag"></i>Montenegro</div>
<div class="item" data-value="ms"><i class="ms flag"></i>Montserrat</div>
<div class="item" data-value="ma"><i class="ma flag"></i>Morocco</div>
<div class="item" data-value="mz"><i class="mz flag"></i>Mozambique</div>
<div class="item" data-value="na"><i class="na flag"></i>Namibia</div>
<div class="item" data-value="nr"><i class="nr flag"></i>Nauru</div>
<div class="item" data-value="np"><i class="np flag"></i>Nepal</div>
<div class="item" data-value="an"><i class="an flag"></i>Netherlands Antilles</div>
<div class="item" data-value="nl"><i class="nl flag"></i>Netherlands</div>
<div class="item" data-value="nc"><i class="nc flag"></i>New Caledonia</div>
<div class="item" data-value="pg"><i class="pg flag"></i>New Guinea</div>
<div class="item" data-value="nz"><i class="nz flag"></i>New Zealand</div>
<div class="item" data-value="ni"><i class="ni flag"></i>Nicaragua</div>
<div class="item" data-value="ne"><i class="ne flag"></i>Niger</div>
<div class="item" data-value="ng"><i class="ng flag"></i>Nigeria</div>
<div class="item" data-value="nu"><i class="nu flag"></i>Niue</div>
<div class="item" data-value="nf"><i class="nf flag"></i>Norfolk Island</div>
<div class="item" data-value="kp"><i class="kp flag"></i>North Korea</div>
<div class="item" data-value="mp"><i class="mp flag"></i>Northern Mariana Islands</div>
<div class="item" data-value="no"><i class="no flag"></i>Norway</div>
<div class="item" data-value="om"><i class="om flag"></i>Oman</div>
<div class="item" data-value="pk"><i class="pk flag"></i>Pakistan</div>
<div class="item" data-value="pw"><i class="pw flag"></i>Palau</div>
<div class="item" data-value="ps"><i class="ps flag"></i>Palestine</div>
<div class="item" data-value="pa"><i class="pa flag"></i>Panama</div>
<div class="item" data-value="py"><i class="py flag"></i>Paraguay</div>
<div class="item" data-value="pe"><i class="pe flag"></i>Peru</div>
<div class="item" data-value="ph"><i class="ph flag"></i>Philippines</div>
<div class="item" data-value="pn"><i class="pn flag"></i>Pitcairn Islands</div>
<div class="item" data-value="pl"><i class="pl flag"></i>Poland</div>
<div class="item" data-value="pt"><i class="pt flag"></i>Portugal</div>
<div class="item" data-value="pr"><i class="pr flag"></i>Puerto Rico</div>
<div class="item" data-value="qa"><i class="qa flag"></i>Qatar</div>
<div class="item" data-value="re"><i class="re flag"></i>Reunion</div>
<div class="item" data-value="ro"><i class="ro flag"></i>Romania</div>
<div class="item" data-value="ru"><i class="ru flag"></i>Russia</div>
<div class="item" data-value="rw"><i class="rw flag"></i>Rwanda</div>
<div class="item" data-value="sh"><i class="sh flag"></i>Saint Helena</div>
<div class="item" data-value="kn"><i class="kn flag"></i>Saint Kitts and Nevis</div>
<div class="item" data-value="lc"><i class="lc flag"></i>Saint Lucia</div>
<div class="item" data-value="pm"><i class="pm flag"></i>Saint Pierre</div>
<div class="item" data-value="vc"><i class="vc flag"></i>Saint Vincent</div>
<div class="item" data-value="ws"><i class="ws flag"></i>Samoa</div>
<div class="item" data-value="sm"><i class="sm flag"></i>San Marino</div>
<div class="item" data-value="gs"><i class="gs flag"></i>Sandwich Islands</div>
<div class="item" data-value="st"><i class="st flag"></i>Sao Tome</div>
<div class="item" data-value="sa"><i class="sa flag"></i>Saudi Arabia</div>
<div class="item" data-value="sn"><i class="sn flag"></i>Senegal</div>
<div class="item" data-value="cs"><i class="cs flag"></i>Serbia</div>
<div class="item" data-value="rs"><i class="rs flag"></i>Serbia</div>
<div class="item" data-value="sc"><i class="sc flag"></i>Seychelles</div>
<div class="item" data-value="sl"><i class="sl flag"></i>Sierra Leone</div>
<div class="item" data-value="sg"><i class="sg flag"></i>Singapore</div>
<div class="item" data-value="sk"><i class="sk flag"></i>Slovakia</div>
<div class="item" data-value="si"><i class="si flag"></i>Slovenia</div>
<div class="item" data-value="sb"><i class="sb flag"></i>Solomon Islands</div>
<div class="item" data-value="so"><i class="so flag"></i>Somalia</div>
<div class="item" data-value="za"><i class="za flag"></i>South Africa</div>
<div class="item" data-value="kr"><i class="kr flag"></i>South Korea</div>
<div class="item" data-value="es"><i class="es flag"></i>Spain</div>
<div class="item" data-value="lk"><i class="lk flag"></i>Sri Lanka</div>
<div class="item" data-value="sd"><i class="sd flag"></i>Sudan</div>
<div class="item" data-value="sr"><i class="sr flag"></i>Suriname</div>
<div class="item" data-value="sj"><i class="sj flag"></i>Svalbard</div>
<div class="item" data-value="sz"><i class="sz flag"></i>Swaziland</div>
<div class="item" data-value="se"><i class="se flag"></i>Sweden</div>
<div class="item" data-value="ch"><i class="ch flag"></i>Switzerland</div>
<div class="item" data-value="sy"><i class="sy flag"></i>Syria</div>
<div class="item" data-value="tw"><i class="tw flag"></i>Taiwan</div>
<div class="item" data-value="tj"><i class="tj flag"></i>Tajikistan</div>
<div class="item" data-value="tz"><i class="tz flag"></i>Tanzania</div>
<div class="item" data-value="th"><i class="th flag"></i>Thailand</div>
<div class="item" data-value="tl"><i class="tl flag"></i>Timorleste</div>
<div class="item" data-value="tg"><i class="tg flag"></i>Togo</div>
<div class="item" data-value="tk"><i class="tk flag"></i>Tokelau</div>
<div class="item" data-value="to"><i class="to flag"></i>Tonga</div>
<div class="item" data-value="tt"><i class="tt flag"></i>Trinidad</div>
<div class="item" data-value="tn"><i class="tn flag"></i>Tunisia</div>
<div class="item" data-value="tr"><i class="tr flag"></i>Turkey</div>
<div class="item" data-value="tm"><i class="tm flag"></i>Turkmenistan</div>
<div class="item" data-value="tv"><i class="tv flag"></i>Tuvalu</div>
<div class="item" data-value="ug"><i class="ug flag"></i>Uganda</div>
<div class="item" data-value="ua"><i class="ua flag"></i>Ukraine</div>
<div class="item" data-value="ae"><i class="ae flag"></i>United Arab Emirates</div>
<div class="item" data-value="us"><i class="us flag"></i>United States</div>
<div class="item" data-value="uy"><i class="uy flag"></i>Uruguay</div>
<div class="item" data-value="um"><i class="um flag"></i>Us Minor Islands</div>
<div class="item" data-value="vi"><i class="vi flag"></i>Us Virgin Islands</div>
<div class="item" data-value="uz"><i class="uz flag"></i>Uzbekistan</div>
<div class="item" data-value="vu"><i class="vu flag"></i>Vanuatu</div>
<div class="item" data-value="va"><i class="va flag"></i>Vatican City</div>
<div class="item" data-value="ve"><i class="ve flag"></i>Venezuela</div>
<div class="item" data-value="vn"><i class="vn flag"></i>Vietnam</div>
<div class="item" data-value="wf"><i class="wf flag"></i>Wallis and Futuna</div>
<div class="item" data-value="eh"><i class="eh flag"></i>Western Sahara</div>
<div class="item" data-value="ye"><i class="ye flag"></i>Yemen</div>
<div class="item" data-value="zm"><i class="zm flag"></i>Zambia</div>
<div class="item" data-value="zw"><i class="zw flag"></i>Zimbabwe</div>
</div>
</div>
</div>
<button class="ui basic red button" id="ban-btn" onclick="addCountryToBlacklist();"><i class="ui red ban icon"></i> Blacklist Country</button>
</div>
<table class="ui unstackable basic celled table">
<thead>
<tr>
<th>ISO Code</th>
<th>Remove</th>
</tr>
</thead>
<tbody id="banned-list">
</tbody>
</table>
<h4>IP Blacklist</h4>
<p>Black a certain IP or IP range</p>
<div class="ui form">
<div class="field">
<label>IP Address</label>
<input id="ipAddressInput" type="text" placeholder="IP Address">
</div>
<button id="addIpButton" onclick="addIpBlacklist();" class="ui basic red icon button">
<i class="ban icon"></i> Blacklist IP
</button>
</div>
<div class="ui message">
<i class="ui info circle icon"></i> IP Address support the following formats
<div class="ui bulleted list">
<div class="item">Fixed IP Address (e.g. 192.128.4.100)</div>
<div class="item">IP Wildcard (e.g. 172.164.*.*)</div>
<div class="item">CIDR String (e.g. 128.32.0.1/16)</div>
</div>
</div>
<table class="ui unstackable basic celled table">
<thead>
<tr>
<th>IP Address</th>
<th>Remove</th>
</tr>
</thead>
<tbody id="blacklistIpTable">
</tbody>
</table>
<div class="ui divider"></div>
<h4>Visitor IP list</h4>
<button style="margin-top: -1em;" onclick="initBlacklistQuickBanTable();" class="ui green small right floated circular basic icon button"><i class="ui refresh icon"></i></button>
<p>Observe strange traffic on your sites? Ban them in the list below.</p>
<table class="ui celled unstackable table" id="ipTable">
<thead>
<tr>
<th>IP</th>
<th>Access Count</th>
<th>Blacklist</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<div class="pagination"></div>
</div>
<script>
$(".dropdown").dropdown();
function filterCountries(codesToShow) {
// get all items in the dropdown
const items = document.querySelectorAll('.ui.fluid.search.selection.dropdown .menu .item');
// loop through all items
items.forEach(item => {
// get the value of the item (i.e. the country code)
const code = item.dataset.value;
// if the code is in the array of codes to show, show the item
if (codesToShow.includes(code)) {
item.style.display = 'none';
}
// otherwise, hide the item
else {
item.style.display = 'block';
}
});
}
//Get the country name from the dropdown
function getCountryName(countryCode) {
return $('#countrySelector .item[data-value="' + countryCode.toLowerCase() + '"]').text().trim();
}
function initBannedCountryList(){
$.get("/api/blacklist/list?type=country", function(data) {
let bannedListHtml = '';
data.forEach((countryCode) => {
bannedListHtml += `
<tr>
<td><i class="${countryCode} flag"></i> ${getCountryName(countryCode)} (${countryCode.toUpperCase()})</td>
<td><button class="ui red basic mini icon button" onclick="removeFromBannedList('${countryCode}')"><i class="trash icon"></i></button></td>
</tr>
`;
});
$('#banned-list').html(bannedListHtml);
filterCountries(data);
if (data.length === 0) {
$('#banned-list').append(`
<tr>
<td colspan="2">
<i class="green check circle icon"></i>
There are no blacklisted countries
</td>
</tr>
`);
}
});
}
initBannedCountryList();
function addCountryToBlacklist() {
var countryCode = $("#countrySelector").dropdown("get value").toLowerCase();
$('#countrySelector').dropdown('clear');
$.ajax({
type: "POST",
url: "/api/blacklist/country/add",
data: { cc: countryCode },
success: function(response) {
if (response.error != undefined){
alert(response.error);
}
initBannedCountryList();
},
error: function(xhr, status, error) {
// handle error response
}
});
}
function removeFromBannedList(countryCode){
if (confirm("Confirm removing " + getCountryName(countryCode) + " from blacklist?")){
countryCode = countryCode.toLowerCase();
$.ajax({
url: "/api/blacklist/country/remove",
method: "POST",
data: { cc: countryCode },
success: function(response) {
if (response.error != undefined){
alert(response.error);
}
initBannedCountryList();
},
error: function(xhr, status, error) {
console.error("Error removing country from blacklist: " + error);
// Handle error response
}
});
}
}
function initIpBanTable(){
$.get('/api/blacklist/list?type=ip', function(data) {
$('#blacklistIpTable').html("");
if (data.length === 0) {
$('#blacklistIpTable').append(`
<tr>
<td colspan="2"><i class="green check circle icon"></i>There are no blacklisted IP addresses</td>
</tr>
`);
} else {
$.each(data, function(index, ip) {
let icon = "globe icon";
if (isLAN(ip)){
icon = "desktop icon";
}else if (isHomeAddr(ip)){
icon = "home icon";
}
$('#blacklistIpTable').append(`
<tr class="blacklistItem" ip="${encodeURIComponent(ip)}">
<td><i class="${icon}"></i> ${ip}</td>
<td><button class="ui icon basic mini red button" onclick="removeIpBlacklist('${ip}');"><i class="trash alternate icon"></i></button></td>
</tr>
`);
});
}
initBlacklistQuickBanTable();
});
}
initIpBanTable();
//Check if a input is a valid IP address, wildcard of a IP address or a CIDR string
function isValidIpFilter(input) {
// Check if input is a valid IP address
const isValidIp = /^([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])$/.test(input);
if (isValidIp) {
return true;
}
// Check if input is a wildcard IP address
const isValidWildcardIp = /^([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|\*|\*\/[0-9]|[01]?[0-9]?[0-9]-[01]?[0-9]?[0-9]|\[\d+,\d+\])\.([01]?[0-9]?[0-9]|\*|\*\/[0-9]|[01]?[0-9]?[0-9]-[01]?[0-9]?[0-9]|\[\d+,\d+\])\.([01]?[0-9]?[0-9]|\*|\*\/[0-9]|[01]?[0-9]?[0-9]-[01]?[0-9]?[0-9]|\[\d+,\d+\])$/.test(input);
if (isValidWildcardIp) {
return true;
}
// Check if input is a valid CIDR address string
const isValidCidr = /^([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\/([0-9]|[1-2][0-9]|3[0-2])$/.test(input);
if (isValidCidr) {
return true;
}
// Input is not a valid IP address, wildcard IP address, or CIDR address string
return false;
}
$("#ipAddressInput").on("input", function() {
$(this).val($(this).val().trim());
var ipAddress = $(this).val();
if (!isValidIpFilter(ipAddress)) {
$(this).parent().addClass("error");
} else {
$(this).parent().removeClass("error");
}
});
function isLAN(ipAddress) {
function ip2long(ipAddress) {
// Convert the IP address to a 32-bit integer
const parts = ipAddress.split(".");
return (
(parseInt(parts[0]) << 24) |
(parseInt(parts[1]) << 16) |
(parseInt(parts[2]) << 8) |
parseInt(parts[3])
);
}
// Define the LAN IP address ranges
const LAN_RANGES = [
{ start: "10.0.0.0", end: "10.255.255.255" },
{ start: "172.16.0.0", end: "172.31.255.255" },
{ start: "192.168.0.0", end: "192.168.255.255" }
];
// Check if the IP address is within any of the LAN ranges
for (let i = 0; i < LAN_RANGES.length; i++) {
const rangeStart = ip2long(LAN_RANGES[i].start);
const rangeEnd = ip2long(LAN_RANGES[i].end);
const ipAddressLong = ip2long(ipAddress);
if (ipAddressLong >= rangeStart && ipAddressLong <= rangeEnd) {
return true;
}
}
return false;
}
function isHomeAddr(ipAddress) {
const specialIpAddresses = ['0.0.0.0', '127.0.0.1', '::1'];
return specialIpAddresses.includes(ipAddress);
}
function addIpBlacklist(){
let targetIp = $("#ipAddressInput").val().trim();
if (targetIp == ""){
alert("IP address is empty")
return
}
if (!isValidIpFilter(targetIp)){
if (!confirm("This doesn't seems like a valid IP address. Continue anyway?")){
return;
}
}
$.ajax({
url: "/api/blacklist/ip/add",
type: "POST",
data: {ip: targetIp.toLowerCase()},
success: function(response) {
if (response.error !== undefined) {
alert(response.error);
} else {
initIpBanTable();
}
$("#ipAddressInput").val("");
$("#ipAddressInput").parent().remvoeClass("error");
},
error: function() {
alert("Failed to add IP address to blacklist.");
}
});
}
function removeIpBlacklist(ipaddr){
if (confirm("Confirm remove blacklist for " + ipaddr + " ?")){
$.ajax({
url: "/api/blacklist/ip/remove",
type: "POST",
data: {ip: ipaddr.toLowerCase()},
success: function(response) {
if (response.error !== undefined) {
alert(response.error);
} else {
initIpBanTable();
}
},
error: function() {
alert("Failed to remove IP address from blacklist.");
}
});
}
}
//function to check for blacklist enable
function enableBlacklist() {
var isChecked = $('#enableBlacklist').is(':checked');
$.ajax({
type: 'POST',
url: '/api/blacklist/enable',
data: { enable: isChecked },
success: function(data){
$("#toggleSucc").stop().finish().fadeIn("fast").delay(3000).fadeOut("fast");
}
});
}
function initBlacklistEnableState(){
$.get('/api/blacklist/enable', function(data){
if (data == true){
$('#enableBlacklist').parent().checkbox("set checked");
}
//Register on change event
$("#enableBlacklist").on("change", function(){
enableBlacklist();
})
});
}
initBlacklistEnableState();
//Load the summary to ip access table
function initBlacklistQuickBanTable(){
$.get("/api/stats/summary", function(data){
initIpAccessTable(data.RequestClientIp);
})
}
initBlacklistQuickBanTable();
var blacklist_entriesPerPage = 30;
var blacklist_currentPage = 1;
var blacklist_totalPages = 0;
function initIpAccessTable(ipAccessCounts){
blacklist_totalPages = Math.ceil(Object.keys(ipAccessCounts).length / blacklist_entriesPerPage);
function sortkv(obj) {
var sortable = [];
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
sortable.push([key, obj[key]]);
}
}
sortable.sort(function(a, b) {
return b[1] - a[1];
});
var sortedObj = {};
sortable.forEach(function(item) {
sortedObj[item[0]] = item[1];
});
return sortedObj;
}
ipAccessCounts = sortkv(ipAccessCounts);
function renderTable() {
var tableBody = $("#ipTable tbody");
tableBody.empty();
var startIndex = (blacklist_currentPage - 1) * blacklist_entriesPerPage;
var endIndex = startIndex + blacklist_entriesPerPage;
var slicedEntries = Object.entries(ipAccessCounts).slice(startIndex, endIndex);
slicedEntries.forEach(function([ip, accessCount]) {
var row = $("<tr>").appendTo(tableBody);
$("<td>").text(ip).appendTo(row);
$("<td>").text(accessCount).appendTo(row);
if (ipInBlacklist(ip)){
$("<td>").html(`<button class="ui basic green small icon button" title"Unban IP" onclick="handleUnban('${ip}');"><i class="green check icon"></i></button>`).appendTo(row);
}else{
$("<td>").html(`<button class="ui basic red small icon button" title"Ban IP" onclick="handleBanIp('${ip}');"><i class="red ban icon"></i></button>`).appendTo(row);
}
});
}
function renderPagination() {
var paginationDiv = $(".pagination");
paginationDiv.empty();
for (var i = 1; i <= blacklist_totalPages; i++) {
var button = $("<button>").text(i).addClass("ui small basic compact button");
if (i === blacklist_currentPage) {
button.addClass("disabled");
}
button.click(function() {
blacklist_currentPage = parseInt($(this).text());
renderTable();
renderPagination();
});
button.appendTo(paginationDiv);
}
}
renderTable();
renderPagination();
}
function ipInBlacklist(targetIp){
let inBlacklist = false;
$(".blacklistItem").each(function(){
if ($(this).attr("ip") == encodeURIComponent(targetIp)){
inBlacklist = true;
}
});
return inBlacklist;
}
function handleBanIp(targetIp){
$("#ipAddressInput").val(targetIp);
addIpBlacklist();
}
function handleUnban(targetIp){
removeIpBlacklist(targetIp);
}
</script>

View File

@@ -0,0 +1,288 @@
<div class="standardContainer">
<div class="ui basic segment">
<h2>TLS / SSL Certificates</h2>
<p>Setup TLS cert for different domains of your reverse proxy server names</p>
</div>
<div class="ui divider"></div>
<h4>Default Certificates</h4>
<small>When there are no matching certificate for the requested server name, reverse proxy router will always fallback to this one.<br>Note that you need both of them uploaded for it to fallback properly</small></p>
<table class="ui very basic unstackable celled table">
<thead>
<tr><th class="no-sort">Key Type</th>
<th class="no-sort">Exists</th>
</tr></thead>
<tbody>
<tr>
<td><i class="globe icon"></i> Default Public Key</td>
<td id="pubkeyExists"></td>
</tr>
<tr>
<td><i class="lock icon"></i> Default Private Key</td>
<td id="prikeyExists"></td>
</tr>
</tbody>
</table>
<p style="margin-bottom: 0.4em;"><i class="ui upload icon"></i> Upload Default Keypairs</p>
<div class="ui buttons">
<button class="ui basic grey button" onclick="uploadPublicKey();"><i class="globe icon"></i> Public Key</button>
<button class="ui basic black button" onclick="uploadPrivateKey();"><i class="black lock icon"></i> Private Key</button>
</div>
<div class="ui divider"></div>
<h4>Sub-domain Certificates</h4>
<p>Provide certificates for multiple domains reverse proxy</p>
<div class="ui fluid form">
<div class="three fields">
<div class="field">
<label>Server Name (Domain)</label>
<input type="text" id="certdomain" placeholder="example.com / blog.example.com">
</div>
<div class="field">
<label>Public Key</label>
<input type="file" id="pubkeySelector" onchange="handleFileSelect(event, 'pub')">
</div>
<div class="field">
<label>Private Key</label>
<input type="file" id="prikeySelector" onchange="handleFileSelect(event, 'pri')">
</div>
</div>
<button class="ui basic button" onclick="handleDomainUploadByKeypress();"><i class="ui teal upload icon"></i> Upload</button>
</div>
<div id="certUploadSuccMsg" class="ui green message" style="display:none;">
<i class="ui checkmark icon"></i> Certificate for domain <span id="certUploadingDomain"></span> uploaded.
</div>
<br>
<div>
<table class="ui sortable unstackable celled table">
<thead>
<tr><th>Domain</th>
<th>Last Update</th>
<th class="no-sort">Remove</th>
</tr></thead>
<tbody id="certifiedDomainList">
</tbody>
</table>
<button class="ui basic button" onclick="initManagedDomainCertificateList();"><i class="green refresh icon"></i> Refresh List</button>
</div>
<div class="ui message">
<h4><i class="info circle icon"></i> Sub-domain Certificates</h4>
If you have 3rd or even 4th level subdomains like <code>blog.example.com</code> or <code>en.blog.example.com</code> ,
depending on your certificates coverage, you might need to setup them one by one (i.e. having two seperate certificate for <code>a.example.com</code> and <code>b.example.com</code>).<br>
If you have a wildcard certificate that covers <code>*.example.com</code>, you can just enter <code>example.com</code> as server name in the form below to add a certificate.
</div>
</div>
<script>
var uploadPendingPublicKey = undefined;
var uploadPendingPrivateKey = undefined;
//Delete the certificate by its domain
function deleteCertificate(domain){
if (confirm("Confirm delete certificate for " + domain + " ?")){
$.ajax({
url: "/api/cert/delete",
method: "POST",
data: {domain: domain},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false, 5000);
}else{
initManagedDomainCertificateList();
initDefaultKeypairCheck();
}
}
});
}
}
//List the stored certificates
function initManagedDomainCertificateList(){
$("#certifiedDomainList").html("");
$.get("/api/cert/list?date=true", function(data){
if (data.error != undefined){
msgbox(data.error, false, 5000);
}else{
data.forEach(entry => {
$("#certifiedDomainList").append(`<tr>
<td>${entry.Domain}</td>
<td>${entry.LastModifiedDate}</td>
<td><button title="Delete key-pair" class="ui mini basic red icon button" onclick="deleteCertificate('${entry.Domain}');"><i class="ui red trash icon"></i></button></td>
</tr>`);
})
}
})
}
initManagedDomainCertificateList();
function handleDomainUploadByKeypress(){
handleDomainKeysUpload(function(){
$("#certUploadingDomain").text($("#certdomain").val().trim());
//After uploaded, reset the file selector
document.getElementById('pubkeySelector').value = '';
document.getElementById('prikeySelector').value = '';
document.getElementById('certdomain').value = '';
uploadPendingPublicKey = undefined;
uploadPendingPrivateKey = undefined;
//Show succ
$("#certUploadSuccMsg").stop().finish().slideDown("fast").delay(3000).slideUp("fast");
initManagedDomainCertificateList();
});
}
//Handle domain keys upload
function handleDomainKeysUpload(callback=undefined){
let domain = $("#certdomain").val();
if (domain.trim() == ""){
msgbox("Missing domain", false, 5000);
return;
}
if (uploadPendingPublicKey && uploadPendingPrivateKey && typeof uploadPendingPublicKey === 'object' && typeof uploadPendingPrivateKey === 'object') {
const publicKeyForm = new FormData();
publicKeyForm.append('file', uploadPendingPublicKey, 'publicKey');
const privateKeyForm = new FormData();
privateKeyForm.append('file', uploadPendingPrivateKey, 'privateKey');
const publicKeyRequest = new XMLHttpRequest();
publicKeyRequest.open('POST', '/api/cert/upload?ktype=pub&domain=' + domain);
publicKeyRequest.onreadystatechange = function() {
if (publicKeyRequest.readyState === XMLHttpRequest.DONE) {
if (publicKeyRequest.status !== 200) {
msgbox('Error uploading public key: ' + publicKeyRequest.statusText, false, 5000);
}
if (callback != undefined){
callback();
}
}
};
publicKeyRequest.send(publicKeyForm);
const privateKeyRequest = new XMLHttpRequest();
privateKeyRequest.open('POST', '/api/cert/upload?ktype=pri&domain=' + domain);
privateKeyRequest.onreadystatechange = function() {
if (privateKeyRequest.readyState === XMLHttpRequest.DONE) {
if (privateKeyRequest.status !== 200) {
msgbox('Error uploading private key: ' + privateKeyRequest.statusText, false, 5000);
}
if (callback != undefined){
callback();
}
}
};
privateKeyRequest.send(privateKeyForm);
} else {
msgbox('One or both of the files is missing or not a file object');
}
}
//Handlers for selecting domain based key pairs
//ktype = {"pub" / "pri"}
function handleFileSelect(event, ktype="pub") {
const file = event.target.files[0];
//const fileNameInput = document.getElementById('selected-file-name');
if (ktype == "pub"){
uploadPendingPublicKey = file;
}else if (ktype == "pri"){
uploadPendingPrivateKey = file;
}
//fileNameInput.value = file.name;
}
//Check if the default keypairs exists
function initDefaultKeypairCheck(){
$.get("/api/cert/checkDefault", function(data){
let tick = `<i class="ui green checkmark icon"></i>`;
let cross = `<i class="ui red times icon"></i>`;
$("#pubkeyExists").html(data.DefaultPubExists?tick:cross);
$("#prikeyExists").html(data.DefaultPriExists?tick:cross);
});
}
initDefaultKeypairCheck();
function uploadPrivateKey(){
// create file input element
const input = document.createElement('input');
input.type = 'file';
// add change listener to file input
input.addEventListener('change', () => {
// create form data object
const formData = new FormData();
// add selected file to form data
formData.append('file', input.files[0]);
// send form data to server
fetch('/api/cert/upload?ktype=pri', {
method: 'POST',
body: formData
})
.then(response => {
initDefaultKeypairCheck();
if (response.ok) {
msgbox('File upload successful!');
} else {
response.text().then(text => {
msgbox(text, false, 5000);
});
//console.log(response.text());
//alert('File upload failed!');
}
})
.catch(error => {
msgbox('An error occurred while uploading the file.', false, 5000);
console.error(error);
});
});
// click file input to open file selector
input.click();
}
function uploadPublicKey() {
// create file input element
const input = document.createElement('input');
input.type = 'file';
// add change listener to file input
input.addEventListener('change', () => {
// create form data object
const formData = new FormData();
// add selected file to form data
formData.append('file', input.files[0]);
// send form data to server
fetch('/api/cert/upload?ktype=pub', {
method: 'POST',
body: formData
})
.then(response => {
if (response.ok) {
msgbox('File upload successful!');
initDefaultKeypairCheck();
} else {
response.text().then(text => {
msgbox(text, false, 5000);
});
//console.log(response.text());
//alert('File upload failed!');
}
})
.catch(error => {
msgbox('An error occurred while uploading the file.', false, 5000);
console.error(error);
});
});
// click file input to open file selector
input.click();
}
</script>

228
src/web/components/gan.html Normal file
View File

@@ -0,0 +1,228 @@
<div id="ganetWindow" class="standardContainer">
<div class="ui basic segment">
<h2>Global Area Network</h2>
<p>Virtual Network Hub that allows all networked devices to communicate as if they all reside in the same physical data center or cloud region</p>
</div>
<div class="gansnetworks">
<div class="ganstats ui basic segment">
<div style="float: right; max-width: 300px; margin-top: 0.4em;">
<h1 class="ui header" style="text-align: right;">
<span class="ganControllerID"></span>
<div class="sub header">Network Controller ID</div>
</h1>
</div>
<div class="ui list">
<div class="item">
<i class="exchange icon"></i>
<div class="content">
<div class="header" style="font-size: 1.2em;" id="ganetCount">0</div>
<div class="description">Networks</div>
</div>
</div>
<div class="item">
<i class="desktop icon"></i>
<div class="content">
<div class="header" style="font-size: 1.2em;" id="ganodeCount">0</div>
<div class="description" id="connectedNodes" count="0">Connected Nodes</div>
</div>
</div>
</div>
</div>
<div class="ganlist">
<button class="ui basic orange button" onclick="addGANet();">Create New Network</button>
<div class="ui divider"></div>
<!--
<div class="ui icon input" style="margin-bottom: 1em;">
<input type="text" placeholder="Search a Network">
<i class="circular search link icon"></i>
</div>-->
<div style="width: 100%; overflow-x: auto;">
<table class="ui celled basic unstackable striped table">
<thead>
<tr>
<th>Network ID</th>
<th>Name</th>
<th>Description</th>
<th>Subnet (Assign Range)</th>
<th>Nodes</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="GANetList">
<tr>
<td colspan="6"><i class="ui green circle check icon"></i> No Global Area Network Found on this host</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<script>
/*
Network Management Functions
*/
function handleAddNetwork(){
let networkName = $("#networkName").val().trim();
if (networkName == ""){
msgbox("Network name cannot be empty", false, 5000);
return;
}
//Add network with default settings
addGANet(networkName, "192.168.196.0/24");
$("#networkName").val("");
}
function initGANetID(){
$.get("/api/gan/network/info", function(data){
if (data.error !== undefined){
msgbox(data.error, false, 5000)
}else{
if (data != ""){
$(".ganControllerID").text(data);
}
}
})
}
function addGANet() {
$.ajax({
url: "/api/gan/network/add",
type: "POST",
dataType: "json",
data: {},
success: function(response) {
if (response.error != undefined){
msgbox(response.error, false, 5000);
}else{
msgbox("Network added successfully");
}
console.log("Network added successfully:", response);
listGANet();
},
error: function(xhr, status, error) {
console.log("Error adding network:", error);
}
});
}
function listGANet(){
$("#connectedNodes").attr("count", "0");
$.get("/api/gan/network/list", function(data){
$("#GANetList").empty();
if (data.error != undefined){
console.log(data.error);
msgbox("Unable to load auth token for GANet", false, 5000);
//token error or no zerotier found
$(".gansnetworks").addClass("disabled");
$("#GANetList").append(`<tr>
<td colspan="6"><i class="red times circle icon"></i> Auth token access error or not found</td>
</tr>`);
$(".ganControllerID").text('Access Denied');
}else{
var nodeCount = 0;
data.forEach(function(gan){
$("#GANetList").append(`<tr class="ganetEntry" addr="${gan.nwid}">
<td>${gan.nwid}</td>
<td>${gan.name}</td>
<td class="gandesc" addr="${gan.nwid}"></td>
<td class="ganetSubnet"></td>
<td class="ganetNodes"></td>
<td>
<button onclick="openGANetDetails('${gan.nwid}');" class="ui tiny basic icon button" title="Edit Network"><i class="edit icon"></i></button>
<button onclick="removeGANet('${gan.nwid}');" class="ui tiny basic icon button" title="Remove Network"><i class="red remove icon"></i></button>
</td>
</tr>`);
nodeCount += 0;
});
if (data.length == 0){
$("#GANetList").append(`<tr>
<td colspan="6"><i class="ui green circle check icon"></i> No Global Area Network Found on this host</td>
</tr>`);
}
$("#ganodeCount").text(nodeCount);
$("#ganetCount").text(data.length);
//Load description
$(".gandesc").each(function(){
let addr = $(this).attr("addr");
let domEle = $(this);
$.get("/api/gan/network/name?netid=" + addr, function(data){
$(domEle).text(data[1]);
});
});
$(".ganetEntry").each(function(){
let addr = $(this).attr("addr");
let subnetEle = $(this).find(".ganetSubnet");
let nodeEle = $(this).find(".ganetNodes");
$.get("/api/gan/network/list?netid=" + addr, function(data){
if (data.routes != undefined && data.routes.length > 0){
if (data.ipAssignmentPools != undefined && data.ipAssignmentPools.length > 0){
$(subnetEle).html(`${data.routes[0].target} <br> (${data.ipAssignmentPools[0].ipRangeStart} - ${data.ipAssignmentPools[0].ipRangeEnd})`);
}else{
$(subnetEle).html(`${data.routes[0].target}<br>(Unassigned Range)`);
}
}else{
$(subnetEle).text("Unassigned");
}
//console.log(data);
});
$.get("/api/gan/members/list?netid=" + addr, function(data){
$(nodeEle).text(data.length);
let currentNodesCount = parseInt($("#connectedNodes").attr("count"));
currentNodesCount += data.length;
$("#connectedNodes").attr("count", currentNodesCount);
$("#ganodeCount").text($("#connectedNodes").attr("count"));
})
});
}
})
}
//Remove the given GANet
function removeGANet(netid){
if (confirm("Confirm remove Network " + netid + " PERMANENTLY ?"))
$.ajax({
url: "/api/gan/network/remove",
type: "POST",
dataType: "json",
data: {
id: netid,
},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false, 5000);
}else{
msgbox("Net " + netid + " removed");
}
listGANet();
}
});
}
function openGANetDetails(netid){
$("#ganetWindow").load("./components/gandetails.html", function(){
setTimeout(function(){
initGanetDetails(netid);
});
});
}
//Bind event to tab switch
tabSwitchEventBind["gan"] = function(){
//On switch over to this page, load info
listGANet();
initGANetID();
}
</script>

View File

@@ -0,0 +1,625 @@
<div class="standardContainer">
<button onclick="exitToGanList();" class="ui large circular black icon button"><i class="angle left icon"></i></button>
<div style="max-width: 300px; margin-top: 1em;">
<button onclick='$("#gannetDetailEdit").slideToggle("fast");' class="ui mini basic right floated circular icon button" style="display: inline-block; margin-top: 2.5em;"><i class="ui edit icon"></i></button>
<h1 class="ui header">
<span class="ganetID"></span>
<div class="sub header ganetName"></div>
</h1>
<div class="ui divider"></div>
<p><span class="ganetDesc"></span></p>
</div>
<div id="gannetDetailEdit" class="ui form" style="margin-top: 1em; display:none;">
<div class="ui divider"></div>
<p>You can change the network name and description below. <br>The name and description is only for easy management purpose and will not effect the network operation.</p>
<div class="field">
<label>Network Name</label>
<input type="text" id="gaNetNameInput" placeholder="">
</div>
<div class="field">
<label>Network Description</label>
<textarea id="gaNetDescInput" style="resize: none;"></textarea>
<button onclick="saveNameAndDesc(this);" class="ui basic right floated button" style="margin-top: 0.6em;"><i class="ui save icon"></i> Save</button>
<button onclick='$("#gannetDetailEdit").slideUp("fast");' class="ui basic right floated button" style="margin-top: 0.6em;"><i class="ui red remove icon"></i> Cancel</button>
</div>
<br><br>
</div>
<div class="ui divider"></div>
<h2>Settings</h2>
<div class="" style="overflow-x: auto;">
<table class="ui basic celled unstackable table" style="min-width: 560px;">
<thead>
<tr>
<th colspan="4">IPv4 Auto-Assign</th>
</tr>
</thead>
<tbody id="ganetRangeTable">
</tbody>
</table>
</div>
<br>
<div class="ui form">
<h3>Custom IP Range</h3>
<p>Manual IP Range Configuration. The IP range must be within the selected CIDR range.
<br>Use <code>Utilities > IP to CIDR tool</code> if you are not too familiar with CIDR notations.</p>
<div class="two fields">
<div class="field">
<label>IP Start</label>
<input type="text" class="ganIpStart" placeholder="">
</div>
<div class="field">
<label>IP End</label>
<input type="text" class="ganIpEnd" placeholder="">
</div>
</div>
</div>
<button onclick="setNetworkRange();" class="ui basic button"><i class="ui blue save icon"></i> Save Settings</button>
<div class="ui divider"></div>
<h2>Members</h2>
<p>To join this network using command line, type <code>sudo zerotier-cli join <span class="ganetID"></span></code> on your device terminal</p>
<div class="ui checkbox" style="margin-bottom: 1em;">
<input id="showUnauthorizedMembers" type="checkbox" onchange="changeUnauthorizedVisibility(this.checked);">
<label>Show Unauthorized Members</label>
</div>
<div class="" style="overflow-x: auto;">
<table class="ui celled unstackable table">
<thead>
<tr>
<th>Auth</th>
<th>Address</th>
<th>Name</th>
<th>Managed IP</th>
<th>Authorized Since</th>
<th>Version</th>
<th>Remove</th>
</tr>
</thead>
<tbody id="networkMemeberTable">
<tr>
</tr>
</tbody>
</table>
</div>
<br><br>
</div>
<script>
$(".checkbox").checkbox();
var currentGANetID = "";
var currentGANNetMemeberListener = undefined;
var currentGaNetDetails = {};
var currentGANMemberList = [];
var netRanges = {
"10.147.17.*": "10.147.17.0/24",
"10.147.18.*": "10.147.18.0/24",
"10.147.19.*": "10.147.19.0/24",
"10.147.20.*": "10.147.20.0/24",
"10.144.*.*": "10.144.0.0/16",
"10.241.*.*": "10.241.0.0/16",
"10.242.*.*": "10.242.0.0/16",
"10.243.*.*": "10.243.0.0/16",
"10.244.*.*": "10.244.0.0/16",
"172.22.*.*": "172.22.0.0/15",
"172.23.*.*": "172.23.0.0/16",
"172.24.*.*": "172.24.0.0/14",
"172.25.*.*": "172.25.0.0/16",
"172.26.*.*": "172.26.0.0/15",
"172.27.*.*": "172.27.0.0/16",
"172.28.*.*": "172.28.0.0/15",
"172.29.*.*": "172.29.0.0/16",
"172.30.*.*": "172.30.0.0/15",
"192.168.191.*": "192.168.191.0/24",
"192.168.192.*": "192.168.192.0/24",
"192.168.193.*": "192.168.193.0/24",
"192.168.194.*": "192.168.194.0/24",
"192.168.195.*": "192.168.195.0/24",
"192.168.196.*": "192.168.196.0/24"
}
function generateIPRangeTable(netRanges) {
$("#ganetRangeTable").empty();
const tableBody = document.getElementById('ganetRangeTable');
const cidrs = Object.values(netRanges);
// Set the number of rows and columns to display in the table
const numRows = 6;
const numCols = 4;
let row = document.createElement('tr');
let col = 0;
for (let i = 0; i < cidrs.length; i++) {
if (col >= numCols) {
tableBody.appendChild(row);
row = document.createElement('tr');
col = 0;
}
const td = document.createElement('td');
td.setAttribute('class', `clickable iprange`);
td.setAttribute('CIDR', cidrs[i]);
td.innerHTML = cidrs[i];
let thisCidr = cidrs[i];
td.onclick = function(){
selectNetworkRange(thisCidr, td);
};
row.appendChild(td);
col++;
}
// Add any remaining cells to the table
if (col > 0) {
for (let i = col; i < numCols; i++) {
row.appendChild(document.createElement('td'));
}
tableBody.appendChild(row);
}
}
function highlightCurrentGANetCIDR(){
var currentCIDR = currentGaNetDetails.routes[0].target;
$(".iprange").each(function(){
if ($(this).attr("CIDR") == currentCIDR){
$(this).addClass("active");
populateStartEndIpByCidr(currentCIDR);
}
})
}
function populateStartEndIpByCidr(cidr){
function cidrToRange(cidr) {
var range = [2];
cidr = cidr.split('/');
var start = ip2long(cidr[0]);
range[0] = long2ip(start);
range[1] = long2ip(Math.pow(2, 32 - cidr[1]) + start - 1);
return range;
}
var cidrRange = cidrToRange(cidr);
$(".ganIpStart").val(cidrRange[0]);
$(".ganIpEnd").val(cidrRange[1]);
}
function selectNetworkRange(cidr, object){
populateStartEndIpByCidr(cidr);
$(".iprange.active").removeClass("active");
$(object).addClass("active");
}
function setNetworkRange(){
var ipstart = $(".ganIpStart").val().trim();
var ipend = $(".ganIpEnd").val().trim();
if (ipstart == ""){
$(".ganIpStart").parent().addClass("error");
}else{
$(".ganIpStart").parent().removeClass("error");
}
if (ipend == ""){
$(".ganIpEnd").parent().addClass("error");
}else{
$(".ganIpEnd").parent().removeClass("error");
}
//Get CIDR from selected range group
var cidr = $(".iprange.active").attr("cidr");
$.ajax({
url: "/api/gan/network/setRange",
metohd: "POST",
data:{
netid: currentGANetID,
cidr: cidr,
ipstart: ipstart,
ipend: ipend
},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false, 5000)
}else{
msgbox("Network Range Updated")
}
}
})
}
function saveNameAndDesc(object=undefined){
var name = $("#gaNetNameInput").val();
var desc = $("#gaNetDescInput").val();
if (object != undefined){
$(object).addClass("loading");
}
$.ajax({
url: "/api/gan/network/name",
method: "POST",
data: {
netid: currentGANetID,
name: name,
desc: desc,
},
success: function(data){
initNetNameAndDesc();
if (object != undefined){
$(object).removeClass("loading");
msgbox("Network Metadata Updated");
}
$("#gannetDetailEdit").slideUp("fast");
}
});
}
function initNetNameAndDesc(){
//Get the details of the net
$.get("/api/gan/network/name?netid=" + currentGANetID, function(data){
if (data.error !== undefined){
msgbox(data.error, false, 6000);
}else{
$("#gaNetNameInput").val(data[0]);
$(".ganetName").html(data[0]);
$("#gaNetDescInput").val(data[1]);
$(".ganetDesc").text(data[1]);
}
});
}
function initNetDetails(){
//Get the details of the net
$.get("/api/gan/network/list?netid=" + currentGANetID, function(data){
if (data.error !== undefined){
msgbox(data.error, false, 6000);
}else{
currentGaNetDetails = data;
highlightCurrentGANetCIDR();
}
});
}
//Handle delete IP from memeber
function deleteIpFromMemeber(memberid, ip){
$.ajax({
url: "/api/gan/members/ip",
metohd: "POST",
data: {
netid: currentGANetID,
memid: memberid,
opr: "del",
ip: ip,
},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false, 5000);
}else{
msgbox("IP removed from member " + memberid)
}
renderMemeberTable();
}
});
}
function addIpToMemeberFromInput(memberid, newip){
function isValidIPv4Address(address) {
// Split the address into its 4 components
const parts = address.split('.');
// Check that there are 4 components
if (parts.length !== 4) {
return false;
}
// Check that each component is a number between 0 and 255
for (let i = 0; i < 4; i++) {
const part = parseInt(parts[i], 10);
if (isNaN(part) || part < 0 || part > 255) {
return false;
}
}
// The address is valid
return true;
}
if (!isValidIPv4Address(newip)){
msgbox(newip + " is not a valid IPv4 address", false, 5000)
return
}
$.ajax({
url: "/api/gan/members/ip",
metohd: "POST",
data: {
netid: currentGANetID,
memid: memberid,
opr: "add",
ip: newip,
},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false, 5000);
}else{
msgbox("IP added to member " + memberid)
}
renderMemeberTable();
}
})
}
//Member table populate
function renderMemeberTable(forceUpdate = false) {
$.ajax({
url: '/api/gan/members/list?netid=' + currentGANetID + '&detail=true',
type: 'GET',
success: function(data) {
const tableBody = $('#networkMemeberTable');
data.sort((a, b) => a.address.localeCompare(b.address));
//Check if the new object equal to the old one
if (objectEqual(currentGANMemberList, data) && !forceUpdate){
//Do not need to update it
return;
}
tableBody.empty();
currentGANMemberList = data;
var authroziedCount = 0;
data.forEach((member) => {
let lastAuthTime = new Date(member.lastAuthorizedTime).toLocaleString();
if (member.lastAuthorizedTime == 0){
lastAuthTime = "Never";
}
let version = `${member.vMajor}.${member.vMinor}.${member.vProto}.${member.vRev}`;
if (member.vMajor == -1){
version = "Unknown";
}
let authorizedCheckbox = `<div class="ui fitted checkbox">
<input type="checkbox" addr="${member.address}" name="isAuthrozied" onchange="handleMemberAuth(this);">
<label></label>
</div>`;
if (member.authorized){
authorizedCheckbox = `<div class="ui fitted checkbox">
<input type="checkbox" addr="${member.address}" name="isAuthrozied" onchange="handleMemberAuth(this);" checked="">
<label></label>
</div>`
}
let rowClass = "authorized";
let unauthorizedStyle = "";
if (!$("#showUnauthorizedMembers")[0].checked && !member.authorized){
unauthorizedStyle = "display:none;";
}
if (!member.authorized){
rowClass = "unauthorized";
}else{
authroziedCount++;
}
let assignedIp = "";
if (member.ipAssignments.length == 0){
assignedIp = "Not assigned"
}else{
assignedIp = `<div class="ui list">`
member.ipAssignments.forEach(function(thisIp){
assignedIp += `<div class="item" style="width: 100%;">${thisIp} <a style="cursor:pointer; float: right;" title="Remove IP" onclick="deleteIpFromMemeber('${member.address}','${thisIp}');"><i class="red remove icon"></i></a></div>`;
})
assignedIp += `</div>`
}
const row = $(`<tr class="GANetMemberEntity ${rowClass}" style="${unauthorizedStyle}">`);
row.append($(`<td class="GANetMember ${rowClass}" style="text-align: center;">`).html(authorizedCheckbox));
row.append($('<td>').text(member.address));
row.append($('<td>').html(`<span class="memberName" addr="${member.address}"></span> <a style="cursor:pointer; float: right;" title="Edit Memeber Name" onclick="renameMember('${member.address}');"><i class="grey edit icon"></i></a>`));
row.append($('<td>').html(`${assignedIp}
<div class="ui action mini fluid input" style="min-width: 200px;">
<input type="text" placeholder="IPv4" onchange="$(this).val($(this).val().trim());">
<button onclick="addIpToMemeberFromInput('${member.address}',$(this).parent().find('input').val());" class="ui basic icon button">
<i class="add icon"></i>
</button>
</div>`));
row.append($('<td>').text(lastAuthTime));
row.append($('<td>').text(version));
row.append($(`<td title="Deauthorize & Delete Memeber" style="text-align: center;" onclick="handleMemberDelete('${member.address}');">`).html(`<button class="ui basic mini icon button"><i class="red remove icon"></i></button>`));
tableBody.append(row);
});
if (data.length == 0){
tableBody.append(`<tr>
<td colspan="7"><i class="green check circle icon"></i> No member has joined this network yet.</td>
</tr>`);
}
if (data.length > 0 && authroziedCount == 0 && !$("#showUnauthorizedMembers")[0].checked){
//All nodes are unauthorized. Show tips to enable unauthorize display
tableBody.append(`<tr>
<td colspan="7"><i class="yellow exclamation circle icon"></i> Unauthorized nodes detected. Enable "Show Unauthorized Member" to change member access permission.</td>
</tr>`);
}
initNameForMembers();
},
error: function(xhr, status, error) {
console.log('Error:', error);
}
});
}
function initNameForMembers(){
$(".memberName").each(function(){
let addr = $(this).attr("addr");
let targetDOM = $(this);
$.ajax({
url: "/api/gan/members/name",
method: "POST",
data: {
netid: currentGANetID,
memid: addr,
},
success: function(data){
if (data.error != undefined){
$(targetDOM).text("N/A");
}else{
$(targetDOM).text(data.Name);
}
}
});
})
}
function renameMember(targetMemberAddr){
if (targetMemberAddr == ""){
msgbox("Member address cannot be empty", false, 5000)
return
}
let newname = prompt("Enter a easy manageable name for " + targetMemberAddr, "");
if (newname != null && newname.trim() != "") {
$.ajax({
url: "/api/gan/members/name",
method: "POST",
data: {
netid: currentGANetID,
memid: targetMemberAddr,
name: newname
},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false, 6000);
}else{
msgbox("Member Name Updated");
}
renderMemeberTable(true);
}
})
}
}
//Helper function to check if two objects are equal recursively
function objectEqual(obj1, obj2) {
// compare types
if (typeof obj1 !== typeof obj2) {
return false;
}
// compare values
if (typeof obj1 !== 'object' || obj1 === null) {
return obj1 === obj2;
}
const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);
// compare keys
if (keys1.length !== keys2.length) {
return false;
}
for (const key of keys1) {
if (!keys2.includes(key)) {
return false;
}
// recursively compare values
if (!objectEqual(obj1[key], obj2[key])) {
return false;
}
}
return true;
}
function changeUnauthorizedVisibility(visable){
if(visable){
$(".GANetMemberEntity.unauthorized").show();
}else{
$(".GANetMemberEntity.unauthorized").hide();
}
}
function handleMemberAuth(object){
let targetMemberAddr = $(object).attr("addr");
let isAuthed = object.checked;
$.ajax({
url: "/api/gan/members/authorize",
method: "POST",
data: {
netid:currentGANetID,
memid: targetMemberAddr,
auth: isAuthed
},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false, 6000);
}else{
if (isAuthed){
msgbox("Member Authorized");
}else{
msgbox("Member Deauthorized");
}
}
renderMemeberTable(true);
}
})
}
function handleMemberDelete(addr){
if (confirm("Confirm delete member " + addr + " ?")){
$.ajax({
url: "/api/gan/members/delete",
method: "POST",
data: {
netid:currentGANetID,
memid: addr,
},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false, 6000);
}else{
msgbox("Member Deleted");
}
renderMemeberTable(true);
}
});
}
}
//Entry points
function initGanetDetails(ganetId){
currentGANetID = ganetId;
$(".ganetID").text(ganetId);
initNetNameAndDesc(ganetId);
generateIPRangeTable(netRanges);
initNetDetails();
renderMemeberTable(true);
//Setup a listener to listen for member list change
if (currentGANNetMemeberListener == undefined){
currentGANNetMemeberListener = setInterval(function(){
if ($('#networkMemeberTable').length > 0 && currentGANetID){
renderMemeberTable();
}
}, 3000);
}
}
//Exit point
function exitToGanList(){
$("#gan").load("./components/gan.html", function(){
if (tabSwitchEventBind["gan"]){
tabSwitchEventBind["gan"]();
}
});
}
</script>

View File

@@ -0,0 +1,441 @@
<div class="standardContainer">
<div class="ui basic segment">
<h2>Network Tools</h2>
<p>Network tools to help manage your cluster nodes</p>
</div>
<div class="ui top attached tabular menu">
<a class="nettools item active bluefont" data-tab="tab1">Discovery</a>
<a class="nettools item bluefont" data-tab="tab2">Connection</a>
<a class="nettools item bluefont" data-tab="tab3">Interface</a>
</div>
<div class="ui bottom attached tab segment active" data-tab="tab1">
<h2>Multicast DNS (mDNS) Scanner</h2>
<p>Discover mDNS enabled service in this gateway forwarded network</p>
<button class="ui basic larger circular button" onclick="launchToolWithSize('./tools/mdns.html',1000, 640);">View Discovery</button>
<div class="ui divider"></div>
<h2>IP Scanner</h2>
<p>Discover local area network devices by pinging them one by one</p>
<button class="ui basic larger circular button" onclick="launchToolWithSize('./tools/ipscan.html',1000, 640);">Start Scanner</button>
</div>
<div class="ui bottom attached tab segment" data-tab="tab2">
<div id="websshTool" style="position: relative;">
<h2>Web SSH</h2>
<p>Connect to a network node within the local area network of the gateway</p>
<div class="ui small form">
<div class="three fields">
<div class="field">
<label>Server Name or IP Address</label>
<input type="text" id="ssh_server" placeholder="e.g. example.com or 192.168.1.1">
</div>
<div class="field">
<label>Port Number</label>
<input type="number" id="ssh_port" placeholder="e.g. 22 or 2022">
</div>
<div class="field">
<label>Username</label>
<input type="text" id="ssh_username" placeholder="root">
</div>
</div>
</div>
<button class="ui basic larger orange circular button" onclick="connectSSH();">Connect using SSH</button>
<div class="ui inverted message" style="display: block;">
Copy from Terminal <code style="float: right;">Ctrl + Insert</code><br>
Paste to Terminal <code style="float: right;">Shift + Insert</code>
</div>
</div>
<div class="ui divider"></div>
<h2>Wake On LAN</h2>
<p>Wake up a remote server by WOL Magic Packet or an IoT device</p>
<div class="ui form">
<div class="two fields">
<div class="field">
<input type="text" id="wol_servername" placeholder="Server Name">
</div>
<div class="field">
<input type="text" id="wol_mac" placeholder="MAC Address">
</div>
<div class="field">
<button class="ui basic right floated button" onclick="setWoLAddress();"><i class="ui blue add icon"></i> Add Address</button>
</div>
</div>
</div>
<div class="ui accordion">
<div class="title">
<i class="dropdown icon"></i>
Pick from mDNS scan results
</div>
<div class="content">
<div class="ui basic segment">
<div class="ui selection fluid dropdown" id="mdnsWoL">
<input type="hidden">
<i class="dropdown icon"></i>
<div class="default text">Select a server</div>
<div class="menu" id="mdnsResultForWoL">
</div>
</div>
<br>
<button class="ui basic button" onclick="updateMDNSListForWoL();"><i class="ui green refresh icon"></i> Refresh mDNS Results</button>
<button class="ui basic button" onclick="selectMdnsResultForWol();"><i class="ui blue add icon"></i> Add from mDNS</button>
</div>
</div>
</div>
<table class="ui celled unstackable table">
<thead>
<tr><th>Server Name</th>
<th>MAC Address</th>
<th>Action</th>
</tr></thead>
<tbody id="wolAddressList">
<tr>
<td colspan="3"><i class="ui green circle checkmark"></i> No stored MAC address</td>
</tr>
</tbody>
</table>
<button class="ui basic button" onclick="listWoL();"><i class="ui green refresh icon"></i> Refresh</button>
</div>
<div class="ui bottom attached tab segment" data-tab="tab3">
<h2>Network Interfaces</h2>
<p>Network Interface Card (NIC) currently installed on this host</p>
<table id="network-interfaces-table" class="ui selectable inverted striped celled table">
<thead>
<tr>
<th>Interface Name</th>
<th>ID</th>
<th>IP Address</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
<script>
// Activate the default tab
$('.ui.accordion').accordion();
$('.menu .nettools.item').tab();
$('.menu .nettools.item').addClass("activated");
// Switch tabs when clicking on the menu items
$('.menu .nettools.item').on('click', function() {
$('.menu .item').removeClass('active');
$(this).addClass('active');
var tab = $(this).attr('data-tab');
$('.tab.segment').removeClass('active');
$('div[data-tab="' + tab + '"]').addClass('active');
});
//Check if web.ssh is supported
function checkWebSSHSupport(){
$.get("/api/tools/websshSupported", function(data){
if (data == false){
$("#websshTool").css({
"opacity": "0.6",
"pointer-events": "none",
"user-select": "none",
});
$("#websshTool").find("button").addClass("disabled");
}
})
}
checkWebSSHSupport();
//Connect SSH using web.ssh tool
function connectSSH(){
function validateForm() {
var serverInput = document.getElementById("ssh_server");
var portInput = document.getElementById("ssh_port");
var usernameInput = document.getElementById("ssh_username");
var server = serverInput.value.trim();
var port = parseInt(portInput.value.trim() || "22");
var username = usernameInput.value.trim() || "root";
var isValid = true;
// Validate server input
if (server === "") {
msgbox("Server Name or IP Address is required", false, 5000);
serverInput.focus();
isValid = false;
} else if (!isIpAddress(server) && !isDomainName(server)) {
msgbox("Invalid Server Name or IP Address", false, 5000);
serverInput.focus();
isValid = false;
}
// Validate port input
if (isNaN(port) || port < 2 || port > 65533) {
msgbox("Port Number must be a number between 2 and 65533", false, 5000);
portInput.focus();
isValid = false;
}
if (isValid){
//OK! Launch SSH terminal
let settingPayload = {
server: server,
port: port,
username: username
}
let settings = encodeURIComponent(JSON.stringify(settingPayload));
launchToolWithSize('./tools/sshconn.html#' + settings,1000, 640);
}else{
}
}
// Returns true if the given string is a valid IP address
function isIpAddress(str) {
var pattern = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
return pattern.test(str);
}
// Returns true if the given string is a valid domain name
function isDomainName(str) {
var pattern = /^[a-z\d\-]{1,63}(\.[a-z\d\-]{1,63})*$/i;
return pattern.test(str);
}
validateForm();
}
</script>
<script>
function launchToolWithSize(url, width, height){
let windowName = Date.now();
window.open(url,'w'+windowName,
`toolbar=no,
location=no,
status=no,
menubar=no,
scrollbars=yes,
resizable=yes,
width=${width},
height=${height}`);
}
/*
NIC Info
*/
function renderNICInfo(){
$.get("/api/stats/listnic",function(data){
var tbody = document.querySelector("#network-interfaces-table tbody");
data.forEach(function(item) {
var tr = document.createElement("tr");
var name = document.createElement("td");
name.textContent = item.Name;
var id = document.createElement("td");
id.textContent = item.ID;
var ips = document.createElement("td");
if (item.IPs == null){
ips.innerHTML = "NOT CONNECTED";
}else{
ips.innerHTML = item.IPs.join("<br>");
}
tr.appendChild(name);
tr.appendChild(id);
tr.appendChild(ips);
tbody.appendChild(tr);
});
});
}
renderNICInfo();
/*
Wake On Lan functions
*/
const wake_on_lan_API = "/api/tools/wol";
function selectMdnsResultForWol(){
let selectedText = $("#mdnsWoL").dropdown("get value");
let selectedWoL = JSON.parse(decodeURIComponent(selectedText));
$("#wol_servername").val(selectedWoL[0]);
$("#wol_mac").val(selectedWoL[1]);
setWoLAddress();
$("#mdnsWoL").dropdown("clear");
}
//Set Wake On Lan address
function setWoLAddress() {
var name = $("#wol_servername").val().trim();
var mac = $("#wol_mac").val().trim();
if (name.length == 0){
$("#wol_servername").parent().addClass("error");
}else{
$("#wol_servername").parent().removeClass("error");
}
if (!isValidMacAddress(mac)){
$("#wol_mac").parent().addClass("error");
msgbox("Invalid MAC address given", false, 5000);
return
}else{
$("#wol_mac").parent().removeClass("error");
}
$.ajax({
url: wake_on_lan_API,
type: "POST",
data: {
set: mac,
name: name
},
success: function(result) {
msgbox(result.error || "WoL MAC Added", (result.error == undefined), (result.error == undefined)?3000:5000);
listWoL();
if (result.error == undefined){
$("#wol_servername").val("");
$("#wol_mac").val("");
}
},
error: function(error) {
console.error(error);
}
});
}
function delWoLAddr(mac, name) {
if (confirm(`Confirm remove WoL record for ${name} (${mac}) ?`)){
$.ajax({
url: wake_on_lan_API,
type: "POST",
data: {
del: mac.trim()
},
success: function(result) {
msgbox(result.error || "WoL MAC Removed", (result.error == undefined), (result.error == undefined)?3000:5000);
listWoL();
},
error: function(error) {
console.error(error);
}
});
}
}
function wakeWoL(mac, object=undefined) {
if (object != undefined){
$(object).addClass("loading").addClass("disabled");
}
$.ajax({
url: wake_on_lan_API,
type: "POST",
data: {
wake: mac
},
success: function(result) {
if (result.error != undefined){
msgbox(result.error, false, 5000);
}else{
//Success?
setTimeout(function(){
if (object != undefined){
$(object).removeClass("loading").removeClass("disabled");
}
}, 5000);
}
console.log(result);
},
error: function(error) {
console.error(error);
}
});
}
function listWoL(callback) {
$.ajax({
url: wake_on_lan_API,
type: "GET",
success: function(data) {
// Clear existing rows from the table
$("#wolAddressList").empty();
// Loop through data and create a new row for each object
for (var i = 0; i < data.length; i++) {
let thisServerName = data[i].ServerName;
let thisMacAddr = data[i].MacAddr
$("#wolAddressList").append(`
<tr class="wolmacentry" mac="${thisMacAddr}">
<td>${thisServerName}</td>
<td>${thisMacAddr}</td>
<td>
<button onclick="wakeWoL('${thisMacAddr}', this);" class="ui tiny basic button"><i class="green power icon"></i>Wake on LAN</button>
<button onclick="delWoLAddr('${thisMacAddr}', '${thisServerName}');" class="ui tiny red basic icon button"><i class="red trash icon"></i></button>
</td>
</tr>
`);
}
if (data.length == 0){
$("#wolAddressList").append(`<tr>
<td colspan="3"><i class="ui green circle check icon"></i> No stored MAC address for Wake On Lan (WoL)</td>
</tr>`);
}
//Also update the MDNS list
updateMDNSListForWoL();
if (callback) {
callback(data);
}
},
error: function(error) {
console.error(error);
}
});
}
listWoL();
function isValidMacAddress(macAddress) {
const macRegex = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/;
return macRegex.test(macAddress);
}
function updateMDNSListForWoL(){
let alreadyAddedEntries = [];
$(".wolmacentry").each(function(){
let thisMac = $(this).attr("mac").trim();
if (isValidMacAddress(thisMac)){
console.log(thisMac);
alreadyAddedEntries.push(thisMac);
}
});
$("#mdnsResultForWoL").html("");
$.get("/api/mdns/list", function(data){
data.forEach(thisServer => {
if (thisServer.MacAddr.length > 0){
for (var i = 0; i < thisServer.MacAddr.length; i++){
let thisMacAddr = thisServer.MacAddr[i];
if (!isValidMacAddress(thisMacAddr) || alreadyAddedEntries.includes(thisMacAddr)){
continue;
}
let encodedObject = encodeURIComponent(JSON.stringify([thisServer.HostName, thisMacAddr]));
$("#mdnsResultForWoL").append(`<div class="item" data-value="${encodedObject}"><i class="server icon"></i> ${thisServer.HostName} (${thisMacAddr})</div>`);
}
}
});
$("#mdnsResultForWoL").parent().dropdown();
});
}
updateMDNSListForWoL();
</script>

View File

@@ -0,0 +1,158 @@
<div class="standardContainer">
<div class="ui basic segment">
<h2>Redirection Rules</h2>
<p>Add exception case for redirecting any matching URLs</p>
</div>
<div style="width: 100%; overflow-x: auto;">
<table class="ui sortable unstackable celled table" >
<thead>
<tr>
<th>Redirection URL</th>
<th>Destination URL</th>
<th class="no-sort">Copy Pathname</th>
<th class="no-sort">Status Code</th>
<th class="no-sort">Remove</th>
</tr>
</thead>
<tbody id="redirectionRuleList">
<tr>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
</tbody>
</table>
</div>
<div class="ui green message" id="delRuleSucc" style="display:none;">
<i class="ui green checkmark icon"></i> Redirection Rule Deleted
</div>
<div class="ui divider"></div>
<h4>Add Redirection Rule</h4>
<div class="ui form">
<div class="field">
<label>Redirection URL (From)</label>
<input type="text" id="rurl" name="redirection-url" placeholder="Redirection URL">
<small><i class="ui circle info icon"></i> Any matching prefix of the request URL will be redirected to the destination URL, e.g. redirect.example.com</small>
</div>
<div class="field">
<label>Destination URL (To)</label>
<input type="text" name="destination-url" placeholder="Destination URL">
<small><i class="ui circle info icon"></i> The target URL request being redirected to, e.g. dest.example.com/mysite</small>
</div>
<div class="field">
<div class="ui checkbox">
<input type="checkbox" name="forward-childpath" tabindex="0" class="hidden" checked>
<label>Forward Pathname</label>
</div>
<div class="ui message">
<p>Append the current pathname after the redirect destination</p>
<i class="check square outline icon"></i> old.example.com<b>/blog?post=13</b> <i class="long arrow alternate right icon" style="margin-left: 1em;"></i> new.example.com<b>/blog?post=13</b> <br>
<i class="square outline icon"></i> old.example.com<b>/blog?post=13</b> <i class="long arrow alternate right icon" style="margin-left: 1em;"></i> new.example.com
</div>
</div>
<div class="grouped fields">
<label>Redirection Status Code</label>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" name="redirect-type" value="307" checked>
<label>Temporary Redirect <br><small>Status Code: 307</small></label>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" name="redirect-type" value="301">
<label>Moved Permanently <br><small>Status Code: 301</small></label>
</div>
</div>
</div>
<button class="ui basic button" onclick="addRules();"><i class="ui teal plus icon"></i> Add Redirection Rule</button>
<div class="ui green message" id="ruleAddSucc" style="display:none;">
<i class="ui green checkmark icon"></i> Redirection Rules Added
</div>
</div>
</div>
<script>
$(".checkbox").checkbox();
function resetForm() {
document.getElementById("rurl").value = "";
document.getElementsByName("destination-url")[0].value = "";
document.getElementsByName("forward-childpath")[0].checked = true;
}
function addRules(){
let redirectUrl = document.querySelector('input[name="redirection-url"]').value;
let destUrl = document.querySelector('input[name="destination-url"]').value;
let forwardChildpath = document.querySelector('input[name="forward-childpath"]').checked;
let redirectType = document.querySelector('input[name="redirect-type"]:checked').value;
$.ajax({
url: "/api/redirect/add",
method: "POST",
data: {
redirectUrl: redirectUrl,
destUrl: destUrl,
forwardChildpath: forwardChildpath,
redirectType: parseInt(redirectType),
},
success: function(data){
if (data.error != undefined){
alert(data.error);
}else{
$("#ruleAddSucc").stop().finish().slideDown("fast").delay(3000).slideUp("fast");
}
initRedirectionRuleList();
}
});
}
function deleteRule(obj){
let targetURL = $(obj).attr("rurl");
targetURL = JSON.parse(decodeURIComponent(targetURL));
if (confirm("Confirm remove redirection from " + targetURL + " ?")){
$.ajax({
url: "/api/redirect/delete",
method: "POST",
data: {
redirectUrl: targetURL,
},
success: function(data){
if (data.error != undefined){
alert(data.error);
}else{
$("#delRuleSucc").stop().finish().slideDown("fast").delay(3000).slideUp("fast");
}
initRedirectionRuleList();
}
});
}
}
function initRedirectionRuleList(){
$("#redirectionRuleList").html("");
$.get("/api/redirect/list", function(data){
data.forEach(function(entry){
$("#redirectionRuleList").append(`<tr>
<td>${entry.RedirectURL} </td>
<td>${entry.TargetURL}</td>
<td>${entry.ForwardChildpath?"<i class='ui green checkmark icon'></i>":"<i class='ui red remove icon'></i>"}</td>
<td>${entry.StatusCode==307?"Temporary Redirect (307)":"Moved Permanently (301)"}</td>
<td><button onclick="deleteRule(this);" rurl="${encodeURIComponent(JSON.stringify(entry.RedirectURL))}" title="Delete redirection rule" class="ui mini red icon basic button"><i class="trash icon"></i></button></td>
</tr>`);
});
if (data.length == 0){
$("#redirectionRuleList").append(`<tr colspan="4"><td><i class="checkmark icon"></i> No redirection rule</td></tr>`);
}
});
}
initRedirectionRuleList();
$("#rurl").on('change', (event) => {
const value = event.target.value.trim().replace(/^(https?:\/\/)/, '');
event.target.value = value;
});
</script>

View File

@@ -0,0 +1,79 @@
<div class="standardContainer">
<div class="ui basic segment">
<h2>Set Proxy Root</h2>
<p>For all routing not found in the proxy rules, request will be redirected to the proxy root server.</p>
<div class="ui form">
<div class="field">
<label>Proxy Root</label>
<input type="text" id="proxyRoot" onchange="checkRootRequireTLS(this.value);">
<small>E.g. localhost:8080</small>
</div>
<div class="field">
<div class="ui checkbox">
<input type="checkbox" id="rootReqTLS" >
<label>Root require TLS Connection <br><small>(i.e. Your proxy target starts with https://)</small></label>
</div>
</div>
</div>
<br>
<button class="ui basic button" onclick="setProxyRoot()"><i class="teal home icon" ></i> Update Proxy Root</button>
</div>
</div>
<script>
function initRootInfo(){
$.get("/api/proxy/list?type=root", function(data){
if (data == null){
}else{
$("#proxyRoot").val(data.Domain);
checkRootRequireTLS(data.Domain);
}
});
}
initRootInfo();
function checkRootRequireTLS(targetDomain){
$.ajax({
url: "/api/proxy/tlscheck",
data: {url: targetDomain},
success: function(data){
if (data.error != undefined){
}else if (data == "https"){
$("#rootReqTLS").parent().checkbox("set checked");
}else if (data == "http"){
$("#rootReqTLS").parent().checkbox("set unchecked");
}
}
})
}
function setProxyRoot(){
var newpr = $("#proxyRoot").val();
if (newpr.trim() == ""){
$("#proxyRoot").parent().addClass('error');
return
}else{
$("#proxyRoot").parent().removeClass('error');
}
var rootReqTls = $("#rootReqTLS")[0].checked;
//Create the endpoint by calling add
$.ajax({
url: "/api/proxy/add",
data: {"type": "root", tls: rootReqTls, ep: newpr},
success: function(data){
if (data.error != undefined){
alert(data.error);
}else{
//OK
initRootInfo();
msgbox("Proxy Root Updated")
}
}
});
}
</script>

View File

@@ -0,0 +1,160 @@
<div class="ui stackable grid">
<div class="ten wide column">
<div class="standardContainer">
<div class="ui basic segment" style="margin-top: 1em;">
<h2>New Proxy Rule</h2>
<p>You can create a proxy endpoing by subdomain or virtual directories</p>
<div class="ui form">
<div class="field">
<label>Proxy Type</label>
<div class="ui selection dropdown">
<input type="hidden" id="ptype" value="subd">
<i class="dropdown icon"></i>
<div class="default text">Proxy Type</div>
<div class="menu">
<div class="item" data-value="subd">Sub-domain</div>
<div class="item" data-value="vdir">Virtual Directory</div>
</div>
</div>
</div>
<div class="field">
<label>Subdomain Matching Keyword / Virtual Directory Name</label>
<input type="text" id="rootname" placeholder="s1.mydomain.com">
</div>
<div class="field">
<label>IP Address or Domain Name with port</label>
<input type="text" id="proxyDomain" onchange="autoCheckTls(this.value);">
<small>E.g. 192.168.0.101:8000 or example.com</small>
</div>
<div class="field">
<div class="ui checkbox">
<input type="checkbox" id="reqTls">
<label>Proxy Target require TLS Connection <br><small>(i.e. Your proxy target starts with https://)</small></label>
</div>
</div>
<button class="ui basic button" onclick="newProxyEndpoint();"><i class="blue add icon"></i> Create Endpoint</button>
<br><br>
</div>
</div>
</div>
</div>
<div class="six wide column">
<div class="ui basic segment" style="height: 100%; background-color: var(--theme_grey); color: var(--theme_lgrey);">
<br>
<span style="font-size: 1.2em; font-weight: 300;">Subdomain</span><br>
Example of subdomain matching keyword:<br>
<code>s1.arozos.com</code> <br>(Any access starting with s1.arozos.com will be proxy to the IP address below)<br>
<div class="ui divider"></div>
<span style="font-size: 1.2em; font-weight: 300;">Virtual Directory</span><br>
Example of virtual directory name: <br>
<code>/s1/home/</code> <br>(Any access to {this_server}/s1/home/ will be proxy to the IP address below)<br>
You can also ignore the tailing slash for wildcard like usage.<br>
<code>/s1/room-</code> <br>Any access to {this_server}/s1/classroom_* will be proxied, for example: <br>
<div class="ui list">
<div class="item"><code>/s1/room-101</code></div>
<div class="item"><code>/s1/room-102/</code></div>
<div class="item"><code>/s1/room-103/map.txt</code></div>
</div><br>
<br>
</div>
</div>
</div>
</div>
<script>
//New Proxy Endpoint
function newProxyEndpoint(){
var type = $("#ptype").val();
var rootname = $("#rootname").val();
var proxyDomain = $("#proxyDomain").val();
var useTLS = $("#reqTls")[0].checked;
if (type === "vdir") {
if (!rootname.startsWith("/")) {
rootname = "/" + rootname
$("#rootname").val(rootname);
}
}else{
if (!isSubdomainDomain(rootname)){
//This doesn't seems like a subdomain
if (!confirm(rootname + " does not looks like a subdomain. Continue anyway?")){
return;
}
}
}
if (rootname.trim() == ""){
$("#rootname").parent().addClass("error");
return
}else{
$("#rootname").parent().removeClass("error");
}
if (proxyDomain.trim() == ""){
$("#proxyDomain").parent().addClass("error");
return
}else{
$("#proxyDomain").parent().removeClass("error");
}
//Create the endpoint by calling add
$.ajax({
url: "/api/proxy/add",
data: {type: type, rootname: rootname, tls: useTLS, ep: proxyDomain},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false, 5000);
}else{
//OK
listVdirs();
listSubd();
msgbox("Proxy Endpoint Added");
//Clear old data
$("#rootname").val("");
$("#proxyDomain").val("");
}
}
});
}
//Generic functions for delete rp endpoints
function deleteEndpoint(ptype, epoint){
if (confirm("Confirm remove proxy for :" + epoint + " (type: " + ptype + ")?")){
$.ajax({
url: "/api/proxy/del",
data: {ep: epoint, ptype: ptype},
success: function(){
listVdirs();
listSubd();
}
})
}
}
function autoCheckTls(targetDomain){
$.ajax({
url: "/api/proxy/tlscheck",
data: {url: targetDomain},
success: function(data){
if (data.error != undefined){
}else if (data == "https"){
$("#reqTls").parent().checkbox("set checked");
}else if (data == "http"){
$("#reqTls").parent().checkbox("set unchecked");
}
}
})
}
//Check if a string is a valid subdomain
function isSubdomainDomain(str) {
const regex = /^(localhost|[a-z0-9]+([\-.]{1}[a-z0-9]+)*\.[a-z]{2,}|[a-z0-9]+([\-.]{1}[a-z0-9]+)*\.[a-z]{2,}\.)$/i;
return regex.test(str);
}
</script>

View File

@@ -0,0 +1,580 @@
<script src="./script/useragent.js"></script>
<link href="./script/datepicker/dp-light.css" rel="stylesheet">
<script defer src="./script/datepicker/datepicker.js"></script>
<div class="standardContainer">
<div class="ui basic segment">
<h2>Statistical Analysis</h2>
<p>Statistic of your server in every aspects</p>
<div style="margin-bottom: 0.4em;">
<div class="ui small input">
<input type="text" id="statsRangeStart" placeholder="From date">
</div>
<span style="padding-left: 0.6em; padding-right: 0.6em;"> <i class="ui right arrow icon"></i> </span>
<div class="ui small input">
<input type="text" id="statsRangeEnd" placeholder="End date">
</div>
</div>
<button onclick="handleLoadStatisticButtonPress();" class="ui basic button"><i class="blue search icon"></i> Search</button>
<button onclick="clearStatisticDateRange();" class="ui yellow basic button"><i class="eraser icon"></i> Clear Range</button>
<br>
<small>Leave end range as empty for showing starting day only statistic</small>
</div>
<div class="ui divider"></div>
<div id="statisticRenderNotEnoughData" class="ui segment" style="padding: 2em;" align="center">
<h4 class="ui icon header">
<i class="medium icons" style="color: #dbdbdb !important;">
<i class="chart pie icon"></i>
<i class="small corner remove icon" style="
font-size: 1.4em;
margin-right: -0.2em;
margin-bottom: -0em;
"></i>
</i>
<div class="content" style="margin-top: 1em; color: #7c7c7c !important;">
No Data
<div class="sub header" style="color: #adadad !important;">The selected period contains too little or no request data collected. <br>
Please select a larger range or make sure there are enough traffic routing through this site.</div>
</div>
</h4>
</div>
<div id="statisticRenderElement" class="ui basic segment">
<!-- View Counts Statistics -->
<div class="ui three small statistics">
<div class="statistic">
<div class="value totalViewCount">
</div>
<div class="label">
Total Requests
</div>
</div>
<div class="statistic">
<div class="value">
<i class="ui green check circle icon"></i> <span class="totalSuccCount"></span>
</div>
<div class="label">
Success Requests
</div>
</div>
<div class="statistic">
<div class="value">
<i class="ui red times circle icon"></i> <span class="totalErrorCount"></span>
</div>
<div class="label">
Error Requests
</div>
</div>
</div>
<!-- Forward Type Data -->
<h3>Forward Traffic Types</h3>
<p>Traffic forwarding type classified by their protocols and proxy rules.</p>
<table class="ui celled unstackable table">
<thead>
<tr><th>Forward Type</th>
<th>Counts</th>
<th>Percentage</th>
</tr></thead>
<tbody class="forwardTypeCounts">
</tbody>
</table>
<!-- Client Geolocation Analysis-->
<h3>Visitors Countries</h3>
<p>Distributions of visitors by country code. Access origin are estimated using open source GeoIP database and might not be accurate.</p>
<div style="min-height: 400px;">
<canvas id="stats_visitors"></canvas>
</div>
<div class="ui divider"></div>
<!-- Client IP Analysis -->
<div class="ui stackable grid">
<div class="eight wide column">
<h3>Requests IP Version</h3>
<p>The version of Internet Protocol that the client is using. If the request client is pass through a DNS proxy like CloudFlare,
the CloudFlare proxy server address will be filtered and only the first value in the RemoteAddress field will be analysised.</p>
<div>
<canvas id="stats_ipver"></canvas>
</div>
</div>
<div class="eight wide column">
<h3>Request Origins</h3>
<p>Top 25 request origin sorted by request count</p>
<div style="height: 500px; overflow-y: auto;">
<table class="ui unstackable striped celled table">
<thead>
<tr>
<th class="no-sort">Request Origin</th>
<th class="no-sort">No of Requests</th>
</tr></thead>
<tbody id="stats_requestCountlist">
</tbody>
</table>
</div>
</div>
</div>
<div class="ui divider"></div>
<!-- Client Device Analysis -->
<div class="ui stackable grid">
<div class="eight wide column">
<h3>Client Devices</h3>
<p>Device type analysis by its request interactions.The number of iteration count does not means the number unique device, as no cookie is used to track the devices identify.</p>
<div>
<canvas id="stats_device"></canvas>
</div>
</div>
<div class="eight wide column">
<h3>Client Browsers</h3>
<p>The browsers where your client is using to visit your site</p>
<div>
<canvas id="stats_browsers"></canvas>
</div>
</div>
</div>
<div class="ui stackable grid">
<div class="eight wide column">
<h3>Client OS</h3>
<p>The OS where your client is using. Estimated using the UserAgent header sent by the client browser while requesting a resources from one of your host.</p>
<div>
<canvas id="stats_OS"></canvas>
</div>
</div>
<div class="eight wide column">
<h3>OS Versions</h3>
<p>The OS versions most commonly used by your site's visitors.</p>
<div style="height: 500px; overflow-y: auto;">
<table class="ui unstackable striped celled table">
<thead>
<tr>
<th>OS Version</th>
<th>Request Counts</th>
<th>Percentage</th>
</tr></thead>
<tbody id="stats_OSVersionList">
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- <button class="ui icon right floated basic button" onclick="initStatisticSummery();"><i class="green refresh icon"></i> Refresh</button> -->
<br><br>
</div>
<script>
var statisticCharts = [];
//Start day must be earlier than end date
function initStatisticSummery(startdate="", enddate=""){
if (startdate == "" && enddate == "" ){
let todaykey = getTodayStatisticKey();
loadStatisticByDate(todaykey);
}else if ((startdate != "" && enddate == "") || startdate == enddate){
//Load the target date
let targetDate = startdate.trim();
loadStatisticByDate(targetDate);
}else{
//Two dates are given and they are not identical
loadStatisticByRange(startdate, enddate);
console.log(startdate, enddate);
}
}
function loadStatisticByDate(dateid){
$.getJSON("/api/analytic/load?id=" + dateid, function(data){
if (data.error != undefined){
msgbox(data.error, false, 6000);
return;
}
//Destroy all the previous charts
statisticCharts.forEach(function(thisChart){
thisChart.destroy();
})
if (data.TotalRequest == 0){
//No data to analysis
$("#statisticRenderElement").hide()
$("#statisticRenderNotEnoughData").show();
return;
}else{
$("#statisticRenderElement").show();
$("#statisticRenderNotEnoughData").hide();
}
//Render the text values
$("#statisticRenderElement").find(".totalViewCount").text(abbreviateNumber(data.TotalRequest));
$("#statisticRenderElement").find(".totalSuccCount").text(abbreviateNumber(data.ValidRequest));
$("#statisticRenderElement").find(".totalErrorCount").text(abbreviateNumber(data.ErrorRequest));
//Render forward type data
renderForwardTypeCounts(data.ForwardTypes);
//Render visitor data
renderVisitorChart(data.RequestOrigin);
//Render IP versions
renderIPVersionChart(data.RequestClientIp);
//Render user agent analysis
renderUserAgentCharts(data.UserAgent);
});
}
initStatisticSummery();
$("#statsRangeStart").val(getTodayStatisticKey().split("_").join("-"));
function loadStatisticByRange(startdate, endDate){
$.getJSON("/api/analytic/loadRange?start=" + startdate + "&end=" + endDate, function(data){
console.log(data);
//Destroy all the previous charts
statisticCharts.forEach(function(thisChart){
thisChart.destroy();
})
if (data.Summary.TotalRequest == 0){
//No data to analysis
$("#statisticRenderElement").hide()
$("#statisticRenderNotEnoughData").show();
return;
}else{
$("#statisticRenderElement").show();
$("#statisticRenderNotEnoughData").hide();
}
//Render the text values
$("#statisticRenderElement").find(".totalViewCount").text(abbreviateNumber(data.Summary.TotalRequest));
$("#statisticRenderElement").find(".totalSuccCount").text(abbreviateNumber(data.Summary.ValidRequest));
$("#statisticRenderElement").find(".totalErrorCount").text(abbreviateNumber(data.Summary.ErrorRequest));
//Render forward type data
renderForwardTypeCounts(data.Summary.ForwardTypes);
//Render visitor data
renderVisitorChart(data.Summary.RequestOrigin);
//Render IP versions
renderIPVersionChart(data.Summary.RequestClientIp);
//Render user agent analysis
renderUserAgentCharts(data.Summary.UserAgent);
});
}
picker.attach({ target: document.getElementById("statsRangeStart") });
picker.attach({ target: document.getElementById("statsRangeEnd") });
function renderForwardTypeCounts(forwardTypes){
let tablBody = $("#statisticRenderElement").find(".forwardTypeCounts");
tablBody.empty();
let totalForwardCounts = 0;
for (let [key, value] of Object.entries(forwardTypes)) {
totalForwardCounts += value;
}
for (let [key, value] of Object.entries(forwardTypes)) {
tablBody.append(`<tr>
<td>${key}</td>
<td>${abbreviateNumber(value)} (${value})</td>
<td>${((value/totalForwardCounts)*100).toFixed(3)}%</td>
</tr>
`);
}
}
function getTodayStatisticKey(){
var today = new Date();
var year = today.getFullYear();
var month = String(today.getMonth() + 1).padStart(2, '0');
var day = String(today.getDate()).padStart(2, '0');
var formattedDate = year + '_' + month + '_' + day;
return formattedDate
}
function handleLoadStatisticButtonPress(){
var sd = $("#statsRangeStart").val();
var ed = $("#statsRangeEnd").val();
//Swap them if sd is later than ed
if (sd != "" && ed != "" && sd > ed) {
ed = [sd, sd = ed][0];
$("#statsRangeStart").val(sd);
$("#statsRangeEnd").val(ed);
}
initStatisticSummery(sd, ed);
}
function clearStatisticDateRange(){
$("#statsRangeStart").val("");
$("#statsRangeEnd").val("");
}
function renderUserAgentCharts(userAgentsEntries){
let userAgents = Object.keys(userAgentsEntries);
let requestCounts = Object.values(userAgentsEntries);
let mobileUser = 0;
let desktopUser = 0;
let osTypes = {};
let osVersion = {};
let browserTypes = {};
let totalRequestCounts = 0;
requestCounts.forEach(function(rc){
totalRequestCounts += rc;
})
//Building a statistic summary
userAgents.forEach(function(thisUA){
var uaInfo = parseUserAgent(thisUA);
if (uaInfo.isMobile){
mobileUser+=userAgentsEntries[thisUA];
}else{
desktopUser+=userAgentsEntries[thisUA];
}
let currentNo = osTypes[uaInfo.os];
let osVersionKey = uaInfo.os + " " + uaInfo.version.split("_").join(".");
if (currentNo == undefined){
osTypes[uaInfo.os] = userAgentsEntries[thisUA];
}else{
osTypes[uaInfo.os] = currentNo + userAgentsEntries[thisUA];
}
let p = osVersion[osVersionKey];
if (p == undefined){
osVersion[osVersionKey] = userAgentsEntries[thisUA];
}else{
osVersion[osVersionKey] = p + userAgentsEntries[thisUA];
}
let browserTypeKey = uaInfo.browser;
if (browserTypeKey.indexOf("//") >= 0){
//This is a uncatergorize browser, mostly a bot
browserTypeKey = "Bots";
}else if (browserTypeKey == ""){
//No information
browserTypeKey = "Unknown";
}
let b = browserTypes[browserTypeKey];
if (b == undefined){
browserTypes[browserTypeKey] = userAgentsEntries[thisUA];
}else{
browserTypes[browserTypeKey] = b + userAgentsEntries[thisUA];
}
});
//Create the device chart
let deviceTypeChart = new Chart(document.getElementById("stats_device"), {
type: 'pie',
data: {
labels: ['Desktop', 'Mobile'],
datasets: [{
data: [desktopUser, mobileUser],
backgroundColor: ['#e56b5e', '#6eb9c1'],
hoverBackgroundColor: ['#e56b5e', '#6eb9c1']
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
}
});
statisticCharts.push(deviceTypeChart);
//Create the OS chart
let OSnames = [];
let OSCounts = [];
let OSColors = [];
for (const [key, value] of Object.entries(osTypes)) {
OSnames.push(key);
OSCounts.push(value);
OSColors.push(getOSColorCode(key));
}
let osTypeChart = new Chart(document.getElementById("stats_OS"), {
type: 'pie',
data: {
labels: OSnames,
datasets: [{
data: OSCounts,
backgroundColor: OSColors,
hoverBackgroundColor: OSColors
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
}
});
statisticCharts.push(osTypeChart);
//Populate the OS version table
let sortedOSVersion = Object.entries(osVersion).sort((a, b) => b[1] - a[1]);
$("#stats_OSVersionList").html("");
// Loop through the sorted data and populate the table
for (let i = 0; i < sortedOSVersion.length; i++) {
let osVersion = sortedOSVersion[i][0];
let requestcount = abbreviateNumber(sortedOSVersion[i][1]);
let percentage = (sortedOSVersion[i][1] / totalRequestCounts * 100).toFixed(3);
// Create a new row in the table
let row = $("<tr>");
// Add the OS version and percentage as columns in the row
$("<td>").text(osVersion).appendTo(row);
$("<td>").text(requestcount).appendTo(row);
$("<td>").text(percentage + "%").appendTo(row);
// Add the row to the table body
$("#stats_OSVersionList").append(row);
}
//Create the browser charts
let browserNames = [];
let broserCounts = [];
let browserColors = [];
let sortedBrowserTypes = Object.entries(browserTypes).sort((a, b) => b[1] - a[1]);
console.log(sortedBrowserTypes);
sortedBrowserTypes.forEach(function(entry){
browserNames.push(entry[0]);
broserCounts.push(entry[1]);
browserColors.push(generateColorFromHash(entry[0]));
});
let browserTypeChart = new Chart(document.getElementById("stats_browsers"), {
type: 'pie',
data: {
labels: browserNames,
datasets: [{
data: broserCounts,
backgroundColor: browserColors,
hoverBackgroundColor: browserColors
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
}
});
statisticCharts.push(browserTypeChart);
}
//Generate the IPversion pie chart
function renderIPVersionChart(RequestClientIp){
let ipv4Count = Object.keys(RequestClientIp).filter(ip => ip.includes('.')).length;
let ipv6Count = Object.keys(RequestClientIp).filter(ip => ip.includes(':')).length;
let totalCount = ipv4Count + ipv6Count;
let ipv4Percent = ((ipv4Count / totalCount) * 100).toFixed(2);
let ipv6Percent = ((ipv6Count / totalCount) * 100).toFixed(2);
// Create the chart data object
let chartData = {
labels: ['IPv4', 'IPv6'],
datasets: [{
data: [ipv4Percent, ipv6Percent],
backgroundColor: ['#9295f0', '#bff092'],
hoverBackgroundColor: ['#9295f0', '#bff092']
}]
};
// Create the chart options object
let ipvChartOption = {
responsive: true,
maintainAspectRatio: false,
};
// Create the pie chart
let ipTypeChart = new Chart(document.getElementById("stats_ipver"), {
type: 'pie',
data: chartData,
options: ipvChartOption
});
statisticCharts.push(ipTypeChart);
//Populate the request count table
let requestCounts = Object.entries(RequestClientIp);
// Sort the array by the value (count)
requestCounts.sort((a, b) => b[1] - a[1]);
// Select the table body and empty it
let tableBody = $('#stats_requestCountlist');
tableBody.empty();
// Loop through the sorted array and add the top 25 requested IPs to the table
for (let i = 0; i < 25 && i < requestCounts.length; i++) {
let [ip, count] = requestCounts[i];
let row = $('<tr>').appendTo(tableBody);
$('<td style="word-break: break-all;">').text(ip).appendTo(row);
$('<td>').text(count).appendTo(row);
}
}
//Generate a fixed color code from string hash
function generateColorFromHash(stringToHash){
let hash = 0;
for (let i = 0; i < stringToHash.length; i++) {
hash = stringToHash.charCodeAt(i) + ((hash << 5) - hash);
}
const hue = hash % 300;
const saturation = Math.floor(Math.random() * 20) + 70;
const lightness = Math.floor(Math.random() * 20) + 60;
return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
}
//Generate the visitor country pie chart
function renderVisitorChart(visitorData){
// Extract the labels and data from the visitor data object
let labels = [];
let data = Object.values(visitorData);
Object.keys(visitorData).forEach(function(cc){
if (cc == ""){
labels.push("Local / Unknown")
}else{
labels.push(`${getCountryName(cc)} [${cc.toUpperCase()}]` );
}
});
// Define the colors to be used in the pie chart
let colors = [];
labels.forEach(function(cc){
colors.push(generateColorFromHash(cc));
});
// Create the chart data object
let CCchartData = {
labels: labels,
datasets: [{
data: data,
backgroundColor: colors
}]
};
// Create the chart options object
let CCchartOptions = {
responsive: true,
maintainAspectRatio: false,
};
// Create the pie chart
const visitorsChart = new Chart(document.getElementById("stats_visitors"), {
type: 'pie',
data: CCchartData,
options: CCchartOptions
});
statisticCharts.push(visitorsChart);
}
</script>

View File

@@ -0,0 +1,571 @@
<div class="ui stackable grid">
<div class="ten wide column serverstatusWrapper">
<div id="serverstatus" class="ui statustab inverted segment">
<h1 class="ui header" style="margin-top: 1em; margin-left: 0.4em; padding-bottom: 1em;">
<i id="rpStatusIcon" class="loading spinner icon"></i>
<div class="content">
<span id="statusTitle">Loading</span>
<div class="sub header" id="statusText">Checking server status</div>
</div>
</h1>
<div class="dot-container">
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
</div>
</div>
</div>
<div class="six wide column statisticWrapper">
<div class="ui greybackground statustab segment">
<h5 class="ui header">
<i class="exchange icon"></i>
<div class="content">
<span id="summaryTotalCount"></span> <small>Req. Today</small>
<div class="sub header" style="margin-top: 0.4em;">
<i class="green circle check icon"></i> <span id="summarySuccCount"></span>
/ <i class="red red exclamation circle icon"></i> <span id="summaryErrCount"></span>
</div>
</div>
</h5>
<div class="ui divider"></div>
<h5 class="ui header">
<i class="arrows alternate horizontal icon"></i>
<div class="content">
<span id="forwardtype"></span>
<div class="sub header" id="forwardtypeList">
</div>
</div>
</h5>
<div class="ui divider"></div>
<h5 class="ui header">
<i class="map marker alternate icon"></i>
<div class="content">
<span id="country"></span>
<div class="sub header" id="countryList">
</div>
</div>
</h5>
</div>
</div>
</div>
<div id="networkActWrapper" class="standardContainer" style="position: relative; margin-top: 1em;">
<canvas id="networkActivity"></canvas>
</div>
<div id="networkActivityPlaceHolder">
<p style="opacity: 0.5;"><i class="ui pause icon"></i> Graph Render Paused</p>
</div>
<br>
<div class="standardContainer">
<h4>Basic Settings</h4>
<p>Inbound Port (Port to be proxied)</p>
<div class="ui action fluid notloopbackOnly input">
<input type="text" id="incomingPort" placeholder="Incoming Port" value="80">
<button class="ui basic green notloopbackOnly button" onclick="handlePortChange();">Apply</button>
</div>
<br>
<div id="tls" class="ui toggle notloopbackOnly checkbox">
<input type="checkbox">
<label>Use TLS to serve proxy request</label>
</div>
<br>
<div id="redirect" class="ui toggle notloopbackOnly checkbox" style="margin-top: 0.6em;">
<input type="checkbox">
<label>Force redirect HTTP request to HTTPS<br>
<small>(Only apply when listening port is not 80)</small></label>
</div>
<br><br>
<button id="startbtn" class="ui teal button" onclick="startService();">Start Service</button>
<button id="stopbtn" class="ui red notloopbackOnly disabled button" onclick="stopService();">Stop Service</button>
<div id="rploopbackWarning" class="ui segment" style="display:none;">
<b><i class="yellow warning icon"></i> Loopback Routing Warning</b><br>
<small>This management interface is a loopback proxied service. <br>If you want to shutdown the reverse proxy server, please remove the proxy rule for the management interface and refresh.</small>
</div>
<div class="ui divider"></div>
<div class="">
<h4>Statistic Overview</h4>
<div class="ui two column stackable grid">
<div class="column">
<p>Visitor Counts</p>
<table class="ui unstackable inverted celled table">
<thead>
<tr>
<th>Country ISO Code</th>
<th>Unique Visitors</th>
</tr>
</thead>
<tbody id="countryCodetable">
<tr>
<td colspan="2">No Data</td>
</tr>
</tbody>
</table>
</div>
<div class="column">
<p>Proxy Request Types</p>
<table class="ui unstackable inverted celled table">
<thead>
<tr>
<th>Proxy Type</th>
<th>Count</th>
</tr>
</thead>
<tbody id="forwardTypeTable">
<tr>
<td colspan="2">No Data</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<br>
<button class="ui right floated basic button" onclick="getDailySummaryDetails();"><i class="green refresh icon"></i> Refresh</button>
<br><br>
</div>
<script>
let loopbackProxiedInterface = false;
//Initial the start stop button if this is reverse proxied
$.get("/api/proxy/requestIsProxied", function(data){
if (data == true){
//This management interface is reverse proxied by itself
//do not allow turning off the proxy
$(".notloopbackOnly").addClass("disabled");
loopbackProxiedInterface = true;
$("#rploopbackWarning").show();
}
});
//Get the latest server status from proxy server
function initRPStaste(){
$.get("/api/proxy/status", function(data){
if (data.Running == true){
$("#startbtn").addClass("disabled");
if (!loopbackProxiedInterface){
$("#stopbtn").removeClass("disabled");
}
$("#serverstatus").addClass("green");
$("#statusTitle").text("Online");
$("#rpStatusIcon").attr("class", "green circle check icon");
$("#statusText").text("Serving request on port: " + data.Option.Port);
}else{
$("#startbtn").removeClass("disabled");
$("#stopbtn").addClass("disabled");
$("#statusTitle").text("Offline");
$("#rpStatusIcon").attr("class", "black circle times icon")
$("#statusText").text("Reverse proxy server is offline");
$("#serverstatus").removeClass("green");
}
$("#incomingPort").val(data.Option.Port);
});
}
function getDailySummaryDetails(){
function sortObjectByValue(obj) {
// Convert object to array of [key, value] pairs
const entries = Object.entries(obj);
// Sort array based on value of each pair
entries.sort((a, b) => {
return b[1] - a[1];
});
// Convert sorted array back to object
const sortedObj = {};
for (const [key, value] of entries) {
sortedObj[key] = value;
}
return sortedObj;
}
$.get("/api/stats/countries", function(data){
data = sortObjectByValue(data);
$("#country").html((Object.keys(data)[0])?Object.keys(data)[0]:"No Data");
$("#countryList").html(`
<div>
${(Object.keys(data)[1])?Object.keys(data)[1]:"-"}<br>
${(Object.keys(data)[2])?Object.keys(data)[2]:"-"}
</div>
`);
//populate the table
$("#countryCodetable").html("");
for (const [key, value] of Object.entries(data)) {
var countryName = getCountryName(key);
if (countryName == ""){
countryName = "LAN"
}
$("#countryCodetable").append(`<tr>
<td>${key} (${countryName})</td>
<td>${value}</td>
</tr>`);
}
if (Object.keys(data).length == 0){
$("#countryCodetable").append(`<tr>
<td colspan="2"><i class="ui green circle check icon"></i> No Data</td>
</tr>`);
}
});
//Filter forward type
function fft(ft){
if (ft.indexOf("-") >= 0){
ft = ft.replace("-", " (");
ft = ft + ")";
}
ft = ft.charAt(0).toUpperCase() + ft.slice(1);
return ft;
}
$.get("/api/stats/summary", function(data){
data = sortObjectByValue(data.ForwardTypes);
$("#forwardtype").html((Object.keys(data)[0])?fft(Object.keys(data)[0]) + ": " + abbreviateNumber(data[Object.keys(data)[0]]):"No Data");
$("#forwardtypeList").html(`
<div>
${(Object.keys(data)[1])?fft(Object.keys(data)[1]) + ": " + abbreviateNumber(data[Object.keys(data)[1]]):"-"}<br>
${(Object.keys(data)[2])?fft(Object.keys(data)[2]) + ": " + abbreviateNumber(data[Object.keys(data)[2]]):"-"}
</div>
`);
$("#forwardTypeTable").html("");
for (const [key, value] of Object.entries(data)) {
$("#forwardTypeTable").append(`<tr>
<td>${key}</td>
<td>${value}</td>
</tr>`);
}
if (Object.keys(data).length == 0){
$("#forwardTypeTable").append(`<tr>
<td colspan="2"><i class="ui green circle check icon"></i> No Data</td>
</tr>`);
}
});
}
getDailySummaryDetails();
function getDailySummary(){
$.get("/api/stats/summary?fast=true", function(data){
$("#summaryTotalCount").text(abbreviateNumber(data.TotalRequest));
$("#summarySuccCount").text(abbreviateNumber(data.ValidRequest));
$("#summaryErrCount").text(abbreviateNumber(data.ErrorRequest));
});
}
setInterval(function(){
getDailySummary();
}, 10000);
getDailySummary();
//Start and stop service button
function startService(){
$.post("/api/proxy/enable", {enable: true}, function(data){
if (data.error != undefined){
msgbox(data.error, false, 5000);
}
initRPStaste();
});
}
function stopService(){
$.post("/api/proxy/enable", {enable: false}, function(data){
if (data.error != undefined){
msgbox(data.error, false, 5000);
}
initRPStaste();
});
}
function handlePortChange(){
var newPortValue = $("#incomingPort").val();
if (isNaN(newPortValue - 1) || newPortValue < 1 || newPortValue > 65535){
msgbox("Invalid incoming port value", false, 5000);
return;
}
$.post("/api/proxy/setIncoming", {incoming: newPortValue}, function(data){
if (data.error != undefined){
msgbox(data.error, false, 5000);
return;
}
msgbox("Setting Updated");
initRPStaste();
});
}
function initHTTPtoHTTPSRedirectSetting(){
$.get("/api/proxy/useHttpsRedirect", function(data){
if (data == true){
$("#redirect").checkbox("set checked");
}
//Initiate the input listener on the checkbox
$("#redirect").find("input").on("change", function(){
let thisValue = $("#redirect").checkbox("is checked");
$.ajax({
url: "/api/proxy/useHttpsRedirect",
data: {set: thisValue},
success: function(data){
if (data.error != undefined){
alert(data.error);
}else{
//Updated
msgbox("Setting Updated");
initRPStaste();
}
}
})
});
});
}
initHTTPtoHTTPSRedirectSetting();
function initTlsSetting(){
$.get("/api/cert/tls", function(data){
if (data == true){
$("#tls").checkbox("set checked");
}else{
$("#redirect").addClass('disabled');
}
//Initiate the input listener on the checkbox
$("#tls").find("input").on("change", function(){
let thisValue = $("#tls").checkbox("is checked");
if (thisValue){
$("#redirect").removeClass('disabled');
}else{
$("#redirect").addClass('disabled');
}
$.ajax({
url: "/api/cert/tls",
data: {set: thisValue},
success: function(data){
if (data.error != undefined){
alert(data.error);
}else{
//Updated
msgbox("Setting Updated");
initRPStaste();
}
}
})
});
})
}
initTlsSetting();
</script>
<script>
/*
Render Network Activity Graph
*/
/*
Setup Graph
*/
let rxValues = [];
let txValues = [];
let dataCount = 300;
let timestamps = [];
for(var i = 0; i < dataCount; i++){
timestamps.push(parseInt(Date.now() / 1000) + i);
}
function fetchData() {
$.ajax({
url: '/api/stats/netstatgraph?array=true',
success: function(data){
if (rxValues.length == 0){
rxValues = JSON.parse(JSON.stringify(data.Rx));
}else{
rxValues.push(data.Rx[dataCount-1]);
rxValues.shift();
}
if (txValues.length == 0){
txValues = JSON.parse(JSON.stringify(data.Tx));
}else{
txValues.push(data.Tx[dataCount-1]);
txValues.shift();
}
timestamps.push(parseInt(Date.now() / 1000));
timestamps.shift();
updateChart();
}
})
}
function formatBandwidth(bps) {
const KBPS = 1000;
const MBPS = 1000 * KBPS;
const GBPS = 1000 * MBPS;
if (bps >= GBPS) {
return (bps / GBPS).toFixed(2) + " Gbps";
} else if (bps >= MBPS) {
return (bps / MBPS).toFixed(2) + " Mbps";
} else if (bps >= KBPS) {
return (bps / KBPS).toFixed(2) + " Kbps";
} else {
return bps.toFixed(2) + " bps";
}
}
var networkStatisticChart;
function initChart(){
$.get("/api/stats/netstat", function(data){
networkStatisticChart = new Chart(
document.getElementById('networkActivity'),
{
type: 'line',
responsive: true,
resizeDelay: 100,
options: {
animation: false,
maintainAspectRatio: false,
tooltips: {enabled: false},
hover: {mode: null},
//stepped: 'middle',
plugins: {
legend: {
display: true,
position: "right",
},
title: {
display: false,
text: 'Network Statistic'
},
},
scales: {
x: {
display: false,
},
y: {
display: true,
scaleLabel: {
display: true,
labelString: 'Value'
},
ticks: {
stepSize: 10000000,
callback: function(label, index, labels) {
return formatBandwidth(parseInt(label));
}
},
gridLines: {
display: true
}
}
}
},
data: {
labels: timestamps,
datasets: [
{
label: 'Inbound',
data: rxValues,
borderColor: "#4d9dd9",
borderWidth: 2,
backgroundColor: 'rgba(77, 157, 217, 0.2)',
fill: true,
pointStyle: false,
},
{
label: 'Outbound',
data: txValues,
borderColor: '#ffe32b',
borderWidth: 2,
backgroundColor: 'rgba(255, 227, 43, 0.2)',
fill: true,
pointStyle: false,
}
]
}
}
);
});
}
function updateChart() {
//networkStatisticChart.data.datasets[0].data = rxValues;
//networkStatisticChart.data.datasets[1].data = txValues;
if (networkStatisticChart != undefined){
networkStatisticChart.update();
}
}
function updateChartSize(){
let newSize = $("#networkActWrapper").width() - 300;
if (window.innerWidth > 750){
newSize = window.innerWidth - $(".toolbar").width() - 500;
}else{
newSize = $("#networkActWrapper").width() - 500;
}
if (networkStatisticChart != undefined){
networkStatisticChart.resize(newSize, 200);
}
}
function handleChartAccumulateResize(){
$("#networkActivity").hide();
$("#networkActivityPlaceHolder").show();
if (chartResizeTimeout != undefined){
clearTimeout(chartResizeTimeout);
}
chartResizeTimeout = setTimeout(function(){
chartResizeTimeout = undefined;
$("#networkActivityPlaceHolder").hide();
$("#networkActivity").show();
updateChartSize();
}, 300);
}
var chartResizeTimeout;
window.addEventListener('resize', () => {
handleChartAccumulateResize();
});
//Bind event to tab switch
tabSwitchEventBind["status"] = function(){
//On switch over to this page, resize the chart
$("#networkActivityPlaceHolder").hide();
$("#networkActivity").show().delay(100, function(){
updateChartSize();
});
}
window.addEventListener("focus", function(event){
handleChartAccumulateResize();
});
document.addEventListener("visibilitychange", () => {
// it could be either hidden or visible
//handleChartAccumulateResize();
});
//Initialize chart data
initChart();
fetchData();
setInterval(fetchData, 1000);
</script>

View File

@@ -0,0 +1,55 @@
<div class="standardContainer">
<div class="ui basic segment">
<h2>Subdomain</h2>
<p>Subdomains are a way to organize and identify different sections of a website or domain. They are essentially a prefix to the main domain name, separated by a dot. <br>For example, in the domain "blog.example.com," "blog" is the subdomain.</p>
</div>
<div style="width: 100%; overflow-x: auto; margin-bottom: 1em;">
<table class="ui celled sortable unstackable compact table">
<thead>
<tr>
<th>Matching Domain</th>
<th>Proxy To</th>
<th class="no-sort">Remove</th>
</tr>
</thead>
<tbody id="subdList">
</tbody>
</table>
</div>
<button class="ui icon right floated basic button" onclick="listSubd();"><i class="green refresh icon"></i> Refresh</button>
<br><br>
</div>
<script>
function listSubd(){
$("#subdList").html(``);
$.get("/api/proxy/list?type=subd", function(data){
if (data.error !== undefined){
$("#subdList").append(`<tr>
<td data-label="" colspan="3"><i class="remove icon"></i> ${data.error}</td>
</tr>`);
}else if (data.length == 0){
$("#subdList").append(`<tr>
<td data-label="" colspan="3"><i class="checkmark icon"></i> No Subdomain Proxy Record</td>
</tr>`);
}else{
data.forEach(subd => {
let tlsIcon = "";
if (subd.RequireTLS){
tlsIcon = `<i class="lock icon"></i>`;
}
$("#subdList").append(`<tr>
<td data-label="">${subd.MatchingDomain}</td>
<td data-label="">${subd.Domain} ${tlsIcon}</td>
<td class="center aligned" data-label=""><button class="ui circular mini red basic icon button" onclick='deleteEndpoint("subd","${subd.MatchingDomain}")'><i class="trash icon"></i></button></td>
</tr>`);
});
}
});
}
//Bind on tab switch events
tabSwitchEventBind["subd"] = function(){
listSubd();
}
</script>

View File

@@ -0,0 +1,355 @@
<div class="standardContainer">
<div class="ui basic segment">
<h2>TCP Proxy</h2>
<p>Proxy traffic flow on layer 3 via TCP/IP</p>
</div>
<button class="ui basic orange button" id="addProxyConfigButton"><i class="ui add icon"></i> Add Proxy Config</button>
<button class="ui basic circular right floated icon button" title="Refresh List"><i class="ui green refresh icon"></i></button>
<div class="ui divider"></div>
<div class="ui basic segment" id="addproxyConfig" style="display:none;">
<h3>TCP Proxy Config</h3>
<p>Create or edit a new proxy instance</p>
<form id="tcpProxyForm" class="ui form">
<div class="field" style="display:none;">
<label>UUID</label>
<input type="text" name="uuid">
</div>
<div class="field">
<label>Name</label>
<input type="text" name="name" placeholder="Config Name">
</div>
<div class="field">
<label>Port A</label>
<input type="text" name="porta" placeholder="First address or port">
</div>
<div class="field">
<label>Port B</label>
<input type="text" name="portb" placeholder="Second address or port">
</div>
<div class="field">
<label>Timeout (s)</label>
<input type="text" name="timeout" placeholder="Timeout (s)">
</div>
<div class="field">
<label>Mode</label>
<select name="mode" class="ui dropdown">
<option value="">Select Mode</option>
<option value="listen">Listen</option>
<option value="transport">Transport</option>
<option value="starter">Starter</option>
</select>
</div>
<button id="addTcpProxyButton" class="ui basic button" type="submit"><i class="ui blue add icon"></i> Create</button>
<button id="editTcpProxyButton" class="ui basic button" onclick="confirmEditTCPProxyConfig(event);"><i class="ui blue save icon"></i> Update</button>
<table class="ui celled padded table">
<thead>
<tr><th class="single line">Mode</th>
<th>Public-IP</th>
<th>Concurrent Access</th>
<th>Flow Diagram</th>
</tr></thead>
<tbody>
<tr>
<td>
<h3 class="ui center aligned header">Listen</h3>
</td>
<td class="single line">
Server: <i class="ui green check icon"></i><br>
A: <i class="ui remove icon"></i><br>
B: <i class="ui remove icon"></i><br>
</td>
<td>
<i class="ui red times icon"></i>
</td>
<td>Port A (e.g. 8080) <i class="arrow right icon"></i> Server<br>
Port B (e.g. 8081) <i class="arrow right icon"></i> Server<br>
<small>Server will act as a bridge to proxy traffic between Port A and B</small>
</td>
</tr>
<tr>
<td>
<h3 class="ui center aligned header">Transport</h3>
</td>
<td class="single line">
Server: <i class="ui green check icon"></i><br>
A: <i class="ui remove icon"></i><br>
B: <i class="ui green check icon"></i> (or same LAN)<br>
</td>
<td>
<i class="ui green check icon"></i>
</td>
<td>Port A (e.g. 25565) <i class="arrow right icon"></i> Server<br>
Server <i class="arrow right icon"></i> Port B (e.g. 192.168.0.2:25565)<br>
<small>Traffic from Port A will be forward to Port B's (IP if provided and) Port</small>
</td>
</tr>
<tr>
<td>
<h3 class="ui center aligned header">Starter</h3>
</td>
<td class="single line">
Server: <i class="ui times icon"></i><br>
A: <i class="ui green check icon"></i><br>
B: <i class="ui green check icon"></i><br>
</td>
<td>
<i class="ui red times icon"></i>
</td>
<td>Server <i class="arrow right icon"></i> Port A (e.g. remote.local.:8080) <br>
Server <i class="arrow right icon"></i> Port B (e.g. recv.local.:8081) <br>
<small>Port A and B will be actively bridged</small>
</td>
</tr>
</tbody>
</table>
</form>
<div class="ui divider"></div>
</div>
<div class="ui basic segment">
<div style="overflow-x: auto;">
<h3>TCP Proxy Configs</h3>
<p>A list of TCP proxy configs created on this host. To enable them, use the toggle button on the right.</p>
<table id="proxyTable" class="ui basic celled unstackable table">
<thead>
<tr>
<th>Name</th>
<th>PortA</th>
<th>PortB</th>
<th>Mode</th>
<th>Timeout (s)</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
<script>
let editingTCPProxyConfigUUID = ""; //The current editing TCP Proxy config UUID
$("#tcpProxyForm .dropdown").dropdown();
$('#tcpProxyForm').on('submit', function(event) {
event.preventDefault();
var form = $(this);
var formValid = validateTCPProxyConfig(form);
if (!formValid){
return;
}
// Send the AJAX POST request
$.ajax({
type: 'POST',
url: '/api/tcpprox/config/add',
data: form.serialize(),
success: function(response) {
if (response.error) {
msgbox(response.error, false, 6000);
}else{
msgbox("Config Added");
}
clearTCPProxyAddEditForm();
initProxyConfigList();
$("#addproxyConfig").slideUp("fast");
},
error: function() {
msgbox('An error occurred while processing the request', false);
}
});
});
//Add proxy button pressed. Show add TCP proxy menu
$("#addProxyConfigButton").on("click", function(){
$('#addproxyConfig').slideToggle('fast');
$("#addTcpProxyButton").show();
$("#editTcpProxyButton").hide();
});
function clearTCPProxyAddEditForm(){
$('#tcpProxyForm input, #tcpProxyForm select').val('');
}
function validateTCPProxyConfig(form){
// Validate timeout is an integer
var timeout = parseInt(form.find('input[name="timeout"]').val());
if (isNaN(timeout)) {
form.find('input[name="timeout"]').parent().addClass("error");
msgbox('Timeout must be a valid integer', false, 5000);
return false;
}else{
form.find('input[name="timeout"]').parent().removeClass("error");
}
// Validate mode is selected
var mode = form.find('select[name="mode"]').val();
if (mode === '') {
form.find('select[name="mode"]').parent().addClass("error");
msgbox('Please select a mode', false, 5000);
return false;
}else{
form.find('select[name="mode"]').parent().removeClass("error");
}
return true;
}
function renderProxyConfigs(proxyConfigs) {
var tableBody = $('#proxyTable tbody');
tableBody.empty();
if (proxyConfigs.length === 0) {
var noResultsRow = $('<tr><td colspan="7"><i class="green check circle icon"></i>No Proxy Configs</td></tr>');
tableBody.append(noResultsRow);
} else {
proxyConfigs.forEach(function(config) {
var runningLogo = '<i class="red circle icon"></i>';
var startButton = `<button onclick="startTcpProx('${config.UUID}');" class="ui button" title="Start Proxy"><i class="play icon"></i></button>`;
if (config.Running){
runningLogo = '<i class="green circle icon"></i>';
startButton = `<button onclick="stopTcpProx('${config.UUID}');" class="ui button" title="Start Proxy"><i class="red stop icon"></i></button>`;
}
var modeText = "Unknown";
if (config.Mode == 0){
modeText = "Listen";
}else if (config.Mode == 1){
modeText = "Transport";
}else if (config.Mode == 2){
modeText = "Starter";
}
var thisConfig = encodeURIComponent(JSON.stringify(config));
var row = $(`<tr class="tcproxConfig" uuid="${config.UUID}" config="${thisConfig}">`);
row.append($('<td>').html(runningLogo + config.Name));
row.append($('<td>').text(config.PortA));
row.append($('<td>').text(config.PortB));
row.append($('<td>').text(modeText));
row.append($('<td>').text(config.Timeout));
row.append($('<td>').html(`
<div class="ui basic icon buttons">
<button class="ui button" onclick="validateProxyConfig('${config.UUID}', this);" title="Validate Config"><i class="teal question circle outline icon"></i></button>
${startButton}
<button onclick="editTCPProxyConfig('${config.UUID}');" class="ui button" title="Edit Config"><i class="edit icon"></i></button>
<button onclick="deleteTCPProxyConfig('${config.UUID}');" class="ui red button" title="Delete Config"><i class="trash icon"></i></button>
</div>
`));
tableBody.append(row);
});
}
}
function getConfigDetailsFromDOM(configUUID){
let thisConfig = null;
$(".tcproxConfig").each(function(){
let uuid = $(this).attr("uuid");
if (configUUID == uuid){
//This is the one we are looking for
thisConfig = JSON.parse(decodeURIComponent($(this).attr("config")));
}
});
return thisConfig;
}
function validateProxyConfig(configUUID, btn){
$(btn).html(`<i class="ui loading spinner icon"></i>`);
$.ajax({
url: "/api/tcpprox/config/validate",
data: {uuid: configUUID},
success: function(data){
if (data.error != undefined){
$(btn).html(`<i class="red times icon"></i>`);
msgbox(data.error, false, 6000);
}else{
$(btn).html(`<i class="green check icon"></i>`);
msgbox("Config Check Passed");
}
}
})
}
function editTCPProxyConfig(configUUID){
let targetConfig = getConfigDetailsFromDOM(configUUID);
if (targetConfig != null){
$("#addTcpProxyButton").hide();
$("#editTcpProxyButton").show();
$.each(targetConfig, function(key, value) {
var field = $("#tcpProxyForm").find('[name="' + key.toLowerCase() + '"]');
if (field.length > 0) {
if (field.is('input')) {
field.val(value);
}else if (field.is('select')){
if (key.toLowerCase() == "mode"){
if (value == 0){
value = "listen";
}else if (value == 1){
value = "transport";
}else if (value == 2){
value = "starter";
}
}
$(field).dropdown("set selected", value);
}
}
});
editingTCPProxyConfigUUID = configUUID;
$("#addproxyConfig").slideDown("fast");
}else{
msgbox("Unable to load target config", false);
}
}
function confirmEditTCPProxyConfig(event){
event.preventDefault();
event.stopImmediatePropagation();
var form = $("#tcpProxyForm");
var formValid = validateTCPProxyConfig(form);
if (!formValid){
return;
}
// Send the AJAX POST request
$.ajax({
type: 'POST',
url: '/api/tcpprox/config/edit',
data: form.serialize(),
success: function(response) {
if (response.error) {
msgbox(response.error, false, 6000);
}else{
msgbox("Config Updated");
}
initProxyConfigList();
clearTCPProxyAddEditForm();
$("#addproxyConfig").slideUp("fast");
},
error: function() {
msgbox('An error occurred while processing the request', false);
}
});
}
function deleteTCPProxyConfig(configUUID){
}
function initProxyConfigList(){
$.ajax({
type: 'GET',
url: '/api/tcpprox/config/list',
success: function(response) {
renderProxyConfigs(response);
},
error: function() {
msgbox('Unable to load proxy configs', false);
}
});
}
initProxyConfigList();
</script>
</div>

View File

@@ -0,0 +1,158 @@
<div class="standardContainer">
<div class="ui basic segment">
<h2>Uptime Monitor</h2>
<p>Check the online state of proxied targets</p>
</div>
<div class="ui divider"></div>
<div id="utmrender" class="ui basic segment">
<div class="ui basic segment">
<h4 class="ui header">
<i class="red remove icon"></i>
<div class="content">
Uptime Monitoring service is currently unavailable
<div class="sub header">This might cause by an error in cluster communication within the host servers. Please wait for administrator to resolve the issue.</div>
</div>
</h4>
</div>
</div>
<div align="center">
<button class="ui basic circular green icon button" onclick="reloadUptimeList();"><i class="refresh icon"></i></button>
</div>
</div>
<script>
$('#utmEnable').checkbox({
onChange: function() {
var utmEnable = $('input[name="utmEnable"]').is(":checked");
$.post({
url: '/api/toggle-utm',
data: {utmEnable: utmEnable},
success: function(response) {
console.log(response);
},
error: function(error) {
console.log(error);
}
});
}
});
function initUptimeTable(){
$.get("/api/utm/list", function(data){
let records = data;
renderRecords(records);
})
}
initUptimeTable();
function reloadUptimeList(){
$("#utmrender").html(`<div class="ui segment">
<div class="ui active inverted dimmer" style="z-index: 2;">
<div class="ui text loader">Loading</div>
</div>
<br><br><br><br>
</div>`);
setTimeout(initUptimeTable, 300);
}
//For every 5 minutes
setInterval(function(){
$.get("/api/utm/list", function(data){
console.log("Status Updated");
records = data;
renderRecords(records);
});
}, (300 * 1000));
function renderRecords(records){
$("#utmrender").html("");
for (let [key, value] of Object.entries(records)) {
renderUptimeData(key, value);
}
}
function format_time(s) {
const date = new Date(s * 1e3);
return(date.toLocaleString());
}
function renderUptimeData(key, value){
if (value.length == 0){
return
}
let id = value[0].ID;
let name = value[0].Name;
let url = value[0].URL;
let protocol = value[0].Protocol;
//Generate the status dot
let statusDotList = ``;
for(var i = 0; i < (288 - value.length); i++){
//Padding
statusDotList += `<div class="padding statusDot"></div>`
}
let ontimeRate = 0;
for (var i = 0; i < value.length; i++){
//Render status to html
let thisStatus = value[i];
let dotType = "";
if (thisStatus.Online){
if (thisStatus.StatusCode < 200 || thisStatus.StatusCode >= 300){
dotType = "error";
}else{
dotType = "online";
}
ontimeRate++;
}else{
dotType = "offline";
}
let datetime = format_time(thisStatus.Timestamp);
statusDotList += `<div title="${datetime}" class="${dotType} statusDot"></div>`
}
ontimeRate = ontimeRate / value.length * 100;
let ontimeColor = "#df484a"
if (ontimeRate > 0.8){
ontimeColor = "#3bd671";
}else if(ontimeRate > 0.5) {
ontimeColor = "#f29030";
}
//Check of online status now
let currentOnlineStatus = "Unknown";
let onlineStatusCss = ``;
if (value[value.length - 1].Online){
currentOnlineStatus = `<i class="circle icon"></i> Online`;
onlineStatusCss = `color: #3bd671;`;
}else{
currentOnlineStatus = `<i class="circle icon"></i> Offline`;
onlineStatusCss = `color: #df484a;`;
}
//Generate the html
$("#utmrender").append(`<div class="ui basic segment statusbar">
<div class="domain">
<div style="position: absolute; top: 0; right: 0.4em;">
<p class="onlineStatus" style="display: inline-block; font-size: 1.3em; padding-right: 0.5em; padding-left: 0.3em; ${onlineStatusCss}">${currentOnlineStatus}</p>
</div>
<div>
<h3 class="ui header" style="margin-bottom: 0.2em;">${name}</h3>
<a href="${url}" target="_blank">${url}</a> | <span style="color: ${ontimeColor};">${(ontimeRate).toFixed(2)}%<span>
</div>
<div class="ui basic label protocol" style="position: absolute; bottom: 0; right: 0.2em; margin-bottom: -0.6em;">
proto: ${protocol}
</div>
</div>
<div class="status" style="marign-top: 1em;">
${statusDotList}
</div>
<div class="ui divider"></div>
</div>`);
}
</script>

View File

@@ -0,0 +1,392 @@
<div class="standardContainer">
<div class="ui basic segment">
<h2>Utilities</h2>
<p>You might find these tools or information helpful when setting up your gateway server</p>
</div>
<div class="ui divider"></div>
<div class="selfauthOnly">
<h3>Account Management</h3>
<p>Functions to help management the current account</p>
<div class="ui basic segment">
<h5><i class="chevron down icon"></i> Change Password</h5>
<div class="ui form">
<div class="field">
<label>Current Password</label>
<input type="password" name="oldPassword" placeholder="Current Password">
</div>
<div class="field">
<label>New Password</label>
<input type="password" name="newPassword" placeholder="New Password">
</div>
<div class="field">
<label>Confirm New Password</label>
<input type="password" name="confirmNewPassword" placeholder="Confirm New Password">
</div>
<button class="ui basic button" onclick="changePassword()"><i class="ui teal key icon"></i> Change Password</button>
</div>
<div id="passwordChangeSuccMsg" class="ui green message" style="display:none;">
<i class="ui circle checkmark green icon "></i> Password Updated
</div>
</div>
<div class="ui divider"></div>
<h3>Forget Password Email</h3>
<p>The following SMTP settings help you to reset your password in case you have lost your management account.</p>
<form id="email-form" class="ui form">
<div class="field">
<div class="fields">
<div class="twelve wide field">
<label>SMTP Provider Hostname</label>
<input type="text" name="hostname" placeholder="E.g. mail.gandi.net">
</div>
<div class="four wide field">
<label>Port</label>
<input type="number" name="port" placeholder="E.g. 587" value="587">
</div>
</div>
</div>
<div class="field">
<label>Sender Domain</label>
<input type="text" name="domain" min="1" max="65534" placeholder="E.g. arozos.com">
</div>
<div class="field">
<label>Sender Address</label>
<input type="text" name="senderAddr" placeholder="E.g. admin@zoraxy.arozos.com">
</div>
<div class="field">
<div class="two fields">
<div class="field">
<label>Sender Username</label>
<input type="text" name="username" placeholder="Username of the email account">
</div>
<div class="field">
<label>Sender Password</label>
<input type="password" name="password" placeholder="Password of the email account">
<small>Leave empty to use the old password</small>
</div>
</div>
</div>
<div class="ui divider"></div>
<p> Email for sending account reset link</p>
<div class="field">
<label>Admin Address</label>
<input type="text" name="recvAddr" placeholder="E.g. personalEmail@gmail.com">
</div>
<button class="ui basic button" type="submit"><i class="blue save icon"></i> Set SMTP Configs</button>
<button class="ui basic button" onclick="event.preventDefault(); sendTestEmail(this);"><i class="teal mail icon"></i> Send Test Email</button>
</form>
</div>
<h3> IP Address to CIDR</h3>
<p>No experience with CIDR notations? Here are some tools you can use to make setting up easier.</p>
<div class="ui basic segment">
<h5><i class="chevron down icon"></i> IP Range to CIDR Conversion</h5>
<div class="ui message">
<i class="info circle icon"></i> Note that the CIDR generated here covers additional IP address before or after the given range. If you need more details settings, please use CIDR with a smaller range and add additional IPs for detail range adjustment.
</div>
<div class="ui input">
<input type="text" placeholder="Start IP" id="startIpInput">
</div>
<div class="ui input">
<input type="text" placeholder="End IP" id="endIpInput">
</div>
<br>
<button style="margin-top: 0.6em;" class="ui basic button" onclick="convertToCIDR()">Convert</button>
<p>Results: <div id="cidrOutput">N/A</div></p>
</div>
<div class="ui basic segment">
<h5><i class="chevron down icon"></i> CIDR to IP Range Conversion</h5>
<div class="ui action input">
<input type="text" placeholder="CIDR" id="cidrInput">
<button class="ui basic button" onclick="convertToIPRange()">Convert</button>
</div>
<p>Results: <div id="ipRangeOutput">N/A</div></p>
</div>
</div>
<script>
/*
Account Password utilities
*/
$.get("/api/auth/userCount", function(data){
if (data == 0){
//Using external auth manager. Hide options
$(".selfauthOnly").hide();
}
})
function changePassword() {
const oldPassword = document.getElementsByName('oldPassword')[0].value;
const newPassword = document.getElementsByName('newPassword')[0].value;
const confirmNewPassword = document.getElementsByName('confirmNewPassword')[0].value;
$.ajax({
type: "POST",
url: "/api/auth/changePassword",
data: {
oldPassword: oldPassword,
newPassword: newPassword,
confirmPassword: confirmNewPassword,
},
success: function (data) {
if (data.error != undefined){
alert(data.error);
}else{
$("#passwordChangeSuccMsg").stop().finish().slideDown("fast").delay(3000).slideUp("fast");
$('[name="oldPassword"]').val('');
$('[name="newPassword"]').val('');
$('[name="confirmNewPassword"]').val('');
}
},
error: function (xhr, status, error) {
alert("Error changing password: " + error);
},
});
}
/*
SMTP Settings
*/
//Bind events to the form
$('#email-form').submit(function(e) {
e.preventDefault();
var data = {
hostname: $('input[name=hostname]').val(),
domain: $('input[name=domain]').val(),
port: parseInt($('input[name=port]').val()),
username: $('input[name=username]').val(),
password: $('input[name=password]').val(),
senderAddr: $('input[name=senderAddr]').val(),
adminAddr: $('input[name=recvAddr]').val()
};
var inputValid = validateSMTPInputs();
if (!inputValid){
msgbox("SMTP input not valid", false, 5000);
return;
}
$.ajax({
type: "POST",
url: "/api/tools/smtp/set",
data: data,
success: function(data) {
if (data.error != undefined){
msgbox(data.error, false, 5000);
}else{
msgbox("SMTP Account Updated")
}
},
error: function(xhr, status, error) {
msgbox(xhr.responseText, false, 5000);
}
});
});
function initSMTPSettings(){
$.get("/api/tools/smtp/get", function(data){
$('#email-form input[name=hostname]').val(data.Hostname);
$('#email-form input[name=domain]').val(data.Domain);
$('#email-form input[name=port]').val(data.Port);
$('#email-form input[name=username]').val(data.Username);
$('#email-form input[name=senderAddr]').val(data.SenderAddr);
});
$.get("/api/tools/smtp/admin", function(data){
$('#email-form input[name=recvAddr]').val(data);
});
}
initSMTPSettings();
function sendTestEmail(btn){
$(btn).addClass("loading").addClass("disabled");
$.get("/api/tools/smtp/test", function(data){
if (data.error !== undefined){
msgbox(data.error, false, 5000);
}else{
msgbox("Test Email Sent")
}
$(btn).removeClass("loading").removeClass("disabled");
})
}
function validateSMTPInputs() {
let isValid = true;
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; // email regex pattern
const domainRegex = /^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/i; // domain/subdomain regex pattern
const form = $('#email-form');
// validate hostname
const hostname = form.find('input[name="hostname"]').val().trim();
if (!domainRegex.test(hostname)) {
form.find('input[name="hostname"]').parent().addClass('error');
isValid = false;
} else {
form.find('input[name="hostname"]').parent().removeClass('error');
}
// validate domain
const domain = form.find('input[name="domain"]').val().trim();
if (!domainRegex.test(domain)) {
form.find('input[name="domain"]').parent().addClass('error');
isValid = false;
} else {
form.find('input[name="domain"]').parent().removeClass('error');
}
// validate username
const username = form.find('input[name="username"]').val().trim();
if (username === '') {
form.find('input[name="username"]').parent().addClass('error');
isValid = false;
} else {
form.find('input[name="username"]').parent().removeClass('error');
}
// validate password
const password = form.find('input[name="password"]').val().trim();
if (password === '') {
form.find('input[name="password"]').parent().addClass('error');
isValid = false;
} else {
form.find('input[name="password"]').parent().removeClass('error');
}
// validate sender address
const senderAddr = form.find('input[name="senderAddr"]').val().trim();
if (!emailRegex.test(senderAddr)) {
form.find('input[name="senderAddr"]').parent().addClass('error');
isValid = false;
} else {
form.find('input[name="senderAddr"]').parent().removeClass('error');
}
// validate receiver address
const recvAddr = form.find('input[name="recvAddr"]').val().trim();
if (!emailRegex.test(recvAddr)) {
form.find('input[name="recvAddr"]').parent().addClass('error');
isValid = false;
} else {
form.find('input[name="recvAddr"]').parent().removeClass('error');
}
return isValid;
}
/*
IP Address Utilities
*/
//events handler
function convertToCIDR() {
const startIp = document.getElementById('startIpInput').value.trim();
const endIp = document.getElementById('endIpInput').value.trim();
const cidrOutput = document.getElementById('cidrOutput');
const cidr = ipRangeToCIDR(startIp, endIp);
const ipRange = cidrToRange(cidr);
cidrOutput.innerHTML = `CIDR: ${cidr} <br> (Cover range: ${ipRange[0]} to ${ipRange[1]})`;
}
// CIDR to IP Range Conversion
function convertToIPRange() {
const cidr = document.getElementById('cidrInput').value.trim();
const ipRangeOutput = document.getElementById('ipRangeOutput');
const ipRange = cidrToRange(cidr);
ipRangeOutput.innerHTML = `Start IP: ${ipRange[0]}<br>End IP: ${ipRange[1]}`;
}
//Ip conversion function
function cidrToRange(cidr) {
var range = [2];
cidr = cidr.split('/');
var cidr_1 = parseInt(cidr[1])
range[0] = long2ip((ip2long(cidr[0])) & ((-1 << (32 - cidr_1))));
start = ip2long(range[0])
range[1] = long2ip( start + Math.pow(2, (32 - cidr_1)) - 1);
return range;
}
function ipRangeToCIDR(ipStart, ipEnd) {
var start = ip2long(ipStart);
var end = ip2long(ipEnd);
var cidr = 32;
while (start != end) {
start >>= 1;
end >>= 1;
cidr--;
}
return ipStart + '/' + cidr;
}
function ip2long (argIP) {
// discuss at: https://locutus.io/php/ip2long/
// original by: Waldo Malqui Silva (https://waldo.malqui.info)
// improved by: Victor
// revised by: fearphage (https://my.opera.com/fearphage/)
// revised by: Theriault (https://github.com/Theriault)
// estarget: es2015
// example 1: ip2long('192.0.34.166')
// returns 1: 3221234342
// example 2: ip2long('0.0xABCDEF')
// returns 2: 11259375
// example 3: ip2long('255.255.255.256')
// returns 3: false
let i = 0
// PHP allows decimal, octal, and hexadecimal IP components.
// PHP allows between 1 (e.g. 127) to 4 (e.g 127.0.0.1) components.
const pattern = new RegExp([
'^([1-9]\\d*|0[0-7]*|0x[\\da-f]+)',
'(?:\\.([1-9]\\d*|0[0-7]*|0x[\\da-f]+))?',
'(?:\\.([1-9]\\d*|0[0-7]*|0x[\\da-f]+))?',
'(?:\\.([1-9]\\d*|0[0-7]*|0x[\\da-f]+))?$'
].join(''), 'i')
argIP = argIP.match(pattern) // Verify argIP format.
if (!argIP) {
// Invalid format.
return false
}
// Reuse argIP variable for component counter.
argIP[0] = 0
for (i = 1; i < 5; i += 1) {
argIP[0] += !!((argIP[i] || '').length)
argIP[i] = parseInt(argIP[i]) || 0
}
// Continue to use argIP for overflow values.
// PHP does not allow any component to overflow.
argIP.push(256, 256, 256, 256)
// Recalculate overflow of last component supplied to make up for missing components.
argIP[4 + argIP[0]] *= Math.pow(256, 4 - argIP[0])
if (argIP[1] >= argIP[5] ||
argIP[2] >= argIP[6] ||
argIP[3] >= argIP[7] ||
argIP[4] >= argIP[8]) {
return false
}
return argIP[1] * (argIP[0] === 1 || 16777216) +
argIP[2] * (argIP[0] <= 2 || 65536) +
argIP[3] * (argIP[0] <= 3 || 256) +
argIP[4] * 1
}
function long2ip (ip) {
// discuss at: https://locutus.io/php/long2ip/
// original by: Waldo Malqui Silva (https://fayr.us/waldo/)
// example 1: long2ip( 3221234342 )
// returns 1: '192.0.34.166'
if (!isFinite(ip)) {
return false
}
return [ip >>> 24 & 0xFF, ip >>> 16 & 0xFF, ip >>> 8 & 0xFF, ip & 0xFF].join('.')
}
</script>

View File

@@ -0,0 +1,58 @@
<div class="standardContainer">
<div class="ui basic segment">
<h2>Virtual Directory</h2>
<p>A virtual directory is a consolidated view of multiple directories that provides a unified entry point for users to access disparate sources.</p>
</div>
<div style="width: 100%; overflow-x: auto; margin-bottom: 1em;">
<table class="ui celled sortable unstackable compact table">
<thead>
<tr>
<th>Virtual Directory</th>
<th>Proxy To</th>
<th class="no-sort">Remove</th>
</tr>
</thead>
<tbody id="vdirList">
<tr>
<td data-label=""><button class="ui circular mini red basic button"><i class="remove icon"></i> Remove Proxy</button></td>
</tr>
</tbody>
</table>
</div>
<button class="ui icon right floated basic button" onclick="listVdirs();"><i class="green refresh icon"></i> Refresh</button>
<br><br>
</div>
<script>
//Virtual directories functions
function listVdirs(){
$("#vdirList").html(``);
$.get("/api/proxy/list?type=vdir", function(data){
if (data.error !== undefined){
$("#vdirList").append(`<tr>
<td data-label="" colspan="3"><i class="remove icon"></i> ${data.error}</td>
</tr>`);
}else if (data.length == 0){
$("#vdirList").append(`<tr>
<td data-label="" colspan="3"><i class="checkmark icon"></i> No Virtual Directory Record</td>
</tr>`);
}else{
data.forEach(vdir => {
let tlsIcon = "";
if (vdir.RequireTLS){
tlsIcon = `<i title="TLS mode" class="lock icon"></i>`;
}
$("#vdirList").append(`<tr>
<td data-label="">${vdir.Root}</td>
<td data-label="">${vdir.Domain} ${tlsIcon}</td>
<td class="center aligned" data-label=""><button class="ui circular mini red basic icon button" onclick='deleteEndpoint("vdir","${vdir.Root}")'><i class="trash icon"></i></button></td>
</tr>`);
});
}
});
}
//Bind on tab switch events
tabSwitchEventBind["vdir"] = function(){
listVdirs();
}
</script>