Self-Host

To Self-host @ https://4g.flossboxin.org.in
This commit is contained in:
vdbhb59 2025-02-21 22:04:53 +05:30
commit c6e404d2af
132 changed files with 34951 additions and 0 deletions

53
README.md Normal file
View file

@ -0,0 +1,53 @@
# 4Get Search
**4Get** is a proxy search engine that doesn't suck.
## About 4Get
https://4g.flossboxin.org.in/about
## Totally unbiased comparison between alternatives
| | 4Get | searx(ng) | FbIN-LibreY | araa | hearch.co |
|----------------------------|-------------------------|-----------|-------------|-----------|-------------------|
| RAM usage | 200-400mb~ | 2GB~ | 200-400mb~ | 2GB~ | idk |
| Does it suck | no (debunked by snopes) | yes | yes | a little | better than searx |
| Does it work | ye | sometimes | sometimes | sometimes | yes |
## Features
1. Rotating proxies on a per-scraper basis
2. Search filters, which SearxNG lacks for the most part
3. Bot protection that *actually* filters out the bots (when configured)
4. Interface doesn't require javascript
5. Favicon fetcher with caching support & image proxy
6. Bunch of other shits
tl;dr 4Get is the best way to browse for shit.
# Supported websites
| Web | Images | Videos | News | Music | Autocompleter |
|------------|--------------|------------|------------|------------|---------------|
| DuckDuckGo | DuckDuckGo | YouTube | DuckDuckGo | Soundcloud | Brave |
| Brave | Brave | DuckDuckGo | Brave | | DuckDuckGo |
| Yandex | Yandex | Brave | Google | | Yandex |
| Google | Google | Yandex | Startpage | | Google |
| Startpage | Startpage | Google | Qwant | | Startpage |
| Qwant | Qwant | Startpage | Mojeek | | Kagi |
| Ghostery | Yep | Qwant | | | Qwant |
| Yep | Solofield | Solofield | | | Ghostery |
| Greppr | Pinterest | | | | Yep |
| Crowdview | 500px | | | | Marginalia |
| Mwmbl | VSCO | | | | YouTube |
| Mojeek | Imgur | | | | Soundcloud |
| Solofield | FindThatMeme | | | | |
| Marginalia | | | | | |
| wiby | | | | | |
| Curlie | | | | | |
# Installation
Refer to the <a href="https://git.flossboxin.org.in/FbIN/4Get/src/branch/main/docs/">documentation index</a>. I recommend following the <a href="https://git.flossboxin.org.in/FbIN/4Get/src/branch/main/docs/apache2.md">apache2 guide</a>.
## Contact
Shit breaks all the time but I repair it all the time too. Email me here: <b>dev(at)flossboxin(dot)org(dot)in</b> or create an issue.
## License
AGPL

39
about.php Normal file
View file

@ -0,0 +1,39 @@
<?php
include "data/config.php";
include "lib/frontend.php";
$frontend = new frontend();
echo
$frontend->load(
"header_nofilters.html",
[
"title" => "About",
"class" => " class=\"about\""
]
);
$left =
explode(
"\n",
file_get_contents("template/about.html")
);
$out = "";
foreach($left as $line){
$out .= trim($line);
}
echo
$frontend->load(
"search.html",
[
"timetaken" => null,
"class" => "",
"right-left" => "",
"right-right" => "",
"left" => $out
]
);

27
ami4get.php Normal file
View file

@ -0,0 +1,27 @@
<?php
header("Content-Type: application/json");
header("Access-Control-Allow-Origin: *");
include "data/config.php";
$real_requests = apcu_fetch("real_requests");
$bot_requests = apcu_fetch("captcha_gen");
echo json_encode(
[
"status" => "ok",
"service" => "4get",
"server" => [
"name" => config::SERVER_NAME,
"description" => config::SERVER_LONG_DESCRIPTION,
"bot_protection" => config::BOT_PROTECTION,
"real_requests" => $real_requests === false ? 0 : $real_requests,
"bot_requests" => $bot_requests === false ? 0 : $bot_requests,
"api_enabled" => config::API_ENABLED,
"alt_addresses" => config::ALT_ADDRESSES,
"version" => config::VERSION
],
"instances" => config::INSTANCES
]
);

