Compare commits

..

113 commits
v0.8.2 ... main

Author SHA1 Message Date
Ben Busby
e70bbabd63 Bump version to 0.9.1 2024-10-31 17:36:37 -06:00
dependabot[bot]
4fad68adf2
Bump waitress from 2.1.2 to 3.0.1 (#1192)
Bumps [waitress](https://github.com/Pylons/waitress) from 2.1.2 to 3.0.1.
- [Release notes](https://github.com/Pylons/waitress/releases)
- [Changelog](https://github.com/Pylons/waitress/blob/main/CHANGES.txt)
- [Commits](https://github.com/Pylons/waitress/compare/v2.1.2...v3.0.1)

---
updated-dependencies:
- dependency-name: waitress
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-31 17:20:55 -06:00
dependabot[bot]
89d07abb6c
Bump werkzeug from 3.0.3 to 3.0.6 (#1190)
Bumps [werkzeug](https://github.com/pallets/werkzeug) from 3.0.3 to 3.0.6.
- [Release notes](https://github.com/pallets/werkzeug/releases)
- [Changelog](https://github.com/pallets/werkzeug/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/werkzeug/compare/3.0.3...3.0.6)

---
updated-dependencies:
- dependency-name: werkzeug
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-31 17:17:11 -06:00
Dave Fatkin
a8205c3043
Use alternate USPS link for package tracking [skip ci] (#1191)
The previous tracking link would sometimes lead to a blank page
2024-10-31 17:11:36 -06:00
Roman Štefko
1b9f7990b6
Allow setting fallback search engine (#1184)
Allow setting a fallback Search Engine URL when there is internal server error
or instance is rate-limited. Search query is appended to the end of the URL
(eg. https://duckduckgo.com/?k1=-1&q=).
2024-10-31 17:06:47 -06:00
Tarek
c54f4a79a6
Add ability to set custom dotenv path (#1183)
* add ability to set custom dotenv path

* add `WHOOGLE_DOTENV_PATH` to environment variables table
2024-10-31 16:52:18 -06:00
Ben Busby
223f00c3c0 Switch from pickle->json for config encoding
`pickle.loads` is insecure (and unnecessary in this context). The
simpler data types of the config model can easily be serialized to json
instead.
2024-10-31 16:43:29 -06:00
Ben Busby
466cb63d0e
Bump version to 0.9.0 2024-09-30 12:18:09 -06:00
Ben Busby
c056322037
Use .lower() when accessing the cache busting map
Cache busted resources will always have a lowercase name. For some
reason, a user encountered a capitalized css file name in #1142. This
isn't present anywhere in the Whoogle repo, but could be a byproduct of
some self-hosting configuration. Either way, the key used should always
be lowercase.

Fixes #1142
2024-09-30 12:00:52 -06:00
Ben Busby
5eb609d0b2
Hide line below search bar in dark themes
The results return an empty div with a background color set (not sure
why, not generated on Whoogle's side). This unsets the background color
so that at least it isn't visible.
2024-09-30 11:57:12 -06:00
Ben Busby
a016a1bcf4
Use raw string for matching regex in results.py
Fixes #1144
2024-09-30 11:41:13 -06:00
Ben Busby
6924f5ce0d
Update ad filter keyword for Dutch language
Fixes #1172
2024-09-30 11:33:50 -06:00
Ben Busby
6abe5511f4
Update ad filter keyword for Korean language
Fixes #1162
2024-09-30 11:32:29 -06:00
xatier
9d7ab1e2f8
ci: update GitHub actions (#1155) 2024-09-30 11:22:42 -06:00
dependabot[bot]
441ce72527
Bump idna from 3.4 to 3.7 (#1137)
Bumps [idna](https://github.com/kjd/idna) from 3.4 to 3.7.
- [Release notes](https://github.com/kjd/idna/releases)
- [Changelog](https://github.com/kjd/idna/blob/master/HISTORY.rst)
- [Commits](https://github.com/kjd/idna/compare/v3.4...v3.7)

---
updated-dependencies:
- dependency-name: idna
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-30 11:18:24 -06:00
Andrew
58d54c6384
Update ad filter for Czech language (#1141) 2024-09-30 11:12:37 -06:00
dependabot[bot]
add9313a99
Bump werkzeug from 3.0.1 to 3.0.3 (#1150)
Bumps [werkzeug](https://github.com/pallets/werkzeug) from 3.0.1 to 3.0.3.
- [Release notes](https://github.com/pallets/werkzeug/releases)
- [Changelog](https://github.com/pallets/werkzeug/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/werkzeug/compare/3.0.1...3.0.3)

---
updated-dependencies:
- dependency-name: werkzeug
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-30 11:12:04 -06:00
dependabot[bot]
436233f718
Bump jinja2 from 3.1.3 to 3.1.4 (#1151)
Bumps [jinja2](https://github.com/pallets/jinja) from 3.1.3 to 3.1.4.
- [Release notes](https://github.com/pallets/jinja/releases)
- [Changelog](https://github.com/pallets/jinja/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/jinja/compare/3.1.3...3.1.4)

---
updated-dependencies:
- dependency-name: jinja2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-30 11:09:38 -06:00
dependabot[bot]
58b7512707
Bump requests from 2.31.0 to 2.32.2 (#1180)
Bumps [requests](https://github.com/psf/requests) from 2.31.0 to 2.32.2.
- [Release notes](https://github.com/psf/requests/releases)
- [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md)
- [Commits](https://github.com/psf/requests/compare/v2.31.0...v2.32.2)

---
updated-dependencies:
- dependency-name: requests
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-30 11:06:55 -06:00
dependabot[bot]
cc7907fb84
Bump cryptography from 42.0.4 to 43.0.1 (#1179)
* Bump cryptography from 42.0.4 to 43.0.1

Bumps [cryptography](https://github.com/pyca/cryptography) from 3.3.2 to 43.0.1.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/3.3.2...43.0.1)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* Update pyopenssl version

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ben Busby <contact@benbusby.com>
2024-09-30 11:00:43 -06:00
dependabot[bot]
ee5a536861
Bump urllib3 from 1.26.18 to 1.26.19 (#1161)
Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.26.18 to 1.26.19.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/1.26.19/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/1.26.18...1.26.19)

---
updated-dependencies:
- dependency-name: urllib3
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-30 10:52:25 -06:00
dependabot[bot]
5b8ac0c52e
Bump certifi from 2023.7.22 to 2024.7.4 (#1164)
Bumps [certifi](https://github.com/certifi/python-certifi) from 2023.7.22 to 2024.7.4.
- [Commits](https://github.com/certifi/python-certifi/compare/2023.07.22...2024.07.04)

---
updated-dependencies:
- dependency-name: certifi
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-30 10:49:01 -06:00
OldSoul.io
97394f2b02
Fix Alfred instructions in readme [skip ci] (#1156)
Fixed the inline codeblock for Alfred's search URL to display the url properly in the block.
2024-09-30 10:44:21 -06:00
Akopov
91112f1b7b
Refactor utils/misc.py (#1165)
* Improving readability, removing unnecessary elements, etc.

* Minor changes to comment style and favicon response code

---------

Co-authored-by: Ben Busby <contact@benbusby.com>
2024-09-30 10:42:12 -06:00
Akopov
81420e035a
Update pipx instructions in readme [skip ci] (#1166)
If `pipx install git+<link>` is used, the user must have git installed,
otherwise the package cannot be installed. In the case of a direct link to the
archive, this is not required.
2024-09-30 10:35:58 -06:00
Matt Burns
a509169110
Add SO as a default site alternative (#1168)
SO -> farside.link/anonymousoverflow
2024-09-30 10:34:54 -06:00
Amin
79b6e9a157
Add X-Forwarded-Host to nginx example [skip ci] (#1169) 2024-09-30 10:34:15 -06:00
Alpha
5cf5ad992a
docs: update fly.io free plan details [skip ci] (#1173)
Free plan no longer available in fly.io
https://fly.io/docs/about/pricing/#free-allowances
2024-09-30 10:33:08 -06:00
Vinfall
95f872ccde
fix: description in opensearch template (#1175) 2024-09-30 10:32:46 -06:00
Ben Busby
06c2310e3a
Update Docker base images, update workflows
Should fix issues with tomllib not being available in the docker related
builds, as well as the move from docker-compose to "docker compose" in
gh actions
2024-09-30 10:27:01 -06:00
Ben Busby
9bfdd88a5e
Fix input cursor position bug on mobile
Refactors how search suggestions are added to the view

Fixes #1133
2024-04-19 14:00:12 -06:00
Ben Busby
37ff61dfac
Fall back to random secret key on permission exception
Fixes #1136
2024-04-19 12:49:52 -06:00
David Shen
80e41e6b44
Attempt to query on error condition and print trace (#1131) 2024-04-19 12:47:07 -06:00
David Shen
f18bf07ac3
Fix feeling lucky (#1130)
* Fix feeling lucky, fall through to display results if doesn't work

* Allow lucky bang anywhere

* Update feeling lucky test
2024-04-19 12:40:06 -06:00
David Shen
fd20135af0
Add support for custom bangs (#1132)
Add the possibility for user-defined bangs, stored in app/static/bangs. 

These are parsed in alphabetical order, with the DDG bangs parsed first.
2024-04-19 12:26:42 -06:00
Ben Busby
7a1ebfe975
Temporarily disable linux/arm/v7 builds
The arm/v7 builds have caused lots of problems due to the lack of
support from the cryptography library, and now issues related to
installing the latest version of cffi. As a result, this build variant
has been removed for now. It may or may not come back later, since the
amount of work just to figure out which library is broken and how to fix
it doesn't feel worth it anymore.
2024-03-06 12:31:38 -07:00
Ben Busby
efbbd6b9d6
Add render.com deployment instructions [skip ci]
Closes #833
2024-03-06 11:13:51 -07:00
Ben Busby
4d1d3f4984
Add public instance [skip ci]
https://search.snine.nl

Closes #1115
2024-03-06 10:58:32 -07:00
Ben Busby
c389c26220
Add favicon.ico endpoint
Closes #1121
2024-03-06 10:37:48 -07:00
Ben Busby
ef54f00212
Only show redirects on error page if query is available
The redirects portion of the error page is only needed in scenarios
where the instance is rate limited, in which case the user's query is
provided to the error template. If this isn't provided, it should just
display the error and allow the user to redirect to the home page.

Fixes #1122
2024-03-06 10:14:52 -07:00
dependabot[bot]
af60509a8d
Bump cryptography to 42.0.4, pyopenssl to 24.0.0 (#1123)
* Bump cryptography to 42.0.4

* Bump pyopenssl to 24.0.0

* Squashed commit of the following:

commit 2395bb7a6a
Author: Ben Busby <contact@benbusby.com>
Date:   Wed Mar 6 09:35:48 2024 -0700

    Remove version from DDG bangs url

    Including the version portion of the URL now redirects to search results
    for the name of the bang file, rather than returning the bang file
    itself. Removing the version from the URL returns the correct bang file.

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ben Busby <contact@benbusby.com>
2024-03-06 09:51:16 -07:00
Ben Busby
2395bb7a6a
Remove version from DDG bangs url
Including the version portion of the URL now redirects to search results
for the name of the bang file, rather than returning the bang file
itself. Removing the version from the URL returns the correct bang file.
2024-03-06 09:35:48 -07:00
dependabot[bot]
c216c033ef
Bump jinja2 from 3.1.2 to 3.1.3 (#1111)
Bumps [jinja2](https://github.com/pallets/jinja) from 3.1.2 to 3.1.3.
- [Release notes](https://github.com/pallets/jinja/releases)
- [Changelog](https://github.com/pallets/jinja/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/jinja/compare/3.1.2...3.1.3)

---
updated-dependencies:
- dependency-name: jinja2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-05 09:55:19 -07:00
Ben Busby
aaf90b52bb
Add public instance [skip ci]
Closes #1105
2024-01-08 10:46:42 -07:00
Ben Busby
7313edff46
Include more options for continuing search in error tmpl
The error template previously only included the option to continue a
user's search via Farside (whoogle or searxng), and would only appear
when an instance was ratelimited. This has been updated to display
anytime an exception has occurred, and now includes other options for
continuing a search, such as Kagi, DDG, Brave, Ecosia, etc.

Closes #1099
2023-12-20 12:14:34 -07:00
Ben Busby
cdbe550737
Add env vars for hiding favicons and removing daily update check
- WHOOGLE_SHOW_FAVICONS: Default on, can be set to 0 to hide favicons
  and skip the request for fetching them
- WHOOGLE_UPDATE_CHECK: Default on, can be set to 0 to disable the
  daily check for new versions released on github

Closes #1098
Closes #1059
2023-12-20 11:28:00 -07:00
Vivek
70dc750c7a
Add arg for configuring unix socket perms (#1103)
The default unix socket permissions of 600 is too restrictive for many use
cases.

Added a new argument --unix-socket-perms which is passed to waitress to allow
for user configurable socket permissions
2023-12-20 11:05:38 -07:00
Ben Busby
b5ae07613b
Use url params for testing time based result filtering
The ":past <duration>" query string filtering isn't used anymore since
adding the option to filter by time in the result view.
2023-12-20 11:00:10 -07:00
Ben Busby
166b28040a
Remove outdated instance [skip ci]
Closes #1102
2023-12-05 15:23:59 -07:00
dependabot[bot]
57398a9b3b
Bump cryptography from 3.3.2 to 41.0.6 (#1101)
Bumps [cryptography](https://github.com/pyca/cryptography) from 3.3.2 to 41.0.6.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/3.3.2...41.0.6)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-05 15:22:52 -07:00
Ben Busby
b97f3dd4c0
Bump version to 0.8.4 2023-11-01 14:45:42 -06:00
Ben Busby
9f68c843d6
Specify links that should trigger div removal from results
There are certain links (such as the age verification link mentioned in
issue #1083) that should trigger removal of the entire container div on
the results page, rather than just hiding the link itself.

This introduces a new `unsupported_g_divs` list that holds links that
will trigger a removal of the result div on the result page.

Fixes #1083
2023-11-01 14:30:23 -06:00
dependabot[bot]
2a0b9a47b2
Bump werkzeug from 2.3.3 to 3.0.1 (#1093)
Bumps [werkzeug](https://github.com/pallets/werkzeug) from 2.3.3 to 3.0.1.
- [Release notes](https://github.com/pallets/werkzeug/releases)
- [Changelog](https://github.com/pallets/werkzeug/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/werkzeug/compare/2.3.3...3.0.1)

---
updated-dependencies:
- dependency-name: werkzeug
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-01 14:08:49 -06:00
dependabot[bot]
1644a4a04e
Bump urllib3 from 1.26.17 to 1.26.18 (#1085)
Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.26.17 to 1.26.18.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/1.26.17...1.26.18)

---
updated-dependencies:
- dependency-name: urllib3
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-01 14:08:29 -06:00
Zhiyong Dou
6a10efbd9e
fix: add missing dependency validators to setup.cfg (#1087) 2023-11-01 14:08:08 -06:00
Gautam Korlam
9cc1004fb8
fix: correctly handle skip_prefix logic for site_alts (#1092)
Fixes #1091
2023-11-01 14:07:45 -06:00
Ben Busby
cdf0b50284
Update search tests to only perform GET requests
Since POST requests are now redirected to GET requests (with an
encrypted query string), POST searches are no longer the correct
approach to use for testing purposes.
2023-10-16 16:46:15 -06:00
Ben Busby
2950aa869b
Redirect POST search -> enc GET request
This should fix the annoyance with browsers like Firefox not caching
POST request responses. By redirecting a POST search to be a GET request
instead (with an encrypted query string), the page can be cached and
successfully navigated back to after visiting a result.
2023-10-16 16:28:36 -06:00
Ben Busby
7bda165ca3
Fetch fallback site icons from DDG
DDG provides favicons using the url format
icons.duckduckgo.com/ip2/{site}.ico

This can be used to fetch favicons in the event that the default
"/favicon.ico" path does not work.
2023-10-11 17:26:12 -06:00
Ben Busby
81b7fd1876
Encrypt site icon requests
Paths to favicons are now encrypted with the user's Fernet key, the same
as any other external result page element
2023-10-11 17:18:25 -06:00
Ben Busby
a7e937f7c6
Skip scrollers when applying site icons to results
Scroller results (like the "latest from ___" or "top stories" results)
shouldn't have a site icon associated with them. This extracts the class
that those types of results have and skips over the process of inserting
an icon.
2023-10-11 15:58:52 -06:00
Ben Busby
c2873190c9
Display audio controls, refactor site icon placement
Audio controls are now always shown by default (mostly found in searches
that contain word pronunciation guides).

Site icons were moved to the left side of the results.
2023-10-11 15:41:48 -06:00
Ben Busby
f7513bab69
Echo address:port in run script 2023-10-11 14:12:40 -06:00
Ben Busby
330ae964f3
Only sanitize result content on main result page
The other result tabs (images/maps/videos/news) don't have text content
that needs sanitizing.

Fixes #1080
2023-10-11 11:09:09 -06:00
Ben Busby
67b6110087
Display an empty img if a site icon can't be found
This improves the search result icon feature by "hiding" the site's icon
if one was not found. This happens in scenarios where a site doesn't
have a /favicon.ico due to having a unique path or using javascript to
load the icon.
2023-10-11 11:05:53 -06:00
Ben Busby
4292ec7f63
Add icons for each search result
This appends an icon element to each search result, using the result
domain's "/favicon.ico" path.

Note that some sites do not have a standard /favicon.ico, but have a
unique path to a specifically sized favicon instead. Worse still, some
sites use javascript to load their favicon, which would make it even
more difficult for Whoogle to figure out.

For now this approach is fine, but can be expanded upon in the future
if desired.
2023-10-11 11:05:53 -06:00
Ben Busby
3ef191b5d8
Add missing env var to readme [skip ci]
Closes #1075
2023-10-10 16:42:42 -06:00
Ben Busby
c36396e9cb
Sanitize valid html in result text content
This inspects the text content of each individual result div and strips
out valid 'script' or 'iframe' tags from the result.

Closes #1076
2023-10-10 16:38:13 -06:00
dependabot[bot]
0d013c788f
Bump urllib3 from 1.26.14 to 1.26.17 (#1073)
Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.26.14 to 1.26.17.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/1.26.14...1.26.17)

---
updated-dependencies:
- dependency-name: urllib3
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-10 15:22:47 -06:00
AlekseiKondrashov
ec05f8d52f
Remove a public instance [skip ci] (#1074)
Removes https://search.neadond.space

---------

Co-authored-by: Ben Busby <contact@benbusby.com>
2023-10-10 15:22:29 -06:00
AlekseiKondrashov
e2070045bf
Add new public instance [skip ci] (#1069)
New instance: https://search.neadond.space

---------

Co-authored-by: Ben Busby <contact@benbusby.com>
2023-09-29 10:40:09 -06:00
dependabot[bot]
b093d39ed1
Bump cryptography from 3.3.2 to 41.0.4 (#1067)
Bumps [cryptography](https://github.com/pyca/cryptography) from 3.3.2 to 41.0.4.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/3.3.2...41.0.4)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-28 14:32:09 -06:00
Ben Busby
e9c4ed6399
Add WorkingDirectory to systemd example [skip ci] 2023-09-13 15:58:01 -06:00
Ben Busby
8b99a87020
Include validators in requirements.txt 2023-09-13 15:51:35 -06:00
Ben Busby
3a2e0b262e
Validate urls in element and window endpoints
Domains were previously not validated before being handled, leading to a
potential scenario where someone could pass something like
"element_url=127.0.0.1:<port>/<resource>" to access other resources on a
machine running Whoogle. This change ensures that the resource used in
both endpoints is a valid domain.

This also includes validation of config names to prevent names from
including path values such as "../../(etc)".
2023-09-13 15:50:04 -06:00
Anton Mironov
8830615abc
[Helm] Provide auth in probes if it is set (#1065)
If the auth is set, the probes fail with a 401 error.
2023-09-13 14:14:03 -06:00
MoistCat
693ca3a9a8
Fix invalid calculator widget path (#1064)
When starting whoogle from another directory, the path to the calculator
widget was previously invalid. It now specifies the path relative to the widget
loader file.
2023-09-13 14:13:21 -06:00
Ben Busby
a35b1dabbc
Use filtered query for map tab
The map tab should only ever pass the raw query (i.e. no "-site:..."
strings), otherwise the maps page will return an error.

Fixes #1048
2023-09-08 16:44:04 -06:00
Ben Busby
a623210244
Match exact words to trigger calculator widget
The calculator was previously triggered for partial matches with words
like "calc", which meant searches containing the word "calcium" would
cause the calculator widget to appear.
2023-09-08 16:19:39 -06:00
Ben Busby
abbe29d9d3
Add public instance to readme [skip ci]
whoogle.yepserver.xyz

Closes #1061
2023-09-08 16:08:12 -06:00
satoru
19be033ba2
Update fly.io doc in readme [skip ci] (#1058) 2023-09-08 16:01:13 -06:00
Ben Busby
92e8ede24e
Bump version to 0.8.3 2023-08-21 15:06:17 -06:00
Ahmad Alkadri
4a0089686e
Fix: keep_blank_values = True to handle blank q input (#1052) 2023-08-21 14:53:10 -06:00
Fabian Wunsch
a40b98341b
Change the consent cookies (#1054)
* Changed the consent cookies

* Shorter cookie thanks to @Imaskiller
2023-08-21 14:50:38 -06:00
Ben Busby
18fc14dc5b
Add public instance [skip ci]
* whoogle.datura.network

Closes #1034
2023-08-02 11:02:27 -06:00
dependabot[bot]
8a3c9ea397
Bump cryptography from 41.0.0 to 41.0.3 (#1044)
Bumps [cryptography](https://github.com/pyca/cryptography) from 41.0.0 to 41.0.3.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/41.0.0...41.0.3)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-02 10:47:41 -06:00
dependabot[bot]
ee25d3a23d
Bump certifi from 2022.12.7 to 2023.7.22 (#1038)
Bumps [certifi](https://github.com/certifi/python-certifi) from 2022.12.7 to 2023.7.22.
- [Commits](https://github.com/certifi/python-certifi/compare/2022.12.07...2023.07.22)

---
updated-dependencies:
- dependency-name: certifi
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-02 10:46:09 -06:00
PrivacyDevel
c705bc7391
Update location of privacydev.net instances [skip ci] (#1036) 2023-08-02 10:44:01 -06:00
cgi-bin/
2ac9e37696
Add public instance [skip ci] (#1037)
* search.notrustverify.ch
2023-08-02 10:43:28 -06:00
Simon Rusinov
6f9d11a6ed
Optionally build obfs4proxy in docker (#1032) 2023-08-02 10:42:46 -06:00
Ben Busby
63a2ea56ed
Add public instance [skip ci]
https://whoogle-search--replitcomreside.repl.co
2023-07-05 14:05:25 -06:00
Ben Busby
4962659acb
Serve basic robots.txt to avoid indexing
Closes #1015
2023-06-26 16:16:45 -06:00
Cx
8a2a6f3265
Update translations.json [skip ci] (#1025) 2023-06-26 15:49:19 -06:00
PrivacyDevel
1b4e4e144e
Update country for PrivacyDev instances [skip ci] (#1024) 2023-06-26 15:48:22 -06:00
Andiru
29992985bc
Fix incorrect link replacements (#1016)
Fix link/result description getting replaced when alternative is disabled
(set to empty string)

Replace medium.com links with value from constant
2023-06-26 15:47:43 -06:00
Ben Busby
179fc5e020
Update cryptography + pyopenssl 2023-06-26 15:43:18 -06:00
Ben Busby
421a8ac054
Replace broken public instance url [skip ci]
Fixes #1013
2023-06-14 14:44:41 -06:00
Ben Busby
0003da4bb9
Update public instances [skip ci]
Removes whoogle.esmailelbob.xyz

Closes #1006
2023-05-25 15:51:17 -06:00
dependabot[bot]
2b8d100cfb
Bump requests from 2.28.2 to 2.31.0 (#1008)
Bumps [requests](https://github.com/psf/requests) from 2.28.2 to 2.31.0.
- [Release notes](https://github.com/psf/requests/releases)
- [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md)
- [Commits](https://github.com/psf/requests/compare/v2.28.2...v2.31.0)

---
updated-dependencies:
- dependency-name: requests
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-25 15:50:12 -06:00
fredster33
f0a1223f40
Fix incorrect TLD in readme [skip ci] (#1007)
* Wikipedia.org, not .com

* Capitalization of JavaScript
2023-05-25 15:49:34 -06:00
Felix
958f74e1f5
Fix deadlinks and markdown [skip ci] (#1009) 2023-05-25 15:19:15 -06:00
Ben Busby
08732fac9e
Fix link typo in readme [skip ci] 2023-05-19 12:27:04 -06:00
Ben Busby
f65529f328
Allow defining custom redirects with WHOOGLE_REDIRECTS
Redirects to alternative frontends can now be defined using the
WHOOGLE_REDIRECTS environment variable. Usage is documented in the
readme, but is basically defined as <parent>:<new>.

Closes #988
2023-05-19 12:15:15 -06:00
Ben Busby
f213a2a64a
Ensure b64 prefs always have min padding
This relates to an issue with an unknown cause (unable to reproduce on
my end) where the preferences string does not contain the correct amount
of padding on a base64 encoded value. This is mediated by appending
padding to the end of the encoded value, since any extra padding is
removed anyways.

Fixes #987
2023-05-19 11:39:51 -06:00
Roman Štefko
2cb4b9e3ca
Allow setting mobile/desktop UAs using env vars (#1003)
Defines separate environment variables for setting mobile vs desktop user
agents

Defines an environment variable for using the client's User-Agent

Co-authored-by: Ben Busby <contact@benbusby.com>
2023-05-19 11:32:05 -06:00
Ben Busby
17dd1f1df1
Add public instance [skip ci]
https://whoogle.link

Closes #1001
2023-05-08 17:24:33 -06:00
xatier
4b7ab3b283
Bump flask from 2.2.3 to 2.3.2 (#1000)
Note that Flask 2.3.2 also requires Werkzeug>=2.3.3

Ref: https://github.com/pallets/flask/blob/main/pyproject.toml
2023-05-08 17:22:07 -06:00
Abhishek M J
349b87ec18
Fix unsupported_g_pages in result list (#996)
Closes #995
2023-05-01 10:23:57 -06:00
Ben Busby
1eb8e04ed1
Add public instance to README [skip ci]
https://search.rubberverse.xyz
2023-04-28 12:59:58 -06:00
No-Logs.com
74a5e24901
Add public instance to readme [skip ci] (#994)
https://whoogle.no-logs.com
2023-04-28 12:57:44 -06:00
João
bf6c2505b1
Update handling of custom css (#965)
"Default" css is no longer required to be part of the request
when updating styles.

Closes #934
2023-04-13 14:19:36 -06:00
Ben Busby
9d9022ed99
Bump version to 0.8.2 2023-04-13 13:03:30 -06:00
Ben Busby
25e7134bef
Remove public instance [skip ci]
- whoogle.rhyshl.live

Closes #977
2023-03-28 10:45:01 -06:00
46 changed files with 978 additions and 326 deletions

View file

@ -42,10 +42,10 @@ jobs:
docker buildx ls
docker buildx build --push \
--tag benbusby/whoogle-search:latest \
--platform linux/amd64,linux/arm/v7,linux/arm64 .
--platform linux/amd64,linux/arm64 .
docker buildx build --push \
--tag ghcr.io/benbusby/whoogle-search:latest \
--platform linux/amd64,linux/arm/v7,linux/arm64 .
--platform linux/amd64,linux/arm64 .
- name: build and push tag
if: startsWith(github.ref, 'refs/tags')
run: |

View file

@ -23,6 +23,6 @@ jobs:
- name: build and test (docker-compose)
run: |
docker rm -f whoogle-search-nocompose
WHOOGLE_IMAGE="whoogle-search:test" docker-compose up --detach
WHOOGLE_IMAGE="whoogle-search:test" docker compose up --detach
sleep 15
docker exec whoogle-search curl -f http://localhost:5000/healthz || exit 1

View file

@ -18,9 +18,9 @@ jobs:
docker run --publish 5000:5000 --detach --name whoogle-search-nocompose whoogle-search:test
sleep 15
docker exec whoogle-search-nocompose curl -f http://localhost:5000/healthz || exit 1
- name: build and test (docker-compose)
- name: build and test (docker compose)
run: |
docker rm -f whoogle-search-nocompose
WHOOGLE_IMAGE="whoogle-search:test" docker-compose up --detach
WHOOGLE_IMAGE="whoogle-search:test" docker compose up --detach
sleep 15
docker exec whoogle-search curl -f http://localhost:5000/healthz || exit 1

View file

@ -10,59 +10,58 @@ jobs:
name: Build and publish to TestPyPI
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.9
uses: actions/setup-python@v1
with:
python-version: 3.9
- name: Install pypa/build
run: >-
python -m
pip install
build
setuptools
--user
- name: Set dev timestamp
run: echo "DEV_BUILD=$(date +%s)" >> $GITHUB_ENV
- name: Build binary wheel and source tarball
run: >-
python -m
build
--sdist
--wheel
--outdir dist/
.
- name: Publish distribution to TestPyPI
uses: pypa/gh-action-pypi-publish@master
with:
password: ${{ secrets.TEST_PYPI_API_TOKEN }}
repository_url: https://test.pypi.org/legacy/
- uses: actions/checkout@v4
- name: Set up Python 3.9
uses: actions/setup-python@v5
with:
python-version: 3.9
- name: Install pypa/build
run: >-
python -m
pip install
build
setuptools
--user
- name: Set dev timestamp
run: echo "DEV_BUILD=$(date +%s)" >> $GITHUB_ENV
- name: Build binary wheel and source tarball
run: >-
python -m
build
--sdist
--wheel
--outdir dist/
.
- name: Publish distribution to TestPyPI
uses: pypa/gh-action-pypi-publish@master
with:
password: ${{ secrets.TEST_PYPI_API_TOKEN }}
repository_url: https://test.pypi.org/legacy/
publish:
name: Build and publish to PyPI
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.9
uses: actions/setup-python@v1
with:
python-version: 3.9
- name: Install pypa/build
run: >-
python -m
pip install
build
--user
- name: Build binary wheel and source tarball
run: >-
python -m
build
--sdist
--wheel
--outdir dist/
.
- name: Publish distribution to PyPI
if: startsWith(github.ref, 'refs/tags')
uses: pypa/gh-action-pypi-publish@master
with:
password: ${{ secrets.PYPI_API_TOKEN }}
- uses: actions/checkout@v4
- name: Set up Python 3.9
uses: actions/setup-python@v5
with:
python-version: 3.9
- name: Install pypa/build
run: >-
python -m
pip install
build
--user
- name: Build binary wheel and source tarball
run: >-
python -m
build
--sdist
--wheel
--outdir dist/
.
- name: Publish distribution to PyPI
if: startsWith(github.ref, 'refs/tags')
uses: pypa/gh-action-pypi-publish@master
with:
password: ${{ secrets.PYPI_API_TOKEN }}

View file

@ -6,12 +6,12 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.x'
- name: Install dependencies
run: pip install --upgrade pip && pip install -r requirements.txt
- name: Run tests
run: ./run test
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Install dependencies
run: pip install --upgrade pip && pip install -r requirements.txt
- name: Run tests
run: ./run test

4
.gitignore vendored
View file

@ -1,4 +1,5 @@
venv/
.venv/
.idea/
__pycache__/
*.pyc
@ -10,7 +11,8 @@ test/static
flask_session/
app/static/config
app/static/custom_config
app/static/bangs
app/static/bangs/*
!app/static/bangs/00-whoogle.json
# pip stuff
/build/

View file

@ -1,4 +1,4 @@
FROM python:3.11.0a5-alpine as builder
FROM python:3.12.6-alpine3.20 AS builder
RUN apk --update add \
build-base \
@ -12,13 +12,20 @@ COPY requirements.txt .
RUN pip install --upgrade pip
RUN pip install --prefix /install --no-warn-script-location --no-cache-dir -r requirements.txt
FROM python:3.11.0a5-alpine
FROM python:3.12.6-alpine3.20
RUN apk add --update --no-cache tor curl openrc libstdc++
# git go //for obfs4proxy
# libcurl4-openssl-dev
RUN apk -U upgrade
# uncomment to build obfs4proxy
# RUN git clone https://gitlab.com/yawning/obfs4.git
# WORKDIR /obfs4
# RUN go build -o obfs4proxy/obfs4proxy ./obfs4proxy
# RUN cp ./obfs4proxy/obfs4proxy /usr/bin/obfs4proxy
ARG DOCKER_USER=whoogle
ARG DOCKER_USERID=927
ARG config_dir=/config
@ -45,6 +52,7 @@ ARG imgur_alt='farside.link/rimgo'
ARG wikipedia_alt='farside.link/wikiless'
ARG imdb_alt='farside.link/libremdb'
ARG quora_alt='farside.link/quetre'
ARG so_alt='farside.link/anonymousoverflow'
ENV CONFIG_VOLUME=$config_dir \
WHOOGLE_URL_PREFIX=$url_prefix \
@ -65,7 +73,8 @@ ENV CONFIG_VOLUME=$config_dir \
WHOOGLE_ALT_IMG=$imgur_alt \
WHOOGLE_ALT_WIKI=$wikipedia_alt \
WHOOGLE_ALT_IMDB=$imdb_alt \
WHOOGLE_ALT_QUORA=$quora_alt
WHOOGLE_ALT_QUORA=$quora_alt \
WHOOGLE_ALT_SO=$so_alt
WORKDIR /whoogle

107
README.md
View file

@ -14,12 +14,13 @@
</tr>
</table>
Get Google search results, but without any ads, javascript, AMP links, cookies, or IP address tracking. Easily deployable in one click as a Docker app, and customizable with a single config file. Quick and simple to implement as a primary search engine replacement on both desktop and mobile.
Get Google search results, but without any ads, JavaScript, AMP links, cookies, or IP address tracking. Easily deployable in one click as a Docker app, and customizable with a single config file. Quick and simple to implement as a primary search engine replacement on both desktop and mobile.
Contents
1. [Features](#features)
3. [Install/Deploy Options](#install)
1. [Heroku Quick Deploy](#heroku-quick-deploy)
1. [Render.com](#render)
1. [Repl.it](#replit)
1. [Fly.io](#flyio)
1. [Koyeb](#koyeb)
@ -33,10 +34,12 @@ Contents
5. [Usage](#usage)
6. [Extra Steps](#extra-steps)
1. [Set Primary Search Engine](#set-whoogle-as-your-primary-search-engine)
2. [Prevent Downtime (Heroku Only)](#prevent-downtime-heroku-only)
3. [Manual HTTPS Enforcement](#https-enforcement)
4. [Using with Firefox Containers](#using-with-firefox-containers)
5. [Reverse Proxying](#reverse-proxying)
2. [Custom Redirecting](#custom-redirecting)
2. [Custom Bangs](#custom-bangs)
3. [Prevent Downtime (Heroku Only)](#prevent-downtime-heroku-only)
4. [Manual HTTPS Enforcement](#https-enforcement)
5. [Using with Firefox Containers](#using-with-firefox-containers)
6. [Reverse Proxying](#reverse-proxying)
1. [Nginx](#nginx)
7. [Contributing](#contributing)
8. [FAQ](#faq)
@ -59,6 +62,7 @@ Contents
- Randomly generated User Agent
- Easy to install/deploy
- DDG-style bang (i.e. `!<tag> <query>`) searches
- User-defined [custom bangs](#custom-bangs)
- Optional location-based searching (i.e. results near \<city\>)
- Optional NoJS mode to view search results in a separate window with JavaScript blocked
@ -86,6 +90,16 @@ Notes:
___
### [Render](https://render.com)
Create an account on [render.com](https://render.com) and import the Whoogle repo with the following settings:
- Runtime: `Python 3`
- Build Command: `pip install -r requirements.txt`
- Run Command: `./run`
___
### [Repl.it](https://repl.it)
[![Run on Repl.it](https://repl.it/badge/github/benbusby/whoogle-search)](https://repl.it/github/benbusby/whoogle-search)
@ -95,13 +109,13 @@ Provides:
- Free deployment of app
- Free HTTPS url (https://\<app name\>.\<username\>\.repl\.co)
- Supports custom domains
- Downtime after periods of inactivity \([solution 1](https://repl.it/talk/ask/use-this-pingmat1replco-just-enter/28821/101298), [solution 2](https://repl.it/talk/learn/How-to-use-and-setup-UptimeRobot/9003)\)
- Downtime after periods of inactivity ([solution](https://repl.it/talk/learn/How-to-use-and-setup-UptimeRobot/9003)\)
___
### [Fly.io](https://fly.io)
You will need a **PAID** [Fly.io](https://fly.io) account to deploy Whoogle.
You will need a [Fly.io](https://fly.io) account to deploy Whoogle.
#### Install the CLI: https://fly.io/docs/hands-on/installing/
@ -117,6 +131,9 @@ To fix this, open the generated `fly.toml` file, set `services.internal_port` to
Your app is now available at `https://<app-name>.fly.dev`.
Notes:
- Requires a [**PAID**](https://fly.io/docs/about/pricing/#free-allowances) Fly.io Account.
___
### [Koyeb](https://www.koyeb.com)
@ -131,7 +148,7 @@ ___
### [pipx](https://github.com/pipxproject/pipx#install-pipx)
Persistent install:
`pipx install git+https://github.com/benbusby/whoogle-search.git`
`pipx install https://github.com/benbusby/whoogle-search/archive/refs/heads/main.zip`
Sandboxed temporary instance:
@ -221,8 +238,11 @@ Description=Whoogle
#Environment=WHOOGLE_ALT_WIKI=farside.link/wikiless
#Environment=WHOOGLE_ALT_IMDB=farside.link/libremdb
#Environment=WHOOGLE_ALT_QUORA=farside.link/quetre
#Environment=WHOOGLE_ALT_SO=farside.link/anonymousoverflow
# Load values from dotenv only
#Environment=WHOOGLE_DOTENV=1
# specify dotenv location if not in default location
#Environment=WHOOGLE_DOTENV_PATH=<path/to>/whoogle.env
Type=simple
User=<username>
# If installed as a package, add:
@ -233,6 +253,7 @@ ExecStart=<python_install_dir>/python3 <whoogle_install_dir>/whoogle-search --ho
ExecStart=<whoogle_repo_dir>/run
# For example:
# ExecStart=/var/www/whoogle-search/run
WorkingDirectory=<whoogle_repo_dir>
ExecReload=/bin/kill -HUP $MAINPID
Restart=always
RestartSec=3
@ -392,12 +413,17 @@ There are a few optional environment variables available for customizing a Whoog
| -------------------- | ----------------------------------------------------------------------------------------- |
| WHOOGLE_URL_PREFIX | The URL prefix to use for the whoogle instance (i.e. "/whoogle") |
| WHOOGLE_DOTENV | Load environment variables in `whoogle.env` |
| WHOOGLE_DOTENV_PATH | The path to `whoogle.env` if not in default location |
| WHOOGLE_USER | The username for basic auth. WHOOGLE_PASS must also be set if used. |
| WHOOGLE_PASS | The password for basic auth. WHOOGLE_USER must also be set if used. |
| WHOOGLE_PROXY_USER | The username of the proxy server. |
| WHOOGLE_PROXY_PASS | The password of the proxy server. |
| WHOOGLE_PROXY_TYPE | The type of the proxy server. Can be "socks5", "socks4", or "http". |
| WHOOGLE_PROXY_LOC | The location of the proxy server (host or ip). |
| WHOOGLE_USER_AGENT | The desktop user agent to use. Defaults to a randomly generated one. |
| WHOOGLE_USER_AGENT_MOBILE | The mobile user agent to use. Defaults to a randomly generated one. |
| WHOOGLE_USE_CLIENT_USER_AGENT | Enable to use your own user agent for all requests. Defaults to false. |
| WHOOGLE_REDIRECTS | Specify sites that should be redirected elsewhere. See [custom redirecting](#custom-redirecting). |
| EXPOSE_PORT | The port where Whoogle will be exposed. |
| HTTPS_ONLY | Enforce HTTPS. (See [here](https://github.com/benbusby/whoogle-search#https-enforcement)) |
| WHOOGLE_ALT_TW | The twitter.com alternative to use when site alternatives are enabled in the config. Set to "" to disable. |
@ -406,9 +432,10 @@ There are a few optional environment variables available for customizing a Whoog
| WHOOGLE_ALT_TL | The Google Translate alternative to use. This is used for all "translate ____" searches. Set to "" to disable. |
| WHOOGLE_ALT_MD | The medium.com alternative to use when site alternatives are enabled in the config. Set to "" to disable. |
| WHOOGLE_ALT_IMG | The imgur.com alternative to use when site alternatives are enabled in the config. Set to "" to disable. |
| WHOOGLE_ALT_WIKI | The wikipedia.com alternative to use when site alternatives are enabled in the config. Set to "" to disable. |
| WHOOGLE_ALT_WIKI | The wikipedia.org alternative to use when site alternatives are enabled in the config. Set to "" to disable. |
| WHOOGLE_ALT_IMDB | The imdb.com alternative to use when site alternatives are enabled in the config. Set to "" to disable. |
| WHOOGLE_ALT_QUORA | The quora.com alternative to use when site alternatives are enabled in the config. Set to "" to disable. |
| WHOOGLE_ALT_SO | The stackoverflow.com alternative to use when site alternatives are enabled in the config. Set to "" to disable. |
| WHOOGLE_AUTOCOMPLETE | Controls visibility of autocomplete/search suggestions. Default on -- use '0' to disable. |
| WHOOGLE_MINIMAL | Remove everything except basic result cards from all search queries. |
| WHOOGLE_CSP | Sets a default set of 'Content-Security-Policy' headers |
@ -416,6 +443,9 @@ There are a few optional environment variables available for customizing a Whoog
| WHOOGLE_TOR_SERVICE | Enable/disable the Tor service on startup. Default on -- use '0' to disable. |
| WHOOGLE_TOR_USE_PASS | Use password authentication for tor control port. |
| WHOOGLE_TOR_CONF | The absolute path to the config file containing the password for the tor control port. Default: ./misc/tor/control.conf WHOOGLE_TOR_PASS must be 1 for this to work.|
| WHOOGLE_SHOW_FAVICONS | Show/hide favicons next to search result URLs. Default on. |
| WHOOGLE_UPDATE_CHECK | Enable/disable the automatic daily check for new versions of Whoogle. Default on. |
| WHOOGLE_FALLBACK_ENGINE_URL | Set a fallback Search Engine URL when there is internal server error or instance is rate-limited. Search query is appended to the end of the URL (eg. https://duckduckgo.com/?k1=-1&q=). |
### Config Environment Variables
These environment variables allow setting default config values, but can be overwritten manually by using the home page config menu. These allow a shortcut for destroying/rebuilding an instance to the same config state every time.
@ -441,6 +471,7 @@ These environment variables allow setting default config values, but can be over
| WHOOGLE_CONFIG_STYLE | The custom CSS to use for styling (should be single line) |
| WHOOGLE_CONFIG_PREFERENCES_ENCRYPTED | Encrypt preferences token, requires preferences key |
| WHOOGLE_CONFIG_PREFERENCES_KEY | Key to encrypt preferences in URL (REQUIRED to show url) |
| WHOOGLE_CONFIG_ANON_VIEW | Include the "anonymous view" option for each search result |
## Usage
Same as most search engines, with the exception of filtering by time range.
@ -448,6 +479,7 @@ Same as most search engines, with the exception of filtering by time range.
To filter by a range of time, append ":past <time>" to the end of your search, where <time> can be `hour`, `day`, `month`, or `year`. Example: `coronavirus updates :past hour`
## Extra Steps
### Set Whoogle as your primary search engine
*Note: If you're using a reverse proxy to run Whoogle Search, make sure the "Root URL" config option on the home page is set to your URL before going through these steps.*
@ -481,7 +513,7 @@ Browser settings:
- Search string to use: `https://\<your whoogle url\>/search?q=%s`
- [Alfred](https://www.alfredapp.com/) (Mac OS X)
1. Go to `Alfred Preferences` > `Features` > `Web Search` and click `Add Custom Search`. Then configure these settings
- Search URL: `https://\<your whoogle url\>/search?q={query}
- Search URL: `https://\<your whoogle url\>/search?q={query}`
- Title: `Whoogle for '{query}'` (or whatever you want)
- Keyword: `whoogle`
@ -492,6 +524,40 @@ Browser settings:
- Manual
- Under search engines > manage search engines > add, manually enter your Whoogle instance details with a `<whoogle url>/search?q=%s` formatted search URL.
### Custom Redirecting
You can set custom site redirects using the `WHOOGLE_REDIRECTS` environment
variable. A lot of sites, such as Twitter, Reddit, etc, have built-in redirects
to [Farside links](https://sr.ht/~benbusby/farside), but you may want to define
your own.
To do this, you can use the following syntax:
```
WHOOGLE_REDIRECTS="<parent_domain>:<new_domain>"
```
For example, if you want to redirect from "badsite.com" to "goodsite.com":
```
WHOOGLE_REDIRECTS="badsite.com:goodsite.com"
```
This can be used for multiple sites as well, with comma separation:
```
WHOOGLE_REDIRECTS="badA.com:goodA.com,badB.com:goodB.com"
```
NOTE: Do not include "http(s)://" when defining your redirect.
### Custom Bangs
You can create your own custom bangs. By default, bangs are stored in
`app/static/bangs`. See [`00-whoogle.json`](https://github.com/benbusby/whoogle-search/blob/main/app/static/bangs/00-whoogle.json)
for an example. These are parsed in alphabetical order with later files
overriding bangs set in earlier files, with the exception that DDG bangs
(downloaded to `app/static/bangs/bangs.json`) are always parsed first. Thus,
any custom bangs will always override the DDG ones.
### Prevent Downtime (Heroku only)
Part of the deal with Heroku's free tier is that you're allocated 550 hours/month (meaning it can't stay active 24/7), and the app is temporarily shut down after 30 minutes of inactivity. Once it becomes inactive, any Whoogle searches will still work, but it'll take an extra 10-15 seconds for the app to come back online before displaying the result, which can be frustrating if you're in a hurry.
@ -539,6 +605,7 @@ server {
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $host;
proxy_set_header X-NginX-Proxy true;
proxy_set_header X-Forwarded-Host $http_host;
proxy_pass http://localhost:5000;
}
}
@ -568,7 +635,7 @@ Under the hood, Whoogle is a basic Flask app with the following structure:
- `opensearch.xml`: A template used for supporting [OpenSearch](https://developer.mozilla.org/en-US/docs/Web/OpenSearch).
- `imageresults.html`: An "experimental" template used for supporting the "Full Size" image feature on desktop.
- `static/<css|js>`
- CSS/Javascript files, should be self-explanatory
- CSS/JavaScript files, should be self-explanatory
- `static/settings`
- Key-value JSON files for establishing valid configuration values
@ -607,7 +674,7 @@ I'm a huge fan of Searx though and encourage anyone to use that instead if they
**Why does the image results page look different?**
A lot of the app currently piggybacks on Google's existing support for fetching results pages with Javascript disabled. To their credit, they've done an excellent job with styling pages, but it seems that the image results page - particularly on mobile - is a little rough. Moving forward, with enough interest, I'd like to transition to fetching the results and parsing them into a unique Whoogle-fied interface that I can style myself.
A lot of the app currently piggybacks on Google's existing support for fetching results pages with JavaScript disabled. To their credit, they've done an excellent job with styling pages, but it seems that the image results page - particularly on mobile - is a little rough. Moving forward, with enough interest, I'd like to transition to fetching the results and parsing them into a unique Whoogle-fied interface that I can style myself.
## Public Instances
@ -621,15 +688,22 @@ A lot of the app currently piggybacks on Google's existing support for fetching
| [https://s.tokhmi.xyz](https://s.tokhmi.xyz) | 🇺🇸 US | Multi-choice | ✅ |
| [https://search.sethforprivacy.com](https://search.sethforprivacy.com) | 🇩🇪 DE | English | |
| [https://whoogle.dcs0.hu](https://whoogle.dcs0.hu) | 🇭🇺 HU | Multi-choice | |
| [https://whoogle.esmailelbob.xyz](https://whoogle.esmailelbob.xyz) | 🇨🇦 CA | Multi-choice | |
| [https://gowogle.voring.me](https://gowogle.voring.me) | 🇺🇸 US | Multi-choice | |
| [https://whoogle.privacydev.net](https://whoogle.privacydev.net) | 🇳🇱 NL | English | |
| [https://whoogle.privacydev.net](https://whoogle.privacydev.net) | 🇫🇷 FR | English | |
| [https://wg.vern.cc](https://wg.vern.cc) | 🇺🇸 US | English | |
| [https://whoogle.hxvy0.gq](https://whoogle.hxvy0.gq) | 🇨🇦 CA | Turkish Only | ✅ |
| [https://whoogle.hostux.net](https://whoogle.hostux.net) | 🇫🇷 FR | Multi-choice | |
| [https://whoogle.lunar.icu](https://whoogle.lunar.icu) | 🇩🇪 DE | Multi-choice | ✅ |
| [https://whoogle.rhyshl.live](https://whoogle.rhyshl.live) | 🇬🇧 GB | Multi-choice | ✅ |
| [https://wgl.frail.duckdns.org](https://wgl.frail.duckdns.org) | 🇧🇷 BR | Multi-choice | |
| [https://whoogle.no-logs.com](https://whoogle.no-logs.com/) | 🇸🇪 SE | Multi-choice | |
| [https://whoogle.ftw.lol](https://whoogle.ftw.lol) | 🇩🇪 DE | Multi-choice | |
| [https://whoogle-search--replitcomreside.repl.co](https://whoogle-search--replitcomreside.repl.co) | 🇺🇸 US | English | |
| [https://search.notrustverify.ch](https://search.notrustverify.ch) | 🇨🇭 CH | Multi-choice | |
| [https://whoogle.datura.network](https://whoogle.datura.network) | 🇩🇪 DE | Multi-choice | |
| [https://whoogle.yepserver.xyz](https://whoogle.yepserver.xyz) | 🇺🇦 UA | Multi-choice | |
| [https://search.nezumi.party](https://search.nezumi.party) | 🇮🇹 IT | Multi-choice | |
| [https://search.snine.nl](https://search.snine.nl) | 🇳🇱 NL | Mult-choice | ✅ |
* A checkmark in the "Cloudflare" category here refers to the use of the reverse proxy, [Cloudflare](https://cloudflare.com). The checkmark will not be listed for a site which uses Cloudflare DNS but rather the proxying service which grants Cloudflare the ability to monitor traffic to the website.
@ -640,7 +714,8 @@ A lot of the app currently piggybacks on Google's existing support for fetching
| [http://whoglqjdkgt2an4tdepberwqz3hk7tjo4kqgdnuj77rt7nshw2xqhqad.onion](http://whoglqjdkgt2an4tdepberwqz3hk7tjo4kqgdnuj77rt7nshw2xqhqad.onion) | 🇺🇸 US | Multi-choice
| [http://nuifgsnbb2mcyza74o7illtqmuaqbwu4flam3cdmsrnudwcmkqur37qd.onion](http://nuifgsnbb2mcyza74o7illtqmuaqbwu4flam3cdmsrnudwcmkqur37qd.onion) | 🇩🇪 DE | English
| [http://whoogle.vernccvbvyi5qhfzyqengccj7lkove6bjot2xhh5kajhwvidqafczrad.onion](http://whoogle.vernccvbvyi5qhfzyqengccj7lkove6bjot2xhh5kajhwvidqafczrad.onion/) | 🇺🇸 US | English |
| [http://whoogle.g4c3eya4clenolymqbpgwz3q3tawoxw56yhzk4vugqrl6dtu3ejvhjid.onion](http://whoogle.g4c3eya4clenolymqbpgwz3q3tawoxw56yhzk4vugqrl6dtu3ejvhjid.onion/) | 🇳🇱 NL | English |
| [http://whoogle.g4c3eya4clenolymqbpgwz3q3tawoxw56yhzk4vugqrl6dtu3ejvhjid.onion](http://whoogle.g4c3eya4clenolymqbpgwz3q3tawoxw56yhzk4vugqrl6dtu3ejvhjid.onion/) | 🇫🇷 FR | English |
| [http://whoogle.daturab6drmkhyeia4ch5gvfc2f3wgo6bhjrv3pz6n7kxmvoznlkq4yd.onion](http://whoogle.daturab6drmkhyeia4ch5gvfc2f3wgo6bhjrv3pz6n7kxmvoznlkq4yd.onion/) | 🇩🇪 DE | Multi-choice | |
#### I2P Instances

View file

@ -95,6 +95,11 @@
"value": "farside.link/quetre",
"required": false
},
"WHOOGLE_ALT_SO": {
"description": "The site to use as a replacement for stackoverflow.com when site alternatives are enabled in the config.",
"value": "farside.link/anonymousoverflow",
"required": false
},
"WHOOGLE_MINIMAL": {
"description": "Remove everything except basic result cards from all search queries (set to 1 or leave blank)",
"value": "",

View file

@ -1,7 +1,7 @@
from app.filter import clean_query
from app.request import send_tor_signal
from app.utils.session import generate_key
from app.utils.bangs import gen_bangs_json
from app.utils.bangs import gen_bangs_json, load_all_bangs
from app.utils.misc import gen_file_hash, read_config_bool
from base64 import b64encode
from bs4 import MarkupResemblesLocatorWarning
@ -25,9 +25,10 @@ app = Flask(__name__, static_folder=os.path.dirname(
app.wsgi_app = ProxyFix(app.wsgi_app)
dot_env_path = (
os.path.join(os.path.dirname(os.path.abspath(__file__)),
'../whoogle.env'))
# look for WHOOGLE_ENV, else look in parent directory
dot_env_path = os.getenv(
"WHOOGLE_DOTENV_PATH",
os.path.join(os.path.dirname(os.path.abspath(__file__)), "../whoogle.env"))
# Load .env file if enabled
if os.path.exists(dot_env_path):
@ -101,7 +102,10 @@ if not os.path.exists(app.config['BUILD_FOLDER']):
# Session values
app_key_path = os.path.join(app.config['CONFIG_PATH'], 'whoogle.key')
if os.path.exists(app_key_path):
app.config['SECRET_KEY'] = open(app_key_path, 'r').read()
try:
app.config['SECRET_KEY'] = open(app_key_path, 'r').read()
except PermissionError:
app.config['SECRET_KEY'] = str(b64encode(os.urandom(32)))
else:
app.config['SECRET_KEY'] = str(b64encode(os.urandom(32)))
with open(app_key_path, 'w') as key_file:
@ -139,7 +143,9 @@ app.config['CSP'] = 'default-src \'none\';' \
'connect-src \'self\';'
# Generate DDG bang filter
generating_bangs = False
if not os.path.exists(app.config['BANG_FILE']):
generating_bangs = True
json.dump({}, open(app.config['BANG_FILE'], 'w'))
bangs_thread = threading.Thread(
target=gen_bangs_json,
@ -171,7 +177,7 @@ for cb_dir in cache_busting_dirs:
# Templating functions
app.jinja_env.globals.update(clean_query=clean_query)
app.jinja_env.globals.update(
cb_url=lambda f: app.config['CACHE_BUSTING_MAP'][f])
cb_url=lambda f: app.config['CACHE_BUSTING_MAP'][f.lower()])
# Attempt to acquire tor identity, to determine if Tor config is available
send_tor_signal(Signal.HEARTBEAT)
@ -181,6 +187,11 @@ warnings.simplefilter('ignore', MarkupResemblesLocatorWarning)
from app import routes # noqa
# The gen_bangs_json function takes care of loading bangs, so skip it here if
# it's already being loaded
if not generating_bangs:
load_all_bangs(app.config['BANG_FILE'])
# Disable logging from imported modules
logging.config.dictConfig({
'version': 1,

View file

@ -3,6 +3,7 @@ from bs4 import BeautifulSoup
from bs4.element import ResultSet, Tag
from cryptography.fernet import Fernet
from flask import render_template
import html
import urllib.parse as urlparse
from urllib.parse import parse_qs
import re
@ -28,9 +29,12 @@ unsupported_g_pages = [
'google.com/preferences',
'google.com/intl',
'advanced_search',
'tbm=shop'
'tbm=shop',
'ageverification.google.co.kr'
]
unsupported_g_divs = ['google.com/preferences?hl=', 'ageverification.google.co.kr']
def extract_q(q_str: str, href: str) -> str:
"""Extracts the 'q' element from a result link. This is typically
@ -44,7 +48,7 @@ def extract_q(q_str: str, href: str) -> str:
Returns:
str: The 'q' element of the link, or an empty string
"""
return parse_qs(q_str)['q'][0] if ('&q=' in href or '?q=' in href) else ''
return parse_qs(q_str, keep_blank_values=True)['q'][0] if ('&q=' in href or '?q=' in href) else ''
def build_map_url(href: str) -> str:
@ -160,14 +164,22 @@ class Filter:
self.update_styling()
self.remove_block_tabs()
# self.main_divs is only populated for the main page of search results
# (i.e. not images/news/etc).
if self.main_divs:
for div in self.main_divs:
self.sanitize_div(div)
for img in [_ for _ in self.soup.find_all('img') if 'src' in _.attrs]:
self.update_element_src(img, 'image/png')
for audio in [_ for _ in self.soup.find_all('audio') if 'src' in _.attrs]:
self.update_element_src(audio, 'audio/mpeg')
audio['controls'] = ''
for link in self.soup.find_all('a', href=True):
self.update_link(link)
self.add_favicon(link)
if self.config.alts:
self.site_alt_swap()
@ -197,6 +209,87 @@ class Filter:
self.remove_site_blocks(self.soup)
return self.soup
def sanitize_div(self, div) -> None:
"""Removes escaped script and iframe tags from results
Returns:
None (The soup object is modified directly)
"""
if not div:
return
for d in div.find_all('div', recursive=True):
d_text = d.find(text=True, recursive=False)
# Ensure we're working with tags that contain text content
if not d_text or not d.string:
continue
d.string = html.unescape(d_text)
div_soup = BeautifulSoup(d.string, 'html.parser')
# Remove all valid script or iframe tags in the div
for script in div_soup.find_all('script'):
script.decompose()
for iframe in div_soup.find_all('iframe'):
iframe.decompose()
d.string = str(div_soup)
def add_favicon(self, link) -> None:
"""Adds icons for each returned result, using the result site's favicon
Returns:
None (The soup object is modified directly)
"""
# Skip empty, parentless, or internal links
show_favicons = read_config_bool('WHOOGLE_SHOW_FAVICONS', True)
is_valid_link = link and link.parent and link['href'].startswith('http')
if not show_favicons or not is_valid_link:
return
parent = link.parent
is_result_div = False
# Check each parent to make sure that the div doesn't already have a
# favicon attached, and that the div is a result div
while parent:
p_cls = parent.attrs.get('class') or []
if 'has-favicon' in p_cls or GClasses.scroller_class in p_cls:
return
elif GClasses.result_class_a not in p_cls:
parent = parent.parent
else:
is_result_div = True
break
if not is_result_div:
return
# Construct the html for inserting the icon into the parent div
parsed = urlparse.urlparse(link['href'])
favicon = self.encrypt_path(
f'{parsed.scheme}://{parsed.netloc}/favicon.ico',
is_element=True)
src = f'{self.root_url}/{Endpoint.element}?url={favicon}' + \
'&type=image/x-icon'
html = f'<img class="site-favicon" src="{src}">'
favicon = BeautifulSoup(html, 'html.parser')
link.parent.insert(0, favicon)
# Update all parents to indicate that a favicon has been attached
parent = link.parent
while parent:
p_cls = parent.get('class') or []
p_cls.append('has-favicon')
parent['class'] = p_cls
parent = parent.parent
if GClasses.result_class_a in p_cls:
break
def remove_site_blocks(self, soup) -> None:
if not self.config.block or not soup.body:
return
@ -466,7 +559,7 @@ class Filter:
link['href'] = link_netloc
parent = link.parent
if 'google.com/preferences?hl=' in link_netloc:
if any(divlink in link_netloc for divlink in unsupported_g_divs):
# Handle case where a search is performed in a different
# language than what is configured. This usually returns a
# div with the same classes as normal search results, but with
@ -486,7 +579,9 @@ class Filter:
if parent.name == 'footer' or f'{GClasses.footer}' in p_cls:
link.decompose()
parent = parent.parent
return
if link.decomposed:
return
# Replace href with only the intended destination (no "utm" type tags)
href = link['href'].replace('https://www.google.com', '')
@ -555,18 +650,19 @@ class Filter:
is enabled
"""
for site, alt in SITE_ALTS.items():
for div in self.soup.find_all('div', text=re.compile(site)):
# Use the number of words in the div string to determine if the
# string is a result description (shouldn't replace domains used
# in desc text).
# Also ignore medium.com replacements since these are handled
if site != "medium.com" and alt != "":
# Ignore medium.com replacements since these are handled
# specifically in the link description replacement, and medium
# results are never given their own "card" result where this
# replacement would make sense.
if site == 'medium.com' or len(div.string.split(' ')) > 1:
continue
div.string = div.string.replace(site, alt)
# Also ignore if the alt is empty, since this is used to indicate
# that the alt is not enabled.
for div in self.soup.find_all('div', text=re.compile(site)):
# Use the number of words in the div string to determine if the
# string is a result description (shouldn't replace domains used
# in desc text).
if len(div.string.split(' ')) == 1:
div.string = div.string.replace(site, alt)
for link in self.soup.find_all('a', href=True):
# Search and replace all link descriptions
@ -590,7 +686,7 @@ class Filter:
# replaced (i.e. 'philomedium.com' should stay as it is).
if 'medium.com' in link_str:
if link_str.startswith('medium.com') or '.medium.com' in link_str:
link_str = 'farside.link/scribe' + link_str[
link_str = SITE_ALTS['medium.com'] + link_str[
link_str.find('medium.com') + len('medium.com'):]
new_desc.string = link_str
else:

View file

@ -1,13 +1,38 @@
from inspect import Attribute
from typing import Optional
from app.utils.misc import read_config_bool
from flask import current_app
import os
import re
from base64 import urlsafe_b64encode, urlsafe_b64decode
import pickle
from cryptography.fernet import Fernet
import hashlib
import brotli
import logging
import json
import cssutils
from cssutils.css.cssstylesheet import CSSStyleSheet
from cssutils.css.cssstylerule import CSSStyleRule
# removes warnings from cssutils
cssutils.log.setLevel(logging.CRITICAL)
def get_rule_for_selector(stylesheet: CSSStyleSheet,
selector: str) -> Optional[CSSStyleRule]:
"""Search for a rule that matches a given selector in a stylesheet.
Args:
stylesheet (CSSStyleSheet) -- the stylesheet to search
selector (str) -- the selector to search for
Returns:
Optional[CSSStyleRule] -- the rule that matches the selector or None
"""
for rule in stylesheet.cssRules:
if hasattr(rule, "selectorText") and selector == rule.selectorText:
return rule
return None
class Config:
@ -16,10 +41,8 @@ class Config:
self.url = os.getenv('WHOOGLE_CONFIG_URL', '')
self.lang_search = os.getenv('WHOOGLE_CONFIG_SEARCH_LANGUAGE', '')
self.lang_interface = os.getenv('WHOOGLE_CONFIG_LANGUAGE', '')
self.style = os.getenv(
'WHOOGLE_CONFIG_STYLE',
open(os.path.join(app_config['STATIC_FOLDER'],
'css/variables.css')).read())
self.style_modified = os.getenv(
'WHOOGLE_CONFIG_STYLE', '')
self.block = os.getenv('WHOOGLE_CONFIG_BLOCK', '')
self.block_title = os.getenv('WHOOGLE_CONFIG_BLOCK_TITLE', '')
self.block_url = os.getenv('WHOOGLE_CONFIG_BLOCK_URL', '')
@ -38,7 +61,7 @@ class Config:
self.anon_view = read_config_bool('WHOOGLE_CONFIG_ANON_VIEW')
self.preferences_encrypted = read_config_bool('WHOOGLE_CONFIG_PREFERENCES_ENCRYPTED')
self.preferences_key = os.getenv('WHOOGLE_CONFIG_PREFERENCES_KEY', '')
self.accept_language = False
self.safe_keys = [
@ -88,12 +111,39 @@ class Config:
if not name.startswith("__")
and (type(attr) is bool or type(attr) is str)}
@property
def style(self) -> str:
"""Returns the default style updated with specified modifications.
Returns:
str -- the new style
"""
style_sheet = cssutils.parseString(
open(os.path.join(current_app.config['STATIC_FOLDER'],
'css/variables.css')).read()
)
modified_sheet = cssutils.parseString(self.style_modified)
for rule in modified_sheet:
rule_default = get_rule_for_selector(style_sheet,
rule.selectorText)
# if modified rule is in default stylesheet, update it
if rule_default is not None:
# TODO: update this in a smarter way to handle :root better
# for now if we change a varialbe in :root all other default
# variables need to be also present
rule_default.style = rule.style
# else add the new rule to the default stylesheet
else:
style_sheet.add(rule)
return str(style_sheet.cssText, 'utf-8')
@property
def preferences(self) -> str:
# if encryption key is not set will uncheck preferences encryption
if self.preferences_encrypted:
self.preferences_encrypted = bool(self.preferences_key)
# add a tag for visibility if preferences token startswith 'e' it means
# the token is encrypted, 'u' means the token is unencrypted and can be
# used by other whoogle instances
@ -186,33 +236,32 @@ class Config:
return key
def _encode_preferences(self) -> str:
encoded_preferences = brotli.compress(pickle.dumps(self.get_attrs()))
if self.preferences_encrypted:
if self.preferences_key != '':
key = self._get_fernet_key(self.preferences_key)
encoded_preferences = Fernet(key).encrypt(encoded_preferences)
encoded_preferences = brotli.compress(encoded_preferences)
preferences_json = json.dumps(self.get_attrs()).encode()
compressed_preferences = brotli.compress(preferences_json)
return urlsafe_b64encode(encoded_preferences).decode()
if self.preferences_encrypted and self.preferences_key:
key = self._get_fernet_key(self.preferences_key)
encrypted_preferences = Fernet(key).encrypt(compressed_preferences)
compressed_preferences = brotli.compress(encrypted_preferences)
return urlsafe_b64encode(compressed_preferences).decode()
def _decode_preferences(self, preferences: str) -> dict:
mode = preferences[0]
preferences = preferences[1:]
if mode == 'e': # preferences are encrypted
try:
try:
decoded_data = brotli.decompress(urlsafe_b64decode(preferences.encode() + b'=='))
if mode == 'e' and self.preferences_key:
# preferences are encrypted
key = self._get_fernet_key(self.preferences_key)
decrypted_data = Fernet(key).decrypt(decoded_data)
decoded_data = brotli.decompress(decrypted_data)
config = Fernet(key).decrypt(
brotli.decompress(urlsafe_b64decode(preferences.encode()))
)
config = pickle.loads(brotli.decompress(config))
except Exception:
config = {}
elif mode == 'u': # preferences are not encrypted
config = pickle.loads(
brotli.decompress(urlsafe_b64decode(preferences.encode()))
)
else: # preferences are incorrectly formatted
config = json.loads(decoded_data)
except Exception:
config = {}
return config

View file

@ -14,6 +14,8 @@ class GClasses:
footer = 'TuS8Ad'
result_class_a = 'ZINbbc'
result_class_b = 'luh4td'
scroller_class = 'idg8be'
line_tag = 'BsXmcf'
result_classes = {
result_class_a: ['Gx5Zad'],

View file

@ -73,6 +73,14 @@ def send_tor_signal(signal: Signal) -> bool:
def gen_user_agent(is_mobile) -> str:
user_agent = os.environ.get('WHOOGLE_USER_AGENT', '')
user_agent_mobile = os.environ.get('WHOOGLE_USER_AGENT_MOBILE', '')
if user_agent and not is_mobile:
return user_agent
if user_agent_mobile and is_mobile:
return user_agent_mobile
firefox = random.choice(['Choir', 'Squier', 'Higher', 'Wire']) + 'fox'
linux = random.choice(['Win', 'Sin', 'Gin', 'Fin', 'Kin']) + 'ux'
@ -261,7 +269,7 @@ class Request:
return []
def send(self, base_url='', query='', attempt=0,
force_mobile=False) -> Response:
force_mobile=False, user_agent='') -> Response:
"""Sends an outbound request to a URL. Optionally sends the request
using Tor, if enabled by the user.
@ -277,10 +285,14 @@ class Request:
Response: The Response object returned by the requests call
"""
if force_mobile and not self.mobile:
modified_user_agent = self.modified_user_agent_mobile
use_client_user_agent = int(os.environ.get('WHOOGLE_USE_CLIENT_USER_AGENT', '0'))
if user_agent and use_client_user_agent == 1:
modified_user_agent = user_agent
else:
modified_user_agent = self.modified_user_agent
if force_mobile and not self.mobile:
modified_user_agent = self.modified_user_agent_mobile
else:
modified_user_agent = self.modified_user_agent
headers = {
'User-Agent': modified_user_agent
@ -295,9 +307,8 @@ class Request:
# view is suppressed correctly
now = datetime.now()
cookies = {
'CONSENT': 'YES+cb.{:d}{:02d}{:02d}-17-p0.de+F+678'.format(
now.year, now.month, now.day
)
'CONSENT': 'PENDING+987',
'SOCS': 'CAESHAgBEhIaAB',
}
# Validate Tor conn and request new identity if the last one failed

View file

@ -4,8 +4,12 @@ import io
import json
import os
import pickle
import re
import urllib.parse as urlparse
import uuid
import validators
import sys
import traceback
from datetime import datetime, timedelta
from functools import wraps
@ -14,11 +18,12 @@ from app import app
from app.models.config import Config
from app.models.endpoint import Endpoint
from app.request import Request, TorError
from app.utils.bangs import resolve_bang
from app.utils.misc import get_proxy_host_url
from app.utils.bangs import suggest_bang, resolve_bang
from app.utils.misc import empty_gif, placeholder_img, get_proxy_host_url, \
fetch_favicon
from app.filter import Filter
from app.utils.misc import read_config_bool, get_client_ip, get_request_url, \
check_for_update
check_for_update, encrypt_string
from app.utils.widgets import *
from app.utils.results import bold_search_terms,\
add_currency_card, check_currency, get_tabs_content
@ -31,9 +36,7 @@ from requests import exceptions
from requests.models import PreparedRequest
from cryptography.fernet import Fernet, InvalidToken
from cryptography.exceptions import InvalidSignature
# Load DDG bang json files only on init
bang_json = json.load(open(app.config['BANG_FILE'])) or {}
from werkzeug.datastructures import MultiDict
ac_var = 'WHOOGLE_AUTOCOMPLETE'
autocomplete_enabled = os.getenv(ac_var, '1')
@ -126,12 +129,12 @@ def session_required(f):
@app.before_request
def before_request_func():
global bang_json
session.permanent = True
# Check for latest version if needed
now = datetime.now()
if now - timedelta(hours=24) > app.config['LAST_UPDATE_CHECK']:
needs_update_check = now - timedelta(hours=24) > app.config['LAST_UPDATE_CHECK']
if read_config_bool('WHOOGLE_UPDATE_CHECK', True) and needs_update_check:
app.config['LAST_UPDATE_CHECK'] = now
app.config['HAS_UPDATE'] = check_for_update(
app.config['RELEASES_URL'],
@ -167,20 +170,12 @@ def before_request_func():
g.app_location = g.user_config.url
# Attempt to reload bangs json if not generated yet
if not bang_json and os.path.getsize(app.config['BANG_FILE']) > 4:
try:
bang_json = json.load(open(app.config['BANG_FILE']))
except json.decoder.JSONDecodeError:
# Ignore decoding error, can occur if file is still
# being written
pass
@app.after_request
def after_request_func(resp):
resp.headers['X-Content-Type-Options'] = 'nosniff'
resp.headers['X-Frame-Options'] = 'DENY'
resp.headers['Cache-Control'] = 'max-age=86400'
if os.getenv('WHOOGLE_CSP', False):
resp.headers['Content-Security-Policy'] = app.config['CSP']
@ -276,8 +271,7 @@ def autocomplete():
# Search bangs if the query begins with "!", but not "! " (feeling lucky)
if q.startswith('!') and len(q) > 1 and not q.startswith('! '):
return jsonify([q, [bang_json[_]['suggestion'] for _ in bang_json if
_.startswith(q)]])
return jsonify([q, suggest_bang(q)])
if not q and not request.data:
return jsonify({'?': []})
@ -298,10 +292,17 @@ def autocomplete():
@session_required
@auth_required
def search():
if request.method == 'POST':
# Redirect as a GET request with an encrypted query
post_data = MultiDict(request.form)
post_data['q'] = encrypt_string(g.session_key, post_data['q'])
get_req_str = urlparse.urlencode(post_data)
return redirect(url_for('.search') + '?' + get_req_str)
search_util = Search(request, g.user_config, g.session_key)
query = search_util.new_search_query()
bang = resolve_bang(query, bang_json)
bang = resolve_bang(query)
if bang:
return redirect(bang)
@ -338,6 +339,10 @@ def search():
# Return 503 if temporarily blocked by captcha
if has_captcha(str(response)):
app.logger.error('503 (CAPTCHA)')
fallback_engine = os.environ.get('WHOOGLE_FALLBACK_ENGINE_URL', '')
if (fallback_engine):
return redirect(fallback_engine + query)
return render_template(
'error.html',
blocked=True,
@ -420,13 +425,18 @@ def config():
config_disabled = (
app.config['CONFIG_DISABLE'] or
not valid_user_session(session))
name = ''
if 'name' in request.args:
name = os.path.normpath(request.args.get('name'))
if not re.match(r'^[A-Za-z0-9_.+-]+$', name):
return make_response('Invalid config name', 400)
if request.method == 'GET':
return json.dumps(g.user_config.__dict__)
elif request.method == 'PUT' and not config_disabled:
if 'name' in request.args:
config_pkl = os.path.join(
app.config['CONFIG_PATH'],
request.args.get('name'))
if name:
config_pkl = os.path.join(app.config['CONFIG_PATH'], name)
session['config'] = (pickle.load(open(config_pkl, 'rb'))
if os.path.exists(config_pkl)
else session['config'])
@ -444,7 +454,7 @@ def config():
config_data,
open(os.path.join(
app.config['CONFIG_PATH'],
request.args.get('name')), 'wb'))
name), 'wb'))
session['config'] = config_data
return redirect(config_data['url'])
@ -475,8 +485,23 @@ def element():
src_type = request.args.get('type')
# Ensure requested element is from a valid domain
domain = urlparse.urlparse(src_url).netloc
if not validators.domain(domain):
return send_file(io.BytesIO(empty_gif), mimetype='image/gif')
try:
file_data = g.user_request.send(base_url=src_url).content
response = g.user_request.send(base_url=src_url)
# Display an empty gif if the requested element couldn't be retrieved
if response.status_code != 200 or len(response.content) == 0:
if 'favicon' in src_url:
favicon = fetch_favicon(src_url)
return send_file(io.BytesIO(favicon), mimetype='image/png')
else:
return send_file(io.BytesIO(empty_gif), mimetype='image/gif')
file_data = response.content
tmp_mem = io.BytesIO()
tmp_mem.write(file_data)
tmp_mem.seek(0)
@ -485,8 +510,6 @@ def element():
except exceptions.RequestException:
pass
empty_gif = base64.b64decode(
'R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==')
return send_file(io.BytesIO(empty_gif), mimetype='image/gif')
@ -504,6 +527,13 @@ def window():
root_url=request.url_root,
config=g.user_config)
target = urlparse.urlparse(target_url)
# Ensure requested URL has a valid domain
if not validators.domain(target.netloc):
return render_template(
'error.html',
error_message='Invalid location'), 400
host_url = f'{target.scheme}://{target.netloc}'
get_body = g.user_request.send(base_url=target_url).text
@ -557,11 +587,58 @@ def window():
)
@app.route('/robots.txt')
def robots():
response = make_response(
'''User-Agent: *
Disallow: /''', 200)
response.mimetype = 'text/plain'
return response
@app.route('/favicon.ico')
def favicon():
return app.send_static_file('img/favicon.ico')
@app.errorhandler(404)
def page_not_found(e):
return render_template('error.html', error_message=str(e)), 404
@app.errorhandler(Exception)
def internal_error(e):
query = ''
if request.method == 'POST':
query = request.form.get('q')
else:
query = request.args.get('q')
# Attempt to parse the query
try:
search_util = Search(request, g.user_config, g.session_key)
query = search_util.new_search_query()
except Exception:
pass
print(traceback.format_exc(), file=sys.stderr)
fallback_engine = os.environ.get('WHOOGLE_FALLBACK_ENGINE_URL', '')
if (fallback_engine):
return redirect(fallback_engine + query)
localization_lang = g.user_config.get_localization_lang()
translation = app.config['TRANSLATIONS'][localization_lang]
return render_template(
'error.html',
error_message='Internal server error (500)',
translation=translation,
farside='https://farside.link',
config=g.user_config,
query=urlparse.unquote(query),
params=g.user_config.to_params(keys=['preferences'])), 500
def run_app() -> None:
parser = argparse.ArgumentParser(
description='Whoogle Search console runner')
@ -580,6 +657,11 @@ def run_app() -> None:
default='',
metavar='</path/to/unix.sock>',
help='Listen for app on unix socket instead of host:port')
parser.add_argument(
'--unix-socket-perms',
default='600',
metavar='<octal permissions>',
help='Octal permissions to use for the Unix domain socket (default 600)')
parser.add_argument(
'--debug',
default=False,
@ -631,7 +713,7 @@ def run_app() -> None:
if args.debug:
app.run(host=args.host, port=args.port, debug=args.debug)
elif args.unix_socket:
waitress.serve(app, unix_socket=args.unix_socket)
waitress.serve(app, unix_socket=args.unix_socket, unix_socket_perms=args.unix_socket_perms)
else:
waitress.serve(
app,

View file

@ -0,0 +1,14 @@
{
"!i": {
"url": "search?q={}&tbm=isch",
"suggestion": "!i (Whoogle Images)"
},
"!v": {
"url": "search?q={}&tbm=vid",
"suggestion": "!v (Whoogle Videos)"
},
"!n": {
"url": "search?q={}&tbm=nws",
"suggestion": "!n (Whoogle News)"
}
}

View file

@ -70,6 +70,10 @@ select {
border-radius: 8px !important;
}
.BsXmcf {
background-color: unset !important;
}
.KP7LCb {
box-shadow: 0 0 0 0 !important;
}

View file

@ -45,6 +45,10 @@ select {
box-shadow: 0 1px 6px rgba(32,33,36,0.28) !important;
}
.BsXmcf {
background-color: unset !important;
}
.BVG0Nb {
background-color: var(--whoogle-result-bg) !important;
}

View file

@ -58,6 +58,26 @@ details summary span {
text-align: center;
}
.site-favicon {
float: left;
width: 25px;
padding-right: 5px;
}
.has-favicon .sCuL3 {
padding-left: 30px;
}
#flex_text_audio_icon_chunk {
display: none;
}
audio {
display: block;
margin-right: auto;
padding-bottom: 5px;
}
@media (min-width: 801px) {
body {
min-width: 736px !important;

View file

@ -21,16 +21,6 @@ const handleUserInput = () => {
xhrRequest.send('q=' + searchInput.value);
};
const closeAllLists = el => {
// Close all autocomplete suggestions
let suggestions = document.getElementsByClassName("autocomplete-items");
for (let i = 0; i < suggestions.length; i++) {
if (el !== suggestions[i] && el !== searchInput) {
suggestions[i].parentNode.removeChild(suggestions[i]);
}
}
};
const removeActive = suggestion => {
// Remove "autocomplete-active" class from previously active suggestion
for (let i = 0; i < suggestion.length; i++) {
@ -71,7 +61,7 @@ const addActive = (suggestion) => {
const autocompleteInput = (e) => {
// Handle navigation between autocomplete suggestions
let suggestion = document.getElementById(this.id + "-autocomplete-list");
let suggestion = document.getElementById("autocomplete-list");
if (suggestion) suggestion = suggestion.getElementsByTagName("div");
if (e.keyCode === 40) { // down
e.preventDefault();
@ -92,29 +82,28 @@ const autocompleteInput = (e) => {
};
const updateAutocompleteList = () => {
let autocompleteList, autocompleteItem, i;
let autocompleteItem, i;
let val = originalSearch;
closeAllLists();
let autocompleteList = document.getElementById("autocomplete-list");
autocompleteList.innerHTML = "";
if (!val || !autocompleteResults) {
return false;
}
currentFocus = -1;
autocompleteList = document.createElement("div");
autocompleteList.setAttribute("id", this.id + "-autocomplete-list");
autocompleteList.setAttribute("class", "autocomplete-items");
searchInput.parentNode.appendChild(autocompleteList);
for (i = 0; i < autocompleteResults.length; i++) {
if (autocompleteResults[i].substr(0, val.length).toUpperCase() === val.toUpperCase()) {
autocompleteItem = document.createElement("div");
autocompleteItem.setAttribute("class", "autocomplete-item");
autocompleteItem.innerHTML = "<strong>" + autocompleteResults[i].substr(0, val.length) + "</strong>";
autocompleteItem.innerHTML += autocompleteResults[i].substr(val.length);
autocompleteItem.innerHTML += "<input type=\"hidden\" value=\"" + autocompleteResults[i] + "\">";
autocompleteItem.addEventListener("click", function () {
searchInput.value = this.getElementsByTagName("input")[0].value;
closeAllLists();
autocompleteList.innerHTML = "";
document.getElementById("search-form").submit();
});
autocompleteList.appendChild(autocompleteItem);
@ -123,10 +112,16 @@ const updateAutocompleteList = () => {
};
document.addEventListener("DOMContentLoaded", function() {
let autocompleteList = document.createElement("div");
autocompleteList.setAttribute("id", "autocomplete-list");
autocompleteList.setAttribute("class", "autocomplete-items");
searchInput = document.getElementById("search-bar");
searchInput.parentNode.appendChild(autocompleteList);
searchInput.addEventListener("keydown", (event) => autocompleteInput(event));
document.addEventListener("click", function (e) {
closeAllLists(e.target);
autocompleteList.innerHTML = "";
});
});
});

View file

@ -16,7 +16,7 @@ const checkForTracking = () => {
]
},
"usps": {
"link": `https://tools.usps.com/go/TrackConfirmAction?tLabels=${query}`,
"link": `https://tools.usps.com/go/TrackConfirmAction_input?origTrackNum=${query}`,
"expr": [
/(\b\d{30}\b)|(\b91\d+\b)|(\b\d{20}\b)/,
/^E\D{1}\d{9}\D{2}$|^9\d{15,21}$/,

View file

@ -13,7 +13,7 @@
},
"maps": {
"tbm": null,
"href": "https://maps.google.com/maps?q={query}",
"href": "https://maps.google.com/maps?q={map_query}",
"name": "Maps",
"selected": false
},

View file

@ -1056,12 +1056,12 @@
"books": "Pirtûk",
"anon-view": "Dîtina Nenas",
"": "--",
"qdr:h": "Saet berê",
"qdr:d": "24 saetên borî",
"qdr:h": "Demjimêra borî",
"qdr:d": "24 Demjimêrên borî",
"qdr:w": "Hefteya borî",
"qdr:m": "Meha borî",
"qdr:y": "Sala borî",
"config-time-period": "Dem Period"
"config-time-period": "Pêşsazkariyên demê"
},
"lang_th": {
"search": "ค้นหา",

View file

@ -19,22 +19,88 @@
{{ error_message }}
</p>
<hr>
<p>
{% if blocked is defined %}
<h4><a class="link" href="https://farside.link">{{ translation['continue-search'] }}</a></h4>
Whoogle:
<br>
<a class="link-color" href="{{farside}}/whoogle/search?q={{query}}{{params}}">
{{farside}}/whoogle/search?q={{query}}
</a>
<br><br>
Searx:
<br>
<a class="link-color" href="{{farside}}/searx/search?q={{query}}">
{{farside}}/searx/search?q={{query}}
</a>
<hr>
{% if query and translation %}
<p>
<h4><a class="link" href="https://farside.link">{{ translation['continue-search'] }}</a></h4>
<ul>
<li>
<a href="https://github.com/benbusby/whoogle-search">Whoogle</a>
<ul>
<li>
<a class="link-color" href="{{farside}}/whoogle/search?q={{query}}{{params}}">
{{farside}}/whoogle/search?q={{query}}
</a>
</li>
</ul>
</li>
<li>
<a href="https://github.com/searxng/searxng">SearXNG</a>
<ul>
<li>
<a class="link-color" href="{{farside}}/searxng/search?q={{query}}">
{{farside}}/searxng/search?q={{query}}
</a>
</li>
</ul>
</li>
</ul>
<hr>
<h4>Other options:</h4>
<ul>
<li>
<a href="https://kagi.com">Kagi</a>
<ul>
<li>Requires account</li>
<li>
<a class="link-color" href="https://kagi.com/search?q={{query}}">
kagi.com/search?q={{query}}
</a>
</li>
</ul>
</li>
<li>
<a href="https://duckduckgo.com">DuckDuckGo</a>
<ul>
<li>
<a class="link-color" href="https://duckduckgo.com/search?q={{query}}">
duckduckgo.com/search?q={{query}}
</a>
</li>
</ul>
</li>
<li>
<a href="https://search.brave.com">Brave Search</a>
<ul>
<li>
<a class="link-color" href="https://search.brave.com/search?q={{query}}">
search.brave.com/search?q={{query}}
</a>
</li>
</ul>
</li>
<li>
<a href="https://ecosia.com">Ecosia</a>
<ul>
<li>
<a class="link-color" href="https://ecosia.com/search?q={{query}}">
ecosia.com/search?q={{query}}
</a>
</li>
</ul>
</li>
<li>
<a href="https://google.com">Google</a>
<ul>
<li>
<a class="link-color" href="https://google.com/search?q={{query}}">
google.com/search?q={{query}}
</a>
</li>
</ul>
</li>
</ul>
<hr>
</p>
{% endif %}
</p>
<a class="link" href="home">Return Home</a>
</div>

View file

@ -243,15 +243,13 @@
{{ translation['config-css'] }}:
</a>
<textarea
name="style"
name="style_modified"
id="config-style"
autocapitalize="off"
autocomplete="off"
spellcheck="false"
autocorrect="off"
value="">
{{ config.style.replace('\t', '') }}
</textarea>
value="">{{ config.style_modified.replace('\t', '') }}</textarea>
</div>
<div class="config-div config-div-pref-url">
<label for="config-pref-encryption">{{ translation['config-pref-encryption'] }}: </label>

File diff suppressed because one or more lines are too long

View file

@ -1,8 +1,56 @@
import json
import requests
import urllib.parse as urlparse
import os
import glob
DDG_BANGS = 'https://duckduckgo.com/bang.v255.js'
bangs_dict = {}
DDG_BANGS = 'https://duckduckgo.com/bang.js'
def load_all_bangs(ddg_bangs_file: str, ddg_bangs: dict = {}):
"""Loads all the bang files in alphabetical order
Args:
ddg_bangs_file: The str path to the new DDG bangs json file
ddg_bangs: The dict of ddg bangs. If this is empty, it will load the
bangs from the file
Returns:
None
"""
global bangs_dict
ddg_bangs_file = os.path.normpath(ddg_bangs_file)
if (bangs_dict and not ddg_bangs) or os.path.getsize(ddg_bangs_file) <= 4:
return
bangs = {}
bangs_dir = os.path.dirname(ddg_bangs_file)
bang_files = glob.glob(os.path.join(bangs_dir, '*.json'))
# Normalize the paths
bang_files = [os.path.normpath(f) for f in bang_files]
# Move the ddg bangs file to the beginning
bang_files = sorted([f for f in bang_files if f != ddg_bangs_file])
if ddg_bangs:
bangs |= ddg_bangs
else:
bang_files.insert(0, ddg_bangs_file)
for i, bang_file in enumerate(bang_files):
try:
bangs |= json.load(open(bang_file))
except json.decoder.JSONDecodeError:
# Ignore decoding error only for the ddg bangs file, since this can
# occur if file is still being written
if i != 0:
raise
bangs_dict = dict(sorted(bangs.items()))
def gen_bangs_json(bangs_file: str) -> None:
@ -37,22 +85,35 @@ def gen_bangs_json(bangs_file: str) -> None:
json.dump(bangs_data, open(bangs_file, 'w'))
print('* Finished creating ddg bangs json')
load_all_bangs(bangs_file, bangs_data)
def resolve_bang(query: str, bangs_dict: dict) -> str:
def suggest_bang(query: str) -> list[str]:
"""Suggests bangs for a user's query
Args:
query: The search query
Returns:
list[str]: A list of bang suggestions
"""
global bangs_dict
return [bangs_dict[_]['suggestion'] for _ in bangs_dict if _.startswith(query)]
def resolve_bang(query: str) -> str:
"""Transform's a user's query to a bang search, if an operator is found
Args:
query: The search query
bangs_dict: The dict of available bang operators, with corresponding
format string search URLs
(i.e. "!w": "https://en.wikipedia.org...?search={}")
Returns:
str: A formatted redirect for a bang search, or an empty str if there
wasn't a match or didn't contain a bang operator
"""
global bangs_dict
#if ! not in query simply return (speed up processing)
if '!' not in query:

View file

@ -1,10 +1,50 @@
from bs4 import BeautifulSoup as bsoup
from flask import Request
import base64
import hashlib
import contextlib
import io
import os
import re
from requests import exceptions, get
from urllib.parse import urlparse
from bs4 import BeautifulSoup as bsoup
from cryptography.fernet import Fernet
from flask import Request
ddg_favicon_site = 'http://icons.duckduckgo.com/ip2'
empty_gif = base64.b64decode(
'R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==')
placeholder_img = base64.b64decode(
'iVBORw0KGgoAAAANSUhEUgAAABkAAAAZCAYAAADE6YVjAAABF0lEQVRIS8XWPw9EMBQA8Eok' \
'JBKrMFqMBt//GzAYLTZ/VomExPDu6uLiaPteqVynBn0/75W2Vp7nEIYhe6p1XcespmmAd7Is' \
'M+4URcGiKPogvMMvmIS2eN9MOMKbKWgf54SYgI4vKkTuQKJKSJErkKzUSkQHUs0lilAg7GMh' \
'ISoIA/hYMiKCKIA2soeowCWEMkfHtUmrXLcyGYYBfN9HF8djiaglWzNZlgVs21YisoAUaEXG' \
'cQTP86QIFgi7vyLzPIPjOEIEC7ANQv/4aZrAdd0TUtc1i+MYnSsMWjPp+x6CIPgJVlUVS5KE' \
'DKig/+wnVzM4pnzaGeHd+ENlWbI0TbVLJBtw2uMfP63wc9d2kDCWxi5Q27bsBerSJ9afJbeL' \
'AAAAAElFTkSuQmCC'
)
def fetch_favicon(url: str) -> bytes:
"""Fetches a favicon using DuckDuckGo's favicon retriever
Args:
url: The url to fetch the favicon from
Returns:
bytes - the favicon bytes, or a placeholder image if one
was not returned
"""
response = get(f'{ddg_favicon_site}/{urlparse(url).netloc}.ico')
if response.status_code == 200 and len(response.content) > 0:
tmp_mem = io.BytesIO()
tmp_mem.write(response.content)
tmp_mem.seek(0)
return tmp_mem.read()
return placeholder_img
def gen_file_hash(path: str, static_file: str) -> str:
@ -12,23 +52,22 @@ def gen_file_hash(path: str, static_file: str) -> str:
file_hash = hashlib.md5(file_contents).hexdigest()[:8]
filename_split = os.path.splitext(static_file)
return filename_split[0] + '.' + file_hash + filename_split[-1]
return f'{filename_split[0]}.{file_hash}{filename_split[-1]}'
def read_config_bool(var: str) -> bool:
val = os.getenv(var, '0')
def read_config_bool(var: str, default: bool=False) -> bool:
val = os.getenv(var, '1' if default else '0')
# user can specify one of the following values as 'true' inputs (all
# variants with upper case letters will also work):
# ('true', 't', '1', 'yes', 'y')
val = val.lower() in ('true', 't', '1', 'yes', 'y')
return val
return val.lower() in ('true', 't', '1', 'yes', 'y')
def get_client_ip(r: Request) -> str:
if r.environ.get('HTTP_X_FORWARDED_FOR') is None:
return r.environ['REMOTE_ADDR']
else:
return r.environ['HTTP_X_FORWARDED_FOR']
return r.environ['HTTP_X_FORWARDED_FOR']
def get_request_url(url: str) -> str:
@ -57,25 +96,43 @@ def get_proxy_host_url(r: Request, default: str, root=False) -> str:
def check_for_update(version_url: str, current: str) -> int:
# Check for the latest version of Whoogle
try:
has_update = ''
with contextlib.suppress(exceptions.ConnectionError, AttributeError):
update = bsoup(get(version_url).text, 'html.parser')
latest = update.select_one('[class="Link--primary"]').string[1:]
current = int(''.join(filter(str.isdigit, current)))
latest = int(''.join(filter(str.isdigit, latest)))
has_update = '' if current >= latest else latest
except (exceptions.ConnectionError, AttributeError):
# Ignore failures, assume current version is up to date
has_update = ''
return has_update
def get_abs_url(url, page_url):
# Creates a valid absolute URL using a partial or relative URL
if url.startswith('//'):
return f'https:{url}'
elif url.startswith('/'):
return f'{urlparse(page_url).netloc}{url}'
elif url.startswith('./'):
return f'{page_url}{url[2:]}'
urls = {
"//": f"https:{url}",
"/": f"{urlparse(page_url).netloc}{url}",
"./": f"{page_url}{url[2:]}"
}
for start in urls:
if url.startswith(start):
return urls[start]
return url
def list_to_dict(lst: list) -> dict:
if len(lst) < 2:
return {}
return {lst[i].replace(' ', ''): lst[i+1].replace(' ', '')
for i in range(0, len(lst), 2)}
def encrypt_string(key: bytes, string: str) -> str:
cipher_suite = Fernet(key)
return cipher_suite.encrypt(string.encode()).decode()
def decrypt_string(key: bytes, string: str) -> str:
cipher_suite = Fernet(g.session_key)
return cipher_suite.decrypt(string.encode()).decode()

View file

@ -1,5 +1,6 @@
from app.models.config import Config
from app.models.endpoint import Endpoint
from app.utils.misc import list_to_dict
from bs4 import BeautifulSoup, NavigableString
import copy
from flask import current_app
@ -11,7 +12,7 @@ import re
import warnings
SKIP_ARGS = ['ref_src', 'utm']
SKIP_PREFIX = ['//www.', '//mobile.', '//m.', 'www.', 'mobile.', 'm.']
SKIP_PREFIX = ['//www.', '//mobile.', '//m.']
GOOG_STATIC = 'www.gstatic.com'
G_M_LOGO_URL = 'https://www.gstatic.com/m/images/icons/googleg.gif'
GOOG_IMG = '/images/branding/searchlogo/1x/googlelogo'
@ -26,7 +27,8 @@ BLACKLIST = [
'Reklama', 'Реклама', 'Anunț', '광고', 'annons', 'Annonse', 'Iklan',
'広告', 'Augl.', 'Mainos', 'Advertentie', 'إعلان', 'Գովազդ', 'विज्ञापन',
'Reklam', 'آگهی', 'Reklāma', 'Reklaam', 'Διαφήμιση', 'מודעה', 'Hirdetés',
'Anúncio', 'Quảng cáo','โฆษณา', 'sponsored', 'patrocinado', 'gesponsert'
'Anúncio', 'Quảng cáo', 'โฆษณา', 'sponsored', 'patrocinado', 'gesponsert',
'Sponzorováno', '스폰서', 'Gesponsord'
]
SITE_ALTS = {
@ -40,9 +42,13 @@ SITE_ALTS = {
'imgur.com': os.getenv('WHOOGLE_ALT_IMG', 'farside.link/rimgo'),
'wikipedia.org': os.getenv('WHOOGLE_ALT_WIKI', 'farside.link/wikiless'),
'imdb.com': os.getenv('WHOOGLE_ALT_IMDB', 'farside.link/libremdb'),
'quora.com': os.getenv('WHOOGLE_ALT_QUORA', 'farside.link/quetre')
'quora.com': os.getenv('WHOOGLE_ALT_QUORA', 'farside.link/quetre'),
'stackoverflow.com': os.getenv('WHOOGLE_ALT_SO', 'farside.link/anonymousoverflow')
}
# Include custom site redirects from WHOOGLE_REDIRECTS
SITE_ALTS.update(list_to_dict(re.split(',|:', os.getenv('WHOOGLE_REDIRECTS', ''))))
def contains_cjko(s: str) -> bool:
"""This function check whether or not a string contains Chinese, Japanese,
@ -93,7 +99,7 @@ def bold_search_terms(response: str, query: str) -> BeautifulSoup:
else:
reg_pattern = fr'\b((?![{{}}<>-]){target_word}(?![{{}}<>-]))\b'
if re.match('.*[@_!#$%^&*()<>?/\|}{~:].*', target_word) or (
if re.match(r'.*[@_!#$%^&*()<>?/\|}{~:].*', target_word) or (
element.parent and element.parent.name == 'style'):
return
@ -140,19 +146,34 @@ def get_first_link(soup: BeautifulSoup) -> str:
str: A str link to the first result
"""
first_link = ''
orig_details = []
# Temporarily remove details so we don't grab those links
for details in soup.find_all('details'):
temp_details = soup.new_tag('removed_details')
orig_details.append(details.replace_with(temp_details))
# Replace hrefs with only the intended destination (no "utm" type tags)
for a in soup.find_all('a', href=True):
# Return the first search result URL
if 'url?q=' in a['href']:
return filter_link_args(a['href'])
return ''
if a['href'].startswith('http://') or a['href'].startswith('https://'):
first_link = a['href']
break
# Add the details back
for orig_detail, details in zip(orig_details, soup.find_all('removed_details')):
details.replace_with(orig_detail)
return first_link
def get_site_alt(link: str) -> str:
def get_site_alt(link: str, site_alts: dict = SITE_ALTS) -> str:
"""Returns an alternative to a particular site, if one is configured
Args:
link: A string result URL to check against the SITE_ALTS map
link: A string result URL to check against the site_alts map
site_alts: A map of site alternatives to replace with. defaults to SITE_ALTS
Returns:
str: An updated (or ignored) result link
@ -174,9 +195,9 @@ def get_site_alt(link: str) -> str:
# "https://medium.com/..." should match, but "philomedium.com" should not)
hostcomp = f'{parsed_link.scheme}://{hostname}'
for site_key in SITE_ALTS.keys():
for site_key in site_alts.keys():
site_alt = f'{parsed_link.scheme}://{site_key}'
if not hostname or site_alt not in hostcomp or not SITE_ALTS[site_key]:
if not hostname or site_alt not in hostcomp or not site_alts[site_key]:
continue
# Wikipedia -> Wikiless replacements require the subdomain (if it's
@ -189,9 +210,8 @@ def get_site_alt(link: str) -> str:
elif 'medium' in hostname and len(subdomain) > 0:
hostname = f'{subdomain}.{hostname}'
parsed_alt = urlparse.urlparse(SITE_ALTS[site_key])
link = link.replace(hostname, SITE_ALTS[site_key]) + params
parsed_alt = urlparse.urlparse(site_alts[site_key])
link = link.replace(hostname, site_alts[site_key]) + params
# If a scheme is specified in the alternative, this results in a
# replaced link that looks like "https://http://altservice.tld".
# In this case, we can remove the original scheme from the result
@ -201,9 +221,12 @@ def get_site_alt(link: str) -> str:
for prefix in SKIP_PREFIX:
if parsed_alt.scheme:
link = link.replace(prefix, '')
# If a scheme is specified, remove everything before the
# first occurence of it
link = f'{parsed_alt.scheme}{link.split(parsed_alt.scheme, 1)[-1]}'
else:
link = link.replace(prefix, '//')
# Otherwise, replace the first occurrence of the prefix
link = link.replace(prefix, '//', 1)
break
return link
@ -414,6 +437,10 @@ def get_tabs_content(tabs: dict,
Returns:
dict: contains the name, the href and if the tab is selected or not
"""
map_query = full_query
if '-site:' in full_query:
block_idx = full_query.index('-site:')
map_query = map_query[:block_idx]
tabs = copy.deepcopy(tabs)
for tab_id, tab_content in tabs.items():
# update name to desired language
@ -429,7 +456,9 @@ def get_tabs_content(tabs: dict,
if preferences:
query = f"{query}&preferences={preferences}"
tab_content['href'] = tab_content['href'].format(query=query)
tab_content['href'] = tab_content['href'].format(
query=query,
map_query=map_query)
# update if selected tab (default all tab is selected)
if tab_content['tbm'] == search_type:

View file

@ -102,14 +102,22 @@ class Search:
except InvalidToken:
pass
# Strip leading '! ' for "feeling lucky" queries
self.feeling_lucky = q.startswith('! ')
self.query = q[2:] if self.feeling_lucky else q
# Strip '!' for "feeling lucky" queries
if match := re.search("(^|\s)!($|\s)", q):
self.feeling_lucky = True
start, end = match.span()
self.query = " ".join([seg for seg in [q[:start], q[end:]] if seg])
else:
self.feeling_lucky = False
self.query = q
# Check for possible widgets
self.widget = "ip" if re.search("([^a-z0-9]|^)my *[^a-z0-9] *(ip|internet protocol)" +
"($|( *[^a-z0-9] *(((addres|address|adres|" +
"adress)|a)? *$)))", self.query.lower()) else self.widget
self.widget = 'calculator' if re.search("calculator|calc|calclator|math", self.query.lower()) else self.widget
self.widget = 'calculator' if re.search(
r"\bcalculator\b|\bcalc\b|\bcalclator\b|\bmath\b",
self.query.lower()) else self.widget
return self.query
def generate_response(self) -> str:
@ -144,7 +152,8 @@ class Search:
and not g.user_request.mobile)
get_body = g.user_request.send(query=full_query,
force_mobile=view_image)
force_mobile=view_image,
user_agent=self.user_agent)
# Produce cleanable html soup from response
get_body_safed = get_body.text.replace("&lt;","andlt;").replace("&gt;","andgt;")
@ -158,22 +167,25 @@ class Search:
if g.user_request.tor_valid:
html_soup.insert(0, bsoup(TOR_BANNER, 'html.parser'))
formatted_results = content_filter.clean(html_soup)
if self.feeling_lucky:
return get_first_link(html_soup)
else:
formatted_results = content_filter.clean(html_soup)
if lucky_link := get_first_link(formatted_results):
return lucky_link
# Append user config to all search links, if available
param_str = ''.join('&{}={}'.format(k, v)
for k, v in
self.request_params.to_dict(flat=True).items()
if self.config.is_safe_key(k))
for link in formatted_results.find_all('a', href=True):
link['rel'] = "nofollow noopener noreferrer"
if 'search?' not in link['href'] or link['href'].index(
'search?') > 1:
continue
link['href'] += param_str
# Fall through to regular search if unable to find link
self.feeling_lucky = False
return str(formatted_results)
# Append user config to all search links, if available
param_str = ''.join('&{}={}'.format(k, v)
for k, v in
self.request_params.to_dict(flat=True).items()
if self.config.is_safe_key(k))
for link in formatted_results.find_all('a', href=True):
link['rel'] = "nofollow noopener noreferrer"
if 'search?' not in link['href'] or link['href'].index(
'search?') > 1:
continue
link['href'] += param_str
return str(formatted_results)

View file

@ -1,5 +1,10 @@
from pathlib import Path
from bs4 import BeautifulSoup
# root
BASE_DIR = Path(__file__).parent.parent.parent
def add_ip_card(html_soup: BeautifulSoup, ip: str) -> BeautifulSoup:
"""Adds the client's IP address to the search results
if query contains keywords
@ -48,7 +53,8 @@ def add_calculator_card(html_soup: BeautifulSoup) -> BeautifulSoup:
"""
main_div = html_soup.select_one('#main')
if main_div:
widget_file = open('app/static/widgets/calculator.html')
# absolute path
widget_file = open(BASE_DIR / 'app/static/widgets/calculator.html', encoding="utf8")
widget_tag = html_soup.new_tag('div')
widget_tag['class'] = 'ZINbbc xpd O9g5cc uUPGi'
widget_tag['id'] = 'calculator-wrapper'
@ -56,7 +62,7 @@ def add_calculator_card(html_soup: BeautifulSoup) -> BeautifulSoup:
calculator_text['class'] = 'kCrYT ip-address-div'
calculator_text.string = 'Calculator'
calculator_widget = html_soup.new_tag('div')
calculator_widget.append(BeautifulSoup(widget_file, 'html.parser'));
calculator_widget.append(BeautifulSoup(widget_file, 'html.parser'))
calculator_widget['class'] = 'kCrYT ip-text-div'
widget_tag.append(calculator_text)
widget_tag.append(calculator_widget)

View file

@ -4,4 +4,4 @@ optional_dev_tag = ''
if os.getenv('DEV_BUILD'):
optional_dev_tag = '.dev' + os.getenv('DEV_BUILD')
__version__ = '0.8.1' + optional_dev_tag
__version__ = '0.9.1' + optional_dev_tag

View file

@ -3,7 +3,7 @@ name: whoogle
description: A self hosted search engine on Kubernetes
type: application
version: 0.1.0
appVersion: 0.8.1
appVersion: 0.9.1
icon: https://github.com/benbusby/whoogle-search/raw/main/app/static/img/favicon/favicon-96x96.png

View file

@ -52,10 +52,20 @@ spec:
httpGet:
path: /
port: http
{{- if and .Values.conf.WHOOGLE_USER .Values.conf.WHOOGLE_PASS }}
httpHeaders:
- name: Authorization
value: Basic {{ b64enc (printf "%s:%s" .Values.conf.WHOOGLE_USER .Values.conf.WHOOGLE_PASS) }}
{{- end }}
readinessProbe:
httpGet:
path: /
port: http
{{- if and .Values.conf.WHOOGLE_USER .Values.conf.WHOOGLE_PASS }}
httpHeaders:
- name: Authorization
value: Basic {{ b64enc (printf "%s:%s" .Values.conf.WHOOGLE_USER .Values.conf.WHOOGLE_PASS) }}
{{- end }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.nodeSelector }}

View file

@ -43,6 +43,7 @@ conf: {}
# WHOOGLE_ALT_WIKI: "" # The wikipedia.com alternative to use when site alternatives are enabled in the config.
# WHOOGLE_ALT_IMDB: "" # The imdb.com alternative to use. Set to "" to continue using imdb.com when site alternatives are enabled.
# WHOOGLE_ALT_QUORA: "" # The quora.com alternative to use. Set to "" to continue using quora.com when site alternatives are enabled.
# WHOOGLE_ALT_SO: "" # The stackoverflow.com alternative to use. Set to "" to continue using stackoverflow.com when site alternatives are enabled.
# WHOOGLE_AUTOCOMPLETE: "" # Controls visibility of autocomplete/search suggestions. Default on -- use '0' to disable
# WHOOGLE_MINIMAL: "" # Remove everything except basic result cards from all search queries.

View file

@ -68,6 +68,7 @@ services:
#- WHOOGLE_ALT_WIKI=farside.link/wikiless
#- WHOOGLE_ALT_IMDB=farside.link/libremdb
#- WHOOGLE_ALT_QUORA=farside.link/quetre
#- WHOOGLE_ALT_SO=farside.link/anonymousoverflow
# - WHOOGLE_CONFIG_DISABLE=1
# - WHOOGLE_CONFIG_SEARCH_LANGUAGE=lang_en
# - WHOOGLE_CONFIG_GET_ONLY=1

View file

@ -42,6 +42,7 @@ services:
#- WHOOGLE_ALT_WIKI=farside.link/wikiless
#- WHOOGLE_ALT_IMDB=farside.link/libremdb
#- WHOOGLE_ALT_QUORA=farside.link/quetre
#- WHOOGLE_ALT_SO=farside.link/anonymousoverflow
#env_file: # Alternatively, load variables from whoogle.env
#- whoogle.env
ports:

View file

@ -1,12 +1,11 @@
https://search.albony.xyz
https://search.garudalinux.org
https://search.dr460nf1r3.org
https://search.nezumi.party
https://s.tokhmi.xyz
https://search.sethforprivacy.com
https://whoogle.dcs0.hu
https://whoogle.esmailelbob.xyz
https://whoogle.lunar.icu
https://whoogle.rhyshl.live
https://gowogle.voring.me
https://whoogle.privacydev.net
https://whoogle.hostux.net
@ -16,3 +15,10 @@ https://whoogle.ungovernable.men
https://whoogle2.ungovernable.men
https://whoogle3.ungovernable.men
https://wgl.frail.duckdns.org
https://whoogle.no-logs.com
https://whoogle.ftw.lol
https://whoogle-search--replitcomreside.repl.co
https://search.notrustverify.ch
https://whoogle.datura.network
https://whoogle.yepserver.xyz
https://search.snine.nl

View file

@ -7,3 +7,6 @@ ExtORPortCookieAuthFileGroupReadable 1
CacheDirectoryGroupReadable 1
CookieAuthFile /var/lib/tor/control_auth_cookie
Log debug-notice file /dev/null
# UseBridges 1
# ClientTransportPlugin obfs4 exec /usr/bin/obfs4proxy
# Bridge obfs4 ip and so on

View file

@ -2,35 +2,36 @@ attrs==22.2.0
beautifulsoup4==4.11.2
brotli==1.0.9
cachelib==0.10.2
certifi==2022.12.7
cffi==1.15.1
certifi==2024.7.4
cffi==1.17.1
chardet==5.1.0
click==8.1.3
cryptography==3.3.2; platform_machine == 'armv7l'
cryptography==39.0.1; platform_machine != 'armv7l'
cryptography==43.0.1; platform_machine != 'armv7l'
cssutils==2.6.0
defusedxml==0.7.1
Flask==2.2.3
idna==3.4
Flask==2.3.2
idna==3.7
itsdangerous==2.1.2
Jinja2==3.1.2
Jinja2==3.1.4
MarkupSafe==2.1.2
more-itertools==9.0.0
packaging==23.0
pluggy==1.0.0
pycodestyle==2.10.0
pycparser==2.21
pycparser==2.22
pyOpenSSL==19.1.0; platform_machine == 'armv7l'
pyOpenSSL==23.0.0; platform_machine != 'armv7l'
pyOpenSSL==24.2.1; platform_machine != 'armv7l'
pyparsing==3.0.9
PySocks==1.7.1
pytest==7.2.1
python-dateutil==2.8.2
requests==2.28.2
requests==2.32.2
soupsieve==2.4
stem==1.8.1
urllib3==1.26.14
waitress==2.1.2
urllib3==1.26.19
validators==0.22.0
waitress==3.0.1
wcwidth==0.2.6
Werkzeug==2.2.3
Werkzeug==3.0.6
python-dotenv==0.21.1

1
run
View file

@ -29,6 +29,7 @@ else
python3 -um app \
--unix-socket "$UNIX_SOCKET"
else
echo "Running on http://${ADDRESS:-0.0.0.0}:${PORT:-"${EXPOSE_PORT:-5000}"}"
python3 -um app \
--host "${ADDRESS:-0.0.0.0}" \
--port "${PORT:-"${EXPOSE_PORT:-5000}"}"

View file

@ -27,6 +27,7 @@ install_requires=
python-dotenv
requests
stem
validators
waitress
[options.extras_require]

View file

@ -4,19 +4,11 @@ from app import app
from app.models.endpoint import Endpoint
from app.utils.session import generate_key, valid_user_session
JAPAN_PREFS = 'uG-gGIJwHdqxl6DrS3mnu_511HlQcRpxYlG03Xs-' \
+ '_znXNiJWI9nLOkRLkiiFwIpeUYMTGfUF5-t9fP5DGmzDLEt04DCx703j3nPf' \
+ '29v_RWkU7gXw_44m2oAFIaKGmYlu4Z0bKyu9k5WXfL9Dy6YKKnpcR5CiaFsG' \
+ 'rccNRkAPYm-eYGAFUV8M59f8StsGd_M-gHKGS9fLok7EhwBWjHxBJ2Kv8hsT' \
+ '87zftP2gMJOevTdNnezw2Y5WOx-ZotgeheCW1BYCFcRqatlov21PHp22NGVG' \
+ '8ZuBNAFW0bE99WSdyT7dUIvzeWCLJpbdSsq-3FUUZkxbRdFYlGd8vY1UgVAp' \
+ 'OSie2uAmpgLFXygO-VfNBBZ68Q7gAap2QtzHCiKD5cFYwH3LPgVJ-DoZvJ6k' \
+ 'alt34TaYiJphgiqFKV4SCeVmLWTkr0SF3xakSR78yYJU_d41D2ng-TojA9XZ' \
+ 'uR2ZqjSvPKOWvjimu89YhFOgJxG1Po8Henj5h9OL9VXXvdvlJwBSAKw1E3FV' \
+ '7UHWiglMxPblfxqou1cYckMYkFeIMCD2SBtju68mBiQh2k328XRPTsQ_ocby' \
+ 'cgVKnleGperqbD6crRk3Z9xE5sVCjujn9JNVI-7mqOITMZ0kntq9uJ3R5n25' \
+ 'Vec0TJ0P19nEtvjY0nJIrIjtnBg=='
JAPAN_PREFS = 'uG7IBICwK7FgMJNpUawp2tKDb1Omuv_euy-cJHVZ' \
+ 'BSydthgwxRFIHxiVA8qUGavKaDXyiM5uNuPIjKbEAW-zB_vzNXWVaafFhW7k2' \
+ 'fO2_mS5e5eK41XXWwiViTz2VVmGWje0UgQwwVPe1A7aH0s10FgARsd2xl5nlg' \
+ 'RLHT2krPUw-iLQ5uHZSnYXFuF4caYemWcj4vqB2ocHkt-aqn04jgnnlWWME_K' \
+ '9ySWdWmPyS66HtLt1tCwc_-xGZklvbHw=='
def test_generate_user_keys():

View file

@ -2,6 +2,7 @@ from bs4 import BeautifulSoup
from app.filter import Filter
from app.models.config import Config
from app.models.endpoint import Endpoint
from app.utils import results
from app.utils.session import generate_key
from datetime import datetime
from dateutil.parser import ParserError, parse
@ -44,17 +45,11 @@ def test_get_results(client):
def test_post_results(client):
rv = client.post(f'/{Endpoint.search}', data=dict(q='test'))
assert rv._status_code == 200
# Depending on the search, there can be more
# than 10 result divs
results = get_search_results(rv.data)
assert len(results) >= 10
assert len(results) <= 15
assert rv._status_code == 302
def test_translate_search(client):
rv = client.post(f'/{Endpoint.search}', data=dict(q='translate hola'))
rv = client.get(f'/{Endpoint.search}?q=translate hola')
assert rv._status_code == 200
# Pretty weak test, but better than nothing
@ -64,7 +59,7 @@ def test_translate_search(client):
def test_block_results(client):
rv = client.post(f'/{Endpoint.search}', data=dict(q='pinterest'))
rv = client.get(f'/{Endpoint.search}?q=pinterest')
assert rv._status_code == 200
has_pinterest = False
@ -79,7 +74,7 @@ def test_block_results(client):
rv = client.post(f'/{Endpoint.config}', data=demo_config)
assert rv._status_code == 302
rv = client.post(f'/{Endpoint.search}', data=dict(q='pinterest'))
rv = client.get(f'/{Endpoint.search}?q=pinterest')
assert rv._status_code == 200
for link in BeautifulSoup(rv.data, 'html.parser').find_all('a', href=True):
@ -90,7 +85,7 @@ def test_block_results(client):
def test_view_my_ip(client):
rv = client.post(f'/{Endpoint.search}', data=dict(q='my ip address'))
rv = client.get(f'/{Endpoint.search}?q=my ip address')
assert rv._status_code == 200
# Pretty weak test, but better than nothing
@ -101,13 +96,13 @@ def test_view_my_ip(client):
def test_recent_results(client):
times = {
'past year': 365,
'past month': 31,
'past week': 7
'tbs=qdr:y': 365,
'tbs=qdr:m': 31,
'tbs=qdr:w': 7
}
for time, num_days in times.items():
rv = client.post(f'/{Endpoint.search}', data=dict(q='test :' + time))
rv = client.get(f'/{Endpoint.search}?q=test&' + time)
result_divs = get_search_results(rv.data)
current_date = datetime.now()
@ -142,3 +137,22 @@ def test_leading_slash_search(client):
continue
assert link['href'].startswith(f'{Endpoint.search}')
def test_site_alt_prefix_skip():
# Ensure prefixes are skipped correctly for site alts
# default silte_alts (farside.link)
assert results.get_site_alt(link = 'https://www.reddit.com') == 'https://farside.link/libreddit'
assert results.get_site_alt(link = 'https://www.twitter.com') == 'https://farside.link/nitter'
assert results.get_site_alt(link = 'https://www.youtube.com') == 'https://farside.link/invidious'
test_site_alts = {
'reddit.com': 'reddit.endswithmobile.domain',
'twitter.com': 'https://twitter.endswithm.domain',
'youtube.com': 'http://yt.endswithwww.domain',
}
# Domains with part of SKIP_PREFIX in them
assert results.get_site_alt(link = 'https://www.reddit.com', site_alts = test_site_alts) == 'https://reddit.endswithmobile.domain'
assert results.get_site_alt(link = 'https://www.twitter.com', site_alts = test_site_alts) == 'https://twitter.endswithm.domain'
assert results.get_site_alt(link = 'https://www.youtube.com', site_alts = test_site_alts) == 'http://yt.endswithwww.domain'

View file

@ -17,8 +17,15 @@ def test_search(client):
def test_feeling_lucky(client):
rv = client.get(f'/{Endpoint.search}?q=!%20test')
# Bang at beginning of query
rv = client.get(f'/{Endpoint.search}?q=!%20wikipedia')
assert rv._status_code == 303
assert rv.headers.get('Location').startswith('https://www.wikipedia.org')
# Move bang to end of query
rv = client.get(f'/{Endpoint.search}?q=github%20!')
assert rv._status_code == 303
assert rv.headers.get('Location').startswith('https://github.com')
def test_ddg_bang(client):
@ -48,6 +55,13 @@ def test_ddg_bang(client):
assert rv.headers.get('Location').startswith('https://github.com')
def test_custom_bang(client):
# Bang at beginning of query
rv = client.get(f'/{Endpoint.search}?q=!i%20whoogle')
assert rv._status_code == 302
assert rv.headers.get('Location').startswith('search?q=')
def test_config(client):
rv = client.post(f'/{Endpoint.config}', data=demo_config)
assert rv._status_code == 302

View file

@ -17,6 +17,7 @@
#WHOOGLE_ALT_WIKI=farside.link/wikiless
#WHOOGLE_ALT_IMDB=farside.link/libremdb
#WHOOGLE_ALT_QUORA=farside.link/quetre
#WHOOGLE_ALT_SO=farside.link/anonymousoverflow
#WHOOGLE_USER=""
#WHOOGLE_PASS=""
#WHOOGLE_PROXY_USER=""