diff --git a/src/pentesting-web/h2c-smuggling.md b/src/pentesting-web/h2c-smuggling.md index 8685d2522..619ae3aca 100644 --- a/src/pentesting-web/h2c-smuggling.md +++ b/src/pentesting-web/h2c-smuggling.md @@ -6,19 +6,19 @@ #### HTTP2 Over Cleartext (H2C) -H2C, au **http2 over cleartext**, inatofautiana na kawaida ya muunganisho wa HTTP wa muda mfupi kwa kuboresha **muunganisho wa kawaida wa HTTP kuwa wa kudumu**. Muunganisho huu ulioimarishwa unatumia itifaki ya http2 ya binary kwa mawasiliano ya kuendelea, tofauti na asili ya ombi moja ya HTTP ya maandiko. +H2C, au **http2 juu ya cleartext**, inatofautiana na kawaida ya muunganisho wa HTTP wa muda mfupi kwa kuboresha **muunganisho wa kawaida wa HTTP kuwa wa kudumu**. Muunganisho huu ulioimarishwa unatumia itifaki ya http2 ya binary kwa mawasiliano ya kuendelea, tofauti na asili ya ombi moja ya HTTP ya plaintext. -Kiini cha tatizo la smuggling kinatokea na matumizi ya **reverse proxy**. Kawaida, reverse proxy inashughulikia na kupeleka maombi ya HTTP kwa backend, ikirejesha jibu la backend baada ya hapo. Hata hivyo, wakati kichwa cha `Connection: Upgrade` kinapokuwepo katika ombi la HTTP (ambalo mara nyingi huonekana na muunganisho wa websocket), **proxy inashikilia muunganisho wa kudumu** kati ya mteja na seva, ikirahisisha ubadilishanaji wa kuendelea unaohitajika na itifaki fulani. Kwa muunganisho wa H2C, kufuata RFC kunahitaji kuwepo kwa vichwa vitatu maalum: +Kiini cha tatizo la smuggling kinatokea na matumizi ya **reverse proxy**. Kawaida, reverse proxy inashughulikia na kupeleka maombi ya HTTP kwa backend, ikirejesha jibu la backend baada ya hapo. Hata hivyo, wakati kichwa cha `Connection: Upgrade` kinapokuwepo katika ombi la HTTP (ambalo mara nyingi huonekana na muunganisho wa websocket), **proxy inashikilia muunganisho wa kudumu** kati ya mteja na seva, ikirahisisha ubadilishanaji wa kuendelea unaohitajika na itifaki fulani. Kwa muunganisho wa H2C, kufuata RFC kunahitaji uwepo wa vichwa vitatu maalum: ``` Upgrade: h2c HTTP2-Settings: AAMAAABkAARAAAAAAAIAAAAA Connection: Upgrade, HTTP2-Settings ``` -Vulnerability inatokea wakati, baada ya kuboresha muunganisho, reverse proxy inacha kusimamia maombi binafsi, ikidhani kazi yake ya kuelekeza imekamilika baada ya kuanzishwa kwa muunganisho. Kutumia H2C Smuggling kunaruhusu kupita sheria za reverse proxy zinazotumika wakati wa usindikaji wa maombi, kama vile kuelekeza kwa msingi wa njia, uthibitishaji, na usindikaji wa WAF, ikidhani muunganisho wa H2C umeanzishwa kwa mafanikio. +Vulnerability inatokea wakati, baada ya kuboresha muunganisho, proxy ya nyuma inacha kusimamia maombi binafsi, ikidhani kazi yake ya kuelekeza imekamilika baada ya kuanzishwa kwa muunganisho. Kutumia H2C Smuggling kunaruhusu kupita sheria za proxy ya nyuma zinazotumika wakati wa usindikaji wa maombi, kama vile kuelekeza kwa msingi wa njia, uthibitishaji, na usindikaji wa WAF, ikidhani muunganisho wa H2C umeanzishwa kwa mafanikio. #### Proxies Zenye Uthibitisho -Uthibitisho unategemea jinsi reverse proxy inavyoshughulikia vichwa vya `Upgrade` na wakati mwingine `Connection`. Proxies zifuatazo kwa asili hupeleka vichwa hivi wakati wa proxy-pass, hivyo kwa asili zinawezesha H2C smuggling: +Uthibitisho unategemea jinsi proxy ya nyuma inavyoshughulikia vichwa vya `Upgrade` na wakati mwingine `Connection`. Proxies zifuatazo kwa asili hupeleka vichwa hivi wakati wa proxy-pass, hivyo kwa asili zinawezesha H2C smuggling: - HAProxy - Traefik @@ -37,47 +37,47 @@ Kwa upande mwingine, huduma hizi hazipeleki vichwa vyote viwili kwa asili wakati #### Utekelezaji -Ni muhimu kutambua kwamba si seva zote kwa asili hupeleka vichwa vinavyohitajika kwa kuboresha muunganisho wa H2C unaokubalika. Hivyo, seva kama AWS ALB/CLB, NGINX, na Apache Traffic Server, miongoni mwa zingine, kwa asili zinakataa muunganisho wa H2C. Hata hivyo, inafaa kujaribu na toleo lisilo la kawaida la `Connection: Upgrade`, ambalo linatenga thamani ya `HTTP2-Settings` kutoka kwa kichwa cha `Connection`, kwani baadhi ya backends zinaweza kutokubaliana na viwango. +Ni muhimu kutambua kwamba si seva zote kwa asili hupeleka vichwa vinavyohitajika kwa kuboresha muunganisho wa H2C unaokubalika. Hivyo, seva kama AWS ALB/CLB, NGINX, na Apache Traffic Server, miongoni mwa zingine, kwa asili zinakataa muunganisho wa H2C. Hata hivyo, inafaa kujaribu na toleo lisilo la kawaida la `Connection: Upgrade`, ambalo linatenga thamani ya `HTTP2-Settings` kutoka kwa kichwa cha `Connection`, kwani baadhi ya nyuma zinaweza kutokubaliana na viwango. > [!CAUTION] -> Bila kujali **njia** maalum iliyotolewa katika URL ya `proxy_pass` (kwa mfano, `http://backend:9999/socket.io`), muunganisho ulioanzishwa unarudi kwa `http://backend:9999`. Hii inaruhusu mwingiliano na njia yoyote ndani ya mwisho huo wa ndani, ikitumia mbinu hii. Kwa hivyo, ufafanuzi wa njia katika URL ya `proxy_pass` hauzuia ufikiaji. +> Bila kujali **njia** maalum iliyotolewa katika URL ya `proxy_pass` (mfano, `http://backend:9999/socket.io`), muunganisho ulioanzishwa unarudi kwa `http://backend:9999`. Hii inaruhusu mwingiliano na njia yoyote ndani ya mwisho huo wa ndani, ikitumia mbinu hii. Kwa hivyo, ufafanuzi wa njia katika URL ya `proxy_pass` hauzuia ufikiaji. -Vifaa [**h2csmuggler by BishopFox**](https://github.com/BishopFox/h2csmuggler) na [**h2csmuggler by assetnote**](https://github.com/assetnote/h2csmuggler) vinasaidia juhudi za **kupita ulinzi wa proxy** kwa kuanzisha muunganisho wa H2C, hivyo kuruhusu ufikiaji wa rasilimali zilizofichwa na proxy. +Zana [**h2csmuggler by BishopFox**](https://github.com/BishopFox/h2csmuggler) na [**h2csmuggler by assetnote**](https://github.com/assetnote/h2csmuggler) zinasaidia juhudi za **kupita ulinzi wa proxy** kwa kuanzisha muunganisho wa H2C, hivyo kuruhusu ufikiaji wa rasilimali zilizofichwa na proxy. Kwa maelezo zaidi kuhusu uthibitisho huu, hasa kuhusu NGINX, rejelea [**rasilimali hii ya kina**](../network-services-pentesting/pentesting-web/nginx.md#proxy_set_header-upgrade-and-connection). ## Websocket Smuggling -Websocket smuggling, tofauti na kuunda tunnel ya HTTP2 kwa mwisho unaopatikana kupitia proxy, inaweka tunnel ya Websocket ili kupita mipaka ya proxy na kuwezesha mawasiliano ya moja kwa moja na mwisho. +Websocket smuggling, tofauti na kuunda tunnel ya HTTP2 kwa mwisho unaopatikana kupitia proxy, inaanzisha tunnel ya Websocket ili kupita mipaka ya proxy na kuwezesha mawasiliano ya moja kwa moja na mwisho. ### Hali 1 -Katika hali hii, backend inayotoa API ya WebSocket ya umma pamoja na API ya REST ya ndani isiyopatikana inashambuliwa na mteja mbaya anayejaribu kupata ufikiaji wa API ya REST ya ndani. Shambulio linafanyika kwa hatua kadhaa: +Katika hali hii, nyuma inayotoa API ya WebSocket ya umma pamoja na API ya REST ya ndani isiyopatikana inashambuliwa na mteja mbaya anayejaribu kupata ufikiaji wa API ya REST ya ndani. Shambulio linafanyika kwa hatua kadhaa: -1. Mteja anaanza kwa kutuma ombi la Upgrade kwa reverse proxy na toleo la protokali isiyo sahihi ya `Sec-WebSocket-Version` katika kichwa. Proxy, ikishindwa kuthibitisha kichwa cha `Sec-WebSocket-Version`, inadhani ombi la Upgrade ni halali na linaelekeza kwa backend. -2. Backend inajibu kwa msimbo wa hali `426`, ikionyesha toleo la protokali isiyo sahihi katika kichwa cha `Sec-WebSocket-Version`. Reverse proxy, ikipuuza hali ya majibu ya backend, inadhani iko tayari kwa mawasiliano ya WebSocket na inapeleka majibu kwa mteja. -3. Kwa hivyo, reverse proxy inadanganywa kuamini kuwa muunganisho wa WebSocket umeanzishwa kati ya mteja na backend, wakati kwa kweli, backend ilikuwa imekataa ombi la Upgrade. Licha ya hili, proxy inashikilia muunganisho wa TCP au TLS wazi kati ya mteja na backend, ikiruhusu mteja kupata ufikiaji usio na vizuizi wa API ya REST ya kibinafsi kupitia muunganisho huu. +1. Mteja anaanzisha kwa kutuma ombi la Upgrade kwa proxy ya nyuma na toleo la protokali isiyo sahihi ya `Sec-WebSocket-Version` katika kichwa. Proxy, ikishindwa kuthibitisha kichwa cha `Sec-WebSocket-Version`, inadhani ombi la Upgrade ni sahihi na linaelekeza kwa nyuma. +2. Nyuma inajibu kwa msimbo wa hali `426`, ikionyesha toleo la protokali isiyo sahihi katika kichwa cha `Sec-WebSocket-Version`. Proxy ya nyuma, ikipuuza hali ya jibu la nyuma, inadhani iko tayari kwa mawasiliano ya WebSocket na inapeleka jibu kwa mteja. +3. Kwa hivyo, proxy ya nyuma inadanganywa kuamini kuwa muunganisho wa WebSocket umeanzishwa kati ya mteja na nyuma, wakati kwa kweli, nyuma ilikuwa imekataa ombi la Upgrade. Licha ya hili, proxy inashikilia muunganisho wa TCP au TLS wazi kati ya mteja na nyuma, ikiruhusu mteja kupata ufikiaji usio na vizuizi wa API ya REST ya kibinafsi kupitia muunganisho huu. -Proxies za reverse zilizoathirika ni pamoja na Varnish, ambayo ilikataa kushughulikia tatizo, na toleo la proxy la Envoy 1.8.0 au la zamani, huku matoleo ya baadaye yakiwa yamebadilisha mekanismu ya kuboresha. Proxies zingine pia zinaweza kuwa hatarini. +Proxies za nyuma zilizoathirika ni pamoja na Varnish, ambayo ilikataa kushughulikia tatizo, na toleo la proxy ya Envoy 1.8.0 au zamani, huku toleo za baadaye zikibadilisha mekanismu ya kuboresha. Proxies zingine pia zinaweza kuwa hatarini. ![https://github.com/0ang3el/websocket-smuggle/raw/master/img/2-4.png](https://github.com/0ang3el/websocket-smuggle/raw/master/img/2-4.png) ### Hali 2 -Hali hii inahusisha backend yenye API ya WebSocket ya umma na API ya REST ya umma kwa ajili ya kuangalia afya, pamoja na API ya REST ya ndani isiyopatikana. Shambulio, lililo ngumu zaidi, linajumuisha hatua zifuatazo: +Hali hii inahusisha nyuma yenye API ya WebSocket ya umma na API ya REST ya umma kwa ajili ya kuangalia afya, pamoja na API ya REST ya ndani isiyopatikana. Shambulio, ambalo ni gumu zaidi, linajumuisha hatua zifuatazo: -1. Mteja anatumia ombi la POST kuanzisha API ya kuangalia afya, akijumuisha kichwa cha ziada cha HTTP `Upgrade: websocket`. NGINX, ikihudumu kama reverse proxy, inachukulia hii kama ombi la Upgrade la kawaida kulingana tu na kichwa cha `Upgrade`, ikipuuza vipengele vingine vya ombi, na linaelekeza kwa backend. -2. Backend inatekeleza API ya kuangalia afya, ikifikia rasilimali ya nje inayodhibitiwa na mshambuliaji ambayo inarudisha majibu ya HTTP yenye msimbo wa hali `101`. Majibu haya, yanapopokelewa na backend na kupelekwa kwa NGINX, yanadanganya proxy kuamini kuwa muunganisho wa WebSocket umeanzishwa kutokana na uthibitisho wake wa msimbo wa hali pekee. +1. Mteja anatumia ombi la POST kuanzisha API ya kuangalia afya, akijumuisha kichwa cha ziada cha HTTP `Upgrade: websocket`. NGINX, ikihudumu kama proxy ya nyuma, inachukulia hii kama ombi la kawaida la Upgrade kulingana tu na kichwa cha `Upgrade`, ikipuuza vipengele vingine vya ombi, na linaelekeza kwa nyuma. +2. Nyuma inatekeleza API ya kuangalia afya, ikifikia rasilimali ya nje inayodhibitiwa na mshambuliaji ambayo inarudisha jibu la HTTP lenye msimbo wa hali `101`. Jibu hili, likipokelewa na nyuma na kupelekwa kwa NGINX, linadanganya proxy kuamini kuwa muunganisho wa WebSocket umeanzishwa kwa sababu ya kuthibitisha kwake tu msimbo wa hali. ![https://github.com/0ang3el/websocket-smuggle/raw/master/img/3-4.png](https://github.com/0ang3el/websocket-smuggle/raw/master/img/3-4.png) > **Warning:** Ugumu wa mbinu hii unakua kadri inavyohitaji uwezo wa kuingiliana na mwisho unaoweza kurudisha msimbo wa hali 101. -Hatimaye, NGINX inadanganywa kuamini kuwa muunganisho wa WebSocket upo kati ya mteja na backend. Kwa kweli, hakuna muunganisho kama huo; API ya kuangalia afya ilikuwa lengo. Hata hivyo, reverse proxy inashikilia muunganisho wazi, ikiruhusu mteja kupata ufikiaji wa API ya REST ya kibinafsi kupitia hiyo. +Hatimaye, NGINX inadanganywa kuamini kuwa muunganisho wa WebSocket upo kati ya mteja na nyuma. Kwa kweli, hakuna muunganisho kama huo; API ya kuangalia afya ilikuwa lengo. Hata hivyo, proxy ya nyuma inashikilia muunganisho wazi, ikiruhusu mteja kupata ufikiaji wa API ya REST ya kibinafsi kupitia hiyo. ![https://github.com/0ang3el/websocket-smuggle/raw/master/img/3-5.png](https://github.com/0ang3el/websocket-smuggle/raw/master/img/3-5.png) -Proxies nyingi za reverse zina hatari katika hali hii, lakini utekelezaji unategemea uwepo wa udhaifu wa SSRF wa nje, ambao kwa kawaida unachukuliwa kama tatizo la chini ya kiwango. +Proxies nyingi za nyuma zina hatari katika hali hii, lakini utekelezaji unategemea uwepo wa udhaifu wa SSRF wa nje, ambao kwa kawaida unachukuliwa kama tatizo la chini ya kiwango. #### Maabara diff --git a/theme/elasticlunr.min.js b/theme/elasticlunr.min.js new file mode 100644 index 000000000..32cb1bce1 --- /dev/null +++ b/theme/elasticlunr.min.js @@ -0,0 +1,10 @@ +/** + * elasticlunr - http://weixsong.github.io + * Lightweight full-text search engine in Javascript for browser search and offline search. - 0.9.5 + * + * Copyright (C) 2016 Oliver Nightingale + * Copyright (C) 2016 Wei Song + * MIT Licensed + * @license + */ +!function(){function e(e){if(null===e||"object"!=typeof e)return e;var t=e.constructor();for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n]);return t}var t=function(e){var n=new t.Index;return n.pipeline.add(t.trimmer,t.stopWordFilter,t.stemmer),e&&e.call(n,n),n};t.version="0.9.5",lunr=t,t.utils={},t.utils.warn=function(e){return function(t){e.console&&console.warn&&console.warn(t)}}(this),t.utils.toString=function(e){return void 0===e||null===e?"":e.toString()},t.EventEmitter=function(){this.events={}},t.EventEmitter.prototype.addListener=function(){var e=Array.prototype.slice.call(arguments),t=e.pop(),n=e;if("function"!=typeof t)throw new TypeError("last argument must be a function");n.forEach(function(e){this.hasHandler(e)||(this.events[e]=[]),this.events[e].push(t)},this)},t.EventEmitter.prototype.removeListener=function(e,t){if(this.hasHandler(e)){var n=this.events[e].indexOf(t);-1!==n&&(this.events[e].splice(n,1),0==this.events[e].length&&delete this.events[e])}},t.EventEmitter.prototype.emit=function(e){if(this.hasHandler(e)){var t=Array.prototype.slice.call(arguments,1);this.events[e].forEach(function(e){e.apply(void 0,t)},this)}},t.EventEmitter.prototype.hasHandler=function(e){return e in this.events},t.tokenizer=function(e){if(!arguments.length||null===e||void 0===e)return[];if(Array.isArray(e)){var n=e.filter(function(e){return null===e||void 0===e?!1:!0});n=n.map(function(e){return t.utils.toString(e).toLowerCase()});var i=[];return n.forEach(function(e){var n=e.split(t.tokenizer.seperator);i=i.concat(n)},this),i}return e.toString().trim().toLowerCase().split(t.tokenizer.seperator)},t.tokenizer.defaultSeperator=/[\s\-]+/,t.tokenizer.seperator=t.tokenizer.defaultSeperator,t.tokenizer.setSeperator=function(e){null!==e&&void 0!==e&&"object"==typeof e&&(t.tokenizer.seperator=e)},t.tokenizer.resetSeperator=function(){t.tokenizer.seperator=t.tokenizer.defaultSeperator},t.tokenizer.getSeperator=function(){return t.tokenizer.seperator},t.Pipeline=function(){this._queue=[]},t.Pipeline.registeredFunctions={},t.Pipeline.registerFunction=function(e,n){n in t.Pipeline.registeredFunctions&&t.utils.warn("Overwriting existing registered function: "+n),e.label=n,t.Pipeline.registeredFunctions[n]=e},t.Pipeline.getRegisteredFunction=function(e){return e in t.Pipeline.registeredFunctions!=!0?null:t.Pipeline.registeredFunctions[e]},t.Pipeline.warnIfFunctionNotRegistered=function(e){var n=e.label&&e.label in this.registeredFunctions;n||t.utils.warn("Function is not registered with pipeline. This may cause problems when serialising the index.\n",e)},t.Pipeline.load=function(e){var n=new t.Pipeline;return e.forEach(function(e){var i=t.Pipeline.getRegisteredFunction(e);if(!i)throw new Error("Cannot load un-registered function: "+e);n.add(i)}),n},t.Pipeline.prototype.add=function(){var e=Array.prototype.slice.call(arguments);e.forEach(function(e){t.Pipeline.warnIfFunctionNotRegistered(e),this._queue.push(e)},this)},t.Pipeline.prototype.after=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var i=this._queue.indexOf(e);if(-1===i)throw new Error("Cannot find existingFn");this._queue.splice(i+1,0,n)},t.Pipeline.prototype.before=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var i=this._queue.indexOf(e);if(-1===i)throw new Error("Cannot find existingFn");this._queue.splice(i,0,n)},t.Pipeline.prototype.remove=function(e){var t=this._queue.indexOf(e);-1!==t&&this._queue.splice(t,1)},t.Pipeline.prototype.run=function(e){for(var t=[],n=e.length,i=this._queue.length,o=0;n>o;o++){for(var r=e[o],s=0;i>s&&(r=this._queue[s](r,o,e),void 0!==r&&null!==r);s++);void 0!==r&&null!==r&&t.push(r)}return t},t.Pipeline.prototype.reset=function(){this._queue=[]},t.Pipeline.prototype.get=function(){return this._queue},t.Pipeline.prototype.toJSON=function(){return this._queue.map(function(e){return t.Pipeline.warnIfFunctionNotRegistered(e),e.label})},t.Index=function(){this._fields=[],this._ref="id",this.pipeline=new t.Pipeline,this.documentStore=new t.DocumentStore,this.index={},this.eventEmitter=new t.EventEmitter,this._idfCache={},this.on("add","remove","update",function(){this._idfCache={}}.bind(this))},t.Index.prototype.on=function(){var e=Array.prototype.slice.call(arguments);return this.eventEmitter.addListener.apply(this.eventEmitter,e)},t.Index.prototype.off=function(e,t){return this.eventEmitter.removeListener(e,t)},t.Index.load=function(e){e.version!==t.version&&t.utils.warn("version mismatch: current "+t.version+" importing "+e.version);var n=new this;n._fields=e.fields,n._ref=e.ref,n.documentStore=t.DocumentStore.load(e.documentStore),n.pipeline=t.Pipeline.load(e.pipeline),n.index={};for(var i in e.index)n.index[i]=t.InvertedIndex.load(e.index[i]);return n},t.Index.prototype.addField=function(e){return this._fields.push(e),this.index[e]=new t.InvertedIndex,this},t.Index.prototype.setRef=function(e){return this._ref=e,this},t.Index.prototype.saveDocument=function(e){return this.documentStore=new t.DocumentStore(e),this},t.Index.prototype.addDoc=function(e,n){if(e){var n=void 0===n?!0:n,i=e[this._ref];this.documentStore.addDoc(i,e),this._fields.forEach(function(n){var o=this.pipeline.run(t.tokenizer(e[n]));this.documentStore.addFieldLength(i,n,o.length);var r={};o.forEach(function(e){e in r?r[e]+=1:r[e]=1},this);for(var s in r){var u=r[s];u=Math.sqrt(u),this.index[n].addToken(s,{ref:i,tf:u})}},this),n&&this.eventEmitter.emit("add",e,this)}},t.Index.prototype.removeDocByRef=function(e){if(e&&this.documentStore.isDocStored()!==!1&&this.documentStore.hasDoc(e)){var t=this.documentStore.getDoc(e);this.removeDoc(t,!1)}},t.Index.prototype.removeDoc=function(e,n){if(e){var n=void 0===n?!0:n,i=e[this._ref];this.documentStore.hasDoc(i)&&(this.documentStore.removeDoc(i),this._fields.forEach(function(n){var o=this.pipeline.run(t.tokenizer(e[n]));o.forEach(function(e){this.index[n].removeToken(e,i)},this)},this),n&&this.eventEmitter.emit("remove",e,this))}},t.Index.prototype.updateDoc=function(e,t){var t=void 0===t?!0:t;this.removeDocByRef(e[this._ref],!1),this.addDoc(e,!1),t&&this.eventEmitter.emit("update",e,this)},t.Index.prototype.idf=function(e,t){var n="@"+t+"/"+e;if(Object.prototype.hasOwnProperty.call(this._idfCache,n))return this._idfCache[n];var i=this.index[t].getDocFreq(e),o=1+Math.log(this.documentStore.length/(i+1));return this._idfCache[n]=o,o},t.Index.prototype.getFields=function(){return this._fields.slice()},t.Index.prototype.search=function(e,n){if(!e)return[];var i=null;null!=n&&(i=JSON.stringify(n));var o=new t.Configuration(i,this.getFields()).get(),r=this.pipeline.run(t.tokenizer(e)),s={};for(var u in o){var a=this.fieldSearch(r,u,o),l=o[u].boost;for(var d in a)a[d]=a[d]*l;for(var d in a)d in s?s[d]+=a[d]:s[d]=a[d]}var c=[];for(var d in s)c.push({ref:d,score:s[d]});return c.sort(function(e,t){return t.score-e.score}),c},t.Index.prototype.fieldSearch=function(e,t,n){var i=n[t].bool,o=n[t].expand,r=n[t].boost,s=null,u={};return 0!==r?(e.forEach(function(e){var n=[e];1==o&&(n=this.index[t].expandToken(e));var r={};n.forEach(function(n){var o=this.index[t].getDocs(n),a=this.idf(n,t);if(s&&"AND"==i){var l={};for(var d in s)d in o&&(l[d]=o[d]);o=l}n==e&&this.fieldSearchStats(u,n,o);for(var d in o){var c=this.index[t].getTermFrequency(n,d),f=this.documentStore.getFieldLength(d,t),h=1;0!=f&&(h=1/Math.sqrt(f));var p=1;n!=e&&(p=.15*(1-(n.length-e.length)/n.length));var v=c*a*h*p;d in r?r[d]+=v:r[d]=v}},this),s=this.mergeScores(s,r,i)},this),s=this.coordNorm(s,u,e.length)):void 0},t.Index.prototype.mergeScores=function(e,t,n){if(!e)return t;if("AND"==n){var i={};for(var o in t)o in e&&(i[o]=e[o]+t[o]);return i}for(var o in t)o in e?e[o]+=t[o]:e[o]=t[o];return e},t.Index.prototype.fieldSearchStats=function(e,t,n){for(var i in n)i in e?e[i].push(t):e[i]=[t]},t.Index.prototype.coordNorm=function(e,t,n){for(var i in e)if(i in t){var o=t[i].length;e[i]=e[i]*o/n}return e},t.Index.prototype.toJSON=function(){var e={};return this._fields.forEach(function(t){e[t]=this.index[t].toJSON()},this),{version:t.version,fields:this._fields,ref:this._ref,documentStore:this.documentStore.toJSON(),index:e,pipeline:this.pipeline.toJSON()}},t.Index.prototype.use=function(e){var t=Array.prototype.slice.call(arguments,1);t.unshift(this),e.apply(this,t)},t.DocumentStore=function(e){this._save=null===e||void 0===e?!0:e,this.docs={},this.docInfo={},this.length=0},t.DocumentStore.load=function(e){var t=new this;return t.length=e.length,t.docs=e.docs,t.docInfo=e.docInfo,t._save=e.save,t},t.DocumentStore.prototype.isDocStored=function(){return this._save},t.DocumentStore.prototype.addDoc=function(t,n){this.hasDoc(t)||this.length++,this.docs[t]=this._save===!0?e(n):null},t.DocumentStore.prototype.getDoc=function(e){return this.hasDoc(e)===!1?null:this.docs[e]},t.DocumentStore.prototype.hasDoc=function(e){return e in this.docs},t.DocumentStore.prototype.removeDoc=function(e){this.hasDoc(e)&&(delete this.docs[e],delete this.docInfo[e],this.length--)},t.DocumentStore.prototype.addFieldLength=function(e,t,n){null!==e&&void 0!==e&&0!=this.hasDoc(e)&&(this.docInfo[e]||(this.docInfo[e]={}),this.docInfo[e][t]=n)},t.DocumentStore.prototype.updateFieldLength=function(e,t,n){null!==e&&void 0!==e&&0!=this.hasDoc(e)&&this.addFieldLength(e,t,n)},t.DocumentStore.prototype.getFieldLength=function(e,t){return null===e||void 0===e?0:e in this.docs&&t in this.docInfo[e]?this.docInfo[e][t]:0},t.DocumentStore.prototype.toJSON=function(){return{docs:this.docs,docInfo:this.docInfo,length:this.length,save:this._save}},t.stemmer=function(){var e={ational:"ate",tional:"tion",enci:"ence",anci:"ance",izer:"ize",bli:"ble",alli:"al",entli:"ent",eli:"e",ousli:"ous",ization:"ize",ation:"ate",ator:"ate",alism:"al",iveness:"ive",fulness:"ful",ousness:"ous",aliti:"al",iviti:"ive",biliti:"ble",logi:"log"},t={icate:"ic",ative:"",alize:"al",iciti:"ic",ical:"ic",ful:"",ness:""},n="[^aeiou]",i="[aeiouy]",o=n+"[^aeiouy]*",r=i+"[aeiou]*",s="^("+o+")?"+r+o,u="^("+o+")?"+r+o+"("+r+")?$",a="^("+o+")?"+r+o+r+o,l="^("+o+")?"+i,d=new RegExp(s),c=new RegExp(a),f=new RegExp(u),h=new RegExp(l),p=/^(.+?)(ss|i)es$/,v=/^(.+?)([^s])s$/,g=/^(.+?)eed$/,m=/^(.+?)(ed|ing)$/,y=/.$/,S=/(at|bl|iz)$/,x=new RegExp("([^aeiouylsz])\\1$"),w=new RegExp("^"+o+i+"[^aeiouwxy]$"),I=/^(.+?[^aeiou])y$/,b=/^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/,E=/^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/,D=/^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/,F=/^(.+?)(s|t)(ion)$/,_=/^(.+?)e$/,P=/ll$/,k=new RegExp("^"+o+i+"[^aeiouwxy]$"),z=function(n){var i,o,r,s,u,a,l;if(n.length<3)return n;if(r=n.substr(0,1),"y"==r&&(n=r.toUpperCase()+n.substr(1)),s=p,u=v,s.test(n)?n=n.replace(s,"$1$2"):u.test(n)&&(n=n.replace(u,"$1$2")),s=g,u=m,s.test(n)){var z=s.exec(n);s=d,s.test(z[1])&&(s=y,n=n.replace(s,""))}else if(u.test(n)){var z=u.exec(n);i=z[1],u=h,u.test(i)&&(n=i,u=S,a=x,l=w,u.test(n)?n+="e":a.test(n)?(s=y,n=n.replace(s,"")):l.test(n)&&(n+="e"))}if(s=I,s.test(n)){var z=s.exec(n);i=z[1],n=i+"i"}if(s=b,s.test(n)){var z=s.exec(n);i=z[1],o=z[2],s=d,s.test(i)&&(n=i+e[o])}if(s=E,s.test(n)){var z=s.exec(n);i=z[1],o=z[2],s=d,s.test(i)&&(n=i+t[o])}if(s=D,u=F,s.test(n)){var z=s.exec(n);i=z[1],s=c,s.test(i)&&(n=i)}else if(u.test(n)){var z=u.exec(n);i=z[1]+z[2],u=c,u.test(i)&&(n=i)}if(s=_,s.test(n)){var z=s.exec(n);i=z[1],s=c,u=f,a=k,(s.test(i)||u.test(i)&&!a.test(i))&&(n=i)}return s=P,u=c,s.test(n)&&u.test(n)&&(s=y,n=n.replace(s,"")),"y"==r&&(n=r.toLowerCase()+n.substr(1)),n};return z}(),t.Pipeline.registerFunction(t.stemmer,"stemmer"),t.stopWordFilter=function(e){return e&&t.stopWordFilter.stopWords[e]!==!0?e:void 0},t.clearStopWords=function(){t.stopWordFilter.stopWords={}},t.addStopWords=function(e){null!=e&&Array.isArray(e)!==!1&&e.forEach(function(e){t.stopWordFilter.stopWords[e]=!0},this)},t.resetStopWords=function(){t.stopWordFilter.stopWords=t.defaultStopWords},t.defaultStopWords={"":!0,a:!0,able:!0,about:!0,across:!0,after:!0,all:!0,almost:!0,also:!0,am:!0,among:!0,an:!0,and:!0,any:!0,are:!0,as:!0,at:!0,be:!0,because:!0,been:!0,but:!0,by:!0,can:!0,cannot:!0,could:!0,dear:!0,did:!0,"do":!0,does:!0,either:!0,"else":!0,ever:!0,every:!0,"for":!0,from:!0,get:!0,got:!0,had:!0,has:!0,have:!0,he:!0,her:!0,hers:!0,him:!0,his:!0,how:!0,however:!0,i:!0,"if":!0,"in":!0,into:!0,is:!0,it:!0,its:!0,just:!0,least:!0,let:!0,like:!0,likely:!0,may:!0,me:!0,might:!0,most:!0,must:!0,my:!0,neither:!0,no:!0,nor:!0,not:!0,of:!0,off:!0,often:!0,on:!0,only:!0,or:!0,other:!0,our:!0,own:!0,rather:!0,said:!0,say:!0,says:!0,she:!0,should:!0,since:!0,so:!0,some:!0,than:!0,that:!0,the:!0,their:!0,them:!0,then:!0,there:!0,these:!0,they:!0,"this":!0,tis:!0,to:!0,too:!0,twas:!0,us:!0,wants:!0,was:!0,we:!0,were:!0,what:!0,when:!0,where:!0,which:!0,"while":!0,who:!0,whom:!0,why:!0,will:!0,"with":!0,would:!0,yet:!0,you:!0,your:!0},t.stopWordFilter.stopWords=t.defaultStopWords,t.Pipeline.registerFunction(t.stopWordFilter,"stopWordFilter"),t.trimmer=function(e){if(null===e||void 0===e)throw new Error("token should not be undefined");return e.replace(/^\W+/,"").replace(/\W+$/,"")},t.Pipeline.registerFunction(t.trimmer,"trimmer"),t.InvertedIndex=function(){this.root={docs:{},df:0}},t.InvertedIndex.load=function(e){var t=new this;return t.root=e.root,t},t.InvertedIndex.prototype.addToken=function(e,t,n){for(var n=n||this.root,i=0;i<=e.length-1;){var o=e[i];o in n||(n[o]={docs:{},df:0}),i+=1,n=n[o]}var r=t.ref;n.docs[r]?n.docs[r]={tf:t.tf}:(n.docs[r]={tf:t.tf},n.df+=1)},t.InvertedIndex.prototype.hasToken=function(e){if(!e)return!1;for(var t=this.root,n=0;n0&&t.push(e);for(var i in n)"docs"!==i&&"df"!==i&&this.expandToken(e+i,t,n[i]);return t},t.InvertedIndex.prototype.toJSON=function(){return{root:this.root}},t.Configuration=function(e,n){var e=e||"";if(void 0==n||null==n)throw new Error("fields should not be null");this.config={};var i;try{i=JSON.parse(e),this.buildUserConfig(i,n)}catch(o){t.utils.warn("user configuration parse failed, will use default configuration"),this.buildDefaultConfig(n)}},t.Configuration.prototype.buildDefaultConfig=function(e){this.reset(),e.forEach(function(e){this.config[e]={boost:1,bool:"OR",expand:!1}},this)},t.Configuration.prototype.buildUserConfig=function(e,n){var i="OR",o=!1;if(this.reset(),"bool"in e&&(i=e.bool||i),"expand"in e&&(o=e.expand||o),"fields"in e)for(var r in e.fields)if(n.indexOf(r)>-1){var s=e.fields[r],u=o;void 0!=s.expand&&(u=s.expand),this.config[r]={boost:s.boost||0===s.boost?s.boost:1,bool:s.bool||i,expand:u}}else t.utils.warn("field name in user configuration not found in index instance fields");else this.addAllFields2UserConfig(i,o,n)},t.Configuration.prototype.addAllFields2UserConfig=function(e,t,n){n.forEach(function(n){this.config[n]={boost:1,bool:e,expand:t}},this)},t.Configuration.prototype.get=function(){return this.config},t.Configuration.prototype.reset=function(){this.config={}},lunr.SortedSet=function(){this.length=0,this.elements=[]},lunr.SortedSet.load=function(e){var t=new this;return t.elements=e,t.length=e.length,t},lunr.SortedSet.prototype.add=function(){var e,t;for(e=0;e1;){if(r===e)return o;e>r&&(t=o),r>e&&(n=o),i=n-t,o=t+Math.floor(i/2),r=this.elements[o]}return r===e?o:-1},lunr.SortedSet.prototype.locationFor=function(e){for(var t=0,n=this.elements.length,i=n-t,o=t+Math.floor(i/2),r=this.elements[o];i>1;)e>r&&(t=o),r>e&&(n=o),i=n-t,o=t+Math.floor(i/2),r=this.elements[o];return r>e?o:e>r?o+1:void 0},lunr.SortedSet.prototype.intersect=function(e){for(var t=new lunr.SortedSet,n=0,i=0,o=this.length,r=e.length,s=this.elements,u=e.elements;;){if(n>o-1||i>r-1)break;s[n]!==u[i]?s[n]u[i]&&i++:(t.add(s[n]),n++,i++)}return t},lunr.SortedSet.prototype.clone=function(){var e=new lunr.SortedSet;return e.elements=this.toArray(),e.length=e.elements.length,e},lunr.SortedSet.prototype.union=function(e){var t,n,i;this.length>=e.length?(t=this,n=e):(t=e,n=this),i=t.clone();for(var o=0,r=n.toArray();o { + const WRAPPER = document.getElementById('search-wrapper'); + const TOGGLE = document.getElementById('search-toggle'); + const INPUT = document.getElementById('searchbar'); + const LIST = document.getElementById('searchresults'); + const HOTKEY = 83; // “s” + let worker, debounce; -/* ──────────────────────────────────────────────────────────────── - search.js - ─────────────────────────────────────────────────────────────── */ - -"use strict"; -window.search = window.search || {}; -(function search(search) { - // Search functionality - // - // You can use !hasFocus() to prevent keyhandling in your key - // event handlers while the user is typing their search. - - if (!Mark || !elasticlunr) { - return; + function startWorker() { + if (worker) return; + worker = new Worker('/search-worker.js', { type:'module' }); + worker.onmessage = ({data}) => { + LIST.innerHTML = data.slice(0,30).map(h => + `
  • ${h.doc.title}
  • ` + ).join(''); + }; } - - //IE 11 Compatibility from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith - if (!String.prototype.startsWith) { - String.prototype.startsWith = function(search, pos) { - return this.substr(!pos || pos < 0 ? 0 : +pos, search.length) === search; - }; + + async function openUI() { + WRAPPER.classList.remove('hidden'); + INPUT.focus(); + startWorker(); // fetches CDN/GitHub in parallel } - - var search_wrap = document.getElementById('search-wrapper'), - search_modal = document.getElementById('search-modal'), - searchbar = document.getElementById('searchbar'), - searchbar_outer = document.getElementById('searchbar-outer'), - searchresults = document.getElementById('searchresults'), - searchresults_outer = document.getElementById('searchresults-outer'), - searchresults_header = document.getElementById('searchresults-header'), - searchicon = document.getElementById('search-toggle'), - content = document.getElementById('content'), - - searchindex = null, - doc_urls = [], - results_options = { - teaser_word_count: 30, - limit_results: 30, - }, - search_options = { - bool: "AND", - expand: true, - fields: { - title: {boost: 1}, - body: {boost: 1}, - breadcrumbs: {boost: 0} - } - }, - mark_exclude = [], - marker = new Mark(content), - current_searchterm = "", - URL_SEARCH_PARAM = 'search', - URL_MARK_PARAM = 'highlight', - teaser_count = 0, - - SEARCH_HOTKEY_KEYCODE = 83, - ESCAPE_KEYCODE = 27, - DOWN_KEYCODE = 40, - UP_KEYCODE = 38, - SELECT_KEYCODE = 13; - - function hasFocus() { - return searchbar === document.activeElement; - } - - function removeChildren(elem) { - while (elem.firstChild) { - elem.removeChild(elem.firstChild); - } - } - - // Helper to parse a url into its building blocks. - function parseURL(url) { - var a = document.createElement('a'); - a.href = url; - return { - source: url, - protocol: a.protocol.replace(':',''), - host: a.hostname, - port: a.port, - params: (function(){ - var ret = {}; - var seg = a.search.replace(/^\?/,'').split('&'); - var len = seg.length, i = 0, s; - for (;i': '>', - '"': '"', - "'": ''' - }; - var repl = function(c) { return MAP[c]; }; - return function(s) { - return s.replace(/[&<>'"]/g, repl); - }; - })(); - - function formatSearchMetric(count, searchterm) { - if (count == 1) { - return count + " search result for '" + searchterm + "':"; - } else if (count == 0) { - return "No search results for '" + searchterm + "'."; - } else { - return count + " search results for '" + searchterm + "':"; - } - } - - function formatSearchResult(result, searchterms) { - var teaser = makeTeaser(escapeHTML(result.doc.body), searchterms); - teaser_count++; - - // The ?URL_MARK_PARAM= parameter belongs inbetween the page and the #heading-anchor - var url = doc_urls[result.ref].split("#"); - if (url.length == 1) { // no anchor found - url.push(""); - } - - // encodeURIComponent escapes all chars that could allow an XSS except - // for '. Due to that we also manually replace ' with its url-encoded - // representation (%27). - var searchterms = encodeURIComponent(searchterms.join(" ")).replace(/\'/g, "%27"); - - return '' + result.doc.breadcrumbs - + '' - + teaser + '' + ''; - } - - function makeTeaser(body, searchterms) { - // The strategy is as follows: - // First, assign a value to each word in the document: - // Words that correspond to search terms (stemmer aware): 40 - // Normal words: 2 - // First word in a sentence: 8 - // Then use a sliding window with a constant number of words and count the - // sum of the values of the words within the window. Then use the window that got the - // maximum sum. If there are multiple maximas, then get the last one. - // Enclose the terms in . - var stemmed_searchterms = searchterms.map(function(w) { - return elasticlunr.stemmer(w.toLowerCase()); - }); - var searchterm_weight = 40; - var weighted = []; // contains elements of ["word", weight, index_in_document] - // split in sentences, then words - var sentences = body.toLowerCase().split('. '); - var index = 0; - var value = 0; - var searchterm_found = false; - for (var sentenceindex in sentences) { - var words = sentences[sentenceindex].split(' '); - value = 8; - for (var wordindex in words) { - var word = words[wordindex]; - if (word.length > 0) { - for (var searchtermindex in stemmed_searchterms) { - if (elasticlunr.stemmer(word).startsWith(stemmed_searchterms[searchtermindex])) { - value = searchterm_weight; - searchterm_found = true; - } - }; - weighted.push([word, value, index]); - value = 2; - } - index += word.length; - index += 1; // ' ' or '.' if last word in sentence - }; - index += 1; // because we split at a two-char boundary '. ' - }; - - if (weighted.length == 0) { - return body; - } - - var window_weight = []; - var window_size = Math.min(weighted.length, results_options.teaser_word_count); - - var cur_sum = 0; - for (var wordindex = 0; wordindex < window_size; wordindex++) { - cur_sum += weighted[wordindex][1]; - }; - window_weight.push(cur_sum); - for (var wordindex = 0; wordindex < weighted.length - window_size; wordindex++) { - cur_sum -= weighted[wordindex][1]; - cur_sum += weighted[wordindex + window_size][1]; - window_weight.push(cur_sum); - }; - - if (searchterm_found) { - var max_sum = 0; - var max_sum_window_index = 0; - // backwards - for (var i = window_weight.length - 1; i >= 0; i--) { - if (window_weight[i] > max_sum) { - max_sum = window_weight[i]; - max_sum_window_index = i; - } - }; - } else { - max_sum_window_index = 0; - } - - // add around searchterms - var teaser_split = []; - var index = weighted[max_sum_window_index][2]; - for (var i = max_sum_window_index; i < max_sum_window_index+window_size; i++) { - var word = weighted[i]; - if (index < word[2]) { - // missing text from index to start of `word` - teaser_split.push(body.substring(index, word[2])); - index = word[2]; - } - if (word[1] == searchterm_weight) { - teaser_split.push("") - } - index = word[2] + word[0].length; - teaser_split.push(body.substring(word[2], index)); - if (word[1] == searchterm_weight) { - teaser_split.push("") - } - }; - - return teaser_split.join(''); - } - - function init(config) { - results_options = config.results_options; - search_options = config.search_options; - searchbar_outer = config.searchbar_outer; - doc_urls = config.doc_urls; - searchindex = elasticlunr.Index.load(config.index); - - // Set up events - searchicon.addEventListener('click', function(e) { searchIconClickHandler(); }, false); - search_wrap.addEventListener('click', function(e) { searchIconClickHandler(); }, false); - search_modal.addEventListener('click', function(e) { e.stopPropagation(); }, false); - searchbar.addEventListener('keyup', function(e) { searchbarKeyUpHandler(); }, false); - document.addEventListener('keydown', function(e) { globalKeyHandler(e); }, false); - // If the user uses the browser buttons, do the same as if a reload happened - window.onpopstate = function(e) { doSearchOrMarkFromUrl(); }; - // Suppress "submit" events so the page doesn't reload when the user presses Enter - document.addEventListener('submit', function(e) { e.preventDefault(); }, false); - - // If reloaded, do the search or mark again, depending on the current url parameters - doSearchOrMarkFromUrl(); - } - - function unfocusSearchbar() { - // hacky, but just focusing a div only works once - var tmp = document.createElement('input'); - tmp.setAttribute('style', 'position: absolute; opacity: 0;'); - searchicon.appendChild(tmp); - tmp.focus(); - tmp.remove(); - } - - // On reload or browser history backwards/forwards events, parse the url and do search or mark - function doSearchOrMarkFromUrl() { - // Check current URL for search request - var url = parseURL(window.location.href); - if (url.params.hasOwnProperty(URL_SEARCH_PARAM) - && url.params[URL_SEARCH_PARAM] != "") { - showSearch(true); - searchbar.value = decodeURIComponent( - (url.params[URL_SEARCH_PARAM]+'').replace(/\+/g, '%20')); - searchbarKeyUpHandler(); // -> doSearch() - } else { - showSearch(false); - } - - if (url.params.hasOwnProperty(URL_MARK_PARAM)) { - var words = decodeURIComponent(url.params[URL_MARK_PARAM]).split(' '); - marker.mark(words, { - exclude: mark_exclude - }); - - var markers = document.querySelectorAll("mark"); - function hide() { - for (var i = 0; i < markers.length; i++) { - markers[i].classList.add("fade-out"); - window.setTimeout(function(e) { marker.unmark(); }, 300); - } - } - for (var i = 0; i < markers.length; i++) { - markers[i].addEventListener('click', hide); - } - } - } - - // Eventhandler for keyevents on `document` - function globalKeyHandler(e) { - if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey || e.target.type === 'textarea' || e.target.type === 'text' || !hasFocus() && /^(?:input|select|textarea)$/i.test(e.target.nodeName)) { return; } - - if (e.keyCode === ESCAPE_KEYCODE) { - e.preventDefault(); - searchbar.classList.remove("active"); - setSearchUrlParameters("", - (searchbar.value.trim() !== "") ? "push" : "replace"); - if (hasFocus()) { - unfocusSearchbar(); - } - showSearch(false); - marker.unmark(); - } else if (!hasFocus() && e.keyCode === SEARCH_HOTKEY_KEYCODE) { - e.preventDefault(); - showSearch(true); - window.scrollTo(0, 0); - searchbar.select(); - } else if (hasFocus() && e.keyCode === DOWN_KEYCODE) { - e.preventDefault(); - unfocusSearchbar(); - searchresults.firstElementChild.classList.add("focus"); - } else if (!hasFocus() && (e.keyCode === DOWN_KEYCODE - || e.keyCode === UP_KEYCODE - || e.keyCode === SELECT_KEYCODE)) { - // not `:focus` because browser does annoying scrolling - var focused = searchresults.querySelector("li.focus"); - if (!focused) return; - e.preventDefault(); - if (e.keyCode === DOWN_KEYCODE) { - var next = focused.nextElementSibling; - if (next) { - focused.classList.remove("focus"); - next.classList.add("focus"); - } - } else if (e.keyCode === UP_KEYCODE) { - focused.classList.remove("focus"); - var prev = focused.previousElementSibling; - if (prev) { - prev.classList.add("focus"); - } else { - searchbar.select(); - } - } else { // SELECT_KEYCODE - window.location.assign(focused.querySelector('a')); - } - } - } - - function showSearch(yes) { - if (yes) { - search_wrap.classList.remove('hidden'); - searchicon.setAttribute('aria-expanded', 'true'); - } else { - search_wrap.classList.add('hidden'); - searchicon.setAttribute('aria-expanded', 'false'); - var results = searchresults.children; - for (var i = 0; i < results.length; i++) { - results[i].classList.remove("focus"); - } - } - } - - function showResults(yes) { - if (yes) { - searchresults_outer.classList.remove('hidden'); - } else { - searchresults_outer.classList.add('hidden'); - } - } - - // Eventhandler for search icon - function searchIconClickHandler() { - if (search_wrap.classList.contains('hidden')) { - showSearch(true); - window.scrollTo(0, 0); - searchbar.select(); - } else { - showSearch(false); - } - } - - // Eventhandler for keyevents while the searchbar is focused - function searchbarKeyUpHandler() { - var searchterm = searchbar.value.trim(); - if (searchterm != "") { - searchbar.classList.add("active"); - doSearch(searchterm); - } else { - searchbar.classList.remove("active"); - showResults(false); - removeChildren(searchresults); - } - - setSearchUrlParameters(searchterm, "push_if_new_search_else_replace"); - - // Remove marks - marker.unmark(); - } - - // Update current url with ?URL_SEARCH_PARAM= parameter, remove ?URL_MARK_PARAM and #heading-anchor . - // `action` can be one of "push", "replace", "push_if_new_search_else_replace" - // and replaces or pushes a new browser history item. - // "push_if_new_search_else_replace" pushes if there is no `?URL_SEARCH_PARAM=abc` yet. - function setSearchUrlParameters(searchterm, action) { - var url = parseURL(window.location.href); - var first_search = ! url.params.hasOwnProperty(URL_SEARCH_PARAM); - if (searchterm != "" || action == "push_if_new_search_else_replace") { - url.params[URL_SEARCH_PARAM] = searchterm; - delete url.params[URL_MARK_PARAM]; - url.hash = ""; - } else { - delete url.params[URL_MARK_PARAM]; - delete url.params[URL_SEARCH_PARAM]; - } - // A new search will also add a new history item, so the user can go back - // to the page prior to searching. A updated search term will only replace - // the url. - if (action == "push" || (action == "push_if_new_search_else_replace" && first_search) ) { - history.pushState({}, document.title, renderURL(url)); - } else if (action == "replace" || (action == "push_if_new_search_else_replace" && !first_search) ) { - history.replaceState({}, document.title, renderURL(url)); - } - } - - function doSearch(searchterm) { - - // Don't search the same twice - if (current_searchterm == searchterm) { return; } - else { current_searchterm = searchterm; } - - if (searchindex == null) { return; } - - // Do the actual search - var results = searchindex.search(searchterm, search_options); - var resultcount = Math.min(results.length, results_options.limit_results); - - // Display search metrics - searchresults_header.innerText = formatSearchMetric(resultcount, searchterm); - - // Clear and insert results - var searchterms = searchterm.split(' '); - removeChildren(searchresults); - for(var i = 0; i < resultcount ; i++){ - var resultElem = document.createElement('li'); - resultElem.innerHTML = formatSearchResult(results[i], searchterms); - searchresults.appendChild(resultElem); - } - - // Display results - showResults(true); - } - - (async function loadSearchIndex(lang = window.lang || "en") { - const branch = lang === "en" ? "master" : lang; - const rawUrl = - `https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/refs/heads/${branch}/searchindex.js`; - const localJs = "/searchindex.js"; - const TIMEOUT_MS = 10_000; - - const injectScript = (src) => - new Promise((resolve, reject) => { - const s = document.createElement("script"); - s.src = src; - s.onload = () => resolve(src); - s.onerror = (e) => reject(e); - document.head.appendChild(s); - }); - - try { - /* 1 — download raw JS from GitHub */ - const controller = new AbortController(); - const timer = setTimeout(() => controller.abort(), TIMEOUT_MS); - - const res = await fetch(rawUrl, { signal: controller.signal }); - clearTimeout(timer); - if (!res.ok) throw new Error(`HTTP ${res.status}`); - - /* 2 — wrap in a Blob so the browser sees application/javascript */ - const code = await res.text(); - const blobUrl = URL.createObjectURL( - new Blob([code], { type: "application/javascript" }) - ); - - /* 3 — execute it */ - await injectScript(blobUrl); - - /* ───────────── PATCH ───────────── - heavy parsing now deferred to idle time - */ - requestIdleCallback(() => init(window.search)); - return; // ✔ UI remains responsive - } catch (eRemote) { - console.warn("Remote JS failed →", eRemote); - } - - /* ───────── fallback: local copy ───────── */ - try { - await injectScript(localJs); - - /* ───────────── PATCH ───────────── */ - requestIdleCallback(() => init(window.search)); - return; - } catch (eLocal) { - console.error("Local JS failed →", eLocal); - } - })(); - - // Exported functions - search.hasFocus = hasFocus; -})(window.search); \ No newline at end of file + + TOGGLE.addEventListener('click', openUI); + document.addEventListener('keydown', e => { + if (!e.metaKey && !e.ctrlKey && !e.altKey && e.keyCode === HOTKEY) { + e.preventDefault(); openUI(); + } + }); + + INPUT.addEventListener('input', e => { + clearTimeout(debounce); + debounce = setTimeout(() => { + worker?.postMessage(e.target.value.trim()); + }, 120); // small debounce keeps typing smooth + }); + })(); + \ No newline at end of file diff --git a/theme/search-worker.js b/theme/search-worker.js new file mode 100644 index 000000000..8bbbb4b88 --- /dev/null +++ b/theme/search-worker.js @@ -0,0 +1,40 @@ +/* search-worker.js ------------------------------------------------------- */ +/* Make code written for window work in a worker: */ +self.window = self; + +//////////////////////////////////////////////////////////////////////////// +// 1. elasticlunr.min.js : CDN first → local fallback +//////////////////////////////////////////////////////////////////////////// +try { + importScripts('https://cdn.jsdelivr.net/npm/elasticlunr@0.9.5/elasticlunr.min.js'); +} catch (e) { + importScripts('/elasticlunr.min.js'); // ship this with your site +} + +//////////////////////////////////////////////////////////////////////////// +// 2. searchindex.js : GitHub Raw first → local fallback +// We fetch → wrap in a Blob({type:'application/javascript'}) to bypass +// GitHub’s text/plain + nosniff MIME blocking. +//////////////////////////////////////////////////////////////////////////// +try { + const res = await fetch( + 'https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/refs/heads/master/searchindex.js', + {mode: 'cors'} + ); + if (!res.ok) throw new Error(res.status); + const blobUrl = URL.createObjectURL( + new Blob([await res.text()], { type:'application/javascript' }) + ); + importScripts(blobUrl); // correct MIME, runs once +} catch (e) { + importScripts('/searchindex.js'); // offline fallback +} + +//////////////////////////////////////////////////////////////////////////// +// 3. Build the index once and answer queries +//////////////////////////////////////////////////////////////////////////// +const idx = elasticlunr.Index.load(self.search.index); + +self.onmessage = ({data: q}) => { + postMessage(idx.search(q, { bool:'AND', expand:true })); +};