357
api.txt Normal file
View file

@ -0,0 +1,357 @@
44
4444444 44
44444444 44444 444
44444444 444444 444444444
44444 44444444 444444444
444444444 4444444
4444444444 444444
4444444444444
444444444444444444
444444444444444
44444444
4444
44
+ Welcome to the 4Get API documentation +
+ Terms of use
Do NOT misuse the API. Misuses can include... ::
1. Serp SEO scanning
2. Intensive scraping
3. Any other activity that isn't triggered by a human
4. Illegal activities in Canada
5. Constant "test" queries while developping your program
(please cache the API responses!)
Examples of good uses of the API ::
1. A chatroom bot that presents users with search results
2. Personal use
3. Any other activity that is initiated by a human
If you wish to engage in the activities listed under "misuses", feel
free to download the source code of the project and running 4get
under your own terms. Please respect the terms of use listed here so
that this website may be available to all in the far future.
P.s fuck whoever botted my site for months on end, choke on my dick
lol!!!!
Get your instance running here ::
https://git.flossboxin.org.in/FbIN/4get
Thanks!
+ Passes
Depending of the instance, you may need to provide a "pass" token
in the cookies of your request. These can be obtained from solving
a captcha which will allow you to make 100 requests in the next 24
hours. In the future, you will be able to ask the serber maintainer
for a "pass" which will allow you to bypass the captcha requirement.
The captcha doesn't need javascript to work.
+ Decode the data
All payloads returned by the API are encoded in the JSON format. If
you don't know how to tackle the problem, maybe programming is not
for you.
All of the endpoints use the GET method.
+ Check if an API call was successful
All API responses come with an array index named "status". If the
status is something else than the string "ok", something went wrong.
You can supply the content of the "status" string back to your
application to inform the user of what went wrong.
The HTTP code will be 429 if your pass is invalid. It is set to 200
otherwise.
+ Get the next page of results
All API responses come with an array index named "npt". To get the
next page of results, you must make another API call with &npt.
Example ::
+ First API call
/api/v1/web?s=higurashi
+ Second API call
/api/v1/web?npt=ddg1._rJ2hWmYSjpI2hsXWmYajJx < ... >
You shouldn't specify the search term, only the &npt parameter
suffices.
The first part of the token before the dot (ddg1) refers to an
array position on the serber's memory. The second part is an
encryption key used to decode the data at that position. This way,
it is impossible to supply invalid pagination data and it is
impossible for a 4get operator to peek at the private data of the
user after a request has been made.
The tokens will expire as soon as they are used or after a 15
minutes inactivity period, whichever comes first.
+ Beware of null values!
Most fields in the API responses can return "null". You don't need
to worry about unset values.
+ API Parameters
To construct a valid request, you can use the 4get web interface
to craft a valid request, and replace "/web" with "/api/v1/web".
+ "date" and "time" parameters
"date" always refer to a calendar date.
"time" always refer to the duration of some media.
They are both integers that uses seconds as its unit. The "date"
parameter specifies the number of seconds that passed since January
1st 1970.
______ __ _ __
/ ____/___ ____/ /___ ____ (_)___ / /______
/ __/ / __ \/ __ / __ \/ __ \/ / __ \/ __/ ___/
/ /___/ / / / /_/ / /_/ / /_/ / / / / / /_(__ )
/_____/_/ /_/\__,_/ .___/\____/_/_/ /_/\__/____/
/_/
+ /ami4get
Tells you basic information about the 4get instance. CORS requests
are allowed on this endpoint.
+ /api/v1/web
+ &extendedsearch
When using the ddg(DuckDuckGo) scraper, you may make use of the
&extendedsearch parameter. If you need rich answer data from
additional sources like StackOverflow, music lyrics sites, etc.,
you need to specify the value of (string)"true".
The default value is "false" for API calls.
+ Parse the "spelling"
The array index named "spelling" contains 3 indexes ::
spelling:
type: "including"
using: "4chan"
correction: '"4cha"'
The "type" may be any of these 3 values. When rendering the
autocorrect text inside your application, it should look like
what follows right after the parameter value ::
no_correction <Empty>
including Including results for %using%. Did you mean
%correction%?
not_many Not many results for %using%. Did you mean
%correction%?
As of right now, the "spelling" is only available on
"/api/v1/web".
+ Parse the "answer"
The array index named "answer" may contain a list of multiple
answers. The array index "description" contains a linear list of
nodes that can help you construct rich formatted data inside of
your application. The structure is similar to the one below:
answer:
0:
title: "Higurashi"
description:
0:
type: "text"
value: "Higurashi is a great show!"
1:
type: "quote"
value: "Source: my ass"
Each "description" node contains an array index named "type".
Here is a list of them:
text
+ title
italic
+ quote
+ code
inline_code
link
+ image
+ audio
Each individual node prepended with a "+" should be prepended by
a newline when constructing the rendered description object.
There are some nodes that differ from the type-value format.
Please parse them accordingly ::
+ link
type: "link"
url: "https://flossboxin.org.in"
value: "Visit my website!"
+ image
type: "image"
url: "https://flossboxin.org.in/static/pixels.png"
+ audio
type: "audio"
url: "https://flossboxin.org.in/static/whatever.mp3"
The array index named "table" is an associative array. You can
loop over the data using this PHP code, for example ::
foreach($table as $website_name => $url){ // ...
The rest of the JSON is pretty self explanatory.
+ /api/v1/images
All images are contained within "image". The structure looks like
below ::
image:
0:
title: "My awesome Higurashi image"
source:
0:
url: "https://flossboxin.org.in/static/profile_pix.png"
width: 400
height: 400
1:
url: "https://flossboxin.org.in/static/pixels.png"
width: 640
height: 640
2:
url: "https://tse1.mm.bing.net/th?id=OIP.VBM3BQg
euf0-xScO1bl1UgHaGG"
width: 194
height: 160
The last image of the "source" array is always the thumbnail, and is
a good fallback to use when other sources fail to load. There can be
more than 1 source; this is especially true when using the Yandex
scraper, but beware of captcha rate limits.
+ /api/v1/videos
The "time" parameter for videos may be set to "_LIVE". For live
streams, the amount of people currently watching is passed in
"views".
+ /api/v1/news
Just make a request to "/api/v1/news?s=elon+musk". The payload
has nothing special about it and is very self explanatory, just like
the endpoint above.
+ /api/v1/music
Each entry under "song" contains a array index called "stream" that
looks like this ::
endpoint: sc
url: https://api-v2.soundcloud <...>
When the endpoint is something else than "linear", you MUST use
the specified endpoint. Otherwise, you are free to handle that
json+m3u8 crap yourself. If the endpoint is equal to "linear", the
URL should return a valid HTTP audio stream. To access the endpoint,
you must add the following prefix in your request, like so:
https://4g.flossboxin.org.in/audio/<endpoint>?s=<url>
+ /favicon
Get the favicon for a website. The only parameter is "s", and must
include the protocol for fetching in case the favicon is not cached
yet.
Example ::
/favicon?s=https://flossboxin.org.in
If we had to revert to using Google's favicon cache, it will throw
an error in the X-Error header field. If Google's favicon cache
also failed to return an image, or if you're too retarded to specify
a valid domain name, a default placeholder image will be returned
alongside the "404" HTTP error code.
+ /proxy
Get a proxied image. Useful if you don't want to leak your user's IP
address. The parameters are "i" for the image link and "s" for the
size.
Acceptable "s" parameters:
portrait 90x160
landscape 160x90
square 90x90
thumb 236x180
cover 207x270
original <Original resolution>
You can also ommit the "s" parameter if you wish to view the
original image. When an error occurs, an "X-Error" header field
is set.
+ /audio/linear
Get a proxied audio file. Does not support "Range" headers, as it's
only used to proxy small files (hence why it's called linear DUH)
The parameter is "s" for the audio link.
+ /audio/sc
Get a proxied audio file for SoundCloud. Does not support downloads
trough WGET or CURL, since it returns 30kb~160kb "206 Partial
Content" parts, due to technical limitations that comes with
converting m3u8 playlists to seekable audio files. If you use this
endpoint, you must support these 206 codes and also handle the
initial 302 HTTP redirect. I used this method as I didn't want to
store information about your request needlessly. This method also
allows noJS users to access the files.
The parameter is "s" for the SoundCloud JSON m3u8 abomination. It
does not support "normal" SoundCloud URLs at this time.
+ /audio/spotify
Get a proxied Spotify audio file. Accepts a track ID for the "s"
parameter. Will only allow you to fetch the 30 second preview since
I don't feel like fucking with cookies and accounts every fucking
living moment of my life. You must handle the initial 302 redirect
to the /audio/linear endpoint.
+ Appendix
If you have any questions or need clarifications, please send an
email my way to will at flossboxin.org.in

10
api/index.php Normal file
View file

@ -0,0 +1,10 @@
<?php
header("Content-Type: application/json");
http_response_code(404);
echo json_encode(
[
"status" => "Unknown endpoint"
]
);

243
api/v1/ac.php Normal file
View file

@ -0,0 +1,243 @@
<?php
include "../../data/config.php";
new autocomplete();
class autocomplete{
public function __construct(){
header("Content-Type: application/json");
$this->scrapers = [
"brave" => "https://search.brave.com/api/suggest?q={searchTerms}",
"ddg" => "https://duckduckgo.com/ac/?q={searchTerms}&type=list",
"yandex" => "https://suggest.yandex.com/suggest-ff.cgi?part={searchTerms}&uil=en&v=3&sn=5&lr=21276&yu=4861394161661655015",
"google" => "https://www.google.com/complete/search?client=mobile-gws-lite&q={searchTerms}",
"qwant" => "https://api.qwant.com/v3/suggest/?q={searchTerms}&client=opensearch",
"yep" => "https://api.yep.com/ac/?query={searchTerms}",
"marginalia" => "https://search.marginalia.nu/suggest/?partial={searchTerms}",
"yt" => "https://suggestqueries-clients6.youtube.com/complete/search?client=youtube&q={searchTerms}",
"sc" => "",
"startpage" => "https://www.startpage.com/suggestions?q={searchTerms}&format=opensearch&segment=startpage.defaultffx&lui=english",
"kagi" => "https://kagi.com/api/autosuggest?q={searchTerms}",
"ghostery" => "https://ghosterysearch.com/suggest?q={searchTerms}"
];
/*
Sanitize input
*/
if(!isset($_GET["s"])){
$this->do404("Missing search(s) parameter");
}
if(is_string($_GET["s"]) === false){
$this->do404("Invalid search(s) parameter");
}
if(strlen($_GET["s"]) > 500){
$this->do404("Search(s) exceeds the 500 char length");
}
/*
Get $scraper
*/
if(!isset($_GET["scraper"])){
if(isset($_COOKIE["scraper_ac"])){
$scraper = $_COOKIE["scraper_ac"];
}else{
$scraper = "brave"; // default option
}
}else{
$scraper = $_GET["scraper"];
}
if($scraper == "disabled"){
// this shouldnt happen, but let's handle it anyways
$this->doempty();
}
// make sure it exists
if(!isset($this->scrapers[$scraper])){
$scraper = "brave"; // default option
}
// return results
switch($scraper){
case "google":
case "yt":
// handle google cause they want to be a special snowflake :(
$js = $this->get($this->scrapers[$scraper], $_GET["s"]);
preg_match(
'/\((\[.*\])\)/',
$js,
$js
);
if(!isset($js[1])){
$this->doempty();
}
$js = json_decode($js[1]);
$json = [];
foreach($js[1] as $item){
$json[] = htmlspecialchars_decode(strip_tags($item[0]));
}
echo json_encode(
[
$_GET["s"],
$json
],
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_IGNORE
);
break;
case "sc":
// soundcloud
chdir("../../");
include "scraper/sc.php";
$sc = new sc();
$token = $sc->get_token("raw_ip::::");
$js = $this->get(
"https://api-v2.soundcloud.com/search/queries?q={searchTerms}&client_id=" . $token . "&limit=10&offset=0&linked_partitioning=1&app_version=1693487844&app_locale=en",
$_GET["s"]
);
$js = json_decode($js, true);
if(!isset($js["collection"])){
$this->doempty();
}
$json = [];
foreach($js["collection"] as $item){
$json[] = $item["query"];
}
echo json_encode(
[
$_GET["s"],
$json
],
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_IGNORE
);
break;
case "marginalia":
$json = $this->get($this->scrapers[$scraper], $_GET["s"]);
$json = json_decode($json, true);
if($json === null){
$this->doempty();
}
echo json_encode(
[
$_GET["s"],
$json
],
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_IGNORE
);
break;
default:
// if it respects the openSearch protocol
$json = json_decode($this->get($this->scrapers[$scraper], $_GET["s"]), true);
echo json_encode(
[
$_GET["s"],
$json[1] // ensure it contains valid key 0
],
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_IGNORE
);
break;
}
}
private function get($url, $query){
try{
$curlproc = curl_init();
$url = str_replace("{searchTerms}", urlencode($query), $url);
curl_setopt($curlproc, CURLOPT_URL, $url);
curl_setopt($curlproc, CURLOPT_ENCODING, ""); // default encoding
curl_setopt($curlproc, CURLOPT_HTTPHEADER,
["User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:135.0) Gecko/20100101 Firefox/135.0",
"Accept: application/json, text/javascript, */*; q=0.01",
"Accept-Language: en-US,en;q=0.5",
"Accept-Encoding: gzip",
"DNT: 1",
"Connection: keep-alive",
"Sec-Fetch-Dest: empty",
"Sec-Fetch-Mode: cors",
"Sec-Fetch-Site: same-site"]
);
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;
}catch(Exception $error){
do404("Curl error: " . $error->getMessage());
}
}
private function do404($error){
echo json_encode(
["error" => $error],
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_IGNORE
);
die();
}
private function doempty(){
echo json_encode(
[
$_GET["s"],
[]
],
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_IGNORE
);
die();
}
}

39
api/v1/images.php Normal file
View file

@ -0,0 +1,39 @@
<?php
chdir("../../");
header("Content-Type: application/json");
include "data/config.php";
if(config::API_ENABLED === false){
echo json_encode(["status" => "The server administrator disabled the API!"]);
return;
}
include "lib/frontend.php";
$frontend = new frontend();
/*
Captcha
*/
include "lib/bot_protection.php";
$null = null;
new bot_protection($null, $null, $null, "images", false);
[$scraper, $filters] = $frontend->getscraperfilters(
"images",
isset($_GET["scraper"]) ? $_GET["scraper"] : null
);
$get = $frontend->parsegetfilters($_GET, $filters);
try{
echo json_encode(
$scraper->image($get),
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_IGNORE
);
}catch(Exception $e){
echo json_encode(["status" => $e->getMessage()]);
}

10
api/v1/index.php Normal file
View file

@ -0,0 +1,10 @@
<?php
header("Content-Type: application/json");
http_response_code(404);
echo json_encode(
[
"status" => "Unknown endpoint"
]
);

39
api/v1/music.php Normal file
View file

@ -0,0 +1,39 @@
<?php
chdir("../../");
header("Content-Type: application/json");
include "data/config.php";
if(config::API_ENABLED === false){
echo json_encode(["status" => "The server administrator disabled the API!"]);
return;
}
include "lib/frontend.php";
$frontend = new frontend();
/*
Captcha
*/
include "lib/bot_protection.php";
$null = null;
new bot_protection($null, $null, $null, "music", false);
[$scraper, $filters] = $frontend->getscraperfilters(
"music",
isset($_GET["scraper"]) ? $_GET["scraper"] : null
);
$get = $frontend->parsegetfilters($_GET, $filters);
try{
echo json_encode(
$scraper->music($get),
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_IGNORE
);
}catch(Exception $e){
echo json_encode(["status" => $e->getMessage()]);
}

39
api/v1/news.php Normal file
View file

@ -0,0 +1,39 @@
<?php
chdir("../../");
header("Content-Type: application/json");
include "data/config.php";
if(config::API_ENABLED === false){
echo json_encode(["status" => "The server administrator disabled the API!"]);
return;
}
include "lib/frontend.php";
$frontend = new frontend();
/*
Captcha
*/
include "lib/bot_protection.php";
$null = null;
new bot_protection($null, $null, $null, "news", false);
[$scraper, $filters] = $frontend->getscraperfilters(
"news",
isset($_GET["scraper"]) ? $_GET["scraper"] : null
);
$get = $frontend->parsegetfilters($_GET, $filters);
try{
echo json_encode(
$scraper->news($get),
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_IGNORE
);
}catch(Exception $e){
echo json_encode(["status" => $e->getMessage()]);
}

39
api/v1/videos.php Normal file
View file

@ -0,0 +1,39 @@
<?php
chdir("../../");
header("Content-Type: application/json");
include "data/config.php";
if(config::API_ENABLED === false){
echo json_encode(["status" => "The server administrator disabled the API!"]);
return;
}
include "lib/frontend.php";
$frontend = new frontend();
/*
Captcha
*/
include "lib/bot_protection.php";
$null = null;
new bot_protection($null, $null, $null, "videos", false);
[$scraper, $filters] = $frontend->getscraperfilters(
"videos",
isset($_GET["scraper"]) ? $_GET["scraper"] : null
);
$get = $frontend->parsegetfilters($_GET, $filters);
try{
echo json_encode(
$scraper->video($get),
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_IGNORE
);
}catch(Exception $e){
echo json_encode(["status" => $e->getMessage()]);
}

52
api/v1/web.php Normal file
View file

@ -0,0 +1,52 @@
<?php
chdir("../../");
header("Content-Type: application/json");
include "data/config.php";
if(config::API_ENABLED === false){
echo json_encode(["status" => "The server administrator disabled the API!"]);
return;
}
include "lib/frontend.php";
$frontend = new frontend();
/*
Captcha
*/
include "lib/bot_protection.php";
$null = null;
new bot_protection($null, $null, $null, "web", false);
[$scraper, $filters] = $frontend->getscraperfilters(
"web",
isset($_GET["scraper"]) ? $_GET["scraper"] : null
);
$get = $frontend->parsegetfilters($_GET, $filters);
if(
isset($_GET["extendedsearch"]) &&
$_GET["extendedsearch"] == "yes"
){
$get["extendedsearch"] = "yes";
}else{
$get["extendedsearch"] = "no";
}
try{
echo
json_encode(
$scraper->web($get),
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_IGNORE
);
}catch(Exception $e){
echo json_encode(["status" => $e->getMessage()]);
}

20
audio/linear.php Normal file
View file

@ -0,0 +1,20 @@
<?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());
}

