Compare commits

...

49 Commits

Author SHA1 Message Date
0efbb81d23 remove parseInt on password env 2026-08-04 23:32:43 -07:00
4b83c47637 read values from env and add Dockerfile 2026-08-04 23:03:47 -07:00
538a3f6aa9 fix brave being a bitch on video and news 2026-08-05 01:51:34 -04:00
1aa6278853 fix brave being a little shit about pagination 2026-08-05 01:48:10 -04:00
fdd1b4ab40 clarify ports used on 4play tehehe 2026-08-04 18:32:21 -04:00
4de2712567 systemctl ffffffffuuaaaaaaakkkkk 2026-08-03 19:21:32 -04:00
2d12a9b284 added 4play 4get config options lol 2026-08-03 19:15:54 -04:00
cd31e49785 better documentation for 4play 2026-08-03 19:06:13 -04:00
f6a369ed48 fix annoying bug with regex matching null like a fucking dumbass 2026-08-03 15:33:29 -04:00
e830a8f372 fix failstates not triggering properly 2026-08-03 15:20:09 -04:00
919714d472 i hate change but i promised i would fix it 2026-08-02 17:15:51 -04:00
9a4763ed3c fix proxy function 2026-08-01 23:01:12 -04:00
db90a39c4f google scraper fix 2026-08-01 20:04:04 -04:00
8e7fe83774 my life wouldnt be complete without forgetting to commit settings.php 2026-07-25 20:18:06 -04:00
a43b3e3949 add purili scraper 2026-07-25 20:15:27 -04:00
e57538a296 mojeek captcha detect 2026-07-22 21:14:07 -04:00
5fc80a3672 google cse pagination fix, thanks nikolay 2026-07-22 02:02:54 -04:00
54d420f97c yep fix. fuck fsky 2026-07-15 02:15:43 -04:00
513bb666c5 wordnik formatting fix 2026-07-15 01:50:28 -04:00
3e4c7c1626 forgot to fix qwant images lllllooooooollllllll 2026-07-10 21:49:25 -04:00
e4c7250cad qwant fix, datadome is such a joke lol 2026-07-10 21:42:10 -04:00
c1e9d52775 notes for me 2026-07-05 18:40:43 -04:00
46411a6c83 i overengineered the fuck out of that baidu scraper jesus 2026-07-05 01:57:56 -04:00
8328d93b17 duckduckgo fix 2026-07-04 17:16:35 -04:00
NECOtype
abdb041a64 add Rosé Pine theme 2026-06-27 13:25:50 -04:00
f037ad1d7f serverping inlineJS fix (ty ethan) 2026-06-25 23:20:06 -04:00
347300cc26 for some reason mojeek doesnt want me using region settings anymore lol 2026-06-21 22:15:27 -04:00
5a7cecef11 marginalia api 502 2026-06-14 16:07:33 -04:00
d87bac94b5 handle marginalia internal error 2026-06-14 16:01:56 -04:00
d54e4dfcdd qwant fix 2026-06-14 15:10:55 -04:00
b28767d8f0 fix ACL rules for apache2 2026-06-08 01:06:51 -04:00
7d8b12ba9d cleaning up useless shit 2026-06-08 00:48:34 -04:00
a430ef5a12 remove ghostery search, discontinued 2026-06-06 04:14:50 -04:00
02cab7632d license clarification 2026-06-06 02:18:30 -04:00
76f7d0de19 wtf it works for yep too 2026-06-05 00:22:24 -04:00
7ffbc5f67b why does http1 work jej 2026-06-05 00:18:55 -04:00
50349d2390 why do you fucking do that naver 2026-06-01 02:53:48 -04:00
fc8b41bfad added naver kansanmidaaaa 2026-06-01 02:48:27 -04:00
caa9a6c516 yep api fix 2026-05-31 20:15:09 -04:00
572730415a startpage pagination fix, fucking again 2026-05-31 13:42:50 -04:00
72fc1ae080 stop changing the fucking encoding of the motherfucking word corrector 2026-05-29 21:54:16 -04:00
ba2c6f6bb1 fix startpage pagination, again 2026-05-29 21:48:06 -04:00
d7d35f02f3 holy shit im retarded 2026-05-27 21:55:56 -04:00
5f112737b8 detect ddg captcha 2026-05-27 21:54:36 -04:00
1e3f3d82fd remove baseless degoog slander lolololo 2026-05-24 22:59:20 -04:00
95819bfe52 yep api fix 2026-05-20 11:05:41 -04:00
e1e92d715e add support for yep api 2026-05-20 11:01:18 -04:00
394f401921 Merge pull request 'add lwthiker/curl-impersonate' (#94) from docker-curl-impersonate into master
Reviewed-on: #94
2026-05-20 05:02:30 +00:00
cf3c77ed04 add lwthiker/curl-impersonate 2026-05-19 17:08:10 -07:00
40 changed files with 4366 additions and 1087 deletions

1
.gitignore vendored
View File

@@ -26,3 +26,4 @@ data/captcha/minecraft/
!banner/*default*
scraper/curlie.html
icons/*
node_modules/

View File

@@ -1,8 +1,17 @@
FROM lwthiker/curl-impersonate:0.6.1-ff-alpine AS curl-impersonate
FROM alpine:3.21
WORKDIR /var/www/html/4get
RUN apk update && apk upgrade
RUN apk add php apache2-ssl php84-fileinfo php84-openssl php84-iconv php84-common php84-dom php84-sodium php84-curl curl php84-pecl-apcu php84-apache2 imagemagick php84-pecl-imagick php84-mbstring imagemagick-webp imagemagick-jpeg
RUN apk add php apache2-ssl php84-fileinfo php84-openssl php84-iconv php84-common php84-dom php84-sodium php84-curl curl php84-pecl-apcu php84-apache2 imagemagick php84-pecl-imagick php84-mbstring imagemagick-webp imagemagick-jpeg nss ca-certificates
COPY --from=curl-impersonate /usr/local/bin /usr/local/bin
COPY --from=curl-impersonate /usr/local/lib /usr/local/lib
ENV LD_PRELOAD=/usr/local/lib/libcurl-impersonate-ff.so
ENV CURL_IMPERSONATE=ff117
ENV CURL_IMPERSONATE_HEADERS=no
COPY . .

View File

@@ -13,11 +13,11 @@ _NOT to be confused with 4get.ch, 4get.lol and friends! I **don't** host these._
## Totally unbiased comparison between alternatives
| | 4get | searx(ng) | whoogle | degoog |
|----------------------------|-------------------------|-----------|------------|--------------------------------------|
| RAM usage | 100-400mb~ | 400mb-1GB | 100mb | 200mb-1GB |
| Does it suck | no (debunked by snopes) | yes | kind of? | its kinda cool but no search filters |
| Does it work | ye | lmao | shits dead | works right now... |
| | 4get | searx(ng) | whoogle | degoog |
|----------------------------|-------------------------|-----------|------------|-------------------------------------------|
| RAM usage | 100-400mb~ | 400mb-1GB | 100mb | 100mb-250mb |
| Does it suck | no (debunked by snopes) | yes | kind of? | hit and miss with search filters |
| Does it work | ye | lmao | shits dead | works $rightNow, it's actually kinda cool |
## Features
1. Rotating proxies on a per-scraper basis
@@ -62,4 +62,4 @@ Refer to the <a href="https://git.lolcat.ca/lolcat/4get/src/branch/master/docs/"
Shit breaks all the time but I repair it all the time too. Email me here: <b>will (at) lolcat.ca</b> or create an issue.
## License
AGPL
AGPLv3-only

View File

@@ -1,20 +0,0 @@
<?php
if(!isset($_GET["s"])){
http_response_code(404);
header("X-Error: No SOUND(s) provided!");
die();
}
include "../data/config.php";
include "../lib/curlproxy.php";
$proxy = new proxy();
try{
$proxy->stream_linear_audio($_GET["s"]);
}catch(Exception $error){
header("X-Error: " . $error->getMessage());
}

View File

@@ -1,224 +0,0 @@
<?php
new sc_audio();
class sc_audio{
public function __construct(){
include "../data/config.php";
include "../lib/curlproxy.php";
$this->proxy = new proxy();
if(isset($_GET["u"])){
/*
we're now proxying audio
*/
$viewkey = $_GET["u"];
if(!isset($_GET["r"])){
$this->do404("Ranges(r) are missing");
}
$ranges = explode(",", $_GET["r"]);
// sanitize ranges
foreach($ranges as &$range){
if(!is_numeric($range)){
$this->do404("Invalid range specified");
}
$range = (int)$range;
}
// sort ranges (just to make sure)
sort($ranges);
// convert ranges to pairs
$last = -1;
foreach($ranges as &$r){
$tmp = $r;
$r = [$last + 1, $r];
$last = $tmp;
}
$browser_headers = getallheaders();
// get the requested range from client
$client_range = 0;
foreach($browser_headers as $key => $value){
if(strtolower($key) == "range"){
preg_match(
'/bytes=([0-9]+)/',
$value,
$client_regex
);
if(isset($client_regex[1])){
$client_range = (int)$client_regex[1];
}else{
$client_range = 0;
}
break;
}
}
if(
$client_range < 0 ||
$client_range > $ranges[count($ranges) - 1][1]
){
// range is not satisfiable
http_response_code(416);
header("Content-Type: text/plain");
die();
}
$rng = null;
for($i=0; $i<count($ranges); $i++){
if($ranges[$i][0] <= $client_range){
$rng = $ranges[$i];
}
}
// proxy data!
http_response_code(206); // partial content
header("Accept-Ranges: bytes");
header("Content-Range: bytes {$rng[0]}-{$rng[1]}/" . ($ranges[count($ranges) - 1][1] + 1));
$viewkey =
preg_replace(
'/\/media\/([0-9]+)\/[0-9]+\/[0-9]+/',
'/media/$1/' . $rng[0] . '/' . $rng[1],
$viewkey
);
try{
$this->proxy->stream_linear_audio(
$viewkey
);
}catch(Exception $error){
$this->do404("Could not read stream");
}
die();
}
/*
redirect user to correct resource
we need to scrape and store the byte positions in the result URL
*/
if(!isset($_GET["s"])){
$this->do404("The URL(s) parameter is missing");
}
$viewkey = $_GET["s"];
if(
preg_match(
'/soundcloud\.com$/',
parse_url($viewkey, PHP_URL_HOST)
) === false
){
$this->do404("This endpoint can only be used for soundcloud streams");
}
try{
$json = $this->proxy->get($viewkey)["body"];
}catch(Exception $error){
$this->do404("Curl error: " . $error->getMessage());
}
$json = json_decode($json, true);
if(!isset($json["url"])){
$this->do404("Could not get URL from JSON");
}
$viewkey = $json["url"];
$m3u8 = $this->proxy->get($viewkey)["body"];
$m3u8 = explode("\n", $m3u8);
$lineout = null;
$streampos_arr = [];
foreach($m3u8 as $line){
$line = trim($line);
if($line[0] == "#"){
continue;
}
if($lineout === null){
$lineout = $line;
}
preg_match(
'/\/media\/[0-9]+\/([0-9]+)\/([0-9]+)/',
$line,
$matches
);
if(isset($matches[0])){
$streampos_arr[] = [
(int)$matches[1],
(int)$matches[2]
];
}
}
if($lineout === null){
$this->do404("Could not get stream URL");
}
$lineout =
preg_replace(
'/\/media\/([0-9]+)\/[0-9]+\/[0-9]+/',
'/media/$1/0/0',
$lineout
);
$streampos = [];
foreach($streampos_arr as $pos){
$streampos[] = $pos[1];
}
$streampos = implode(",", $streampos);
header("Location: /audio/sc?u=" . urlencode($lineout) . "&r=$streampos");
header("Accept-Ranges: bytes");
}
private function do404($error){
http_response_code(404);
header("Content-Type: text/plain");
header("X-Error: $error");
die();
}
}

