diff --git a/book.toml b/book.toml index f20e2d2a2..a1b0f52ed 100644 --- a/book.toml +++ b/book.toml @@ -31,6 +31,7 @@ additional-js = [ "theme/tabs.js", "theme/ht_searcher.js", "theme/sponsor.js", + "theme/ai.js" ] no-section-label = true preferred-dark-theme = "hacktricks-dark" diff --git a/src/pentesting-cloud/aws-security/aws-unauthenticated-enum-access/aws-s3-unauthenticated-enum.md b/src/pentesting-cloud/aws-security/aws-unauthenticated-enum-access/aws-s3-unauthenticated-enum.md index d25382e4b..382ed7a70 100644 --- a/src/pentesting-cloud/aws-security/aws-unauthenticated-enum-access/aws-s3-unauthenticated-enum.md +++ b/src/pentesting-cloud/aws-security/aws-unauthenticated-enum-access/aws-s3-unauthenticated-enum.md @@ -2,22 +2,22 @@ {{#include ../../../banners/hacktricks-training.md}} -## S3 Δημόσια Μπακέτα +## S3 Δημόσιοι Κάδοι -Ένα μπακέτο θεωρείται **“δημόσιο”** αν **οποιοσδήποτε χρήστης μπορεί να καταγράψει το περιεχόμενο** του μπακέτου, και **“ιδιωτικό”** αν το περιεχόμενο του μπακέτου μπορεί **να καταγραφεί ή να γραφτεί μόνο από συγκεκριμένους χρήστες**. +Ένας κάδος θεωρείται **“δημόσιος”** αν **οποιοσδήποτε χρήστης μπορεί να καταγράψει το περιεχόμενο** του κάδου, και **“ιδιωτικός”** αν το περιεχόμενο του κάδου μπορεί **να καταγραφεί ή να γραφτεί μόνο από συγκεκριμένους χρήστες**. -Οι εταιρείες μπορεί να έχουν **λανθασμένα ρυθμισμένα δικαιώματα μπακέτων** δίνοντας πρόσβαση είτε σε όλα είτε σε όλους τους πιστοποιημένους χρήστες στο AWS σε οποιονδήποτε λογαριασμό (δηλαδή σε οποιονδήποτε). Σημειώστε ότι ακόμη και με τέτοιες λανθασμένες ρυθμίσεις, ορισμένες ενέργειες μπορεί να μην είναι δυνατές καθώς τα μπακέτα μπορεί να έχουν τις δικές τους λίστες ελέγχου πρόσβασης (ACLs). +Οι εταιρείες μπορεί να έχουν **κακώς ρυθμισμένα δικαιώματα κάδων** που δίνουν πρόσβαση είτε σε όλα είτε σε όλους τους αυθεντικοποιημένους χρήστες στο AWS σε οποιονδήποτε λογαριασμό (δηλαδή σε οποιονδήποτε). Σημειώστε ότι ακόμη και με τέτοιες κακές ρυθμίσεις, ορισμένες ενέργειες μπορεί να μην είναι δυνατές καθώς οι κάδοι μπορεί να έχουν τις δικές τους λίστες ελέγχου πρόσβασης (ACLs). -**Μάθετε για τις λανθασμένες ρυθμίσεις AWS-S3 εδώ:** [**http://flaws.cloud**](http://flaws.cloud/) **και** [**http://flaws2.cloud/**](http://flaws2.cloud) +**Μάθετε για τις κακές ρυθμίσεις του AWS-S3 εδώ:** [**http://flaws.cloud**](http://flaws.cloud/) **και** [**http://flaws2.cloud/**](http://flaws2.cloud) -### Εύρεση Μπακετών AWS +### Εύρεση Κάδων AWS Διαφορετικές μέθοδοι για να βρείτε πότε μια ιστοσελίδα χρησιμοποιεί το AWS για την αποθήκευση κάποιων πόρων: -#### Καταμέτρηση & OSINT: +#### Αρίθμηση & OSINT: - Χρησιμοποιώντας το **wappalyzer** πρόσθετο του προγράμματος περιήγησης -- Χρησιμοποιώντας burp (**spidering** του ιστού) ή πλοηγώντας χειροκίνητα μέσω της σελίδας, όλοι οι **πόροι** **που φορτώνονται** θα αποθηκευτούν στην Ιστορία. +- Χρησιμοποιώντας το burp (**spidering** του ιστού) ή πλοηγώντας χειροκίνητα μέσω της σελίδας, όλοι οι **πόροι** **που φορτώνονται** θα αποθηκευτούν στην Ιστορία. - **Ελέγξτε για πόρους** σε τομείς όπως: ``` @@ -26,18 +26,19 @@ http://[bucket_name].s3.amazonaws.com/ ``` - Ελέγξτε για **CNAMES** καθώς το `resources.domain.com` μπορεί να έχει το CNAME `bucket.s3.amazonaws.com` -- Ελέγξτε [https://buckets.grayhatwarfare.com](https://buckets.grayhatwarfare.com/), μια ιστοσελίδα με ήδη **ανακαλυμμένα ανοιχτά μπακέτα**. -- Το **όνομα μπακέτου** και το **όνομα τομέα μπακέτου** πρέπει να είναι **τα ίδια.** +- **[s3dns](https://github.com/olizimmermann/s3dns)** – Ένας ελαφρύς DNS server που αναγνωρίζει παθητικά τους κάδους αποθήκευσης στο cloud (S3, GCP, Azure) αναλύοντας την κίνηση DNS. Ανιχνεύει CNAMEs, ακολουθεί αλυσίδες επίλυσης και ταιριάζει πρότυπα κάδων, προσφέροντας μια ήσυχη εναλλακτική λύση στην ανακάλυψη μέσω brute-force ή API. Ιδανικό για recon και ροές εργασίας OSINT. +- Ελέγξτε [https://buckets.grayhatwarfare.com](https://buckets.grayhatwarfare.com/), μια ιστοσελίδα με ήδη **ανακαλυμμένους ανοιχτούς κάδους**. +- Το **όνομα του κάδου** και το **όνομα τομέα του κάδου** πρέπει να είναι **τα ίδια.** - **flaws.cloud** είναι σε **IP** 52.92.181.107 και αν πάτε εκεί σας ανακατευθύνει στο [https://aws.amazon.com/s3/](https://aws.amazon.com/s3/). Επίσης, `dig -x 52.92.181.107` δίνει `s3-website-us-west-2.amazonaws.com`. -- Για να ελέγξετε αν είναι μπακέτο μπορείτε επίσης να **επισκεφθείτε** [https://flaws.cloud.s3.amazonaws.com/](https://flaws.cloud.s3.amazonaws.com/). +- Για να ελέγξετε αν είναι κάδος μπορείτε επίσης να **επισκεφθείτε** [https://flaws.cloud.s3.amazonaws.com/](https://flaws.cloud.s3.amazonaws.com/). #### Brute-Force -Μπορείτε να βρείτε μπακέτα **δοκιμάζοντας ονόματα** που σχετίζονται με την εταιρεία που κάνετε pentesting: +Μπορείτε να βρείτε κάδους **καταναγκαστικά** με ονόματα που σχετίζονται με την εταιρεία που κάνετε pentesting: - [https://github.com/sa7mon/S3Scanner](https://github.com/sa7mon/S3Scanner) - [https://github.com/clario-tech/s3-inspector](https://github.com/clario-tech/s3-inspector) -- [https://github.com/jordanpotti/AWSBucketDump](https://github.com/jordanpotti/AWSBucketDump) (Περιέχει μια λίστα με πιθανά ονόματα μπακέτων) +- [https://github.com/jordanpotti/AWSBucketDump](https://github.com/jordanpotti/AWSBucketDump) (Περιέχει μια λίστα με πιθανά ονόματα κάδων) - [https://github.com/fellchase/flumberboozle/tree/master/flumberbuckets](https://github.com/fellchase/flumberboozle/tree/master/flumberbuckets) - [https://github.com/smaranchand/bucky](https://github.com/smaranchand/bucky) - [https://github.com/tomdev/teh_s3_bucketeers](https://github.com/tomdev/teh_s3_bucketeers) @@ -45,47 +46,47 @@ http://[bucket_name].s3.amazonaws.com/ - [https://github.com/Eilonh/s3crets_scanner](https://github.com/Eilonh/s3crets_scanner) - [https://github.com/belane/CloudHunter](https://github.com/belane/CloudHunter) -
# Generate a wordlist to create permutations
+# Δημιουργία μιας λίστας λέξεων για τη δημιουργία παραλλαγών
curl -s https://raw.githubusercontent.com/cujanovic/goaltdns/master/words.txt > /tmp/words-s3.txt.temp
curl -s https://raw.githubusercontent.com/jordanpotti/AWSBucketDump/master/BucketNames.txt >>/tmp/words-s3.txt.temp
cat /tmp/words-s3.txt.temp | sort -u > /tmp/words-s3.txt
-# Generate a wordlist based on the domains and subdomains to test
-## Write those domains and subdomains in subdomains.txt
+# Δημιουργία μιας λίστας λέξεων βασισμένης στους τομείς και υποτομείς για δοκιμή
+## Γράψτε αυτούς τους τομείς και υποτομείς στο subdomains.txt
cat subdomains.txt > /tmp/words-hosts-s3.txt
cat subdomains.txt | tr "." "-" >> /tmp/words-hosts-s3.txt
cat subdomains.txt | tr "." "\n" | sort -u >> /tmp/words-hosts-s3.txt
-# Create permutations based in a list with the domains and subdomains to attack
+# Δημιουργία παραλλαγών βασισμένων σε μια λίστα με τους τομείς και υποτομείς για επίθεση
goaltdns -l /tmp/words-hosts-s3.txt -w /tmp/words-s3.txt -o /tmp/final-words-s3.txt.temp
-## The previous tool is specialized increating permutations for subdomains, lets filter that list
-### Remove lines ending with "."
+## Το προηγούμενο εργαλείο είναι εξειδικευμένο στη δημιουργία παραλλαγών για υποτομείς, ας φιλτράρουμε αυτή τη λίστα
+### Αφαιρέστε τις γραμμές που τελειώνουν με "."
cat /tmp/final-words-s3.txt.temp | grep -Ev "\.$" > /tmp/final-words-s3.txt.temp2
-### Create list without TLD
+### Δημιουργία λίστας χωρίς TLD
cat /tmp/final-words-s3.txt.temp2 | sed -E 's/\.[a-zA-Z0-9]+$//' > /tmp/final-words-s3.txt.temp3
-### Create list without dots
+### Δημιουργία λίστας χωρίς τελείες
cat /tmp/final-words-s3.txt.temp3 | tr -d "." > /tmp/final-words-s3.txt.temp4http://phantom.s3.amazonaws.com/
-### Create list without hyphens
+### Δημιουργία λίστας χωρίς παύλες
cat /tmp/final-words-s3.txt.temp3 | tr "." "-" > /tmp/final-words-s3.txt.temp5
-## Generate the final wordlist
+## Δημιουργία της τελικής λίστας λέξεων
cat /tmp/final-words-s3.txt.temp2 /tmp/final-words-s3.txt.temp3 /tmp/final-words-s3.txt.temp4 /tmp/final-words-s3.txt.temp5 | grep -v -- "-\." | awk '{print tolower($0)}' | sort -u > /tmp/final-words-s3.txt
-## Call s3scanner
+## Κλήση s3scanner
s3scanner --threads 100 scan --buckets-file /tmp/final-words-s3.txt | grep bucket_exists
-#### Loot S3 Buckets
+#### Ληστεία S3 Κάδων
-Δεδομένων των ανοιχτών μπακετών S3, [**BucketLoot**](https://github.com/redhuntlabs/BucketLoot) μπορεί αυτόματα να **αναζητήσει ενδιαφέροντες πληροφορίες**.
+Δεδομένων των ανοιχτών κάδων S3, [**BucketLoot**](https://github.com/redhuntlabs/BucketLoot) μπορεί αυτόματα να **αναζητήσει ενδιαφέροντα πληροφορίες**.
### Βρείτε την Περιοχή
Μπορείτε να βρείτε όλες τις υποστηριζόμενες περιοχές από το AWS στο [**https://docs.aws.amazon.com/general/latest/gr/s3.html**](https://docs.aws.amazon.com/general/latest/gr/s3.html)
-#### Με DNS
+#### Μέσω DNS
-Μπορείτε να αποκτήσετε την περιοχή ενός μπακέτου με ένα **`dig`** και **`nslookup`** κάνοντας ένα **DNS αίτημα της ανακαλυφθείσας IP**:
+Μπορείτε να αποκτήσετε την περιοχή ενός κάδου με ένα **`dig`** και **`nslookup`** κάνοντας ένα **DNS αίτημα της ανακαλυφθείσας IP**:
```bash
dig flaws.cloud
;; ANSWER SECTION:
@@ -99,21 +100,23 @@ Non-authoritative answer:
Μπορείτε να αποκτήσετε πρόσβαση στον στατικό ιστότοπο πηγαίνοντας στο: `flaws.cloud.s3-website-us-west-2.amazonaws.com`\
ή μπορείτε να αποκτήσετε πρόσβαση στον κάδο επισκεπτόμενοι: `flaws.cloud.s3-us-west-2.amazonaws.com`
+
+
#### Δοκιμάζοντας
-Αν προσπαθήσετε να αποκτήσετε πρόσβαση σε έναν κάδο, αλλά στο **όνομα τομέα καθορίσετε άλλη περιοχή** (για παράδειγμα, ο κάδος είναι στο `bucket.s3.amazonaws.com` αλλά προσπαθείτε να αποκτήσετε πρόσβαση στο `bucket.s3-website-us-west-2.amazonaws.com`, τότε θα σας **υποδειχθεί η σωστή τοποθεσία**:
+Αν προσπαθήσετε να αποκτήσετε πρόσβαση σε έναν κάδο, αλλά στο **όνομα τομέα καθορίζετε άλλη περιοχή** (για παράδειγμα, ο κάδος είναι στο `bucket.s3.amazonaws.com` αλλά προσπαθείτε να αποκτήσετε πρόσβαση στο `bucket.s3-website-us-west-2.amazonaws.com`, τότε θα σας **υποδειχθεί η σωστή τοποθεσία**:
.png>)
-### Αριθμητική του κάδου
+### Καταμέτρηση του κάδου
-Για να δοκιμάσετε την ανοιχτότητα του κάδου, ένας χρήστης μπορεί απλά να εισάγει το URL στον περιηγητή του. Ένας ιδιωτικός κάδος θα απαντήσει με "Access Denied". Ένας δημόσιος κάδος θα απαριθμήσει τα πρώτα 1.000 αντικείμενα που έχουν αποθηκευτεί.
+Για να δοκιμάσετε την ανοιχτότητα του κάδου, ένας χρήστης μπορεί απλά να εισάγει το URL στον περιηγητή του. Ένας ιδιωτικός κάδος θα απαντήσει με "Access Denied". Ένας δημόσιος κάδος θα καταγράψει τα πρώτα 1.000 αντικείμενα που έχουν αποθηκευτεί.
Ανοιχτό σε όλους:
.png>)
-Ιδιωτικό:
+Ιδιωτικός:
.png>)
@@ -133,7 +136,7 @@ https://{user_provided}.s3.amazonaws.com
```
### Λάβετε το ID λογαριασμού από δημόσιο Bucket
-Είναι δυνατόν να προσδιορίσετε έναν λογαριασμό AWS εκμεταλλευόμενοι το νέο **`S3:ResourceAccount`** **Κλειδί Συνθήκης Πολιτικής**. Αυτή η συνθήκη **περιορίζει την πρόσβαση με βάση το S3 bucket** στο οποίο βρίσκεται ένας λογαριασμός (άλλες πολιτικές που βασίζονται σε λογαριασμούς περιορίζουν με βάση τον λογαριασμό στον οποίο βρίσκεται ο αιτών κύριος).\
+Είναι δυνατόν να προσδιορίσετε έναν λογαριασμό AWS εκμεταλλευόμενοι το νέο **`S3:ResourceAccount`** **Policy Condition Key**. Αυτή η συνθήκη **περιορίζει την πρόσβαση με βάση το S3 bucket** στο οποίο βρίσκεται ένας λογαριασμός (άλλες πολιτικές που βασίζονται σε λογαριασμούς περιορίζουν με βάση τον λογαριασμό στον οποίο βρίσκεται ο αιτών κύριος).\
Και επειδή η πολιτική μπορεί να περιέχει **wildcards**, είναι δυνατόν να βρείτε τον αριθμό του λογαριασμού **μόνο έναν αριθμό τη φορά**.
Αυτό το εργαλείο αυτοματοποιεί τη διαδικασία:
@@ -146,11 +149,11 @@ s3-account-search arn:aws:iam::123456789012:role/s3_read s3://my-bucket
# With an object
s3-account-search arn:aws:iam::123456789012:role/s3_read s3://my-bucket/path/to/object.ext
```
-Αυτή η τεχνική λειτουργεί επίσης με URLs API Gateway, URLs Lambda, σύνολα δεδομένων Data Exchange και ακόμη και για να αποκτήσετε την τιμή ετικετών (αν γνωρίζετε το κλειδί της ετικέτας). Μπορείτε να βρείτε περισσότερες πληροφορίες στην [**αρχική έρευνα**](https://blog.plerion.com/conditional-love-for-aws-metadata-enumeration/) και το εργαλείο [**conditional-love**](https://github.com/plerionhq/conditional-love/) για να αυτοματοποιήσετε αυτή την εκμετάλλευση.
+Αυτή η τεχνική λειτουργεί επίσης με URLs API Gateway, URLs Lambda, σύνολα δεδομένων Data Exchange και ακόμη και για να αποκτήσετε την τιμή ετικετών (αν γνωρίζετε το κλειδί της ετικέτας). Μπορείτε να βρείτε περισσότερες πληροφορίες στην [**πρωτότυπη έρευνα**](https://blog.plerion.com/conditional-love-for-aws-metadata-enumeration/) και το εργαλείο [**conditional-love**](https://github.com/plerionhq/conditional-love/) για να αυτοματοποιήσετε αυτή την εκμετάλλευση.
### Επιβεβαίωση ότι ένα bucket ανήκει σε έναν λογαριασμό AWS
-Όπως εξηγείται σε [**αυτή την ανάρτηση blog**](https://blog.plerion.com/things-you-wish-you-didnt-need-to-know-about-s3/)**, αν έχετε άδειες για να καταγράψετε ένα bucket** είναι δυνατόν να επιβεβαιώσετε ένα accountID στο οποίο ανήκει το bucket στέλνοντας ένα αίτημα όπως:
+Όπως εξηγείται σε [**αυτή την ανάρτηση στο blog**](https://blog.plerion.com/things-you-wish-you-didnt-need-to-know-about-s3/)**, αν έχετε άδειες για να καταγράψετε ένα bucket** είναι δυνατόν να επιβεβαιώσετε ένα accountID στο οποίο ανήκει το bucket στέλνοντας ένα αίτημα όπως:
```bash
curl -X GET "[bucketname].amazonaws.com/" \
-H "x-amz-expected-bucket-owner: [correct-account-id]"
@@ -160,7 +163,7 @@ curl -X GET "[bucketname].amazonaws.com/" \
```
Αν το σφάλμα είναι “Access Denied” σημαίνει ότι το ID του λογαριασμού ήταν λάθος.
-### Χρησιμοποιούμενα Emails ως αρίθμηση λογαριασμού root
+### Χρησιμοποιούμενα Emails για την αρίθμηση λογαριασμού root
Όπως εξηγείται σε [**αυτή την ανάρτηση στο blog**](https://blog.plerion.com/things-you-wish-you-didnt-need-to-know-about-s3/), είναι δυνατόν να ελέγξετε αν μια διεύθυνση email σχετίζεται με οποιονδήποτε λογαριασμό AWS προσπαθώντας να **παραχωρήσετε άδειες σε μια διεύθυνση email** σε ένα S3 bucket μέσω ACLs. Αν αυτό δεν προκαλέσει σφάλμα, σημαίνει ότι το email είναι χρήστης root κάποιου λογαριασμού AWS:
```python
diff --git a/theme/ai.js b/theme/ai.js
new file mode 100644
index 000000000..bb8af53b7
--- /dev/null
+++ b/theme/ai.js
@@ -0,0 +1,332 @@
+/**
+ * HackTricks AI Chat Widget v1.15 – Markdown rendering + sanitised
+ * ------------------------------------------------------------------------
+ * • Replaces the static “…” placeholder with a three-dot **bouncing** loader
+ * • Renders assistant replies as Markdown while purging any unsafe HTML
+ * (XSS-safe via DOMPurify)
+ * ------------------------------------------------------------------------
+ */
+(function () {
+ const LOG = "[HackTricks-AI]";
+
+ /* ---------------- User-tunable constants ---------------- */
+ const MAX_CONTEXT = 3000; // highlighted-text char limit
+ const MAX_QUESTION = 500; // question char limit
+ const TOOLTIP_TEXT =
+ "💡 Highlight any text on the page,\nthen click to ask HackTricks AI about it";
+
+ const API_BASE = "https://www.hacktricks.ai/api/assistants/threads";
+ const BRAND_RED = "#b31328"; // HackTricks brand
+
+ /* ------------------------------ State ------------------------------ */
+ let threadId = null;
+ let isRunning = false;
+
+ const $ = (sel, ctx = document) => ctx.querySelector(sel);
+ if (document.getElementById("ht-ai-btn")) {
+ console.warn(`${LOG} Widget already injected.`);
+ return;
+ }
+ (document.readyState === "loading"
+ ? document.addEventListener("DOMContentLoaded", init)
+ : init());
+
+ /* ==================================================================== */
+ /* 🔗 1. 3rd-party libs → Markdown & sanitiser */
+ /* ==================================================================== */
+ function loadScript(src) {
+ return new Promise((resolve, reject) => {
+ const s = document.createElement("script");
+ s.src = src;
+ s.onload = resolve;
+ s.onerror = () => reject(new Error(`Failed to load ${src}`));
+ document.head.appendChild(s);
+ });
+ }
+
+ async function ensureDeps() {
+ const deps = [];
+ if (typeof marked === "undefined") {
+ deps.push(loadScript("https://cdn.jsdelivr.net/npm/marked/marked.min.js"));
+ }
+ if (typeof DOMPurify === "undefined") {
+ deps.push(
+ loadScript(
+ "https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.2.5/purify.min.js"
+ )
+ );
+ }
+ if (deps.length) await Promise.all(deps);
+ }
+
+ function mdToSafeHTML(md) {
+ // 1️⃣ Markdown → raw HTML
+ const raw = marked.parse(md, { mangle: false, headerIds: false });
+ // 2️⃣ Purify
+ return DOMPurify.sanitize(raw, { USE_PROFILES: { html: true } });
+ }
+
+ /* ==================================================================== */
+ async function init() {
+ /* ----- make sure marked & DOMPurify are ready before anything else */
+ try {
+ await ensureDeps();
+ } catch (e) {
+ console.error(`${LOG} Could not load dependencies`, e);
+ return;
+ }
+
+ console.log(`${LOG} Injecting widget… v1.15`);
+
+ await ensureThreadId();
+ injectStyles();
+
+ const btn = createFloatingButton();
+ createTooltip(btn);
+ const panel = createSidebar();
+ const chatLog = $("#ht-ai-chat");
+ const sendBtn = $("#ht-ai-send");
+ const inputBox = $("#ht-ai-question");
+ const resetBtn = $("#ht-ai-reset");
+ const closeBtn = $("#ht-ai-close");
+
+ /* ------------------- Selection snapshot ------------------- */
+ let savedSelection = "";
+ btn.addEventListener("pointerdown", () => {
+ savedSelection = window.getSelection().toString().trim();
+ });
+
+ /* ------------------- Helpers ------------------------------ */
+ function addMsg(text, cls) {
+ const b = document.createElement("div");
+ b.className = `ht-msg ${cls}`;
+
+ // ✨ assistant replies rendered as Markdown + sanitised
+ if (cls === "ht-ai") {
+ b.innerHTML = mdToSafeHTML(text);
+ } else {
+ // user / context bubbles stay plain-text
+ b.textContent = text;
+ }
+
+ chatLog.appendChild(b);
+ chatLog.scrollTop = chatLog.scrollHeight;
+ return b;
+ }
+ const LOADER_HTML =
+ '';
+
+ function setInputDisabled(d) {
+ inputBox.disabled = d;
+ sendBtn.disabled = d;
+ }
+ function clearThreadCookie() {
+ document.cookie = "threadId=; Path=/; Max-Age=0";
+ threadId = null;
+ }
+ function resetConversation() {
+ chatLog.innerHTML = "";
+ clearThreadCookie();
+ panel.classList.remove("open");
+ }
+
+ /* ------------------- Panel open / close ------------------- */
+ btn.addEventListener("click", () => {
+ if (!savedSelection) {
+ alert("Please highlight some text first to then ask HackTricks AI about it.");
+ return;
+ }
+ if (savedSelection.length > MAX_CONTEXT) {
+ alert(
+ `Highlighted text is too long (${savedSelection.length} chars). Max allowed: ${MAX_CONTEXT}.`
+ );
+ return;
+ }
+ chatLog.innerHTML = "";
+ addMsg(savedSelection, "ht-context");
+ panel.classList.add("open");
+ inputBox.focus();
+ });
+ closeBtn.addEventListener("click", resetConversation);
+ resetBtn.addEventListener("click", resetConversation);
+
+ /* --------------------------- Messaging --------------------------- */
+ async function sendMessage(question, context = null) {
+ if (!threadId) await ensureThreadId();
+ if (isRunning) {
+ addMsg("Please wait until the current operation completes.", "ht-ai");
+ return;
+ }
+
+ isRunning = true;
+ setInputDisabled(true);
+ const loadingBubble = addMsg("", "ht-ai");
+ loadingBubble.innerHTML = LOADER_HTML;
+
+ const content = context
+ ? `### Context:\n${context}\n\n### Question to answer:\n${question}`
+ : question;
+ try {
+ const res = await fetch(`${API_BASE}/${threadId}/messages`, {
+ method: "POST",
+ credentials: "include",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ content })
+ });
+ if (!res.ok) {
+ let err = `Unknown error: ${res.status}`;
+ try {
+ const e = await res.json();
+ if (e.error) err = `Error: ${e.error}`;
+ else if (res.status === 429)
+ err = "Rate limit exceeded. Please try again later.";
+ } catch (_) {}
+ loadingBubble.textContent = err;
+ return;
+ }
+ const data = await res.json();
+ loadingBubble.remove();
+ if (Array.isArray(data.response))
+ data.response.forEach((p) => {
+ addMsg(
+ p.type === "text" && p.text && p.text.value
+ ? p.text.value
+ : JSON.stringify(p),
+ "ht-ai"
+ );
+ });
+ else if (typeof data.response === "string")
+ addMsg(data.response, "ht-ai");
+ else addMsg(JSON.stringify(data, null, 2), "ht-ai");
+ } catch (e) {
+ console.error("Error sending message:", e);
+ loadingBubble.textContent = "An unexpected error occurred.";
+ } finally {
+ isRunning = false;
+ setInputDisabled(false);
+ chatLog.scrollTop = chatLog.scrollHeight;
+ }
+ }
+
+ async function handleSend() {
+ const q = inputBox.value.trim();
+ if (!q) return;
+ if (q.length > MAX_QUESTION) {
+ alert(
+ `Your question is too long (${q.length} chars). Max allowed: ${MAX_QUESTION}.`
+ );
+ return;
+ }
+ inputBox.value = "";
+ addMsg(q, "ht-user");
+ await sendMessage(q, savedSelection || null);
+ }
+ sendBtn.addEventListener("click", handleSend);
+ inputBox.addEventListener("keydown", (e) => {
+ if (e.key === "Enter" && !e.shiftKey) {
+ e.preventDefault();
+ handleSend();
+ }
+ });
+ }
+
+ /* ==================================================================== */
+ async function ensureThreadId() {
+ const m = document.cookie.match(/threadId=([^;]+)/);
+ if (m && m[1]) {
+ threadId = m[1];
+ return;
+ }
+ try {
+ const r = await fetch(API_BASE, { method: "POST", credentials: "include" });
+ const d = await r.json();
+ if (!r.ok || !d.threadId) throw new Error(`${r.status} ${r.statusText}`);
+ threadId = d.threadId;
+ document.cookie =
+ `threadId=${threadId}; Path=/; Secure; SameSite=Strict; Max-Age=7200`;
+ } catch (e) {
+ console.error("Error creating threadId:", e);
+ alert("Failed to initialise the conversation. Please refresh and try again.");
+ throw e;
+ }
+ }
+
+ /* ==================================================================== */
+ function injectStyles() {
+ const css = `
+ #ht-ai-btn{position:fixed;bottom:20px;left:50%;transform:translateX(-50%);width:60px;height:60px;border-radius:50%;background:#1e1e1e;color:#fff;font-size:28px;display:flex;align-items:center;justify-content:center;cursor:pointer;z-index:99999;box-shadow:0 2px 8px rgba(0,0,0,.4);transition:opacity .2s}
+ #ht-ai-btn:hover{opacity:.85}
+ @media(max-width:768px){#ht-ai-btn{display:none}}
+ #ht-ai-tooltip{position:fixed;padding:6px 8px;background:#111;color:#fff;border-radius:4px;font-size:13px;white-space:pre-wrap;pointer-events:none;opacity:0;transform:translate(-50%,-8px);transition:opacity .15s ease,transform .15s ease;z-index:100000}
+ #ht-ai-tooltip.show{opacity:1;transform:translate(-50%,-12px)}
+ #ht-ai-panel{position:fixed;top:0;right:0;height:100%;width:350px;max-width:90vw;background:#000;color:#fff;display:flex;flex-direction:column;transform:translateX(100%);transition:transform .3s ease;z-index:100000;font-family:system-ui,-apple-system,Segoe UI,Roboto,"Helvetica Neue",Arial,sans-serif}
+ #ht-ai-panel.open{transform:translateX(0)}
+ @media(max-width:768px){#ht-ai-panel{display:none}}
+ #ht-ai-header{display:flex;justify-content:space-between;align-items:center;padding:12px 16px;border-bottom:1px solid #333}
+ #ht-ai-header .ht-actions{display:flex;gap:8px;align-items:center}
+ #ht-ai-close,#ht-ai-reset{cursor:pointer;font-size:18px;background:none;border:none;color:#fff;padding:0}
+ #ht-ai-close:hover,#ht-ai-reset:hover{opacity:.7}
+ #ht-ai-chat{flex:1;overflow-y:auto;padding:16px;display:flex;flex-direction:column;gap:12px;font-size:14px}
+ .ht-msg{max-width:90%;line-height:1.4;padding:10px 12px;border-radius:8px;white-space:pre-wrap;word-wrap:break-word}
+ .ht-user{align-self:flex-end;background:${BRAND_RED}}
+ .ht-ai{align-self:flex-start;background:#222}
+ .ht-context{align-self:flex-start;background:#444;font-style:italic;font-size:13px}
+ #ht-ai-input{display:flex;gap:8px;padding:12px 16px;border-top:1px solid #333}
+ #ht-ai-question{flex:1;min-height:40px;max-height:120px;resize:vertical;padding:8px;border-radius:6px;border:none;font-size:14px}
+ #ht-ai-send{padding:0 18px;border:none;border-radius:6px;background:${BRAND_RED};color:#fff;font-size:14px;cursor:pointer}
+ #ht-ai-send:disabled{opacity:.5;cursor:not-allowed}
+ /* Loader animation */
+ .ht-loading{display:inline-flex;align-items:center;gap:4px}
+ .ht-loading span{width:6px;height:6px;border-radius:50%;background:#888;animation:ht-bounce 1.2s infinite ease-in-out}
+ .ht-loading span:nth-child(2){animation-delay:0.2s}
+ .ht-loading span:nth-child(3){animation-delay:0.4s}
+ @keyframes ht-bounce{0%,80%,100%{transform:scale(0);}40%{transform:scale(1);} }
+ ::selection{background:#ffeb3b;color:#000}
+ ::-moz-selection{background:#ffeb3b;color:#000}`;
+ const s = document.createElement("style");
+ s.id = "ht-ai-style";
+ s.textContent = css;
+ document.head.appendChild(s);
+ }
+
+ function createFloatingButton() {
+ const d = document.createElement("div");
+ d.id = "ht-ai-btn";
+ d.textContent = "🤖";
+ document.body.appendChild(d);
+ return d;
+ }
+
+ function createTooltip(btn) {
+ const t = document.createElement("div");
+ t.id = "ht-ai-tooltip";
+ t.textContent = TOOLTIP_TEXT;
+ document.body.appendChild(t);
+ btn.addEventListener("mouseenter", () => {
+ const r = btn.getBoundingClientRect();
+ t.style.left = `${r.left + r.width / 2}px`;
+ t.style.top = `${r.top}px`;
+ t.classList.add("show");
+ });
+ btn.addEventListener("mouseleave", () => t.classList.remove("show"));
+ }
+
+ function createSidebar() {
+ const p = document.createElement("div");
+ p.id = "ht-ai-panel";
+ p.innerHTML = `
+ HackTricks AI Chat
+
+
+ ✖
+
+
+
+
+
+
+ `;
+ document.body.appendChild(p);
+ return p;
+ }
+})();
diff --git a/theme/ht_searcher.js b/theme/ht_searcher.js
index f2b4de026..f3dc65c1f 100644
--- a/theme/ht_searcher.js
+++ b/theme/ht_searcher.js
@@ -1,524 +1,554 @@
-"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;
- }
-
- //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;
- };
- }
-
- 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}
+/* ────────────────────────────────────────────────────────────────
+ Polyfill so requestIdleCallback works everywhere (IE 11/Safari)
+ ─────────────────────────────────────────────────────────────── */
+ if (typeof window.requestIdleCallback !== "function") {
+ window.requestIdleCallback = function (cb) {
+ const start = Date.now();
+ return setTimeout(function () {
+ cb({
+ didTimeout: false,
+ timeRemaining: function () {
+ return Math.max(0, 50 - (Date.now() - start));
}
- },
- 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
- + '' + '';
- }
-
- 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();
+ }, 1);
+ };
+ window.cancelIdleCallback = window.clearTimeout;
}
- 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();
- }
+
+ /* ────────────────────────────────────────────────────────────────
+ search.js
+ ─────────────────────────────────────────────────────────────── */
- // 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);
+ "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;
}
-
- if (url.params.hasOwnProperty(URL_MARK_PARAM)) {
- var words = decodeURIComponent(url.params[URL_MARK_PARAM]).split(' ');
- marker.mark(words, {
- exclude: mark_exclude
+
+ //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;
+ };
+ }
+
+ 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
+ + '' + '';
+ }
+
+ 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 markers = document.querySelectorAll("mark");
- function hide() {
+ 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].classList.add("fade-out");
- window.setTimeout(function(e) { marker.unmark(); }, 300);
+ markers[i].addEventListener('click', hide);
}
}
- 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; }
- // 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()) {
+ 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();
- }
- 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) {
+ 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");
- next.classList.add("focus");
+ var prev = focused.previousElementSibling;
+ if (prev) {
+ prev.classList.add("focus");
+ } else {
+ searchbar.select();
+ }
+ } else { // SELECT_KEYCODE
+ window.location.assign(focused.querySelector('a'));
}
- } 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 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);
+ 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);
- }
-
- setSearchUrlParameters(searchterm, "push_if_new_search_else_replace");
-
- // Remove marks
- marker.unmark();
- }
+ for(var i = 0; i < resultcount ; i++){
+ var resultElem = document.createElement('li');
+ resultElem.innerHTML = formatSearchResult(results[i], searchterms);
+ searchresults.appendChild(resultElem);
+ }
- // 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];
+ // Display results
+ showResults(true);
}
- // 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-cloud/refs/heads/${branch}/searchindex.js`;
- const localJs = "/searchindex.js";
- const TIMEOUT_MS = 5_000;
-
- /* helper: inject a