224
audio/sc.php Normal file
View file

@ -0,0 +1,224 @@
<?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();
}
}

20
audio/seekable.php Normal file
View file

@ -0,0 +1,20 @@
<?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());
}

214
audio/spotify.php Normal file
View file

@ -0,0 +1,214 @@
<?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();
}
}

203
captcha.php Normal file
View file

@ -0,0 +1,203 @@
<?php
if(
isset($_GET["v"]) === false ||
is_array($_GET["v"]) === true ||
preg_match(
'/^c[0-9]+\.[A-Za-z0-9_]{20}$/',
$_GET["v"]
) === 0
){
http_response_code(401);
header("Content-Type: text/plain");
echo "Fuck my feathered cloaca";
die();
}
//header("Content-Type: image/jpeg");
include "data/config.php";
if(config::BOT_PROTECTION !== 1){
header("Content-Type: text/plain");
echo "The IQ test is disabled";
die();
}
$grid = apcu_fetch($_GET["v"]);
if($grid !== false){
// captcha already generated
http_response_code(304); // not modified
die();
}
header("Content-Type: image/jpeg");
header("Last-Modified: Thu, 01 Oct 1970 00:00:00 GMT");
// ** generate captcha data
// get the positions for the answers
// will return between 3 and 6 answer positions
$range = range(0, 15);
$answer_pos = [];
array_splice($range, 0, 1);
$picks = random_int(3, 6);
for($i=0; $i<$picks; $i++){
$answer_pos_tmp =
array_splice(
$range,
random_int(
0,
14 - $i
),
1
);
$answer_pos[] = $answer_pos_tmp[0];
}
// choose a dataset
$c = count(config::CAPTCHA_DATASET);
$choosen = config::CAPTCHA_DATASET[random_int(0, $c - 1)];
$choices = [];
for($i=0; $i<$c; $i++){
if(config::CAPTCHA_DATASET[$i][0] == $choosen[0]){
continue;
}
$choices[] = config::CAPTCHA_DATASET[$i];
}
// generate grid data
$grid = [];
for($i=0; $i<16; $i++){
if(in_array($i, $answer_pos)){
$grid[] = $choosen;
}else{
$grid[] = $choices[random_int(0, count($choices) - 1)];
}
}
// store grid data for form validation on captcha_gen.php
apcu_store(
$_GET["v"],
$answer_pos,
60 // we give user 1 minute to solve
);
// generate image
if(random_int(0,1) === 0){
$theme = [
"bg" => "#ebdbb2",
"fg" => "#1d2021"
];
}else{
$theme = [
"bg" => "#1d2021",
"fg" => "#ebdbb2"
];
}
$im = new Imagick();
$im->newImage(400, 427, $theme["bg"]);
$im->setImageBackgroundColor($theme["bg"]);
$im->setImageFormat("jpg");
$noise = [
imagick::NOISE_GAUSSIAN,
imagick::NOISE_LAPLACIAN
];
$distort = [
imagick::DISTORTION_AFFINE,
imagick::DISTORTION_SHEPARDS
];
$i = 0;
for($y=0; $y<4; $y++){
for($x=0; $x<4; $x++){
$tmp = new Imagick("./data/captcha/" . $grid[$i][0] . "/" . random_int(1, $grid[$i][1]) . ".png");
// convert transparency correctly
$tmp->setImageBackgroundColor("black");
$tmp->setImageAlphaChannel(Imagick::ALPHACHANNEL_REMOVE);
// randomly mirror
if(random_int(0,1) === 1){
$tmp->flopImage();
}
// distort $tmp
$tmp->distortImage(
$distort[random_int(0,1)],
[
0, 0,
random_int(-15, 15), random_int(-15, 15),
100, 0,
random_int(80, 120), random_int(-15, 15),
100, 100,
random_int(80, 120), random_int(80, 120),
0, 100,
random_int(-15, 15), random_int(80, 120)
],
false
);
$tmp->addNoiseImage($noise[random_int(0, 1)]);
// append image
$im->compositeImage($tmp->getImage(), Imagick::COMPOSITE_DEFAULT, $x * 100, ($y * 100) + 27);
$i++;
}
}
// add text
$draw = new ImagickDraw();
$draw->setFontSize(20);
$draw->setFillColor($theme["fg"]);
//$draw->setTextAntialias(false);
$draw->setFont("./data/fonts/captcha.ttf");
$text = "Pick " . $picks . " images of " . str_replace("_", " ", $choosen[0]);
$pos = 200 - ($im->queryFontMetrics($draw, $text)["textWidth"] / 2);
for($i=0; $i<strlen($text); $i++){
$im->annotateImage(
$draw,
$pos,
20,
random_int(-15, 15),
$text[$i]
);
$pos += $im->queryFontMetrics($draw, $text[$i])["textWidth"];
}
$im->setFormat("jpeg");
$im->setImageCompressionQuality(90);
echo $im->getImageBlob();