View File

@@ -1,20 +0,0 @@
<?php
if(!isset($_GET["s"])){
http_response_code(404);
header("X-Error: No SOUND(s) provided!");
die();
}
include "../data/config.php";
include "../lib/curlproxy.php";
$proxy = new proxy();
try{
$proxy->stream_linear_audio($_GET["s"]);
}catch(Exception $error){
header("X-Error: " . $error->getMessage());
}

View File

@@ -1,214 +0,0 @@
<?php
include "../data/config.php";
new spotify();
class spotify{
public function __construct(){
include "../lib/fuckhtml.php";
$this->fuckhtml = new fuckhtml();
if(
!isset($_GET["s"]) ||
!preg_match(
'/^(track|episode)\.([A-Za-z0-9]{22})$/',
$_GET["s"],
$matches
)
){
$this->do404("The track ID(s) parameter is missing or invalid");
}
try{
if($matches[1] == "episode"){
$uri = "show";
}else{
$uri = $matches[1];
}
$embed =
$this->get("https://embed.spotify.com/{$uri}/" . $matches[2]);
}catch(Exception $error){
$this->do404("Failed to fetch embed data");
}
$this->fuckhtml->load($embed);
$json =
$this->fuckhtml
->getElementById(
"__NEXT_DATA__",
"script"
);
if($json === null){
$this->do404("Failed to extract JSON");
}
$json =
json_decode($json["innerHTML"], true);
if($json === null){
$this->do404("Failed to decode JSON");
}
switch($matches[1]){
case "track":
if(
isset(
$json
["props"]
["pageProps"]
["state"]
["data"]
["entity"]
["audioPreview"]
["url"]
)
){
header("Content-type: audio/mpeg");
header(
"Location: /audio/linear?s=" .
urlencode(
$json
["props"]
["pageProps"]
["state"]
["data"]
["entity"]
["audioPreview"]
["url"]
)
);
}else{
$this->do404("Could not extract playback URL");
}
break;
case "episode":
if(
isset(
$json
["props"]
["pageProps"]
["state"]
["data"]
["entity"]
["id"]
)
){
try{
$json =
$this->get(
"https://spclient.wg.spotify.com/soundfinder/v1/unauth/episode/" .
$json
["props"]
["pageProps"]
["state"]
["data"]
["entity"]
["id"] .
"/com.widevine.alpha"
);
}catch(Exception $error){
$this->do404("Failed to fetch audio resource");
}
$json = json_decode($json, true);
if($json === null){
$this->do404("Failed to decode audio resource JSON");
}
if(
isset($json["passthrough"]) &&
$json["passthrough"] == "ALLOWED" &&
isset($json["passthroughUrl"])
){
header(
"Location:" .
"/audio/linear.php?s=" .
urlencode(
str_replace(
"http://",
"https://",
$json["passthroughUrl"]
)
)
);
}else{
$this->do404("Failed to find passthroughUrl");
}
}else{
$this->do404("Failed to find episode ID");
}
break;
}
}
private function get($url){
$headers = [
"User-Agent: " . config::USER_AGENT,
"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
"Accept-Language: en-US,en;q=0.5",
"Accept-Encoding: gzip",
"DNT: 1",
"Connection: keep-alive",
"Upgrade-Insecure-Requests: 1",
"Sec-Fetch-Dest: document",
"Sec-Fetch-Mode: navigate",
"Sec-Fetch-Site: none",
"Sec-Fetch-User: ?1"
];
$curlproc = curl_init();
curl_setopt($curlproc, CURLOPT_URL, $url);
curl_setopt($curlproc, CURLOPT_ENCODING, ""); // default encoding
curl_setopt($curlproc, CURLOPT_HTTPHEADER, $headers);
curl_setopt($curlproc, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curlproc, CURLOPT_SSL_VERIFYHOST, 2);
curl_setopt($curlproc, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($curlproc, CURLOPT_CONNECTTIMEOUT, 30);
curl_setopt($curlproc, CURLOPT_TIMEOUT, 30);
$data = curl_exec($curlproc);
if(curl_errno($curlproc)){
throw new Exception(curl_error($curlproc));
}
curl_close($curlproc);
return $data;
}
private function do404($error){
http_response_code(404);
header("Content-Type: text/plain");
header("X-Error: $error");
die();
}
}

1
data/api_keys/yep.txt Normal file
View File

@@ -0,0 +1 @@
# Paste Yep API keys here

View File

@@ -23,6 +23,22 @@ class config{
// Enable the API?
const API_ENABLED = true;
//
// 4play settings
//
// Enable 4get's /api/v2/provide_sesh endpoint
const FPLAY_ENABLE_API = false;
// 4play password. Please set this to something secure if you use this extension.
// This password is used to POST sessions to /api/v2/provide_sesh
// It's also used as the external endpoint password.
const FPLAY_PASSWORD = "cnc";
// Endpoint 4get hits to render webpages
// Eg: null=no endpoint provided
// Or "https://your-endpoint.com/my-endpoint" (excluding get parameters like &pw)
const FPLAY_EXTERNAL_ENDPOINT = null;
//
// BOT PROTECTION
//
@@ -118,10 +134,10 @@ class config{
// Default user agent to use for scraper requests. Sometimes ignored to get specific webpages
// Changing this might break things.
const USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0";
const USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:153.0) Gecko/20100101 Firefox/153.0";
// User agent to use with 4get-friendly APIs
const USER_AGENT_FRIENDLY = "4get-scrapist";
const USER_AGENT_FRIENDLY = "4get-scrapist (+https://4get.ca)";
// Proxy pool assignments for each scraper
// false = Use server's raw IP
@@ -131,7 +147,6 @@ class config{
const PROXY_YAHOO = false;
const PROXY_YAHOO_JAPAN = false;
const PROXY_BRAVE = false;
const PROXY_FB = false; // facebook
const PROXY_GOOGLE = false;
const PROXY_GOOGLE_API = false;
const PROXY_GOOGLE_CSE = false;
@@ -146,6 +161,7 @@ class config{
const PROXY_SWISSCOWS = false;
const PROXY_SPOTIFY = false;
const PROXY_SOLOFIELD = false;
const PROXY_PURILI = false;
const PROXY_WIBY = false;
const PROXY_CURLIE = false;
const PROXY_YT = false; // youtube
@@ -155,7 +171,6 @@ class config{
const PROXY_VIMEO = false;
const PROXY_YEP = false;
const PROXY_PINTEREST = false;
const PROXY_SANKAKUCOMPLEX = false;
const PROXY_FLICKR = false;
const PROXY_PIXABAY = false;
const PROXY_UNSPLASH = false;
@@ -164,8 +179,6 @@ class config{
const PROXY_VSCO = false;
const PROXY_SEZNAM = false;
const PROXY_NAVER = false;
const PROXY_GREPPR = false;
const PROXY_CROWDVIEW = false;
const PROXY_MWMBL = false;
const PROXY_FTM = false; // findthatmeme
const PROXY_IMGUR = false;
@@ -173,6 +186,11 @@ class config{
const PROXY_YANDEX_W = false; // yandex web
const PROXY_YANDEX_I = false; // yandex images
const PROXY_YANDEX_V = false; // yandex videos
const PROXY_SAFEBOORU = false;
const PROXY_KONACHAN = false;
const PROXY_YANDERE = false;
const PROXY_TBIB = false;
const PROXY_GELBOORU = false;
//
// Scraper-specific parameters
@@ -185,4 +203,7 @@ class config{
// Use "null" to default out to HTML scraping OR specify a string to
// use the API (Eg: "public"). API has less filters.
const MARGINALIA_API_KEY = null;
// Yep
const YEP_USE_API = false;
}

View File

@@ -49,9 +49,13 @@ Looking for the apache2 guide? <a href="https://git.lolcat.ca/lolcat/4get/src/br
</Directory>
# deny access to private resources
<Directory /var/www/4get/data/>
Order Deny,allow
Deny from all
<DirectoryMatch "^/var/www/4get/(data|lib|scraper|template|oracles|docs|docker|extra)/">
Require all denied
</DirectoryMatch>
# remove directory listing from /icons
<Directory /var/www/4get/icons>
Options -Indexes
</Directory>
</VirtualHost>

View File

@@ -104,9 +104,13 @@ Now, edit the following file: `/etc/apache2/sites-available/000-default.conf`, r
</Directory>
# deny access to private resources
<Directory /var/www/4get/data/>
Order Deny,allow
Deny from all
<DirectoryMatch "^/var/www/4get/(data|lib|scraper|template|oracles|docs|docker|extra)/">
Require all denied
</DirectoryMatch>
# remove directory listing from /icons
<Directory /var/www/4get/icons>
Options -Indexes
</Directory>
</VirtualHost>
```

View File

@@ -79,3 +79,87 @@ Done! The scraper you chose should now be using the rotating proxies. When askin
## Important!
If you ever test out a `socks5` proxy locally on your machine and find out it works but doesn't on your server, try supplying the `socks5_hostname` protocol instead. Hopefully this tip can save you 3 hours of your life!
# 4play setup
4play is a Firefox extension I wrote that lets you control a browser from 4get. When you do a search, 4get sends a query to a nodeJS http server that communicates to the browser via websockets. The browser then reports back various payloads used for scraping. It's very similar to tools like puppeteer or playwright, except it uses websockets to communicate with the browser instead of the browser's debugger. This is needed to prevent `navigator.webdriver` from being set to `true` (also avoids a fuckton of other botchecks, that's just the tip of the iceberg).
## Requirements
- A dedicated laptop or computer running a full desktop environment (like Windows, Mac, or Linux DEs)
- Something to make the computer think it's connected to a screen. You need it, otherwise you'll fallback to software rendering and sites can detect that. You can use:
- Your laptop's screen
- A turned-on screen connected via HDMI
- An EDID adapter to simulate a display (no affiliate links: [HDMI](https://www.aliexpress.com/item/1005011668390064.html), [DisplayPort](https://www.aliexpress.com/item/1005011659561729.html))
- Firefox ESR or the latest version of Firefox
- The [4play extension](https://addons.mozilla.org/en-US/firefox/addon/4play/) installed
## Installation
Install the dependencies on the machine you wish to run 4play on. In a sane setup, you usually want 4play to run on a dedicated user-grade machine. For the sake of simplicity, this machine will be referred as the "render server".
```sh
git clone https://git.lolcat.ca/lolcat/4get
cd 4get/extra/4play
# install nodejs 26.x or later if it's not already installed
npm install @lawlers/4play
sudo npm install -g nodemon
# edit line 7 where it says 'var password = "cnc";'. Use a strong password.
nano page-render.js
# test the server
nodemon page-render.js
```
I recommend setting up a systemd service to auto-start the 4play server:
```sh
sudo nano /etc/systemd/system/fplay.service
```
Paste this in (make sure to update `User` & `WorkingDirectory`)
```sh
[Unit]
Description=4play
After=network.target
[Service]
Type=simple
User=will
WorkingDirectory=/home/will/Desktop/4get/extra/4play/
ExecStart=nodemon /home/will/Desktop/4get/extra/4play/page-render.js
Restart=always
Environment=NODE_ENV=production
[Install]
WantedBy=multi-user.target
```
Now, enable the service
```sh
sudo systemctl enable fplay
sudo service fplay start
sudo service fplay status # check the status. You should see it running
```
This should expose a webservice located at `http://localhost:3000`. I highly recommend you put this endpoint behind apache2 or nginx to add SSL, if the render server sits on a different network.
## Setting up Firefox
- Install Firefox. You can go for the ESR version or the latest version. You'll figure it out.
- Head over to [this page](https://addons.mozilla.org/en-US/firefox/addon/4play/) or search "4play" on the Firefox extension store and install the extension.
- Click the settings cog next to the extension name. Enable "Run in Private Windows" and "Allow automatic updates"
- Click the extension in the top bar. Modify the `ws://localhost:3030/cnc` URL: replace the `cnc` path with the password you set earlier for the server.
- Use a timeout value of `30000` to avoid getting throttled by Firefox during reconnections.
If done correctly, you should see the red dot turn green in the extension's icon. You're ready to load pages!
## Setting up 4get
Now, you need to tell 4get to use the 4play service. Edit `data/config.php` and change these:
```php
const FPLAY_PASSWORD = "cnc"; # Change this to your password
const FPLAY_EXTERNAL_ENDPOINT = null; # Change null to "https://your-endpoint.com/api/example";, with the quotes and semicolon
```
**IMPORTANT:** By default, `localhost:3030` is what Firefox connects to. The render server is exposed at `localhost:3000`. Don't mix them up, otherwise you will get errors about an "invalid Upgrade header".
If everything was done correctly, you should be seeing results on the Google scraper! You can set proxies in `data/config.php` for it, just like any other scraper.

7
docs/develop.md Normal file
View File

@@ -0,0 +1,7 @@
# Develop
Start the PHP webserver like this, assuming you have curl-impersonate installed already
```sh
LD_PRELOAD=/usr/local/lib/libcurl-impersonate-ff.so CURL_IMPERSONATE=ff117 php -S localhost:8000
```

View File

@@ -150,3 +150,7 @@ services:
##### Tor
You can route incoming and outgoing requests through tor by following [docker tor documentation](./docker_tor.md)
##### 4play
You can run the 4play sidecar by following the [docker 4play guide](./docker_4play.md)

48
docs/docker_4play.md Normal file
View File

@@ -0,0 +1,48 @@
### Running 4get and 4play via docker guide
This setup will run both 4get and the 4play sidecar
# Docker compose
```
# docker-compose.yaml
services:
fourget:
image: luuul/4get:latest
restart: unless-stopped
environment:
- FOURGET_PROTO=http
- FOURGET_SERVER_NAME=4get.ca
- FOURGET_FPLAY_EXTERNAL_ENDPOINT=http://fourplay:3000
- FOURGET_FPLAY_PASSWORD=cnc
- FOURGET_FPLAY_TIMEOUT=30000
- FOURGET_FPLAY_ENABLE_API=true
ports:
- "80:80"
fourplay:
image: luuul/4get-4play:latest
environment:
- FOURPLAY_PORT=3030
- FOURPLAY_PASSWORD=cnc
- FOURPLAY_COMMAND_TIMEOUT=5000
ports:
- "3000:3000"
- "3030:3030"
```
There is a example docker compose file available [here](../extra/4play/docker-compose.yaml)
# Steps
1. start the services with
```
docker compose up -d
```
2. configure firefox
folllow the steps in [configure](./configure.md) for firefox setup

19
extra/4play/Dockerfile Normal file
View File

@@ -0,0 +1,19 @@
FROM alpine:3.21
WORKDIR /app
RUN apk update && apk upgrade
RUN apk add nodejs npm
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
COPY . .
EXPOSE 3030
ENTRYPOINT ["node"]
CMD ["page-render.js"]

View File

@@ -0,0 +1,24 @@
# docker-compose.yaml
services:
fourget:
image: luuul/4get:latest
restart: unless-stopped
environment:
- FOURGET_PROTO=http
- FOURGET_SERVER_NAME=4get.ca
- FOURGET_FPLAY_EXTERNAL_ENDPOINT=http://fourplay:3000
- FOURGET_FPLAY_PASSWORD=cnc
- FOURGET_FPLAY_TIMEOUT=30000
- FOURGET_FPLAY_ENABLE_API=true
ports:
- "80:80"
fourplay:
image: luuul/4get-4play:latest
environment:
- FOURPLAY_PORT=3030
- FOURPLAY_PASSWORD=cnc
- FOURPLAY_COMMAND_TIMEOUT=5000
ports:
- "3000:3000"
- "3030:3030"

47
extra/4play/package-lock.json generated Normal file
View File

@@ -0,0 +1,47 @@
{
"name": "4play",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "4play",
"version": "1.0.0",
"license": "AGPL-3.0-only",
"dependencies": {
"@lawlers/4play": "^1.2.5",
"ws": "^8.21.2"
}
},
"node_modules/@lawlers/4play": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/@lawlers/4play/-/4play-1.2.5.tgz",
"integrity": "sha512-mHxCROszXwW3XWdE+OQldJcAo3Qs9bI20VME/ZiPUvz3G2kdJaDbrMSzSwVlHmju+3dydupIE48t/ektKtewhg==",
"license": "AGPL-3.0-only",
"dependencies": {
"ws": "^8.21.1"
}
},
"node_modules/ws": {
"version": "8.21.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.2.tgz",
"integrity": "sha512-54dMVAo4WIe6SKy3vBgN+9bJZqqQ8IMRevAkOLQALhi49qkkQDQfWdAZ8KQlXiEabw88ARXXdUrlvtbKQX+aKw==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
}
}
}

20
extra/4play/package.json Normal file
View File

@@ -0,0 +1,20 @@
{
"name": "4play",
"version": "1.0.0",
"description": "",
"repository": {
"type": "git",
"url": "https://git.lolcat.ca/lolcat/4get/"
},
"license": "AGPL-3.0-only",
"author": "lolcat",
"type": "commonjs",
"main": "page-render.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"@lawlers/4play": "^1.2.5",
"ws": "^8.21.2"
}
}

337
extra/4play/page-render.js Normal file
View File

@@ -0,0 +1,337 @@
const fplay = require("@lawlers/4play");
const http = require("http");
// 4play settings
var port = parseInt(process.env.FOURGET_FPLAY_PASSWORD) || 3030;
var timeout = parseInt(process.env.FOURGET_FPLAY_TIMEOUT) || 30000;
var password = process.env.FOURGET_FPLAY_PASSWORD || "cnc";
// ==================================================
// webserver
var global_ws = null;
const webserver_port = 3000;
var callback_q = new Map();
const server = http.createServer(async function(req, res){
const parsed = new URL(req.url, "http://localhost");
const url = parsed.searchParams.get("url");
const pw = parsed.searchParams.get("pw");
var proxy = parsed.searchParams.get("proxy");
const url_match = parsed.searchParams.get("url_match");
const content_match = parsed.searchParams.get("content_match");
const fail_url_match = parsed.searchParams.get("fail_url_match");
const fail_content_match = parsed.searchParams.get("fail_content_match");
var container = parsed.searchParams.get("container");
if(fplay.browser_connected === false){
res.writeHead(
400,
{
"Content-Type": "text/plain"
}
);
res.end("No browser available to render the motherfucking page");
return;
}
var errors = [];
if(
pw === null ||
pw != password
){
errors.push("pw");
}
if(url === null){ errors.push("url"); }
if(proxy === null){ errors.push("proxy"); }
if(errors.length !== 0){
res.writeHead(
400,
{
"Content-Type": "text/plain"
}
);
res.end("The following parameters are missing or invalid: " + errors.join(", "));
return;
}
console.log("Rendering " + url);
if(
container === null ||
await fplay.container_exists(global_ws, container) === false
){
container = await fplay.container_create(global_ws);
proxy = get_proxy(proxy);
// assign proxy
if(proxy.config !== false){
await fplay.container_attach_proxy(
global_ws,
container,
proxy.config
);
}
}else{
proxy = {raw: proxy};
}
// open tab
const newtab = await fplay.tab_open(global_ws, url, false, container);
if(newtab === false){
// failed to open tab
res.writeHead(
400,
{
"Content-Type": "text/plain",
"X-Proxy": proxy.raw,
"X-Container-ID": typeof container.id == "undefined" ? container : container.id
}
);
res.end("Failed to open tab");
return;
}
// callback will be handled in "web_response"
callback_q.set(
newtab.id,
{
http_res: res,
proxy: proxy,
container: container,
url_match: url_match,
content_match: content_match,
fail_url_match: fail_url_match,
fail_content_match: fail_content_match
}
);
// close tab in 30 seconds
await fplay.wait_random(15000, 35000);
await fplay.tab_close(global_ws, newtab);
});
server.listen(webserver_port, function(){
console.log("http server running");
});
function get_proxy(proxy_line){
parts = proxy_line.split(":");
// format: type:address:port:username:password
// PHP explode(":", $ip, 5) splits into max 5 parts
var type = parts[0];
const address = parts[1];
const port = parts[2];
const username = parts[3];
const password = parts[4];
var proxy_dns = true;
switch(type){
case "raw_ip":
case "ip":
// no proxy, use direct connection
return {
config: false,
raw: "raw_ip::::"
}
break;
case "socks5":
type = "socks";
proxy_dns = false;
break;
case "socks4a":
type = "socks4";
break;
case "socks4":
proxy_dns = false;
break;
case "socks5_hostname":
case "socks5h":
case "socks5a":
type = "socks";
break;
case "http":
case "https":
// do nothing
break;
default:
// invalid proxy provided
return {
config: false,
raw: "raw_ip::::"
}
}
return {
config: {
type: type,
host: address,
port: port,
username: username,
password: password,
proxyDNS: proxy_dns
},
raw: proxy_line
};
}
fplay.event.on("server_ready", function(){
console.log("listening on port " + port + " (timeout=" + timeout + ")");
});
fplay.event.on("browser_connect", async function(ws){
global_ws = ws;
const ua = await fplay.get_ua(ws);
console.log("New browser instance connected: " + ua);
await fplay.close_all_tabs(ws);
await fplay.delete_all_containers(ws);
fplay.web_response_whitelist(ws, ["main_frame"]);
// clean up
const blanktab = await fplay.close_all_tabs(ws);
await fplay.delete_all_containers(ws);
});
fplay.event.on("web_response", async function(page){
var callback_data = callback_q.get(page.id);
if(typeof callback_data == "undefined"){ return; }
var body = page.body.toString();
var url_match = callback_data.url_match !== null ? page.url.match(new RegExp(callback_data.url_match, "i")) : null;
var content_match = callback_data.content_match !== null ? body.match(new RegExp(callback_data.content_match, "i")) : null;
var fail_url_match = callback_data.fail_url_match !== null ? page.url.match(new RegExp(callback_data.fail_url_match, "i")) : null;
var fail_content_match = callback_data.fail_content_match !== null ? body.match(new RegExp(callback_data.fail_content_match, "i")) : null;
// handle negative response
if(
(
callback_data.fail_url_match !== null ||
callback_data.fail_content_match !== null
) &&
(
fail_url_match !== null ||
fail_content_match !== null
)
){
callback_data.http_res.writeHead(
400,
{
"Content-Type": "text/plain",
"X-Proxy": callback_data.proxy.raw,
"X-Container-ID": callback_data.container.id
}
);
callback_data.http_res.end("Reached fail state (matched fail_url_match or fail_content_match)");
// cleanup browser + callback
await fplay.tab_close(global_ws, page.id);
await fplay.container_delete(global_ws, callback_data.container);
callback_q.delete(page.id);
return;
}
// handle positive response
if(
(
callback_data.url_match === null &&
callback_data.content_match === null
) ||
(
callback_data.url_match !== null &&
callback_data.content_match !== null &&
url_match &&
content_match
) ||
(
callback_data.url_match === null &&
callback_data.content_match !== null &&
content_match
) ||
(
callback_data.url_match !== null &&
callback_data.content_match === null &&
url_match
)
){
callback_data.http_res.writeHead(
200,
{
"Content-Type": "text/plain;charset=UTF-8",
"X-Proxy": callback_data.proxy.raw,
"X-Container-ID": typeof callback_data.container.id == "undefined" ? callback_data.container : callback_data.container.id
}
);
callback_data.http_res.end(body);
callback_q.delete(page.id); // cleanup callback only
return;
}
});
fplay.event.on("dom_load_fail", async function(page){
var callback_data = callback_q.get(page.id);
if(typeof callback_data == "undefined"){ return; }
callback_q.delete(page.id);
callback_data.http_res.writeHead(
400,
{
"Content-Type": "text/plain",
"X-Proxy": callback_data.proxy.raw,
"X-Container-ID": typeof callback_data.container.id == "undefined" ? callback_data.container : callback_data.container.id
}
);
callback_data.http_res.end("Network error (" + page.error + ")");
// cleanup browser
await fplay.tab_close(global_ws, page.id);
await fplay.container_delete(global_ws, callback_data.container);
});
fplay.init(port, password, timeout);

View File

@@ -4,6 +4,9 @@ class backend{
public function __construct($scraper){
$this->scraper = $scraper;
$this->db = null;
$this->db_path_prefix = "data/sessions/";
$this->db_path = "{$this->db_path_prefix}{$scraper}.sqlite";
}
/*
@@ -208,4 +211,127 @@ class backend{
$payload[1] // proxy
];
}
//
// Sessions
//
public function session_init_db($force_init = false){
if(
$force_init ||
$this->db === null
){
$this->db = new SQLite3($this->db_path);
// create table if it doesnt exist
$this->db->exec(
"CREATE TABLE IF NOT EXISTS sessions ( " .
"id INTEGER PRIMARY KEY AUTOINCREMENT, " .
"created_at INTEGER NOT NULL, " .
"proxy TEXT NOT NULL, " .
"headers_json TEXT NOT NULL" .
")"
);
return true;
}
return false;
}
public function session_count(){
$this->session_init_db();
$stmt = $this->db->prepare(
"SELECT COUNT(*) as count FROM sessions"
);
$result = $stmt->execute();
$row = $result->fetchArray(SQLITE3_ASSOC);
return (int)($row["count"] ?? 0);
}
public function session_write($proxy, $headers){
$this->session_init_db();
$stmt = $this->db->prepare(
"INSERT INTO sessions ( " .
"created_at, " .
"proxy, " .
"headers_json " .
") VALUES ( " .
":created_at, " .
":proxy, " .
":headers_json" .
")"
);
$stmt->bindValue(':created_at', time(), SQLITE3_INTEGER);
$stmt->bindValue(':proxy', $proxy, SQLITE3_TEXT);
$stmt->bindValue(':headers_json', json_encode($headers), SQLITE3_TEXT);
$stmt->execute();
return $this->db->lastInsertRowID();
}
public function session_get(){
$count = $this->session_count();
if($count === 0){
return false;
}
$offset = apcu_inc("d." . $this->scraper) % $count;
$stmt = $this->db->prepare(
"SELECT * " .
"FROM sessions " .
"ORDER BY id ASC " .
"LIMIT 1 OFFSET :offset"
);
$stmt->bindValue(':offset', $offset, SQLITE3_INTEGER);
$result = $stmt->execute();
$row = $result->fetchArray(SQLITE3_ASSOC);
return [
"id" => $row["id"],
"created_at" => $row["created_at"],
"proxy" => $row["proxy"],
"headers" => json_decode($row["headers_json"], true)
];
}
public function session_destroy_list(){
if(
!preg_match(
'/^[A-Za-z0-9-]+$/',
$this->scraper
)
){
// path traversal
return false;
}
if(
file_exists("{$this->db_path_prefix}{$this->scraper}.sqlite") &&
is_file("{$this->db_path_prefix}{$this->scraper}.sqlite")
){
unlink("{$this->db_path_prefix}{$this->scraper}.sqlite");
return true;
}
return false;
}
}

View File

@@ -634,6 +634,7 @@ class frontend{
$filters["scraper"] = [
"display" => "Scraper",
"option" => [
//"fget" => "fget",
"ddg" => "DuckDuckGo",
//"yahoo" => "Yahoo!",
"brave" => "Brave",
@@ -644,14 +645,15 @@ class frontend{
"yahoo_japan" => "Yahoo! JAPAN",
"startpage" => "Startpage",
"qwant" => "Qwant",
"ghostery" => "Ghostery",
"yep" => "Yep",
"mwmbl" => "Mwmbl",
"mojeek" => "Mojeek",
"naver" => "Naver",
"baidu" => "Baidu",
"coccoc" => "Cốc Cốc",
"solofield" => "Solofield",
"marginalia" => "Marginalia",
"purili" => "Purili",
"wiby" => "wiby"
]
];
@@ -670,6 +672,7 @@ class frontend{
"yahoo_japan" => "Yahoo! JAPAN",
"startpage" => "Startpage",
"qwant" => "Qwant",
"naver" => "Naver",
"baidu" => "Baidu",
"solofield" => "Solofield",
"pinterest" => "Pinterest",
@@ -703,8 +706,10 @@ class frontend{
"yahoo_japan" => "Yahoo! JAPAN",
"startpage" => "Startpage",
"qwant" => "Qwant",
"naver" => "Naver",
"baidu" => "Baidu",
"coccoc" => "Cốc Cốc",
"purili" => "Purili",
"solofield" => "Solofield"
]
];

View File

@@ -666,14 +666,24 @@ class baidu{
//
// Don't parse as a search result if it's a card
//
$card =
$tpl_probe =
$this->fuckhtml
->getElementsByClassName(
"cosc-card",
$div
->getElementsByAttributeValue(
"tpl",
"www_index",
[$datafield]
);
if(count($card) !== 0){
if(
count($tpl_probe) === 0 &&
count(
$this->fuckhtml
->getElementsByClassName(
"cosc-card",
$div
)
) !== 0
){
//
// Parse chinese youtube shorts
@@ -870,10 +880,13 @@ class baidu{
// class:FYB_RD -> News garbage, IGNORE
$result =
$this->fuckhtml
->getElementsByClassName(
"result",
[$datafield]
array_merge(
$tpl_probe,
$this->fuckhtml
->getElementsByClassName(
"result",
[$datafield]
)
);
if(count($result) !== 0){
@@ -885,7 +898,7 @@ class baidu{
$title =
$this->fuckhtml
->getElementsByClassName(
"sc-link",
"cosc-title-a",
"a"
);
@@ -905,8 +918,9 @@ class baidu{
$description =
$this->fuckhtml
->getElementsByClassName(
"c-color",
->getElementsByAttributeValue(
"data-sanssr-cmpt",
"card/www-summary-1",
$div
);

View File

@@ -388,14 +388,14 @@ class brave{
*/
$nextpage =
$this->fuckhtml
->getElementById(
->getElementsByClassName(
"pagination",
"div"
);
if($nextpage){
if(count($nextpage) !== 0){
$this->fuckhtml->load($nextpage);
$this->fuckhtml->load($nextpage[0]);
$nextpage =
$this->fuckhtml
@@ -1773,14 +1773,14 @@ class brave{
$nextpage =
$this->fuckhtml
->getElementById(
->getElementsByClassName(
"pagination",
"div"
);
if($nextpage){
if(count($nextpage) !== 0){
$this->fuckhtml->load($nextpage);
$this->fuckhtml->load($nextpage[0]);
$nextpage =
$this->fuckhtml

View File

@@ -54,8 +54,8 @@ class ddg{
$headers =
["User-Agent: " . config::USER_AGENT,
"Accept: */*",
"Accept-Language: en-US,en;q=0.5",
"Accept-Encoding: gzip",
"Accept-Language: en-US,en;q=0.9",
"Accept-Encoding: gzip, deflate, br, zstd",
"Referer: https://duckduckgo.com/",
"DNT: 1",
"Sec-GPC: 1",
@@ -492,6 +492,17 @@ class ddg{
"form"
);
foreach($forms as $form){
if(
isset($form["attributes"]["id"]) &&
$form["attributes"]["id"] == "challenge-form"
){
throw new Exception("DuckDuckGo returned an image captcha");
}
}
foreach(array_reverse($forms) as $form){
$this->fuckhtml->load($form);
@@ -909,112 +920,6 @@ class ddg{
){
throw new Exception("DuckDuckGo returned a JSA challenge");
// get JSA initial token
if(
!preg_match(
'/let jsa *= *([0-9]+)/',
$js,
$jsa
)
){
$jsa = 0;
}else{
$jsa = (int)$jsa[1];
}
// get function bodies
preg_match_all(
'/let *([A-Za-z0-9]+) *= *function\(.*\) *{(.*)};/sU',
$js,
$functions
);
$parsed_functions = [];
for($i=0; $i<count($functions[0]); $i++){
$functions[2][$i] = trim($functions[2][$i]);
if(
preg_match(
'/return num *\* *([0-9]+)/i',
$functions[2][$i],
$num
)
){
$parsed_functions[$functions[1][$i]] = [
"type" => "multiplication",
"num" => (int)$num[1]
];
continue;
}
if(
preg_match(
'/innerHTML *= *`([^`]+)`/i',
$functions[2][$i],
$challenge
)
){
$challenge[1] =
preg_replace(
'/<\/(br)>/',
'<$1>',
$challenge[1]
);
$parsed_functions[$functions[1][$i]] = [
"type" => "challenge",
"text" => $challenge[1]
];
}
}
// get function call order
preg_match_all(
'/jsa *= *([A-Za-z0-9]+)\(jsa\)/i',
$js,
$call_order
);
foreach($call_order[1] as $order){
if(!isset($parsed_functions[$order])){
throw new Exception("JS challenge solve failure: DuckDuckGo called an unknown function");
}
if($parsed_functions[$order]["type"] == "multiplication"){
$jsa = $jsa * $parsed_functions[$order]["num"];
continue;
}
if($parsed_functions[$order]["type"] == "challenge"){
// @TODO get parsed length
//$parsed_functions[$order]["text"]
$jsa = $jsa + strlen($parsed_functions[$order]["text"]);
}
}
try{
$js = $this->get(
$proxy,
"https://links.duckduckgo.com" . $challenge_url[1] . $jsa,
[],
ddg::req_xhr
);
}catch(Exception $error){
throw new Exception("Failed to get challenged d.js");
}
}
//
@@ -1858,7 +1763,7 @@ class ddg{
if(count($answer) !== 0){
$out["answer"][] = [
"title" => ucfirst($word),
"title" => ucfirst(rawurldecode($word)),
"description" => $answer,
"url" => "https://www.wordnik.com/words/" . $word,
"thumb" => null,

View File

@@ -1,320 +0,0 @@
<?php
class ghostery{
public function __construct(){
include "lib/backend.php";
$this->backend = new backend("ghostery");
include "lib/fuckhtml.php";
$this->fuckhtml = new fuckhtml();
}
public function getfilters($page){
if($page != "web"){
return [];
}
return [
"country" => [
"display" => "Country",
"option" => [
"any" => "All regions",
"AR" => "Argentina",
"AU" => "Australia",
"AT" => "Austria",
"BE" => "Belgium",
"BR" => "Brazil",
"CA" => "Canada",
"CL" => "Chile",
"DK" => "Denmark",
"FI" => "Finland",
"FR" => "France",
"DE" => "Germany",
"HK" => "Hong Kong",
"IN" => "India",
"ID" => "Indonesia",
"IT" => "Italy",
"JP" => "Japan",
"KR" => "Korea",
"MY" => "Malaysia",
"MX" => "Mexico",
"NL" => "Netherlands",
"NZ" => "New Zealand",
"NO" => "Norway",
"CN" => "People's Republic of China",
"PL" => "Poland",
"PT" => "Portugal",
"PH" => "Republic of the Philippines",
"RU" => "Russia",
"SA" => "Saudi Arabia",
"ZA" => "South Africa",
"ES" => "Spain",
"SE" => "Sweden",
"CH" => "Switzerland",
"TW" => "Taiwan",
"TR" => "Turkey",
"GB" => "United Kingdom",
"US" => "United States"
]
]
];
}
private function get($proxy, $url, $get = [], $country){
$curlproc = curl_init();
if($get !== []){
$get = http_build_query($get);
$url .= "?" . $get;
}
curl_setopt($curlproc, CURLOPT_URL, $url);
curl_setopt($curlproc, CURLOPT_ENCODING, ""); // default encoding
curl_setopt($curlproc, CURLOPT_HTTPHEADER,
["User-Agent: " . config::USER_AGENT,
"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
"Accept-Language: en-US,en;q=0.5",
"Accept-Encoding: gzip",
"Referer: https://ghosterysearch.com",
"DNT: 1",
"Sec-GPC: 1",
"Connection: keep-alive",
"Cookie: ctry=" . ($country == "any" ? "--" : $country) . "; noads=true",
"Upgrade-Insecure-Requests: 1",
"Sec-Fetch-Dest: document",
"Sec-Fetch-Mode: navigate",
"Sec-Fetch-Site: same-origin",
"Sec-Fetch-User: ?1",
"Priority: u=0, i"]
);
// http2 bypass
curl_setopt($curlproc, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0);
curl_setopt($curlproc, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curlproc, CURLOPT_SSL_VERIFYHOST, 2);
curl_setopt($curlproc, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($curlproc, CURLOPT_CONNECTTIMEOUT, 30);
curl_setopt($curlproc, CURLOPT_TIMEOUT, 30);
$this->backend->assign_proxy($curlproc, $proxy);
$data = curl_exec($curlproc);
if(curl_errno($curlproc)){
throw new Exception(curl_error($curlproc));
}
curl_close($curlproc);
return $data;
}
public function web($get){
if($get["npt"]){
[$query, $proxy] = $this->backend->get($get["npt"], "web");
parse_str($query, $query);
// country
$country = $query["c"];
unset($query["c"]);
$query = http_build_query($query);
try{
$html =
$this->get(
$proxy,
"https://ghosterysearch.com/search?" . $query,
[],
$country
);
}catch(Exception $error){
throw new Exception("Failed to fetch search page");
}
}else{
$proxy = $this->backend->get_ip();
try{
$html =
$this->get(
$proxy,
"https://ghosterysearch.com/search",
[
"q" => $get["s"]
],
$get["country"]
);
}catch(Exception $error){
throw new Exception("Failed to fetch search page");
}
}
$out = [
"status" => "ok",
"spelling" => [
"type" => "no_correction",
"using" => null,
"correction" => null
],
"npt" => null,
"answer" => [],
"web" => [],
"image" => [],
"video" => [],
"news" => [],
"related" => []
];
$this->fuckhtml->load($html);
$results_wrapper =
$this->fuckhtml
->getElementsByClassName(
"results",
"section"
);
if(count($results_wrapper) === 0){
throw new Exception("Failed to grep result section");
}
$this->fuckhtml->load($results_wrapper[0]);
// get search results
$results =
$this->fuckhtml
->getElementsByClassName(
"result",
"li"
);
if(count($results) === 0){
return $out;
}
foreach($results as $result){
$this->fuckhtml->load($result);
$a =
$this->fuckhtml
->getElementsByClassName(
"url",
"a"
);
if(count($a) === 0){
continue;
}
$a = $a[0];
$out["web"][] = [
"title" =>
$this->titledots(
$this->fuckhtml
->getTextContent(
$this->fuckhtml
->getElementsByTagName(
"h2"
)[0]
)
),
"description" =>
$this->titledots(
$this->fuckhtml
->getTextContent(
$this->fuckhtml
->getElementsByTagName(
"p"
)[0]
)
),
"url" =>
$this->fuckhtml
->getTextContent(
$a
["attributes"]
["href"]
),
"date" => null,
"type" => "web",
"thumb" => [
"url" => null,
"ratio" => null
],
"sublink" => [],
"table" => []
];
}
$this->fuckhtml->load($html);
// get pagination token
$pagination_wrapper =
$this->fuckhtml
->getElementsByClassName(
"pagination",
"div"
);
if(count($pagination_wrapper) !== 0){
// found next page!
$this->fuckhtml->load($pagination_wrapper[0]);
$a =
$this->fuckhtml
->getElementsByTagName(
"a"
);
if(count($a) !== 0){
$q =
parse_url(
$this->fuckhtml
->getTextContent(
$a[count($a) - 1]
["attributes"]
["href"]
),
PHP_URL_QUERY
);
$out["npt"] =
$this->backend
->store(
$q . "&c=" . $get["country"],
"web",
$proxy
);
}
}
return $out;
}
private function titledots($title){
return trim($title, " .\t\n\r\0\x0B");
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -910,11 +910,10 @@ class google_cse{
"image" => []
];
// detect next page
if(
isset($json["cursor"]["isExactTotalResults"]) || // detects last page
!isset($json["cursor"]["pages"]) // detects no results on page
){
// A response can contain a final page of image results while also marking
// the cursor as exact/finished. Parse those results before deciding
// whether another page token should be offered.
if(!isset($json["results"]) || !is_array($json["results"])){
return $out;
}
@@ -939,6 +938,16 @@ class google_cse{
];
}
// Only decide whether a following page exists after preserving every
// result returned on this page.
if(
isset($json["cursor"]["isExactTotalResults"]) || // detects last page
!isset($json["cursor"]["pages"]) // detects no results on page
){
return $out;
}
// get next page
$out["npt"] =
$this->backend->store(

View File

@@ -289,8 +289,18 @@ class marginalia{
throw new Exception("The API key used is rate limited. Please try again in a few minutes.");
}
if(strpos($json, "<title>Bad Gateway (502)</title>")){
throw new Exception("Marginalia returned a 502 error");
}
$json = json_decode($json, true);
if($json === null){
throw new Exception("Failed to decode JSON");
}
foreach($json["results"] as $result){
$out["web"][] = [
@@ -490,6 +500,37 @@ class marginalia{
return $this->web($get, $redirect);
}
// detect internal error
$infobox =
$this->fuckhtml
->getElementsByClassName("infobox");
if(count($infobox) !== 0){
foreach($infobox as $i){
$this->fuckhtml->load($i);
$h2 =
$this->fuckhtml
->getElementsByTagName("h2");
if(
count($h2) !== 0 &&
$this->fuckhtml
->getTextContent(
$h2[0]
) == "Internal error"
){
throw new Exception("Marginalia returned an internal server error");
}
}
// reset
$this->fuckhtml->load($html);
}
$sections =
$this->fuckhtml
->getElementsByClassName(

View File

@@ -340,7 +340,7 @@ class mojeek{
"zm" => "Zambia",
"zw" => "Zimbabwe"
]
],
],/*
"region" => [
"display" => "Region",
"option" => [
@@ -350,7 +350,7 @@ class mojeek{
"fr" => "France",
"uk" => "United Kingdom"
]
],
],*/
"domain" => [
"display" => "Results per domain",
"option" => [
@@ -376,7 +376,7 @@ class mojeek{
$headers = [
"User-Agent: " . config::USER_AGENT,
"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
"Accept-Language: en-US,en;q=0.5",
"Accept-Language: en-US,en;q=0.9",
"Accept-Encoding: gzip",
"DNT: 1",
"Connection: keep-alive",
@@ -447,7 +447,7 @@ class mojeek{
$proxy = $this->backend->get_ip();
$lang = $get["lang"];
$country = $get["country"];
$region = $get["region"];
//$region = $get["region"];
$domain = $get["domain"];
$focus = $get["focus"];
@@ -458,7 +458,7 @@ class mojeek{
"date" => 1, // show date
"tlen" => 128, // max length of title
//"dlen" => 511, // max length of description
"arc" => ($country == "any" ? "none" : $country) // location. don't use autodetect!
//"arc" => ($country == "any" ? "none" : $country) // location. don't use autodetect!
];
switch($focus){
@@ -479,11 +479,11 @@ class mojeek{
$params["lb"] = $lang;
}
/*
if($region != "any"){
$params["reg"] = $region;
}
}*/
if($domain != "1"){
@@ -503,6 +503,13 @@ class mojeek{
}
}
if(strlen($html) === 0){
throw new Exception("Mojeek returned an empty page (probably a homepage redirect)");
}
$this->fuckhtml->load($html);
$out = [
"status" => "ok",
"spelling" => [
@@ -519,8 +526,6 @@ class mojeek{
"related" => []
];
$this->fuckhtml->load($html);
$this->detect_block();
$results =
@@ -1168,6 +1173,18 @@ class mojeek{
private function detect_block(){
$captcha =
$this->fuckhtml
->getElementsByClassName(
"captcha-box",
"div"
);
if(count($captcha) !== 0){
throw new Exception("Mojeek returned a captcha");
}
$title =
$this->fuckhtml
->getElementsByTagName(

1107
scraper/naver.php Normal file

File diff suppressed because it is too large Load Diff

350
scraper/purili.php Normal file
View File

@@ -0,0 +1,350 @@
<?php
class purili{
public function __construct(){
include "lib/backend.php";
$this->backend = new backend("purili");
}
public function getfilters($page){
switch($page){
case "web":
return [
"nsfw" => [
"display" => "NSFW",
"option" => [
"yes" => "Yes",
"maybe" => "Maybe",
"no" => "No"
]
],
"language" => [
"display" => "Language",
"option" => [
"any" => "Any language",
"en" => "English only",
"nl" => "Dutch preferred",
"de" => "German preferred",
"fr" => "French preferred",
"es" => "Spanish preferred",
"it" => "Italian preferred",
"pt" => "Portuguese preferred"
]
]
];
break;
case "videos":
return [];
break;
}
}
private function get($proxy, $url, $get = [], $nsfw = null, $language = null){
$curlproc = curl_init();
switch($nsfw){
case "yes":
case null:
$get["fv"] = "0";
$nsfw = "off";
break;
case "maybe":
$get["fv"] = "1";
$nsfw = "moderate";
break;
case "no":
$get["fv"] = "2";
$nsfw = "strict";
break;
}
switch($language){
case "any":
case null:
$language_cookie = "";
break;
default:
$language_cookie = "; purili-language={$language}";
break;
}
if($get !== []){
$get = http_build_query($get);
$url .= "?" . $get;
}
curl_setopt($curlproc, CURLOPT_URL, $url);
curl_setopt($curlproc, CURLOPT_ENCODING, ""); // default encoding
curl_setopt($curlproc, CURLOPT_HTTPHEADER,
["User-Agent: " . config::USER_AGENT,
"Accept: */*",
"Accept-Language: en-US,en;q=0.9",
"Accept-Encoding: gzip, deflate, br, zstd",
"DNT: 1",
"Sec-GPC: 1",
"Connection: keep-alive",
"Cookie: purili-safesearch={$nsfw}{$language_cookie}",
"Sec-Fetch-Dest: empty",
"Sec-Fetch-Mode: cors",
"Sec-Fetch-Site: same-origin",
"Priority: u=4"]
);
curl_setopt($curlproc, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curlproc, CURLOPT_SSL_VERIFYHOST, 2);
curl_setopt($curlproc, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($curlproc, CURLOPT_CONNECTTIMEOUT, 30);
curl_setopt($curlproc, CURLOPT_TIMEOUT, 30);
$this->backend->assign_proxy($curlproc, $proxy);
$data = curl_exec($curlproc);
if(curl_errno($curlproc)){
throw new Exception(curl_error($curlproc));
}
curl_close($curlproc);
return $data;
}
public function web($get){
if($get["npt"]){
[$q, $proxy] = $this->backend->get($get["npt"], "web");
$q = json_decode($q, true);
$nsfw = $q["nsfw"];
unset($q["nsfw"]);
$language = $q["language"];
unset($q["language"]);
}else{
$search = $get["s"];
if(strlen($search) === 0){
throw new Exception("Search term is empty!");
}
$proxy = $this->backend->get_ip();
$q = [
"q" => $search,
"page" => 1,
// added at $this->get()
// "fv" => 0
];
$nsfw = $get["nsfw"];
$language = $get["language"];
}
// https://puri.li/api/search?q=4get&page=1&fv=0
try{
$json = $this->get(
$proxy,
"https://puri.li/api/search",
$q,
$nsfw,
$language
);
}catch(Exception $error){
throw new Exception("Failed to hit API");
}
$json = json_decode($json, true);
if($json === null){
throw new Exception("Failed to decode JSON");
}
if(!isset($json["results"])){
throw new Exception("Failed to access results array");
}
$out = [
"status" => "ok",
"spelling" => [
"type" => "no_correction",
"using" => null,
"correction" => null
],
"npt" => null,
"answer" => [],
"web" => [],
"image" => [],
"video" => [],
"news" => [],
"related" => []
];
foreach($json["results"] as $result){
$out["web"][] = [
"title" => $result["title"],
"description" => $result["description"],
"url" => $result["url"],
"date" => null,
"type" => "web",
"thumb" => [
"url" => null,
"ratio" => null
],
"sublink" => [],
"table" => []
];
}
// get next page
if($json["hasNext"]){
$q["page"]++;
$q["nsfw"] = $nsfw;
$q["language"] = $language;
$out["npt"] =
$this->backend->store(
json_encode($q),
"web",
$proxy
);
}
return $out;
}
public function video($get){
if($get["npt"]){
[$q, $proxy] = $this->backend->get($get["npt"], "videos");
$q = json_decode($q, true);
}else{
$search = $get["s"];
if(strlen($search) === 0){
throw new Exception("Search term is empty!");
}
$proxy = $this->backend->get_ip();
$q = [
"q" => $search,
"offset" => 0,
"limit" => 12
// added at $this->get()
// "fv" => 0
];
}
// https://puri.li/api/videos/search?q=higurashi&limit=12&fv=1
// hidden &offset parameter only accessible through the API
try{
$json = $this->get(
$proxy,
"https://puri.li/api/videos/search",
$q,
null,
null
);
}catch(Exception $error){
throw new Exception("Failed to hit API");
}
$json = json_decode($json, true);
if($json === null){
throw new Exception("Failed to decode JSON");
}
if(!isset($json["results"])){
throw new Exception("Failed to access results array");
}
$out = [
"status" => "ok",
"npt" => null,
"video" => [],
"author" => [],
"livestream" => [],
"playlist" => [],
"reel" => []
];
foreach($json["results"] as $result){
if(isset($result["thumbnail"])){
$thumb = [
"ratio" => "16:9",
"url" => $result["thumbnail"]
];
}else{
$thumb = [
"ratio" => null,
"url" => null
];
}
$out["video"][] = [
"title" => $result["title"],
"description" =>
str_replace(
"\n", " ",
mb_substr(
$result["description"], 0, 255, "utf-8"
)
),
"author" => [
"name" => null,
"url" => isset($result["channel_id"]) ? "https://www.youtube.com/channel/" . $result["channel_id"] : null,
"avatar" => null
],
"date" => strtotime($result["upload_date"]),
"duration" => $result["duration"],
"views" => $result["view_count"],
"thumb" => $thumb,
"url" => $result["url"]
];
}
// get next page
if($json["limit"] + $json["offset"] < $json["total"]){
$q["offset"] = $q["offset"] + $json["limit"];
$out["npt"] =
$this->backend->store(
json_encode($q),
"videos",
$proxy
);
}
return $out;
}
}

View File

@@ -212,28 +212,58 @@ class qwant{
return $base;
}
private function get($proxy, $url, $get = []){
$headers = [
"User-Agent: " . config::USER_AGENT,
"Accept: application/json, text/plain, */*",
"Accept-Language: en-US,en;q=0.5",
"Accept-Encoding: gzip",
"DNT: 1",
"Connection: keep-alive",
"Origin: https://www.qwant.com",
"Referer: https://www.qwant.com/",
"Sec-Fetch-Dest: empty",
"Sec-Fetch-Mode: cors",
"Sec-Fetch-Site: same-site",
"TE: trailers"
];
private function get($proxy, $url, $get = [], $is_post = false, $cookie = null){
$curlproc = curl_init();
if($get !== []){
$get = http_build_query($get);
$url .= "?" . $get;
if($is_post){
$headers = [
"User-Agent: " . config::USER_AGENT,
"Accept: */*",
"Accept-Language: en-US,en;q=0.9",
"Accept-Encoding: gzip, deflate, br, zstd",
"Referer: https://www.qwant.com/",
"Content-type: application/x-www-form-urlencoded",
"Origin: https://www.qwant.com",
"DNT: 1",
"Sec-GPC: 1",
"Connection: keep-alive",
"Sec-Fetch-Dest: empty",
"Sec-Fetch-Mode: cors",
"Sec-Fetch-Site: same-site",
"Pragma: no-cache",
"Cache-Control: no-cache"
];
curl_setopt($curlproc, CURLOPT_POST, true);
curl_setopt($curlproc, CURLOPT_POSTFIELDS, http_build_query($get));
}else{
$headers = [
"User-Agent: " . config::USER_AGENT,
"Accept: application/json, text/plain, */*",
"Accept-Language: en-US,en;q=0.9",
"Accept-Encoding: gzip, deflate, br, zstd",
"Referer: https://www.qwant.com/",
"Origin: https://www.qwant.com",
"DNT: 1",
($cookie !== null ? "Cookie: {$cookie}" : ""),
"Sec-GPC: 1",
"Connection: keep-alive",
"Sec-Fetch-Dest: empty",
"Sec-Fetch-Mode: cors",
"Sec-Fetch-Site: same-site",
"Priority: u=4",
"Pragma: no-cache",
"Cache-Control: no-cache",
"TE: trailers"
];
if($get !== []){
$get = http_build_query($get);
$url .= "?" . $get;
}
}
curl_setopt($curlproc, CURLOPT_URL, $url);
@@ -271,6 +301,8 @@ class qwant{
[$params, $proxy] = $this->backend->get($get["npt"], "web");
$params = json_decode($params, true);
$cookie = $params["cookie"];
unset($params["cookie"]);
}else{
@@ -297,7 +329,8 @@ class qwant{
"device" => "desktop",
"tgp" => 3,
"safesearch" => 0,
"displayed" => "true"
"displayed" => "true",
"llm" => "false"
];
switch($get["nsfw"]){
@@ -306,18 +339,24 @@ class qwant{
case "maybe": $params["safesearch"] = 1; break;
case "no": $params["safesearch"] = 2; break;
}
$cookie = null;
}
/*
$handle = fopen("scraper/qwant_web.json", "r");
$json = fread($handle, filesize("scraper/qwant_web.json"));
fclose($handle);*/
$cookie = $this->fuck_datadome($proxy, $search);
try{
$json =
$this->get(
$proxy,
"https://fdn.qwant.com/v3/search/web",
$params
"https://api.qwant.com/v3/search/web",
$params,
false,
$cookie
);
}catch(Exception $error){
@@ -353,6 +392,8 @@ class qwant{
"related" => []
];
$this->detect_errors($json);
if(
$json["status"] != "success" &&
$json["data"]["error_code"] === 5
@@ -362,8 +403,6 @@ class qwant{
return $out;
}
$this->detect_errors($json);
if(!isset($json["data"]["result"]["items"]["mainline"])){
throw new Exception("Server did not return a result object");
@@ -436,6 +475,7 @@ class qwant{
if($json["data"]["result"]["lastPage"] === false){
$params["offset"] = $params["offset"] + 10;
$params["cookie"] = $cookie;
$out["npt"] =
$this->backend->store(
@@ -577,6 +617,9 @@ class qwant{
);
$params = json_decode($params, true);
$cookie = $params["cookie"];
unset($params["cookie"]);
}else{
$search = $get["s"];
@@ -617,13 +660,19 @@ class qwant{
case "maybe": $params["safesearch"] = 1; break;
case "no": $params["safesearch"] = 2; break;
}
$cookie = null;
}
$cookie = $this->fuck_datadome($proxy, $search);
try{
$json = $this->get(
$proxy,
"https://api.qwant.com/v3/search/images",
$params,
false,
$cookie
);
}catch(Exception $err){
@@ -1001,4 +1050,53 @@ class qwant{
return "https://" . $image["host"] . "/th?id=" . rawurlencode($id);
}
private function fuck_datadome($proxy, $search){
if($cookie === null){
// get datadome bullshit
// yeah go ahead and patch this you code monkey
// ... you know, instead of fixing this shit, can you make the second page of results return more than 1 result?
// i search shit like "higurashi" and i only get 1 result and then just a bunch of related searches ad finitum
// also, stop pretending you guys have your own index, this is clearly all bing results
try{
$datadome =
$this->get(
$proxy,
"https://dd.qwant.com/js/",
[
"jspl" => "-oirHDeP7G3G9LXY3rhxGDYf6t_ZlYPzaiL2dRhQ34uNUc4dO2Td0SmmKu_VJp7-jrAIoOMrTqKgCtV4cPzntMyNuOYN7QQ1q0Fi2c-679vYpJr9ub5dRFXMhCMQmjZsOWXUa8yafIXf85UA5Goct-n4hVT5dFfBGM-dE6gwZEFkHQ1j1195sLybZFTNA1IfX26MBMxlVHoyT-aHtkd6YjGXbvOPMMsObVHDa_Cf4BmApRGo-eehsAttsCqDRwkl0M2E1b0OXCIwMVnCK-qWZ81MGaJGjqxdj_LaxhAjE9JI_4oNedAg4Yj4m_vkQBKdmWJT_bTUsllAZXqX5IxYQHJ8WvlknOTr6sR3wgoSOa_JNVHkUQxqXBbQcoDdy6VciL5rIqwWl4Y2caVYzeXa8YNXxwsfrPDegH3dQ6vOiIiCDT3bexkcS_y7GAioPPv9qDu7M9y4sI8t0VkPgMbWY6MIu5u9WAv6lEiKIYrtlyU-iiXnNdkd9EnpC1TD8_s12PtCuLg5bGgLVmI1dw8VXVZxmVyFvfBtuqXkzM-kZOyv-YIRcf-wPY8DUtnHox1d6t64oYhGv-pYV5dmZ0rGs99LC6ThiifeF_oHz5ZK46Ms68WsiAjc1HLAbFPFxWw8X6lyLlpT5aHgFdDBeCTExKXyItlc9PBxkgK8UnXHQFiWX3HTen8rziwkI31AbkMU7xGP6cvuwuLG7MA7sH3yLF1uimQlhr6EuTrImWQNV7X81DqyZ0OE7I-uT9bp_xISa5it-6AJpFtGcwz04_OuO2zvZazBeiKJI5Fw0X2r3RHTHdA29SXqTIe4y2LRAAp0MSAZUIJAMIbbrz9BG-3D5BHfQUA1Bbuklxv-tNWiuanWbbTmAiuxTAo9P-D2ZMrWHwdN8sCb3nINLur38z1xviSs2qEEjKo9kzQIWoHgwm_amV-jjDtNBXwpCQnoaqt3FfL8e59ecYWVg61Gnl3YCwq6rubFdkIv6UStcWSlj4pRFlFZSjQxzznvGTAc5Nz1JJWVeBHJItei4wqDUYEw2BNTS_fU9keV3LFyKDH_x248AxHLiXt583DhZZwPAKlpmMo0E7odzfOgVQGM5b9l64NL3RQRq8cDxI8KgmOFnPHdI7Myq1A3nW1uoBB8m9_GR_9orboKl5tAQrT0VTMRd6JyNwOOQ71ZoQpMwsD5Xd9YDZd618TNE9dA38h9OyBco_-hwKCYayyEqib1atLLBO_gnO4InR471AklOokDOKx1oY_xgNhlFrZskE4nQQOqeCQ_47oysob9s3kblA8UqDrL2vM3i2KPLWwlQzMAPrQAIv7Amp6rO-wYwBQdbP7XghYympxx9-hNxc1Duq-nS4R3pjPtZg3LoCvG1N_0JLf9jfPBqw_rdPM6DnjEOvpCdvrfv4wBlEpgqbaRQCZBPEz-O5ZlY24OyPvaNX0krPeXkbi-KYuwZSAbfW0zsksXb8cbRmz6tvh25IRYP1XNDPVd8NFrYOdDH9GzhV5MYZziYMurQlVwgumGLz85Y6qTHZU2D5qkMH7o5C5bzptReO96889oxkReS_LDhzdxH0tAQ5a9GgETJTHAXJ9Lt7X037i0qQlatbPLizOl9fo4vUBGJ7w-uSUmIzg1yIwOo8xhLu39WghXfoJ_EX5ptNi7arpAo11UoJIGEgWgq7bS40q2dtnG5HYpp9ef37iFgztM7PV5dlJf_W-n9uXJyCk2nhxZeuGiIGhMTzjP_8oeyiMFelmoyLlqK8wOs6hvWqgDD4NwtZc-XBkEAKsxuPrirb_sszp0z6BWsh3D6fBxAW_lTrNVkxZ1EBtoEN72HvO1aeX8PVuJP3CKwkkUcAhJ5Zvajo8t4Ai8ave2MTZul4QhDIe7ZALJMo71u-hNSj8YG9JLPqF1jcnbkl8ykt_XMMxX8wSZ_AfTsfmAuOWBfEzxv8Fl2Ne-oxcEtfuYnYNJIjQDHXXdtYbWKBezE41wonQu7FWh3wSSF3DDm9EocWMsUZ610nrIviOFdtTqjrY7KnOZp0hS9Hm2V04z-06Db-To71L5O5poYKuOlDAP_ULfW0hgMgYaPnt4Nive9gtue6ZQ5yNFylKM11zCWcF8vmuEPGx4NIZjvNFB-QSkdxZtBTQOgO6r4BqHJNZIDqiD9A6PyMemVtQEo-JhooX6bwHFHx6C84r-kKq1lg0RaDelbu1h6347kocxOczdgHWlagKPFrUvHpPHvCZeFmzLLmPWbDEpUhhfkbEZlNe47svO5WN1TYZMCAlE0g01pe6b_NB9-twQ4FJB1SkUQRA4o5DxiIHSgXBBfw1X98XPEKcJgCU2zRbYuU69mvyPcsxoPsLo8Cl2OAXj2U1tcLUxVhR_vu3I0SG_Ee8hxBp5b6QbpIu2sE0bgpqfi88XqUXf3qvxGbkoZDZ8dPvEvCVmpDZOOGJY5Db4MnpKBByW79lUnpbLCbsVOoxYu8SVmVQ63eGY9yBlACuWxlXA_PhCpnKYwMppjxKvKqalNJGMMg5WQ8cvhsIftE5wgaj21n0A15GwRO1WgUcsO_dLe1MqO88hTHWHV0Q3gNut3sdsmVr_n-0SniLtVAaw4IEtDZWNUfhpaoC7iyKxoJ86t8r697EYPlWUu4nTQhb3TOXAjHF22rFsJJMldKHVxjxjd086ky3OY3L_7cbjVof60-y4gTJdumw5hch-mhaT93nVnaTuPLGCAC48dZSgOitW8Dw62mPS05fTPAfKUY1HpyIWr9ACk7U2KVAijpzFB-bXvqJeCyBeUHr4cB61K2XxTmkNA8Oo9RC32TeYDrThFzoUyYZ_D6zPe1lLtQaFsPRvDBSsYCcr5fcABlST4DyUgnkgqtx-0-D1QEwtWiH3xiHLQGa82ldjqNIXL9LqwxYoCRBKuljHyqEylZD26A9Jp6SL9rk_Ol6TJlYa3zx44PHyMoA3Coy-HIZW5NMwVKdV7egUDDOchvuFnc1Zv2QpSFKc0JY8lYFtSm1HZFwjq7hf2hBqkU0Xneh8AT4Ty9QVb4mJ8rpzc8m_NBZNrpZVE-pIzmv2yGjnRwKSrwsjat7b2iY34UPFqGBlDk5FX6EfRlN9bq-nztWdextLc47ndr1gxsjC-vr6tbdXl5FxMVhbsnLH80wOC6hHubxCysr5C7Ft59s3MLhVQCc3IIOPMBhLOjjjVsu6K8kAJvvamIpj-iYUQj9T8JztAqfobytzbiwPsQ3mywUGkJpZxTjXjqkKdT6yfFdW5ksR8p3VlKGAAKft9TKKcc5IgErmIRtiZF4hGhB45nwIO5XHA59V94QuV3b0oJrleuDCBj0iDFqWYpsUVHyKl3RjrO86Yq1IbNRwXIIMrlsVYos9CS1I75k9kW4yQC6ni3a2ZitDkiEB-SoQuTlBsvfTs892Wt5q3CWH6_7srZO6l2QEAlWqyQp_P4i8j8uiEbZksGsgghVIgOkriblDP9ml7PdkpRX7S3OpY7jjQgTNR8XHaZ-SEw0-zPiqIIt5VioFK8JljPn-nMIDx-ew_vTDKqmKEOF39ezQcc5oQNTQNUvTAe8qCbK4ghS3fagItezoHtB5zm3qwhx9Iu6JMJy0UhywUZBaInY6NokiVZGDgtoqlVjOk7K4299MVZgsOr2P9QHRDo41lYnCeWWFUf50d-ouEOUO_ngteDpgG1DfV64eycrUOWAnpfI8XOUZ7mxS89SqJriw3T3tcljowHiOFw9QjYsjJi0vuHL5jfoBZVdYAUH6KiLfIScO-KgqitdUTpFwks8Qk7xfd2HcEfArlzSiAt3Hmb2Ty-5mhvtQj2RbPehgoI7gqa60b6Qffth-5tbIjXoKlZCQsaKVMAggF9C3w0Rv5uJ3ExCTGsfKO9IVOf--AfxaA8cNoO0W3NTaMH2iWftY0O1-1x3VJEcerp2v9BcZ0iIL3VFJ9eyOPYNHNwTYrI3S6OrVZxuQXFbu2E_Us32wxYqXvnMx-DEOk7bUUFIanL69Cbn_mR_9Wm1sFixBQ15diJoe1Oyl3XWHWL6_YF4XnHzZlMWySt3Awdeynwu99Ter8WMGXl-6K30novKRWC9jDVOfAtxPvXBkkRtpm8nnJT8fwkmIQoWOEPT2MCg3VYzzHzVEH-AueuRiCZj_o7L-fRqbtFrWPyVnGuetUYS5u0D3n7Rr6kCVqWhLfwjqAwV_calzIQs8-J0-AJe6GGaaONPYfXwXjYan0Sl11CS64lXvx7i3MgeOZuH210T6g6hj54dSLw0upO1L8k0FGli3I5OhLMekeGMLT_N2akyHktjbz8Odpk2ms0pJP_PdqvUegjXgH81F5lr40HAXZs7jfC9ytgcWaEjwlmnoFHSCTdlCIgJqEBzItXiz6ilk5zP35VwxdQTNoms7Meqg-ZrgRuLRaqXNtpQT9qWBmiFP6zoYLwymqytZ_P1SV9DCM-KIZQ2Cq-ew8PsYSrwl8GmfuegoKEMBiON1I4mC9ChDgzJmbo5P9te60T0URS5g9ExiQuSo-JXs0MKC57LLiNo3MPZvqR1wRJOTg0_FqS4JFxzPwN5OqhBzfs25f6kK-axf0wWp-LetUx3U1CTNaxfbCTK5tb8KnLOIs3lDwJw5H5wIIRgXnfNLBJT8A2srfXkdpA",
"eventCounters" => [],
"jsType" => "ch",
"cid" => "3i4vPGHCl74uIBumWaxF2a0PjFkyCJxcSvFnn~heX7keA6sGHgRbnzSYcMr_RzJFlfeeBdg9HeufzFPxnvSHbyWowSvjm6mNCSPhkx6MwDqptANoj93ZgGVbrzxGtcYe",
"ddk" => "78B13B7513D180B7AB6D6FF9EB0A51",
"Referer" => "https://www.qwant.com/?q={$search}&t=web",
"request" => "/?q={$search}&t=web",
"responsePage" => "origin",
"ddv" => "5.8.0"
],
true
);
}catch(Exception $error){
throw new Exception("Failed to fetch JS challenge token");
}
$datadome = json_decode($datadome, true);
if($datadome === null){
throw new Exception("Failed to decode JS challenge JSON");
}
if(!isset($datadome["cookie"])){
throw new Exception("Failed to get cookie from JS challenge endpoint");
}
$cookie = explode(";", $datadome["cookie"])[0];
}
return $cookie;
}
}

View File

@@ -408,6 +408,8 @@ class startpage{
//$html = file_get_contents("scraper/startpage.html");
}
//echo htmlspecialchars($html);
$this->detect_captcha($html);
if(
@@ -570,8 +572,8 @@ class startpage{
$out["spelling"] =
[
"type" => "including",
"using" => $json["render"]["query"],
"correction" => urldecode($category["results"][0]["query"])
"using" => rawurldecode($json["render"]["query"]),
"correction" => html_entity_decode($category["results"][0]["query"])
];
}
break;
@@ -1105,29 +1107,20 @@ class startpage{
if($page["name"] == "Next"){
parse_str(
explode(
"?",
$page["url"],
2
)[1],
$str
);
return
$this->backend->store(
http_build_query(
[
"lui" => "english",
"language" => "english",
"sc" => $str["sc"],
"sc" => $json["render"]["search_sc"],
"t" => "device",
"cat" => $pagetype,
"segment" => "startpage.udog",
"abd" => 0,
"abe" => 0,
"query" => $str["q"],
"page" => $str["page"],
"segment" => "organic",
"abd" => "0",
"abe" => "0",
"query" => rawurldecode($json["render"]["query"]),
"page" => $page["number"],
"qsr" => "all",
"qadf" => "none" // @ todo fix (??)
]

View File

@@ -30,19 +30,18 @@ class vsco{
curl_setopt($curlproc, CURLOPT_HTTPHEADER,
["User-Agent: " . config::USER_AGENT,
"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language: en-US,en;q=0.5",
"Accept-Encoding: gzip",
"Accept-Encoding: gzip, deflate, br, zstd",
"DNT: 1",
"Sec-GPC: 1",
"Connection: keep-alive",
"Upgrade-Insecure-Requests: 1",
"Sec-Fetch-Dest: document",
"Sec-Fetch-Mode: navigate",
"Sec-Fetch-Site: same-origin",
"Sec-Fetch-Site: none",
"Sec-Fetch-User: ?1",
"Priority: u=0, i",
"TE: trailers"]
"Priority: u=0, i"]
);
}else{
@@ -73,8 +72,8 @@ class vsco{
curl_setopt($curlproc, CURLOPT_CONNECTTIMEOUT, 30);
curl_setopt($curlproc, CURLOPT_TIMEOUT, 30);
// http2 bypass
curl_setopt($curlproc, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0);
// http1 bypass?!?!?!
curl_setopt($curlproc, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
$this->backend->assign_proxy($curlproc, $proxy);

View File

@@ -216,7 +216,7 @@ class yep{
];
}
private function get($proxy, $url, $get = []){
private function get($proxy, $url, $get = [], $use_api = false, $post_data = null, $bearer = null){
$curlproc = curl_init();
@@ -227,25 +227,43 @@ class yep{
curl_setopt($curlproc, CURLOPT_URL, $url);
// use http2
curl_setopt($curlproc, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0);
// use http1 lol
curl_setopt($curlproc, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
curl_setopt($curlproc, CURLOPT_ENCODING, ""); // default encoding
curl_setopt($curlproc, CURLOPT_HTTPHEADER,
["User-Agent: " . config::USER_AGENT,
"Accept: */*",
"Accept-Language: en-US,en;q=0.5",
"Accept-Encoding: gzip, deflate, br, zstd",
"Referer: https://yep.com/",
"Origin: https://yep.com",
"DNT: 1",
"Connection: keep-alive",
"Sec-Fetch-Dest: empty",
"Sec-Fetch-Mode: cors",
"Sec-Fetch-Site: same-site",
"Priority: u=4",
"TE: trailers"]
);
if($use_api){
$post_data = json_encode($post_data);
curl_setopt($curlproc, CURLOPT_HTTPHEADER,
["Content-Type: application/json",
"Authorization: Bearer $bearer",
"Content-Length: " . strlen($post_data)]
);
curl_setopt($curlproc, CURLOPT_POST, true);
curl_setopt($curlproc, CURLOPT_POSTFIELDS, $post_data);
}else{
curl_setopt($curlproc, CURLOPT_HTTPHEADER,
["User-Agent: " . config::USER_AGENT,
"Accept: */*",
"Accept-Language: en-US,en;q=0.9",
"Accept-Encoding: gzip, deflate, br, zstd",
"Referer: https://yep.com/",
"Origin: https://yep.com",
"DNT: 1",
"Connection: keep-alive",
"Sec-Fetch-Dest: empty",
"Sec-Fetch-Mode: cors",
"Sec-Fetch-Site: same-site",
"Priority: u=4",
"Pragma: no-cache",
"Cache-Control: no-cache",
"TE: trailers"]
);
}
curl_setopt($curlproc, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curlproc, CURLOPT_SSL_VERIFYHOST, 2);
@@ -270,6 +288,11 @@ class yep{
public function web($get){
if(config::YEP_USE_API){
return $this->web_api($get);
}
$search = $get["s"];
if(strlen($search) === 0){
@@ -392,6 +415,123 @@ class yep{
}
private function web_api($get){
$search = $get["s"];
if(strlen($search) === 0){
throw new Exception("Search term is empty!");
}
$out = [
"status" => "ok",
"spelling" => [
"type" => "no_correction",
"using" => null,
"correction" => null
],
"npt" => null,
"answer" => [],
"web" => [],
"image" => [],
"video" => [],
"news" => [],
"related" => []
];
// parse filters
$filters = [
"query" => $search,
"limit" => 99 // for some reason they dont show anything sometimes at 100 results
];
if($get["nsfw"] == "no"){ $filters["safe_search"] = true; }
if($get["lang"] != "any"){ $filters["language"] = [ $get["lang"] ]; }
// add api key
$key_data = $this->backend->get_key();
try{
$json =
$this->get(
$this->backend->get_ip($key_data["increment"]),
"https://platform.yep.com/api/search",
[],
true,
$filters,
$key_data["key"]
);
}catch(Exception $error){
throw new Exception("Failed to fetch JSON");
}
// should never happen
//$this->detect_cf($json);
$json = json_decode($json, true);
//$json = json_decode(file_get_contents("scraper/yep.json"), true);
if($json === null){
throw new Exception("Failed to decode JSON");
}
if(isset($json["error"])){
throw new Exception("Yep API returned an error: " . $json["error"]);
}
if(isset($json["errors"])){
throw new Exception("Yep API returned the following errors: {$json["message"]}");
}
if(
isset($json["success"]) &&
$json["success"] !== true
){
throw new Exception("Yep API returned a false-y success value");
}
if(!isset($json["results"])){
throw new Exception("Yep API did not return a results object");
}
foreach($json["results"] as $item){
if(
$item["url"] === null ||
$item["url"] == ""
){
// sometimes API fucks up
continue;
}
$out["web"][] = [
"title" => $item["title"],
"description" => $item["description"],
"url" => $item["url"],
"date" => null,
"type" => "web",
"thumb" => [
"url" => null,
"ratio" => null
],
"sublink" => [],
"table" => []
];
}
return $out;
}
private function detect_cf($payload){
// detect cloudflare page

View File

@@ -91,10 +91,6 @@ $settings = [
"value" => "qwant",
"text" => "Qwant"
],
[
"value" => "ghostery",
"text" => "Ghostery"
],
[
"value" => "yep",
"text" => "Yep"
@@ -169,6 +165,10 @@ $settings = [
"value" => "mojeek",
"text" => "Mojeek"
],
[
"value" => "naver",
"text" => "Naver"
],
[
"value" => "baidu",
"text" => "Baidu"
@@ -185,6 +185,10 @@ $settings = [
"value" => "marginalia",
"text" => "Marginalia"
],
[
"value" => "purili",
"text" => "Purili"
],
[
"value" => "wiby",
"text" => "wiby"
@@ -231,6 +235,10 @@ $settings = [
"value" => "qwant",
"text" => "Qwant"
],
[
"value" => "naver",
"text" => "Naver"
],
[
"value" => "baidu",
"text" => "Baidu"
@@ -325,6 +333,10 @@ $settings = [
"value" => "qwant",
"text" => "Qwant"
],
[
"value" => "naver",
"text" => "Naver"
],
[
"value" => "baidu",
"text" => "Baidu"
@@ -333,6 +345,10 @@ $settings = [
"value" => "coccoc",
"text" => "Cốc Cốc"
],
[
"value" => "purili",
"text" => "Purili"
],
[
"value" => "solofield",
"text" => "Solofield"

View File

@@ -80,6 +80,15 @@ document.body.insertBefore(table, quote.nextSibling);
// handle sorting clicks
var tbody = table.getElementsByTagName("tbody")[0];
// server popup without inline javascript
tbody.addEventListener('click', function(event){
var row = event.target.closest('.show_server');
if(!row) return;
show_server(row.id);
})
var th = table.getElementsByTagName("th");
for(var i=0; i<th.length; i++){
@@ -343,7 +352,7 @@ function render_list(){
for(var k=0; k<sorted_list.length; k++){
html += '<tr onclick="show_server(' + sorted_list[k].index + ');">';
html += '<tr class="show_server" id="' + sorted_list[k].index + '">';
for(var i=0; i<7; i++){
@@ -406,6 +415,7 @@ function render_list(){
console.log(html);
tbody.innerHTML = html;
}
var popup_bg = document.getElementById("popup-bg");

View File

@@ -1013,6 +1013,23 @@ table tr a:last-child{
font-weight:bold;
}
/* booru */
#images .image .i-title-wrapper{
margin-bottom:7px;
white-space:nowrap;
overflow:hidden;
text-overflow:ellipsis;
}
#images .image .i-title-wrapper a{
display:inline;
}
#images .image .i-title-wrapper .title{
display:inline;
margin-bottom:0;
}
/* end booru */
#images .image .description{
overflow:hidden;
height:45px;
@@ -1113,6 +1130,17 @@ table tr a:last-child{
/*
Settings page
*/
.web .settings select{
all:unset;
user-select:none;
display:block;
border:1px solid var(--504945);
border-radius:2px;
font-size:14px;
padding:0 20px 0 4px;
height:24px;
}
.web .settings{
margin-top:17px;
border:1px dashed var(--504945);

View File

@@ -0,0 +1,20 @@
:root{
/* background */
--1d2021: #191724;
--282828: #1f1d2e;
--3c3836: #26233a;
--504945: #524f67;
/* font */
--928374: #e0def4;
--a89984: #908caa;
--bdae93: #6e6a86;
--8ec07c: #9ccfd8;
--ebdbb2: #f6c177;
/* code highlighter */
--comment: #6e6a86;
--default: #c4a7e7;
--keyword: #31748f;
--string: #f6c177;
}