mirror of
https://github.com/HackTricks-wiki/hacktricks.git
synced 2025-10-10 18:36:50 +00:00
Translated ['src/pentesting-web/h2c-smuggling.md'] to ja
This commit is contained in:
parent
fdb86c732c
commit
d6b8c10a18
@ -1,24 +1,24 @@
|
|||||||
# アップグレードヘッダーのスムーグリング
|
# アップグレードヘッダーのスムーギング
|
||||||
|
|
||||||
{{#include ../banners/hacktricks-training.md}}
|
{{#include ../banners/hacktricks-training.md}}
|
||||||
|
|
||||||
### H2Cスムーグリング <a href="#http2-over-cleartext-h2c" id="http2-over-cleartext-h2c"></a>
|
### H2Cスムーギング <a href="#http2-over-cleartext-h2c" id="http2-over-cleartext-h2c"></a>
|
||||||
|
|
||||||
#### HTTP2オーバークリアテキスト (H2C) <a href="#http2-over-cleartext-h2c" id="http2-over-cleartext-h2c"></a>
|
#### HTTP2オーバークリアテキスト (H2C) <a href="#http2-over-cleartext-h2c" id="http2-over-cleartext-h2c"></a>
|
||||||
|
|
||||||
H2C、または**http2オーバークリアテキスト**は、標準のHTTP**接続を持続的なものにアップグレードする**ことで、一時的なHTTP接続の規範から逸脱します。このアップグレードされた接続は、平文HTTPの単一リクエストの性質とは対照的に、継続的な通信のためにhttp2バイナリプロトコルを利用します。
|
H2C、または**クリアテキスト上のhttp2**は、標準のHTTP**接続を持続的なものにアップグレードする**ことで、一時的なHTTP接続の規範から逸脱します。このアップグレードされた接続は、平文HTTPの単一リクエストの性質とは対照的に、継続的な通信のためにhttp2バイナリプロトコルを利用します。
|
||||||
|
|
||||||
スムーグリングの問題の核心は、**リバースプロキシ**の使用にあります。通常、リバースプロキシはHTTPリクエストを処理し、バックエンドに転送し、その後バックエンドの応答を返します。しかし、HTTPリクエストに`Connection: Upgrade`ヘッダーが存在する場合(一般的にウェブソケット接続で見られる)、リバース**プロキシはクライアントとサーバーの間に持続的な接続を維持し、特定のプロトコルに必要な継続的な交換を促進します**。H2C接続の場合、RFCに従うためには、3つの特定のヘッダーが必要です:
|
スムーギングの問題の核心は、**リバースプロキシ**の使用にあります。通常、リバースプロキシはHTTPリクエストを処理し、バックエンドに転送し、その後バックエンドの応答を返します。しかし、HTTPリクエストに`Connection: Upgrade`ヘッダーが存在する場合(一般的にウェブソケット接続で見られる)、リバース**プロキシはクライアントとサーバーの間に持続的な接続を維持し、特定のプロトコルに必要な継続的な交換を促進します**。H2C接続の場合、RFCに従うためには、3つの特定のヘッダーが必要です:
|
||||||
```
|
```
|
||||||
Upgrade: h2c
|
Upgrade: h2c
|
||||||
HTTP2-Settings: AAMAAABkAARAAAAAAAIAAAAA
|
HTTP2-Settings: AAMAAABkAARAAAAAAAIAAAAA
|
||||||
Connection: Upgrade, HTTP2-Settings
|
Connection: Upgrade, HTTP2-Settings
|
||||||
```
|
```
|
||||||
脆弱性は、接続をアップグレードした後、リバースプロキシが個々のリクエストの管理を停止し、接続確立後にルーティングの仕事が完了したと仮定する場合に発生します。H2Cスムギングを利用することで、H2C接続が正常に開始されると仮定して、リクエスト処理中に適用されるリバースプロキシルール(パスベースのルーティング、認証、WAF処理など)を回避できます。
|
脆弱性は、接続をアップグレードした後、リバースプロキシが個々のリクエストの管理を停止し、接続確立後にルーティングの仕事が完了したと仮定する場合に発生します。H2C Smugglingを利用することで、リクエスト処理中に適用されるリバースプロキシのルール(パスベースのルーティング、認証、WAF処理など)を回避することが可能になります。H2C接続が正常に開始されることを前提としています。
|
||||||
|
|
||||||
#### 脆弱なプロキシ <a href="#exploitation" id="exploitation"></a>
|
#### 脆弱なプロキシ <a href="#exploitation" id="exploitation"></a>
|
||||||
|
|
||||||
この脆弱性は、リバースプロキシが`Upgrade`および時には`Connection`ヘッダーをどのように処理するかに依存します。以下のプロキシは、プロキシパス中にこれらのヘッダーを本質的に転送し、H2Cスムギングを本質的に可能にします:
|
この脆弱性は、リバースプロキシが`Upgrade`および時には`Connection`ヘッダーをどのように処理するかに依存しています。以下のプロキシは、プロキシパス中にこれらのヘッダーを本質的に転送し、H2Cスムーギングを可能にします:
|
||||||
|
|
||||||
- HAProxy
|
- HAProxy
|
||||||
- Traefik
|
- Traefik
|
||||||
@ -37,18 +37,18 @@ Connection: Upgrade, HTTP2-Settings
|
|||||||
|
|
||||||
#### 悪用 <a href="#exploitation" id="exploitation"></a>
|
#### 悪用 <a href="#exploitation" id="exploitation"></a>
|
||||||
|
|
||||||
すべてのサーバーが、準拠したH2C接続アップグレードに必要なヘッダーを本質的に転送するわけではないことに注意することが重要です。そのため、AWS ALB/CLB、NGINX、Apache Traffic Serverなどのサーバーは、自然にH2C接続をブロックします。それでも、`Connection: Upgrade`の非準拠バリアントをテストする価値があります。これは、`Connection`ヘッダーから`HTTP2-Settings`値を除外します。一部のバックエンドは標準に準拠していない可能性があります。
|
すべてのサーバーがH2C接続アップグレードに必要なヘッダーを本質的に転送するわけではないことに注意が必要です。そのため、AWS ALB/CLB、NGINX、Apache Traffic Serverなどのサーバーは、自然にH2C接続をブロックします。それでも、`Connection: Upgrade`の非準拠バリアントを使用してテストする価値があります。これは、`Connection`ヘッダーから`HTTP2-Settings`値を除外します。一部のバックエンドは標準に準拠していない可能性があります。
|
||||||
|
|
||||||
> [!CAUTION]
|
> [!CAUTION]
|
||||||
> `proxy_pass` URLに指定された特定の**パス**(例:`http://backend:9999/socket.io`)に関係なく、確立された接続は`http://backend:9999`にデフォルト設定されます。これにより、この技術を利用して、その内部エンドポイント内の任意のパスと対話できます。したがって、`proxy_pass` URLにパスを指定してもアクセスは制限されません。
|
> `proxy_pass` URLに指定された特定の**パス**(例:`http://backend:9999/socket.io`)に関係なく、確立された接続は`http://backend:9999`にデフォルト設定されます。これにより、この技術を利用してその内部エンドポイント内の任意のパスと対話することが可能になります。したがって、`proxy_pass` URLにパスを指定してもアクセスは制限されません。
|
||||||
|
|
||||||
ツール[**h2csmuggler by BishopFox**](https://github.com/BishopFox/h2csmuggler)および[**h2csmuggler by assetnote**](https://github.com/assetnote/h2csmuggler)は、H2C接続を確立することにより、**プロキシによって課せられた保護を回避する**試みを支援し、プロキシによって保護されたリソースへのアクセスを可能にします。
|
ツール[**h2csmuggler by BishopFox**](https://github.com/BishopFox/h2csmuggler)および[**h2csmuggler by assetnote**](https://github.com/assetnote/h2csmuggler)は、H2C接続を確立することにより、**プロキシによって課せられた保護を回避する**試みを支援し、プロキシによって保護されたリソースへのアクセスを可能にします。
|
||||||
|
|
||||||
この脆弱性に関する追加情報、特にNGINXに関しては、[**この詳細なリソース**](../network-services-pentesting/pentesting-web/nginx.md#proxy_set_header-upgrade-and-connection)を参照してください。
|
この脆弱性に関する追加情報、特にNGINXに関しては、[**この詳細なリソース**](../network-services-pentesting/pentesting-web/nginx.md#proxy_set_header-upgrade-and-connection)を参照してください。
|
||||||
|
|
||||||
## Websocketスムギング
|
## Websocket Smuggling
|
||||||
|
|
||||||
Websocketスムギングは、プロキシを介してアクセス可能なエンドポイントへのHTTP2トンネルを作成するのとは異なり、潜在的なプロキシの制限を回避し、エンドポイントとの直接通信を促進するWebsocketトンネルを確立します。
|
Websocket smugglingは、プロキシを介してアクセス可能なエンドポイントへのHTTP2トンネルを作成するのとは異なり、プロキシの制限を回避し、エンドポイントとの直接通信を促進するWebsocketトンネルを確立します。
|
||||||
|
|
||||||
### シナリオ1
|
### シナリオ1
|
||||||
|
|
||||||
@ -64,16 +64,16 @@ Websocketスムギングは、プロキシを介してアクセス可能なエ
|
|||||||
|
|
||||||
### シナリオ2
|
### シナリオ2
|
||||||
|
|
||||||
このシナリオでは、公開WebSocket APIとヘルスチェック用の公開REST API、さらにアクセスできない内部REST APIを持つバックエンドが関与します。攻撃はより複雑で、以下のステップを含みます:
|
このシナリオでは、公開WebSocket APIと健康チェック用の公開REST API、アクセスできない内部REST APIを持つバックエンドが関与します。攻撃はより複雑で、以下のステップを含みます:
|
||||||
|
|
||||||
1. クライアントは、ヘルスチェックAPIをトリガーするためにPOSTリクエストを送信し、追加のHTTPヘッダー`Upgrade: websocket`を含めます。リバースプロキシとして機能するNGINXは、これを`Upgrade`ヘッダーのみに基づく標準のUpgradeリクエストとして解釈し、リクエストの他の側面を無視してバックエンドに転送します。
|
1. クライアントは、健康チェックAPIをトリガーするためにPOSTリクエストを送信し、追加のHTTPヘッダー`Upgrade: websocket`を含めます。リバースプロキシとして機能するNGINXは、`Upgrade`ヘッダーのみに基づいてこれを標準のUpgradeリクエストとして解釈し、リクエストの他の側面を無視してバックエンドに転送します。
|
||||||
2. バックエンドはヘルスチェックAPIを実行し、攻撃者が制御する外部リソースにアクセスし、ステータスコード`101`のHTTP応答を返します。この応答はバックエンドによって受信され、NGINXに転送されると、プロキシはステータスコードのみを検証するため、WebSocket接続が確立されたと誤解します。
|
2. バックエンドは健康チェックAPIを実行し、攻撃者が制御する外部リソースに接続し、ステータスコード`101`のHTTP応答を返します。この応答はバックエンドによって受信され、NGINXに転送されると、プロキシはステータスコードのみを検証してWebSocket接続が確立されたと誤解します。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
> **警告:** この技術の複雑さは、ステータスコード101を返すことができるエンドポイントと対話する能力を必要とするため、増加します。
|
> **警告:** この技術の複雑さは、ステータスコード101を返すことができるエンドポイントと対話する能力を必要とするため、増加します。
|
||||||
|
|
||||||
最終的に、NGINXはクライアントとバックエンドの間にWebSocket接続が存在すると信じ込まされます。実際には、そのような接続は存在せず、ヘルスチェックREST APIがターゲットでした。それにもかかわらず、リバースプロキシは接続をオープンに保ち、クライアントがそれを通じてプライベートREST APIにアクセスできるようにします。
|
最終的に、NGINXはクライアントとバックエンドの間にWebSocket接続が存在すると誤解します。実際には、そのような接続は存在せず、健康チェックREST APIがターゲットでした。それにもかかわらず、リバースプロキシは接続をオープンに保ち、クライアントがそれを通じてプライベートREST APIにアクセスできるようにします。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
10
theme/elasticlunr.min.js
vendored
Normal file
10
theme/elasticlunr.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -1,554 +1,40 @@
|
|||||||
/* ────────────────────────────────────────────────────────────────
|
/* ht_searcher.js --------------------------------------------------------- */
|
||||||
Polyfill so requestIdleCallback works everywhere (IE 11/Safari)
|
(() => {
|
||||||
─────────────────────────────────────────────────────────────── */
|
const WRAPPER = document.getElementById('search-wrapper');
|
||||||
if (typeof window.requestIdleCallback !== "function") {
|
const TOGGLE = document.getElementById('search-toggle');
|
||||||
window.requestIdleCallback = function (cb) {
|
const INPUT = document.getElementById('searchbar');
|
||||||
const start = Date.now();
|
const LIST = document.getElementById('searchresults');
|
||||||
return setTimeout(function () {
|
const HOTKEY = 83; // “s”
|
||||||
cb({
|
let worker, debounce;
|
||||||
didTimeout: false,
|
|
||||||
timeRemaining: function () {
|
|
||||||
return Math.max(0, 50 - (Date.now() - start));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, 1);
|
|
||||||
};
|
|
||||||
window.cancelIdleCallback = window.clearTimeout;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* ────────────────────────────────────────────────────────────────
|
function startWorker() {
|
||||||
search.js
|
if (worker) return;
|
||||||
─────────────────────────────────────────────────────────────── */
|
worker = new Worker('/search-worker.js', { type:'module' });
|
||||||
|
worker.onmessage = ({data}) => {
|
||||||
"use strict";
|
LIST.innerHTML = data.slice(0,30).map(h =>
|
||||||
window.search = window.search || {};
|
`<li><a href="${h.doc.url}">${h.doc.title}</a></li>`
|
||||||
(function search(search) {
|
).join('');
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//IE 11 Compatibility from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith
|
async function openUI() {
|
||||||
if (!String.prototype.startsWith) {
|
WRAPPER.classList.remove('hidden');
|
||||||
String.prototype.startsWith = function(search, pos) {
|
INPUT.focus();
|
||||||
return this.substr(!pos || pos < 0 ? 0 : +pos, search.length) === search;
|
startWorker(); // fetches CDN/GitHub in parallel
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var search_wrap = document.getElementById('search-wrapper'),
|
TOGGLE.addEventListener('click', openUI);
|
||||||
search_modal = document.getElementById('search-modal'),
|
document.addEventListener('keydown', e => {
|
||||||
searchbar = document.getElementById('searchbar'),
|
if (!e.metaKey && !e.ctrlKey && !e.altKey && e.keyCode === HOTKEY) {
|
||||||
searchbar_outer = document.getElementById('searchbar-outer'),
|
e.preventDefault(); openUI();
|
||||||
searchresults = document.getElementById('searchresults'),
|
}
|
||||||
searchresults_outer = document.getElementById('searchresults-outer'),
|
});
|
||||||
searchresults_header = document.getElementById('searchresults-header'),
|
|
||||||
searchicon = document.getElementById('search-toggle'),
|
INPUT.addEventListener('input', e => {
|
||||||
content = document.getElementById('content'),
|
clearTimeout(debounce);
|
||||||
|
debounce = setTimeout(() => {
|
||||||
searchindex = null,
|
worker?.postMessage(e.target.value.trim());
|
||||||
doc_urls = [],
|
}, 120); // small debounce keeps typing smooth
|
||||||
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<len;i++) {
|
|
||||||
if (!seg[i]) { continue; }
|
|
||||||
s = seg[i].split('=');
|
|
||||||
ret[s[0]] = s[1];
|
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
})(),
|
|
||||||
file: (a.pathname.match(/\/([^/?#]+)$/i) || [,''])[1],
|
|
||||||
hash: a.hash.replace('#',''),
|
|
||||||
path: a.pathname.replace(/^([^/])/,'/$1')
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper to recreate a url string from its building blocks.
|
|
||||||
function renderURL(urlobject) {
|
|
||||||
var url = urlobject.protocol + "://" + urlobject.host;
|
|
||||||
if (urlobject.port != "") {
|
|
||||||
url += ":" + urlobject.port;
|
|
||||||
}
|
|
||||||
url += urlobject.path;
|
|
||||||
var joiner = "?";
|
|
||||||
for(var prop in urlobject.params) {
|
|
||||||
if(urlobject.params.hasOwnProperty(prop)) {
|
|
||||||
url += joiner + prop + "=" + urlobject.params[prop];
|
|
||||||
joiner = "&";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (urlobject.hash != "") {
|
|
||||||
url += "#" + urlobject.hash;
|
|
||||||
}
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper to escape html special chars for displaying the teasers
|
|
||||||
var escapeHTML = (function() {
|
|
||||||
var MAP = {
|
|
||||||
'&': '&',
|
|
||||||
'<': '<',
|
|
||||||
'>': '>',
|
|
||||||
'"': '"',
|
|
||||||
"'": '''
|
|
||||||
};
|
|
||||||
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 '<a href="' + path_to_root + url[0] + '?' + URL_MARK_PARAM + '=' + searchterms + '#' + url[1]
|
|
||||||
+ '" aria-details="teaser_' + teaser_count + '">' + result.doc.breadcrumbs
|
|
||||||
+ '<span class="teaser" id="teaser_' + teaser_count + '" aria-label="Search Result Teaser">'
|
|
||||||
+ teaser + '</span>' + '</a>';
|
|
||||||
}
|
|
||||||
|
|
||||||
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 <em>.
|
|
||||||
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 <em/> 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("<em>")
|
|
||||||
}
|
|
||||||
index = word[2] + word[0].length;
|
|
||||||
teaser_split.push(body.substring(word[2], index));
|
|
||||||
if (word[1] == searchterm_weight) {
|
|
||||||
teaser_split.push("</em>")
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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);
|
|
40
theme/search-worker.js
Normal file
40
theme/search-worker.js
Normal file
@ -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 }));
|
||||||
|
};
|
Loading…
x
Reference in New Issue
Block a user