157
data/config.php Normal file
View file

@ -0,0 +1,157 @@
<?php
class config{
// Welcome to the 4Get configuration file
// When updating your instance, please make sure this file isn't missing
// any parameters.
// 4Get version. Please keep this updated
const VERSION = 8;
// Will be shown pretty much everywhere.
const SERVER_NAME = "FbIN 4Get";
// Will be shown in <meta> tag on home page
const SERVER_SHORT_DESCRIPTION = "4Get is a proxy search engine that doesn't suck.";
// Will be shown in server list ping (null for no description)
const SERVER_LONG_DESCRIPTION = FbIN;
// Add your own themes in "static/themes". Set to "Dark" for default theme.
// Eg. To use "static/themes/Cream.css", specify "Cream".
const DEFAULT_THEME = "Catppuccin Latte";
// Enable the API?
const API_ENABLED = false;
//
// BOT PROTECTION
//
// 0 = disabled, 1 = ask for image captcha, @TODO: 2 = invite only (users needs a pass)
// VERY useful against a targetted attack
const BOT_PROTECTION = 0;
// if BOT_PROTECTION is set to 1, specify the available datasets here
// images should be named from 1.png to X.png, and be 100x100 in size
// Eg. data/captcha/birds/1.png up to 2263.png
const CAPTCHA_DATASET = [
// example:
//["birds", 2263],
//["fumo_plushies", 1006],
//["minecraft", 848]
];
// If this regex expression matches on the user agent, it blocks the request
// Not useful at all against a targetted attack
const HEADER_REGEX = '/bot|wget|curl|python-requests|scrapy|go-http-client|ruby|yahoo|spider|qwant/i';
// Block clients who present any of the following headers in their request (SPECIFY IN !!lowercase!!)
// Eg: ["x-forwarded-for", "x-via", "forwarded-for", "via"];
// Useful for blocking *some* proxies used for botting
const FILTERED_HEADER_KEYS = [
//"x-forwarded-for",
//"x-cluster-client-ip",
//"x-client-ip",
//"x-real-ip",
//"client-ip",
//"real-ip",
//"forwarded-for",
//"forwarded-for-ip",
//"forwarded",
//"proxy-connection",
//"remote-addr",
//"via"
];
// Block SSL ciphers used by CLI tools used for botting
// Basically a primitive version of Cloudflare's browser integrity check
// ** If curl can still access the site (with spoofed headers), please make sure you use the new apache2 config **
// https://git.flossboxin.org.in/FbIN/4Get/docs/apache2.md
const DISALLOWED_SSL = [
// "TLS_AES_256_GCM_SHA384" // used by WGET and CURL
];
// Maximal number of searches per captcha key/pass issued. Counter gets
// reset on every APCU cache clear (should happen once a day).
// Only useful when BOT_PROTECTION is NOT set to 0
const MAX_SEARCHES = 100;
// List of domains that point to your servers. Include your tor/i2p
// addresses here! Must be a valid URL. Won't affect links placed on
// the homepage.
const ALT_ADDRESSES = [
//"https://4get.alt-tld",
//"http://4getwebfrq5zr4sxugk6htxvawqehxtdgjrbcn2oslllcol2vepa23yd.onion"
];
// Known 4Get instances. MUST use the https protocol if your instance uses
// it. Is used to generate a distributed list of instances.
// To appear in the list of an instance, contact the host and if everyone added
// eachother your serber should appear everywhere.
const INSTANCES = [
"https://4g.flossboxin.org.in",
"https://4get.ca",
"https://4get.nadeko.net",
"https://4get.silly.computer",
"https://4get.sijh.net",
"https://4get.hbubli.cc",
"https://4get.lunar.icu",
"https://4get.neco.lol",
"https://4get.plunked.party/",
"https://4get.ch",
"https://4get.edmateo.site",
"https://4get.sudovanilla.org",
"https://search.mint.lgbt"
];
// 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:135.0) Gecko/20100101 Firefox/135.0";
// Proxy pool assignments for each scraper
// false = Use server's raw IP
// string = will load a proxy list from data/proxies
// Eg. "onion" will load data/proxies/onion.txt
const PROXY_DDG = false; // duckduckgo
const PROXY_BRAVE = false;
const PROXY_FB = false; // facebook
const PROXY_GOOGLE = false;
const PROXY_GOOGLE_CSE = false;
const PROXY_STARTPAGE = false;
const PROXY_QWANT = false;
const PROXY_GHOSTERY = false;
const PROXY_MARGINALIA = false;
const PROXY_MOJEEK = false;
const PROXY_SC = false; // soundcloud
const PROXY_SPOTIFY = false;
const PROXY_SOLOFIELD = false;
const PROXY_WIBY = false;
const PROXY_CURLIE = false;
const PROXY_YT = false; // youtube
const PROXY_YEP = false;
const PROXY_PINTEREST = false;
const PROXY_FIVEHPX = false;
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;
const PROXY_YANDEX_W = false; // yandex web
const PROXY_YANDEX_I = false; // yandex images
const PROXY_YANDEX_V = false; // yandex videos
//
// Scraper-specific parameters
//
// GOOGLE CSE
const GOOGLE_CX_ENDPOINT = "d4e68b99b876541f0";
// MARGINALIA
// 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;
}

BIN
data/fonts/captcha.ttf Normal file

Binary file not shown.

3
data/proxies/.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
*
!.gitignore
!onion.txt

Some files were not shown because too many files have changed in this diff Show more