mirror of
https://github.com/tobychui/zoraxy.git
synced 2025-08-06 21:28:30 +02:00
Added missing web folder
This commit is contained in:
714
src/web/components/blacklist.html
Normal file
714
src/web/components/blacklist.html
Normal 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>
|
288
src/web/components/cert.html
Normal file
288
src/web/components/cert.html
Normal 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
228
src/web/components/gan.html
Normal 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>
|
625
src/web/components/gandetails.html
Normal file
625
src/web/components/gandetails.html
Normal 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>
|
441
src/web/components/networktools.html
Normal file
441
src/web/components/networktools.html
Normal 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>
|
||||
|
||||
|
158
src/web/components/redirection.html
Normal file
158
src/web/components/redirection.html
Normal 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>
|
79
src/web/components/rproot.html
Normal file
79
src/web/components/rproot.html
Normal 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>
|
160
src/web/components/rules.html
Normal file
160
src/web/components/rules.html
Normal 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>
|
580
src/web/components/stats.html
Normal file
580
src/web/components/stats.html
Normal 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>
|
571
src/web/components/status.html
Normal file
571
src/web/components/status.html
Normal 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>
|
55
src/web/components/subd.html
Normal file
55
src/web/components/subd.html
Normal 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>
|
355
src/web/components/tcpprox.html
Normal file
355
src/web/components/tcpprox.html
Normal 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>
|
158
src/web/components/uptime.html
Normal file
158
src/web/components/uptime.html
Normal 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>
|
392
src/web/components/utils.html
Normal file
392
src/web/components/utils.html
Normal 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>
|
58
src/web/components/vdir.html
Normal file
58
src/web/components/vdir.html
Normal 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>
|
Reference in New Issue
Block a user