├── test ├── __init__.py ├── test_autocomplete.py ├── conftest.py ├── test_routes.py ├── test_misc.py └── test_results.py ├── app ├── models │ ├── __init__.py │ ├── endpoint.py │ ├── g_classes.py │ └── config.py ├── utils │ ├── __init__.py │ ├── session.py │ ├── misc.py │ ├── bangs.py │ └── search.py ├── static │ ├── build │ │ └── .gitignore │ ├── settings │ │ ├── themes.json │ │ ├── header_tabs.json │ │ ├── languages.json │ │ └── countries.json │ ├── img │ │ ├── logo.png │ │ ├── favicon.ico │ │ ├── favicon │ │ │ ├── favicon.ico │ │ │ ├── apple-icon.png │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon-96x96.png │ │ │ ├── ms-icon-70x70.png │ │ │ ├── ms-icon-144x144.png │ │ │ ├── ms-icon-150x150.png │ │ │ ├── ms-icon-310x310.png │ │ │ ├── android-icon-36x36.png │ │ │ ├── android-icon-48x48.png │ │ │ ├── android-icon-72x72.png │ │ │ ├── android-icon-96x96.png │ │ │ ├── apple-icon-114x114.png │ │ │ ├── apple-icon-120x120.png │ │ │ ├── apple-icon-144x144.png │ │ │ ├── apple-icon-152x152.png │ │ │ ├── apple-icon-180x180.png │ │ │ ├── apple-icon-57x57.png │ │ │ ├── apple-icon-60x60.png │ │ │ ├── apple-icon-72x72.png │ │ │ ├── apple-icon-76x76.png │ │ │ ├── android-icon-144x144.png │ │ │ ├── android-icon-192x192.png │ │ │ ├── apple-icon-precomposed.png │ │ │ ├── browserconfig.xml │ │ │ └── manifest.json │ │ └── whoogle.svg │ ├── css │ │ ├── error.css │ │ ├── logo.css │ │ ├── input.css │ │ ├── variables.css │ │ ├── search.css │ │ ├── main.css │ │ ├── light-theme.css │ │ ├── header.css │ │ └── dark-theme.css │ └── js │ │ ├── currency.js │ │ ├── keyboard.js │ │ ├── header.js │ │ ├── utils.js │ │ ├── controller.js │ │ └── autocomplete.js ├── __main__.py ├── version.py ├── templates │ ├── footer.html │ ├── search.html │ ├── error.html │ ├── display.html │ ├── opensearch.xml │ ├── header.html │ ├── logo.html │ └── imageresults.html ├── __init__.py └── request.py ├── letsencrypt └── acme.json ├── .dockerignore ├── misc ├── tor │ ├── control.conf │ ├── torrc │ └── start-tor.sh ├── instances.txt └── heroku-regen.sh ├── heroku.yml ├── docs ├── banner.png ├── screenshot_desktop.png └── screenshot_mobile.png ├── pyproject.toml ├── MANIFEST.in ├── .replit ├── .gitignore ├── .github ├── ISSUE_TEMPLATE │ ├── question.md │ ├── feature_request.md │ ├── bug_report.md │ └── new-theme.md ├── workflows │ ├── tests.yml │ ├── scan.yml │ ├── docker_tests.yml │ ├── docker_main.yml │ ├── pypi.yml │ └── buildx.yml └── FUNDING.yml ├── charts └── whoogle │ ├── templates │ ├── serviceaccount.yaml │ ├── service.yaml │ ├── tests │ │ └── test-connection.yaml │ ├── hpa.yaml │ ├── NOTES.txt │ ├── _helpers.tpl │ ├── ingress.yaml │ └── deployment.yaml │ ├── .helmignore │ ├── Chart.yaml │ └── values.yaml ├── requirements.txt ├── run ├── LICENSE ├── setup.cfg ├── docker-compose.yml ├── Dockerfile ├── docker-compose-traefik.yaml ├── whoogle.template.env └── app.json /test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /letsencrypt/acme.json: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git/ 2 | venv/ 3 | test/ 4 | -------------------------------------------------------------------------------- /app/static/build/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /misc/tor/control.conf: -------------------------------------------------------------------------------- 1 | # Place password here. Keep this safe. -------------------------------------------------------------------------------- /app/__main__.py: -------------------------------------------------------------------------------- 1 | from .routes import run_app 2 | 3 | run_app() 4 | -------------------------------------------------------------------------------- /heroku.yml: -------------------------------------------------------------------------------- 1 | build: 2 | docker: 3 | web: Dockerfile 4 | 5 | -------------------------------------------------------------------------------- /app/static/settings/themes.json: -------------------------------------------------------------------------------- 1 | [ 2 | "light", 3 | "dark", 4 | "system" 5 | ] 6 | -------------------------------------------------------------------------------- /docs/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrivacyDevel/whoogle-search/main/docs/banner.png -------------------------------------------------------------------------------- /app/static/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrivacyDevel/whoogle-search/main/app/static/img/logo.png -------------------------------------------------------------------------------- /app/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrivacyDevel/whoogle-search/main/app/static/img/favicon.ico -------------------------------------------------------------------------------- /docs/screenshot_desktop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrivacyDevel/whoogle-search/main/docs/screenshot_desktop.png -------------------------------------------------------------------------------- /docs/screenshot_mobile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrivacyDevel/whoogle-search/main/docs/screenshot_mobile.png -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | -------------------------------------------------------------------------------- /app/static/img/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrivacyDevel/whoogle-search/main/app/static/img/favicon/favicon.ico -------------------------------------------------------------------------------- /app/static/img/favicon/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrivacyDevel/whoogle-search/main/app/static/img/favicon/apple-icon.png -------------------------------------------------------------------------------- /app/static/img/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrivacyDevel/whoogle-search/main/app/static/img/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /app/static/img/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrivacyDevel/whoogle-search/main/app/static/img/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /app/static/img/favicon/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrivacyDevel/whoogle-search/main/app/static/img/favicon/favicon-96x96.png -------------------------------------------------------------------------------- /app/static/img/favicon/ms-icon-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrivacyDevel/whoogle-search/main/app/static/img/favicon/ms-icon-70x70.png -------------------------------------------------------------------------------- /app/static/img/favicon/ms-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrivacyDevel/whoogle-search/main/app/static/img/favicon/ms-icon-144x144.png -------------------------------------------------------------------------------- /app/static/img/favicon/ms-icon-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrivacyDevel/whoogle-search/main/app/static/img/favicon/ms-icon-150x150.png -------------------------------------------------------------------------------- /app/static/img/favicon/ms-icon-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrivacyDevel/whoogle-search/main/app/static/img/favicon/ms-icon-310x310.png -------------------------------------------------------------------------------- /app/static/img/favicon/android-icon-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrivacyDevel/whoogle-search/main/app/static/img/favicon/android-icon-36x36.png -------------------------------------------------------------------------------- /app/static/img/favicon/android-icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrivacyDevel/whoogle-search/main/app/static/img/favicon/android-icon-48x48.png -------------------------------------------------------------------------------- /app/static/img/favicon/android-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrivacyDevel/whoogle-search/main/app/static/img/favicon/android-icon-72x72.png -------------------------------------------------------------------------------- /app/static/img/favicon/android-icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrivacyDevel/whoogle-search/main/app/static/img/favicon/android-icon-96x96.png -------------------------------------------------------------------------------- /app/static/img/favicon/apple-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrivacyDevel/whoogle-search/main/app/static/img/favicon/apple-icon-114x114.png -------------------------------------------------------------------------------- /app/static/img/favicon/apple-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrivacyDevel/whoogle-search/main/app/static/img/favicon/apple-icon-120x120.png -------------------------------------------------------------------------------- /app/static/img/favicon/apple-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrivacyDevel/whoogle-search/main/app/static/img/favicon/apple-icon-144x144.png -------------------------------------------------------------------------------- /app/static/img/favicon/apple-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrivacyDevel/whoogle-search/main/app/static/img/favicon/apple-icon-152x152.png -------------------------------------------------------------------------------- /app/static/img/favicon/apple-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrivacyDevel/whoogle-search/main/app/static/img/favicon/apple-icon-180x180.png -------------------------------------------------------------------------------- /app/static/img/favicon/apple-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrivacyDevel/whoogle-search/main/app/static/img/favicon/apple-icon-57x57.png -------------------------------------------------------------------------------- /app/static/img/favicon/apple-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrivacyDevel/whoogle-search/main/app/static/img/favicon/apple-icon-60x60.png -------------------------------------------------------------------------------- /app/static/img/favicon/apple-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrivacyDevel/whoogle-search/main/app/static/img/favicon/apple-icon-72x72.png -------------------------------------------------------------------------------- /app/static/img/favicon/apple-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrivacyDevel/whoogle-search/main/app/static/img/favicon/apple-icon-76x76.png -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | graft app/static 2 | graft app/templates 3 | graft app/misc 4 | include requirements.txt 5 | recursive-include test 6 | global-exclude *.pyc 7 | -------------------------------------------------------------------------------- /app/static/img/favicon/android-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrivacyDevel/whoogle-search/main/app/static/img/favicon/android-icon-144x144.png -------------------------------------------------------------------------------- /app/static/img/favicon/android-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrivacyDevel/whoogle-search/main/app/static/img/favicon/android-icon-192x192.png -------------------------------------------------------------------------------- /app/static/img/favicon/apple-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrivacyDevel/whoogle-search/main/app/static/img/favicon/apple-icon-precomposed.png -------------------------------------------------------------------------------- /app/static/css/error.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-size: 1.3rem; 3 | } 4 | 5 | @media (max-width: 1000px) { 6 | html { 7 | font-size: 3rem; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /app/version.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | optional_dev_tag = '' 4 | if os.getenv('DEV_BUILD'): 5 | optional_dev_tag = '.dev' + os.getenv('DEV_BUILD') 6 | 7 | __version__ = '0.8.0' + optional_dev_tag 8 | -------------------------------------------------------------------------------- /.replit: -------------------------------------------------------------------------------- 1 | language = "bash" 2 | run = "killall -q python3 > /dev/null 2>&1; pip install -r requirements.txt && ./run" 3 | onBoot = "killall -q python3 > /dev/null 2>&1; pip install -r requirements.txt && ./run" 4 | -------------------------------------------------------------------------------- /app/static/css/logo.css: -------------------------------------------------------------------------------- 1 | .cls-1 { 2 | fill: transparent; 3 | } 4 | 5 | svg { 6 | height: inherit; 7 | } 8 | 9 | a { 10 | height: inherit; 11 | } 12 | 13 | @media (max-width: 1000px) { 14 | svg { 15 | margin-top: .3em; 16 | height: 70%; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | venv/ 2 | .idea/ 3 | __pycache__/ 4 | *.pyc 5 | *.pem 6 | *.conf 7 | *.key 8 | config.json 9 | test/static 10 | flask_session/ 11 | app/static/config 12 | app/static/custom_config 13 | app/static/bangs 14 | 15 | # pip stuff 16 | /build/ 17 | dist/ 18 | *.egg-info/ 19 | 20 | # env 21 | whoogle.env 22 | -------------------------------------------------------------------------------- /app/static/img/favicon/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | #ffffff -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Ask a (simple) question about Whoogle 4 | title: "[QUESTION] " 5 | labels: question 6 | assignees: '' 7 | 8 | --- 9 | 10 | Type out your question here. Please make sure that this is a topic that isn't already covered in the README. 11 | -------------------------------------------------------------------------------- /misc/tor/torrc: -------------------------------------------------------------------------------- 1 | DataDirectory /var/lib/tor 2 | ControlPort 9051 3 | CookieAuthentication 1 4 | DataDirectoryGroupReadable 1 5 | CookieAuthFileGroupReadable 1 6 | ExtORPortCookieAuthFileGroupReadable 1 7 | CacheDirectoryGroupReadable 1 8 | CookieAuthFile /var/lib/tor/control_auth_cookie 9 | Log debug-notice file /dev/null 10 | -------------------------------------------------------------------------------- /misc/instances.txt: -------------------------------------------------------------------------------- 1 | https://search.albony.xyz 2 | https://search.garudalinux.org 3 | https://search.dr460nf1r3.org 4 | https://s.tokhmi.xyz 5 | https://search.sethforprivacy.com 6 | https://whoogle.dcs0.hu 7 | https://whoogle.esmailelbob.xyz 8 | https://gowogle.voring.me 9 | https://whoogle.privacydev.net 10 | https://wg.vern.cc 11 | https://www.indexia.gq 12 | -------------------------------------------------------------------------------- /app/templates/footer.html: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /misc/tor/start-tor.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ "$WHOOGLE_TOR_SERVICE" == "0" ]; then 4 | echo "Skipping Tor startup..." 5 | exit 0 6 | fi 7 | 8 | if [ "$(whoami)" != "root" ]; then 9 | tor -f /etc/tor/torrc 10 | else 11 | if (grep alpine /etc/os-release >/dev/null); then 12 | rc-service tor start 13 | else 14 | service tor start 15 | fi 16 | fi 17 | -------------------------------------------------------------------------------- /charts/whoogle/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "whoogle.serviceAccountName" . }} 6 | labels: 7 | {{- include "whoogle.labels" . | nindent 4 }} 8 | {{- with .Values.serviceAccount.annotations }} 9 | annotations: 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | {{- end }} 13 | -------------------------------------------------------------------------------- /app/static/js/currency.js: -------------------------------------------------------------------------------- 1 | const convert = (n1, n2, conversionFactor) => { 2 | // id's for currency input boxes 3 | let id1 = "cb" + n1; 4 | let id2 = "cb" + n2; 5 | // getting the value of the input box that just got filled 6 | let inputBox = document.getElementById(id1).value; 7 | // updating the other input box after conversion 8 | document.getElementById(id2).value = ((inputBox * conversionFactor).toFixed(2)); 9 | } 10 | -------------------------------------------------------------------------------- /charts/whoogle/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "whoogle.fullname" . }} 5 | labels: 6 | {{- include "whoogle.labels" . | nindent 4 }} 7 | spec: 8 | type: {{ .Values.service.type }} 9 | ports: 10 | - port: {{ .Values.service.port }} 11 | targetPort: http 12 | protocol: TCP 13 | name: http 14 | selector: 15 | {{- include "whoogle.selectorLabels" . | nindent 4 }} 16 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - name: Set up Python 11 | uses: actions/setup-python@v2 12 | with: 13 | python-version: '3.x' 14 | - name: Install dependencies 15 | run: pip install --upgrade pip && pip install -r requirements.txt 16 | - name: Run tests 17 | run: ./run test 18 | -------------------------------------------------------------------------------- /charts/whoogle/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /charts/whoogle/templates/tests/test-connection.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: "{{ include "whoogle.fullname" . }}-test-connection" 5 | labels: 6 | {{- include "whoogle.labels" . | nindent 4 }} 7 | annotations: 8 | "helm.sh/hook": test 9 | spec: 10 | containers: 11 | - name: wget 12 | image: busybox 13 | command: ['wget'] 14 | args: ['{{ include "whoogle.fullname" . }}:{{ .Values.service.port }}'] 15 | restartPolicy: Never 16 | -------------------------------------------------------------------------------- /app/templates/search.html: -------------------------------------------------------------------------------- 1 |
2 | 13 | 14 |
15 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | github: benbusby 3 | ko_fi: benbusby 4 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 5 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 6 | liberapay: # Replace with a single Liberapay username 7 | issuehunt: # Replace with a single IssueHunt username 8 | otechie: # Replace with a single Otechie username 9 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 10 | -------------------------------------------------------------------------------- /.github/workflows/scan.yml: -------------------------------------------------------------------------------- 1 | name: scan 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | 7 | jobs: 8 | scan: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Build the container image 13 | run: | 14 | docker build --tag whoogle-search:test . 15 | - name: Initiate grype scan 16 | run: | 17 | curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b . 18 | chmod +x ./grype 19 | ./grype whoogle-search:test --only-fixed 20 | -------------------------------------------------------------------------------- /test/test_autocomplete.py: -------------------------------------------------------------------------------- 1 | from app.models.endpoint import Endpoint 2 | 3 | 4 | def test_autocomplete_get(client): 5 | rv = client.get(f'/{Endpoint.autocomplete}?q=green+eggs+and') 6 | assert rv._status_code == 200 7 | assert len(rv.data) >= 1 8 | assert b'green eggs and ham' in rv.data 9 | 10 | 11 | def test_autocomplete_post(client): 12 | rv = client.post(f'/{Endpoint.autocomplete}', 13 | data=dict(q='the+cat+in+the')) 14 | assert rv._status_code == 200 15 | assert len(rv.data) >= 1 16 | assert b'the cat in the hat' in rv.data 17 | -------------------------------------------------------------------------------- /app/models/endpoint.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class Endpoint(Enum): 5 | autocomplete = 'autocomplete' 6 | home = 'home' 7 | healthz = 'healthz' 8 | config = 'config' 9 | opensearch = 'opensearch.xml' 10 | search = 'search' 11 | search_html = 'search.html' 12 | url = 'url' 13 | imgres = 'imgres' 14 | element = 'element' 15 | window = 'window' 16 | 17 | def __str__(self): 18 | return self.value 19 | 20 | def in_path(self, path: str) -> bool: 21 | return path.startswith(self.value) or \ 22 | path.startswith(f'/{self.value}') 23 | -------------------------------------------------------------------------------- /charts/whoogle/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: whoogle 3 | description: A self hosted search engine on Kubernetes 4 | type: application 5 | version: 0.1.0 6 | appVersion: 0.8.0 7 | 8 | icon: https://github.com/benbusby/whoogle-search/raw/main/app/static/img/favicon/favicon-96x96.png 9 | 10 | sources: 11 | - https://github.com/benbusby/whoogle-search 12 | - https://gitlab.com/benbusby/whoogle-search 13 | - https://gogs.benbusby.com/benbusby/whoogle-search 14 | 15 | keywords: 16 | - whoogle 17 | - degoogle 18 | - search 19 | - google 20 | - search-engine 21 | - privacy 22 | - tor 23 | - python 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest a feature that would improve Whoogle 4 | title: "[FEATURE] " 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | 15 | 16 | **Describe the feature you'd like to see added** 17 | A short description of the feature, and what it would accomplish. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | attrs==19.3.0 2 | beautifulsoup4==4.10.0 3 | brotli==1.0.9 4 | cachelib==0.4.1 5 | certifi==2020.4.5.1 6 | cffi==1.15.0 7 | chardet==3.0.4 8 | click==8.0.3 9 | cryptography==3.3.2 10 | cssutils==2.4.0 11 | defusedxml==0.7.1 12 | Flask==1.1.1 13 | idna==2.9 14 | itsdangerous==1.1.0 15 | Jinja2==2.11.3 16 | MarkupSafe==1.1.1 17 | more-itertools==8.3.0 18 | packaging==20.4 19 | pluggy==0.13.1 20 | pycodestyle==2.6.0 21 | pycparser==2.21 22 | pyOpenSSL==19.1.0 23 | pyparsing==2.4.7 24 | PySocks==1.7.1 25 | pytest==7.2.0 26 | python-dateutil==2.8.1 27 | requests==2.25.1 28 | soupsieve==1.9.5 29 | stem==1.8.0 30 | urllib3==1.26.5 31 | waitress==2.1.2 32 | wcwidth==0.1.9 33 | Werkzeug==0.16.0 34 | python-dotenv==0.16.0 35 | -------------------------------------------------------------------------------- /app/static/settings/header_tabs.json: -------------------------------------------------------------------------------- 1 | { 2 | "all": { 3 | "tbm": null, 4 | "href": "search?q={query}", 5 | "name": "All", 6 | "selected": true 7 | }, 8 | "images": { 9 | "tbm": "isch", 10 | "href": "search?q={query}", 11 | "name": "Images", 12 | "selected": false 13 | }, 14 | "maps": { 15 | "tbm": null, 16 | "href": "https://maps.google.com/maps?q={query}", 17 | "name": "Maps", 18 | "selected": false 19 | }, 20 | "videos": { 21 | "tbm": "vid", 22 | "href": "search?q={query}", 23 | "name": "Videos", 24 | "selected": false 25 | }, 26 | "news": { 27 | "tbm": "nws", 28 | "href": "search?q={query}", 29 | "name": "News", 30 | "selected": false 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/conftest.py: -------------------------------------------------------------------------------- 1 | from app import app 2 | from app.utils.session import generate_user_key 3 | import pytest 4 | import random 5 | 6 | demo_config = { 7 | 'near': random.choice(['Seattle', 'New York', 'San Francisco']), 8 | 'dark': str(random.getrandbits(1)), 9 | 'nojs': str(random.getrandbits(1)), 10 | 'lang_interface': random.choice(app.config['LANGUAGES'])['value'], 11 | 'lang_search': random.choice(app.config['LANGUAGES'])['value'], 12 | 'country': random.choice(app.config['COUNTRIES'])['value'] 13 | } 14 | 15 | 16 | @pytest.fixture 17 | def client(): 18 | with app.test_client() as client: 19 | with client.session_transaction() as session: 20 | session['uuid'] = 'test' 21 | session['key'] = generate_user_key() 22 | session['config'] = {} 23 | yield client 24 | -------------------------------------------------------------------------------- /.github/workflows/docker_tests.yml: -------------------------------------------------------------------------------- 1 | name: docker_tests 2 | 3 | on: 4 | push: 5 | branches: main 6 | pull_request: 7 | branches: main 8 | 9 | jobs: 10 | docker: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: checkout code 14 | uses: actions/checkout@v2 15 | - name: build and test (docker) 16 | run: | 17 | docker build --tag whoogle-search:test . 18 | docker run --publish 5000:5000 --detach --name whoogle-search-nocompose whoogle-search:test 19 | sleep 15 20 | docker exec whoogle-search-nocompose curl -f http://localhost:5000/healthz || exit 1 21 | - name: build and test (docker-compose) 22 | run: | 23 | docker rm -f whoogle-search-nocompose 24 | WHOOGLE_IMAGE="whoogle-search:test" docker-compose up --detach 25 | sleep 15 26 | docker exec whoogle-search curl -f http://localhost:5000/healthz || exit 1 27 | -------------------------------------------------------------------------------- /app/static/css/input.css: -------------------------------------------------------------------------------- 1 | #search-bar { 2 | background: transparent !important; 3 | padding-right: 50px; 4 | } 5 | 6 | #search-reset { 7 | all: unset; 8 | margin-left: -50px; 9 | text-align: center; 10 | background-color: transparent !important; 11 | cursor: pointer; 12 | height: 40px; 13 | width: 50px; 14 | } 15 | .ZINbbc.xpd.O9g5cc.uUPGi input::-webkit-outer-spin-button, 16 | input::-webkit-inner-spin-button { 17 | -webkit-appearance: none; 18 | margin: 0; 19 | } 20 | 21 | .cb { 22 | width: 40%; 23 | overflow: hidden; 24 | text-align: left; 25 | line-height: 28px; 26 | background: transparent; 27 | border-radius: 6px; 28 | border: 1px solid #5f6368; 29 | font-size: 14px !important; 30 | height: 36px; 31 | padding: 0 0 0 12px; 32 | margin: 10px 10px 10px 0; 33 | } 34 | 35 | .conversion_box { 36 | margin-top: 15px; 37 | } 38 | 39 | .ZINbbc.xpd.O9g5cc.uUPGi input:focus-visible { 40 | outline: 0; 41 | } 42 | -------------------------------------------------------------------------------- /misc/heroku-regen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Assumes this is being executed from a session that has already logged 3 | # into Heroku with "heroku login -i" beforehand. 4 | # 5 | # You can set this up to run every night when you aren't using the 6 | # instance with a cronjob. For example: 7 | # 0 3 * * * /home/pi/whoogle-search/config/heroku-regen.sh 8 | 9 | HEROKU_CLI_SITE="https://devcenter.heroku.com/articles/heroku-cli" 10 | 11 | if ! [[ -x "$(command -v heroku)" ]]; then 12 | echo "Must have heroku cli installed: $HEROKU_CLI_SITE" 13 | exit 1 14 | fi 15 | 16 | cd "$(builtin cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)/../" 17 | 18 | if [[ $# -ne 1 ]]; then 19 | echo -e "Must provide the name of the Whoogle instance to regenerate" 20 | exit 1 21 | fi 22 | 23 | APP_NAME="$1" 24 | 25 | heroku apps:destroy "$APP_NAME" --confirm "$APP_NAME" 26 | heroku apps:create "$APP_NAME" 27 | heroku container:login 28 | heroku container:push web 29 | heroku container:release web 30 | -------------------------------------------------------------------------------- /app/static/img/favicon/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Whoogle Search", 3 | "short_name": "Whoogle", 4 | "display": "fullscreen", 5 | "scope": "/", 6 | "icons": [ 7 | { 8 | "src": "android-icon-36x36.png", 9 | "sizes": "36x36", 10 | "type": "image\/png", 11 | "density": "0.75" 12 | }, 13 | { 14 | "src": "android-icon-48x48.png", 15 | "sizes": "48x48", 16 | "type": "image\/png", 17 | "density": "1.0" 18 | }, 19 | { 20 | "src": "android-icon-72x72.png", 21 | "sizes": "72x72", 22 | "type": "image\/png", 23 | "density": "1.5" 24 | }, 25 | { 26 | "src": "android-icon-96x96.png", 27 | "sizes": "96x96", 28 | "type": "image\/png", 29 | "density": "2.0" 30 | }, 31 | { 32 | "src": "android-icon-144x144.png", 33 | "sizes": "144x144", 34 | "type": "image\/png", 35 | "density": "3.0" 36 | }, 37 | { 38 | "src": "android-icon-192x192.png", 39 | "sizes": "192x192", 40 | "type": "image\/png", 41 | "density": "4.0" 42 | } 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /.github/workflows/docker_main.yml: -------------------------------------------------------------------------------- 1 | name: docker_main 2 | 3 | on: 4 | workflow_run: 5 | workflows: ["tests"] 6 | branches: [main] 7 | types: 8 | - completed 9 | 10 | # TODO: Needs refactoring to use reusable workflows and share w/ docker_tests 11 | jobs: 12 | on-success: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: checkout code 16 | uses: actions/checkout@v2 17 | - name: build and test (docker) 18 | run: | 19 | docker build --tag whoogle-search:test . 20 | docker run --publish 5000:5000 --detach --name whoogle-search-nocompose whoogle-search:test 21 | sleep 15 22 | docker exec whoogle-search-nocompose curl -f http://localhost:5000/healthz || exit 1 23 | - name: build and test (docker-compose) 24 | run: | 25 | docker rm -f whoogle-search-nocompose 26 | WHOOGLE_IMAGE="whoogle-search:test" docker-compose up --detach 27 | sleep 15 28 | docker exec whoogle-search curl -f http://localhost:5000/healthz || exit 1 29 | -------------------------------------------------------------------------------- /run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Usage: 3 | # ./run # Runs the full web app 4 | # ./run test # Runs the testing suite 5 | 6 | set -e 7 | 8 | SCRIPT_DIR="$(CDPATH= command cd -- "$(dirname -- "$0")" && pwd -P)" 9 | 10 | # Set directory to serve static content from 11 | SUBDIR="${1:-app}" 12 | export APP_ROOT="$SCRIPT_DIR/$SUBDIR" 13 | export STATIC_FOLDER="$APP_ROOT/static" 14 | 15 | # Clear out build directory 16 | rm -f "$SCRIPT_DIR"/app/static/build/*.js 17 | rm -f "$SCRIPT_DIR"/app/static/build/*.css 18 | 19 | # Check for regular vs test run 20 | if [ "$SUBDIR" = "test" ]; then 21 | # Set up static files for testing 22 | rm -rf "$STATIC_FOLDER" 23 | ln -s "$SCRIPT_DIR/app/static" "$STATIC_FOLDER" 24 | pytest -sv 25 | else 26 | mkdir -p "$STATIC_FOLDER" 27 | 28 | if [ ! -z "$UNIX_SOCKET" ]; then 29 | python3 -um app \ 30 | --unix-socket "$UNIX_SOCKET" 31 | else 32 | python3 -um app \ 33 | --host "${ADDRESS:-0.0.0.0}" \ 34 | --port "${PORT:-"${EXPOSE_PORT:-5000}"}" 35 | fi 36 | fi 37 | -------------------------------------------------------------------------------- /charts/whoogle/templates/hpa.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.autoscaling.enabled }} 2 | apiVersion: autoscaling/v2beta1 3 | kind: HorizontalPodAutoscaler 4 | metadata: 5 | name: {{ include "whoogle.fullname" . }} 6 | labels: 7 | {{- include "whoogle.labels" . | nindent 4 }} 8 | spec: 9 | scaleTargetRef: 10 | apiVersion: apps/v1 11 | kind: Deployment 12 | name: {{ include "whoogle.fullname" . }} 13 | minReplicas: {{ .Values.autoscaling.minReplicas }} 14 | maxReplicas: {{ .Values.autoscaling.maxReplicas }} 15 | metrics: 16 | {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} 17 | - type: Resource 18 | resource: 19 | name: cpu 20 | targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} 21 | {{- end }} 22 | {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} 23 | - type: Resource 24 | resource: 25 | name: memory 26 | targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} 27 | {{- end }} 28 | {{- end }} 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Ben Busby 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = whoogle-search 3 | version = attr: app.version.__version__ 4 | url = https://github.com/benbusby/whoogle-search 5 | description = Self-hosted, ad-free, privacy-respecting metasearch engine 6 | long_description = file: README.md 7 | long_description_content_type = text/markdown 8 | keywords = search, metasearch, flask, adblock, degoogle, privacy 9 | author = Ben Busby 10 | author_email = contact@benbusby.com 11 | license = MIT 12 | classifiers = 13 | Programming Language :: Python :: 3 14 | License :: OSI Approved :: MIT License 15 | Operating System :: OS Independent 16 | 17 | [options] 18 | packages = find: 19 | include_package_data = True 20 | install_requires= 21 | beautifulsoup4 22 | brotli 23 | cssutils 24 | cryptography 25 | defusedxml 26 | Flask 27 | python-dotenv 28 | requests 29 | stem 30 | waitress 31 | 32 | [options.extras_require] 33 | test = 34 | pytest 35 | python-dateutil 36 | dev = pycodestyle 37 | 38 | [options.packages.find] 39 | exclude = 40 | test* 41 | 42 | [options.entry_points] 43 | console_scripts = 44 | whoogle-search = app.routes:run_app 45 | -------------------------------------------------------------------------------- /app/utils/session.py: -------------------------------------------------------------------------------- 1 | from cryptography.fernet import Fernet 2 | from flask import current_app as app 3 | 4 | REQUIRED_SESSION_VALUES = ['uuid', 'config', 'key'] 5 | 6 | 7 | def generate_user_key() -> bytes: 8 | """Generates a key for encrypting searches and element URLs 9 | 10 | Args: 11 | cookies_disabled: Flag for whether or not cookies are disabled by the 12 | user. If so, the user can only use the default key 13 | generated on app init for queries. 14 | 15 | Returns: 16 | str: A unique Fernet key 17 | 18 | """ 19 | # Generate/regenerate unique key per user 20 | return Fernet.generate_key() 21 | 22 | 23 | def valid_user_session(session: dict) -> bool: 24 | """Validates the current user session 25 | 26 | Args: 27 | session: The current Flask user session 28 | 29 | Returns: 30 | bool: True/False indicating that all required session values are 31 | available 32 | 33 | """ 34 | # Generate secret key for user if unavailable 35 | for value in REQUIRED_SESSION_VALUES: 36 | if value not in session: 37 | return False 38 | 39 | return True 40 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a bug report to help fix an issue with Whoogle 4 | title: "[BUG] " 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Deployment Method** 21 | - [ ] Heroku (one-click deploy) 22 | - [ ] Docker 23 | - [ ] `run` executable 24 | - [ ] pip/pipx 25 | - [ ] Other: [describe setup] 26 | 27 | **Version of Whoogle Search** 28 | - [ ] Latest build from [source] (i.e. GitHub, Docker Hub, pip, etc) 29 | - [ ] Version [version number] 30 | - [ ] Not sure 31 | 32 | 33 | **Desktop (please complete the following information):** 34 | - OS: [e.g. iOS] 35 | - Browser [e.g. chrome, safari] 36 | - Version [e.g. 22] 37 | 38 | **Smartphone (please complete the following information):** 39 | - Device: [e.g. iPhone6] 40 | - OS: [e.g. iOS8.1] 41 | - Browser [e.g. stock browser, safari] 42 | - Version [e.g. 22] 43 | 44 | **Additional context** 45 | Add any other context about the problem here. 46 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/new-theme.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: New theme 3 | about: Create a new theme for Whoogle 4 | title: "[THEME] " 5 | labels: theme 6 | assignees: benbusby 7 | 8 | --- 9 | 10 | Use the following template to design your theme, replacing the blank spaces with the colors of your choice. 11 | 12 | ```css 13 | :root { 14 | /* LIGHT THEME COLORS */ 15 | --whoogle-logo: #______; 16 | --whoogle-page-bg: #______; 17 | --whoogle-element-bg: #______; 18 | --whoogle-text: #______; 19 | --whoogle-contrast-text: #______; 20 | --whoogle-secondary-text: #______; 21 | --whoogle-result-bg: #______; 22 | --whoogle-result-title: #______; 23 | --whoogle-result-url: #______; 24 | --whoogle-result-visited: #______; 25 | 26 | /* DARK THEME COLORS */ 27 | --whoogle-dark-logo: #______; 28 | --whoogle-dark-page-bg: #______; 29 | --whoogle-dark-element-bg: #______; 30 | --whoogle-dark-text: #______; 31 | --whoogle-dark-contrast-text: #______; 32 | --whoogle-dark-secondary-text: #______; 33 | --whoogle-dark-result-bg: #______; 34 | --whoogle-dark-result-title: #______; 35 | --whoogle-dark-result-url: #______; 36 | --whoogle-dark-result-visited: #______; 37 | } 38 | ``` 39 | -------------------------------------------------------------------------------- /app/static/css/variables.css: -------------------------------------------------------------------------------- 1 | /* Colors */ 2 | :root { 3 | /* LIGHT THEME COLORS */ 4 | --whoogle-logo: #685e79; 5 | --whoogle-page-bg: #ffffff; 6 | --whoogle-element-bg: #4285f4; 7 | --whoogle-text: #000000; 8 | --whoogle-contrast-text: #ffffff; 9 | --whoogle-secondary-text: #70757a; 10 | --whoogle-result-bg: #ffffff; 11 | --whoogle-result-title: #1967d2; 12 | --whoogle-result-url: #0d652d; 13 | --whoogle-result-visited: #4b11a8; 14 | 15 | /* DARK THEME COLORS */ 16 | --whoogle-dark-logo: #685e79; 17 | --whoogle-dark-page-bg: #101020; 18 | --whoogle-dark-element-bg: #4285f4; 19 | --whoogle-dark-text: #ffffff; 20 | --whoogle-dark-contrast-text: #ffffff; 21 | --whoogle-dark-secondary-text: #bbbbbb; 22 | --whoogle-dark-result-bg: #212131; 23 | --whoogle-dark-result-title: #64a7f6; 24 | --whoogle-dark-result-url: #34a853; 25 | --whoogle-dark-result-visited: #bbbbff; 26 | } 27 | 28 | #whoogle-w { 29 | fill: #4285f4; 30 | } 31 | 32 | #whoogle-h { 33 | fill: #ea4335; 34 | } 35 | 36 | #whoogle-o-1 { 37 | fill: #fbbc05; 38 | } 39 | 40 | #whoogle-o-2 { 41 | fill: #4285f4; 42 | } 43 | 44 | #whoogle-g { 45 | fill: #34a853; 46 | } 47 | 48 | #whoogle-l { 49 | fill: #ea4335; 50 | } 51 | 52 | #whoogle-e { 53 | fill: #fbbc05; 54 | } 55 | -------------------------------------------------------------------------------- /app/static/css/search.css: -------------------------------------------------------------------------------- 1 | body { 2 | display: block !important; 3 | margin: auto !important; 4 | } 5 | 6 | .vvjwJb { 7 | font-size: 16px !important; 8 | } 9 | 10 | .autocomplete { 11 | position: relative; 12 | display: inline-block; 13 | width: 100%; 14 | } 15 | 16 | .autocomplete-items { 17 | position: absolute; 18 | border-bottom: none; 19 | border-top: none; 20 | z-index: 99; 21 | 22 | /*position the autocomplete items to be the same width as the container:*/ 23 | top: 100%; 24 | left: 0; 25 | right: 0; 26 | } 27 | 28 | .autocomplete-items div { 29 | padding: 10px; 30 | cursor: pointer; 31 | } 32 | 33 | details summary { 34 | margin-bottom: 20px; 35 | font-weight: bold; 36 | padding-left: 10px; 37 | } 38 | 39 | details summary span { 40 | font-weight: normal; 41 | } 42 | 43 | #lingva-iframe { 44 | width: 100%; 45 | height: 650px; 46 | border: 0; 47 | } 48 | 49 | .ip-address-div { 50 | padding-bottom: 0 !important; 51 | } 52 | 53 | .ip-text-div { 54 | padding-top: 0 !important; 55 | } 56 | 57 | .footer { 58 | text-align: center; 59 | } 60 | 61 | @media (min-width: 801px) { 62 | body { 63 | min-width: 736px !important; 64 | } 65 | } 66 | 67 | @media (max-width: 801px) { 68 | details summary { 69 | margin-bottom: 10px !important 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /app/templates/error.html: -------------------------------------------------------------------------------- 1 | {% if config.theme %} 2 | {% if config.theme == 'system' %} 3 | 7 | {% else %} 8 | 9 | {% endif %} 10 | {% else %} 11 | 12 | {% endif %} 13 | 14 | 15 | 16 |
17 |

Error

18 |

19 | {{ error_message }} 20 |

21 |
22 |

23 | {% if blocked is defined %} 24 |

{{ translation['continue-search'] }}

25 | Whoogle: 26 |
27 | 28 | {{farside}}/whoogle/search?q={{query}} 29 | 30 |

31 | Searx: 32 |
33 | 34 | {{farside}}/searx/search?q={{query}} 35 | 36 |
37 | {% endif %} 38 |

39 | Return Home 40 |
41 | -------------------------------------------------------------------------------- /app/models/g_classes.py: -------------------------------------------------------------------------------- 1 | from bs4 import BeautifulSoup 2 | 3 | 4 | class GClasses: 5 | """A class for tracking obfuscated class names used in Google results that 6 | are directly referenced in Whoogle's filtering code. 7 | 8 | Note: Using these should be a last resort. It is always preferred to filter 9 | results using structural cues instead of referencing class names, as these 10 | are liable to change at any moment. 11 | """ 12 | main_tbm_tab = 'KP7LCb' 13 | images_tbm_tab = 'n692Zd' 14 | footer = 'TuS8Ad' 15 | result_class_a = 'ZINbbc' 16 | result_class_b = 'luh4td' 17 | 18 | result_classes = { 19 | result_class_a: ['Gx5Zad'], 20 | result_class_b: ['fP1Qef'] 21 | } 22 | 23 | @classmethod 24 | def replace_css_classes(cls, soup: BeautifulSoup) -> BeautifulSoup: 25 | """Replace updated Google classes with the original class names that 26 | Whoogle relies on for styling. 27 | 28 | Args: 29 | soup: The result page as a BeautifulSoup object 30 | 31 | Returns: 32 | BeautifulSoup: The new BeautifulSoup 33 | """ 34 | result_divs = soup.find_all('div', { 35 | 'class': [_ for c in cls.result_classes.values() for _ in c] 36 | }) 37 | 38 | for div in result_divs: 39 | new_class = ' '.join(div['class']) 40 | for key, val in cls.result_classes.items(): 41 | new_class = ' '.join(new_class.replace(_, key) for _ in val) 42 | div['class'] = new_class.split(' ') 43 | return soup 44 | 45 | def __str__(self): 46 | return self.value 47 | -------------------------------------------------------------------------------- /app/static/js/keyboard.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | let searchBar, results; 3 | let shift = false; 4 | const keymap = { 5 | ArrowUp: goUp, 6 | ArrowDown: goDown, 7 | ShiftTab: goUp, 8 | Tab: goDown, 9 | k: goUp, 10 | j: goDown, 11 | '/': focusSearch, 12 | }; 13 | let activeIdx = -1; 14 | 15 | document.addEventListener('DOMContentLoaded', () => { 16 | searchBar = document.querySelector('#search-bar'); 17 | results = document.querySelectorAll('#main>div>div>div>a'); 18 | }); 19 | 20 | document.addEventListener('keydown', (e) => { 21 | if (e.key === 'Shift') { 22 | shift = true; 23 | } 24 | 25 | if (e.target.tagName === 'INPUT') return true; 26 | if (typeof keymap[e.key] === 'function') { 27 | e.preventDefault(); 28 | 29 | keymap[`${shift && e.key == 'Tab' ? 'Shift' : ''}${e.key}`](); 30 | } 31 | }); 32 | 33 | document.addEventListener('keyup', (e) => { 34 | if (e.key === 'Shift') { 35 | shift = false; 36 | } 37 | }); 38 | 39 | function goUp () { 40 | if (activeIdx > 0) focusResult(activeIdx - 1); 41 | else focusSearch(); 42 | } 43 | 44 | function goDown () { 45 | if (activeIdx < results.length - 1) focusResult(activeIdx + 1); 46 | } 47 | 48 | function focusResult (idx) { 49 | activeIdx = idx; 50 | results[activeIdx].scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' }); 51 | results[activeIdx].focus(); 52 | } 53 | 54 | function focusSearch () { 55 | activeIdx = -1; 56 | searchBar.focus(); 57 | } 58 | }()); 59 | -------------------------------------------------------------------------------- /app/static/js/header.js: -------------------------------------------------------------------------------- 1 | document.addEventListener("DOMContentLoaded", () => { 2 | const advSearchToggle = document.getElementById("adv-search-toggle"); 3 | const advSearchDiv = document.getElementById("adv-search-div"); 4 | const searchBar = document.getElementById("search-bar"); 5 | const countrySelect = document.getElementById("result-country"); 6 | const arrowKeys = [37, 38, 39, 40]; 7 | let searchValue = searchBar.value; 8 | 9 | countrySelect.onchange = () => { 10 | let str = window.location.href; 11 | n = str.lastIndexOf("/search"); 12 | if (n > 0) { 13 | str = str.substring(0, n) + 14 | `/search?q=${searchBar.value}&country=${countrySelect.value}`; 15 | window.location.href = str; 16 | } 17 | } 18 | 19 | const toggleAdvancedSearch = on => { 20 | if (on) { 21 | advSearchDiv.style.maxHeight = "70px"; 22 | } else { 23 | advSearchDiv.style.maxHeight = "0px"; 24 | } 25 | localStorage.advSearchToggled = on; 26 | } 27 | 28 | try { 29 | toggleAdvancedSearch(JSON.parse(localStorage.advSearchToggled)); 30 | } catch (error) { 31 | console.warn("Did not recover advanced search toggle state"); 32 | } 33 | 34 | advSearchToggle.onclick = () => { 35 | toggleAdvancedSearch(advSearchToggle.checked); 36 | } 37 | 38 | searchBar.addEventListener("keyup", function(event) { 39 | if (event.keyCode === 13) { 40 | document.getElementById("search-form").submit(); 41 | } else if (searchBar.value !== searchValue && !arrowKeys.includes(event.keyCode)) { 42 | searchValue = searchBar.value; 43 | handleUserInput(); 44 | } 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /charts/whoogle/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | 1. Get the application URL by running these commands: 2 | {{- if .Values.ingress.enabled }} 3 | {{- range $host := .Values.ingress.hosts }} 4 | {{- range .paths }} 5 | http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} 6 | {{- end }} 7 | {{- end }} 8 | {{- else if contains "NodePort" .Values.service.type }} 9 | export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "whoogle.fullname" . }}) 10 | export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") 11 | echo http://$NODE_IP:$NODE_PORT 12 | {{- else if contains "LoadBalancer" .Values.service.type }} 13 | NOTE: It may take a few minutes for the LoadBalancer IP to be available. 14 | You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "whoogle.fullname" . }}' 15 | export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "whoogle.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") 16 | echo http://$SERVICE_IP:{{ .Values.service.port }} 17 | {{- else if contains "ClusterIP" .Values.service.type }} 18 | export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "whoogle.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") 19 | export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") 20 | echo "Visit http://127.0.0.1:8080 to use your application" 21 | kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT 22 | {{- end }} 23 | -------------------------------------------------------------------------------- /.github/workflows/pypi.yml: -------------------------------------------------------------------------------- 1 | name: pypi 2 | 3 | on: 4 | push: 5 | branches: main 6 | tags: v* 7 | 8 | jobs: 9 | publish-test: 10 | name: Build and publish to TestPyPI 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Set up Python 3.9 15 | uses: actions/setup-python@v1 16 | with: 17 | python-version: 3.9 18 | - name: Install pypa/build 19 | run: >- 20 | python -m 21 | pip install 22 | build 23 | setuptools 24 | --user 25 | - name: Set dev timestamp 26 | run: echo "DEV_BUILD=$(date +%s)" >> $GITHUB_ENV 27 | - name: Build binary wheel and source tarball 28 | run: >- 29 | python -m 30 | build 31 | --sdist 32 | --wheel 33 | --outdir dist/ 34 | . 35 | - name: Publish distribution to TestPyPI 36 | uses: pypa/gh-action-pypi-publish@master 37 | with: 38 | password: ${{ secrets.TEST_PYPI_API_TOKEN }} 39 | repository_url: https://test.pypi.org/legacy/ 40 | publish: 41 | name: Build and publish to PyPI 42 | runs-on: ubuntu-latest 43 | steps: 44 | - uses: actions/checkout@v2 45 | - name: Set up Python 3.9 46 | uses: actions/setup-python@v1 47 | with: 48 | python-version: 3.9 49 | - name: Install pypa/build 50 | run: >- 51 | python -m 52 | pip install 53 | build 54 | --user 55 | - name: Build binary wheel and source tarball 56 | run: >- 57 | python -m 58 | build 59 | --sdist 60 | --wheel 61 | --outdir dist/ 62 | . 63 | - name: Publish distribution to PyPI 64 | if: startsWith(github.ref, 'refs/tags') 65 | uses: pypa/gh-action-pypi-publish@master 66 | with: 67 | password: ${{ secrets.PYPI_API_TOKEN }} 68 | 69 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # can't use mem_limit in a 3.x docker-compose file in non swarm mode 2 | # see https://github.com/docker/compose/issues/4513 3 | version: "2.4" 4 | 5 | services: 6 | whoogle-search: 7 | image: ${WHOOGLE_IMAGE:-benbusby/whoogle-search} 8 | container_name: whoogle-search 9 | restart: unless-stopped 10 | pids_limit: 50 11 | mem_limit: 256mb 12 | memswap_limit: 256mb 13 | # user debian-tor from tor package 14 | user: whoogle 15 | security_opt: 16 | - no-new-privileges 17 | cap_drop: 18 | - ALL 19 | tmpfs: 20 | - /config/:size=10M,uid=927,gid=927,mode=1700 21 | - /var/lib/tor/:size=15M,uid=927,gid=927,mode=1700 22 | - /run/tor/:size=1M,uid=927,gid=927,mode=1700 23 | #environment: # Uncomment to configure environment variables 24 | # Basic auth configuration, uncomment to enable 25 | #- WHOOGLE_USER= 26 | #- WHOOGLE_PASS= 27 | # Proxy configuration, uncomment to enable 28 | #- WHOOGLE_PROXY_USER= 29 | #- WHOOGLE_PROXY_PASS= 30 | #- WHOOGLE_PROXY_TYPE= 32 | # Site alternative configurations, uncomment to enable 33 | # Note: If not set, the feature will still be available 34 | # with default values. 35 | #- WHOOGLE_ALT_TW=farside.link/nitter 36 | #- WHOOGLE_ALT_YT=farside.link/invidious 37 | #- WHOOGLE_ALT_IG=farside.link/bibliogram/u 38 | #- WHOOGLE_ALT_RD=farside.link/libreddit 39 | #- WHOOGLE_ALT_MD=farside.link/scribe 40 | #- WHOOGLE_ALT_TL=farside.link/lingva 41 | #- WHOOGLE_ALT_IMG=farside.link/rimgo 42 | #- WHOOGLE_ALT_WIKI=farside.link/wikiless 43 | #- WHOOGLE_ALT_IMDB=farside.link/libremdb 44 | #- WHOOGLE_ALT_QUORA=farside.link/quetre 45 | #env_file: # Alternatively, load variables from whoogle.env 46 | #- whoogle.env 47 | ports: 48 | - 5000:5000 49 | -------------------------------------------------------------------------------- /charts/whoogle/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "whoogle.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "whoogle.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "whoogle.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "whoogle.labels" -}} 37 | helm.sh/chart: {{ include "whoogle.chart" . }} 38 | {{ include "whoogle.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | {{- end }} 44 | 45 | {{/* 46 | Selector labels 47 | */}} 48 | {{- define "whoogle.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "whoogle.name" . }} 50 | app.kubernetes.io/instance: {{ .Release.Name }} 51 | {{- end }} 52 | 53 | {{/* 54 | Create the name of the service account to use 55 | */}} 56 | {{- define "whoogle.serviceAccountName" -}} 57 | {{- if .Values.serviceAccount.create }} 58 | {{- default (include "whoogle.fullname" .) .Values.serviceAccount.name }} 59 | {{- else }} 60 | {{- default "default" .Values.serviceAccount.name }} 61 | {{- end }} 62 | {{- end }} 63 | -------------------------------------------------------------------------------- /app/templates/display.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% if not search_type %} 6 | 7 | {% else %} 8 | 9 | {% endif %} 10 | 11 | 12 | 13 | 14 | 15 | 16 | {% if config.theme %} 17 | {% if config.theme == 'system' %} 18 | 22 | {% else %} 23 | 24 | {% endif %} 25 | {% else %} 26 | 27 | {% endif %} 28 | 29 | {{ clean_query(query) }} - Whoogle Search 30 | 31 | 32 | {{ search_header|safe }} 33 | {% if is_translation %} 34 | 38 | {% endif %} 39 | {{ response|safe }} 40 | 41 | {% include 'footer.html' %} 42 | {% if autocomplete_enabled == '1' %} 43 | 44 | {% endif %} 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /.github/workflows/buildx.yml: -------------------------------------------------------------------------------- 1 | name: buildx 2 | 3 | on: 4 | workflow_run: 5 | workflows: ["docker_main"] 6 | branches: [main] 7 | types: 8 | - completed 9 | push: 10 | tags: 11 | - '*' 12 | 13 | jobs: 14 | on-success: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Wait for tests to succeed 18 | if: ${{ github.event.workflow_run.conclusion != 'success' && startsWith(github.ref, 'refs/tags') != true }} 19 | run: exit 1 20 | - name: checkout code 21 | uses: actions/checkout@v2 22 | - name: install buildx 23 | id: buildx 24 | uses: crazy-max/ghaction-docker-buildx@v1 25 | with: 26 | version: latest 27 | - name: Login to Docker Hub 28 | uses: docker/login-action@v1 29 | with: 30 | username: ${{ secrets.DOCKER_USERNAME }} 31 | password: ${{ secrets.DOCKER_PASSWORD }} 32 | - name: Login to ghcr.io 33 | uses: docker/login-action@v1 34 | with: 35 | registry: ghcr.io 36 | username: ${{ github.actor }} 37 | password: ${{ secrets.GITHUB_TOKEN }} 38 | - name: build and push the image 39 | if: startsWith(github.ref, 'refs/heads/main') && github.actor == 'benbusby' 40 | run: | 41 | docker run --rm --privileged multiarch/qemu-user-static --reset -p yes 42 | docker buildx ls 43 | docker buildx build --push \ 44 | --tag benbusby/whoogle-search:latest \ 45 | --platform linux/amd64,linux/arm/v7,linux/arm64 . 46 | docker buildx build --push \ 47 | --tag ghcr.io/benbusby/whoogle-search:latest \ 48 | --platform linux/amd64,linux/arm/v7,linux/arm64 . 49 | - name: build and push tag 50 | if: startsWith(github.ref, 'refs/tags') 51 | run: | 52 | docker run --rm --privileged multiarch/qemu-user-static --reset -p yes 53 | docker buildx ls 54 | docker buildx build --push \ 55 | --tag benbusby/whoogle-search:${GITHUB_REF#refs/*/v}\ 56 | --platform linux/amd64,linux/arm/v7,linux/arm64 . 57 | docker buildx build --push \ 58 | --tag ghcr.io/benbusby/whoogle-search:${GITHUB_REF#refs/*/v}\ 59 | --platform linux/amd64,linux/arm/v7,linux/arm64 . 60 | -------------------------------------------------------------------------------- /charts/whoogle/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ingress.enabled -}} 2 | {{- $fullName := include "whoogle.fullname" . -}} 3 | {{- $svcPort := .Values.service.port -}} 4 | {{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} 5 | {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} 6 | {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} 7 | {{- end }} 8 | {{- end }} 9 | {{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} 10 | apiVersion: networking.k8s.io/v1 11 | {{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} 12 | apiVersion: networking.k8s.io/v1beta1 13 | {{- else -}} 14 | apiVersion: extensions/v1beta1 15 | {{- end }} 16 | kind: Ingress 17 | metadata: 18 | name: {{ $fullName }} 19 | labels: 20 | {{- include "whoogle.labels" . | nindent 4 }} 21 | {{- with .Values.ingress.annotations }} 22 | annotations: 23 | {{- toYaml . | nindent 4 }} 24 | {{- end }} 25 | spec: 26 | {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} 27 | ingressClassName: {{ .Values.ingress.className }} 28 | {{- end }} 29 | {{- if .Values.ingress.tls }} 30 | tls: 31 | {{- range .Values.ingress.tls }} 32 | - hosts: 33 | {{- range .hosts }} 34 | - {{ . | quote }} 35 | {{- end }} 36 | secretName: {{ .secretName }} 37 | {{- end }} 38 | {{- end }} 39 | rules: 40 | {{- range .Values.ingress.hosts }} 41 | - host: {{ .host | quote }} 42 | http: 43 | paths: 44 | {{- range .paths }} 45 | - path: {{ .path }} 46 | {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} 47 | pathType: {{ .pathType }} 48 | {{- end }} 49 | backend: 50 | {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} 51 | service: 52 | name: {{ $fullName }} 53 | port: 54 | number: {{ $svcPort }} 55 | {{- else }} 56 | serviceName: {{ $fullName }} 57 | servicePort: {{ $svcPort }} 58 | {{- end }} 59 | {{- end }} 60 | {{- end }} 61 | {{- end }} 62 | -------------------------------------------------------------------------------- /charts/whoogle/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "whoogle.fullname" . }} 5 | labels: 6 | {{- include "whoogle.labels" . | nindent 4 }} 7 | spec: 8 | {{- if not .Values.autoscaling.enabled }} 9 | replicas: {{ .Values.replicaCount }} 10 | {{- end }} 11 | selector: 12 | matchLabels: 13 | {{- include "whoogle.selectorLabels" . | nindent 6 }} 14 | template: 15 | metadata: 16 | {{- with .Values.podAnnotations }} 17 | annotations: 18 | {{- toYaml . | nindent 8 }} 19 | {{- end }} 20 | labels: 21 | {{- include "whoogle.selectorLabels" . | nindent 8 }} 22 | spec: 23 | {{- with .Values.image.pullSecrets }} 24 | imagePullSecrets: 25 | {{- range .}} 26 | - name: {{ . }} 27 | {{- end }} 28 | {{- end }} 29 | serviceAccountName: {{ include "whoogle.serviceAccountName" . }} 30 | securityContext: 31 | {{- toYaml .Values.podSecurityContext | nindent 8 }} 32 | containers: 33 | - name: whoogle 34 | securityContext: 35 | {{- toYaml .Values.securityContext | nindent 12 }} 36 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" 37 | imagePullPolicy: {{ .Values.image.pullPolicy }} 38 | {{- with .Values.conf }} 39 | env: 40 | {{- range $k,$v := . }} 41 | {{- if $v }} 42 | - name: {{ $k }} 43 | value: {{ tpl (toString $v) $ | quote }} 44 | {{- end }} 45 | {{- end }} 46 | {{- end }} 47 | ports: 48 | - name: http 49 | containerPort: {{ default 5000 .Values.conf.EXPOSE_PORT }} 50 | protocol: TCP 51 | livenessProbe: 52 | httpGet: 53 | path: / 54 | port: http 55 | readinessProbe: 56 | httpGet: 57 | path: / 58 | port: http 59 | resources: 60 | {{- toYaml .Values.resources | nindent 12 }} 61 | {{- with .Values.nodeSelector }} 62 | nodeSelector: 63 | {{- toYaml . | nindent 8 }} 64 | {{- end }} 65 | {{- with .Values.affinity }} 66 | affinity: 67 | {{- toYaml . | nindent 8 }} 68 | {{- end }} 69 | {{- with .Values.tolerations }} 70 | tolerations: 71 | {{- toYaml . | nindent 8 }} 72 | {{- end }} 73 | -------------------------------------------------------------------------------- /app/utils/misc.py: -------------------------------------------------------------------------------- 1 | from bs4 import BeautifulSoup as bsoup 2 | from flask import Request 3 | import hashlib 4 | import os 5 | from requests import exceptions, get 6 | from urllib.parse import urlparse 7 | 8 | 9 | def gen_file_hash(path: str, static_file: str) -> str: 10 | file_contents = open(os.path.join(path, static_file), 'rb').read() 11 | file_hash = hashlib.md5(file_contents).hexdigest()[:8] 12 | filename_split = os.path.splitext(static_file) 13 | 14 | return filename_split[0] + '.' + file_hash + filename_split[-1] 15 | 16 | 17 | def read_config_bool(var: str) -> bool: 18 | val = os.getenv(var, '0') 19 | # user can specify one of the following values as 'true' inputs (all 20 | # variants with upper case letters will also work): 21 | # ('true', 't', '1', 'yes', 'y') 22 | val = val.lower() in ('true', 't', '1', 'yes', 'y') 23 | return val 24 | 25 | 26 | def get_client_ip(r: Request) -> str: 27 | if r.environ.get('HTTP_X_FORWARDED_FOR') is None: 28 | return r.environ['REMOTE_ADDR'] 29 | else: 30 | return r.environ['HTTP_X_FORWARDED_FOR'] 31 | 32 | 33 | def get_request_url(url: str) -> str: 34 | if os.getenv('HTTPS_ONLY', False): 35 | return url.replace('http://', 'https://', 1) 36 | 37 | return url 38 | 39 | 40 | def get_proxy_host_url(r: Request, default: str, root=False) -> str: 41 | scheme = r.headers.get('X-Forwarded-Proto', 'https') 42 | http_host = r.headers.get('X-Forwarded-Host') 43 | if http_host: 44 | return f'{scheme}://{http_host}{r.full_path if not root else "/"}' 45 | 46 | return default 47 | 48 | 49 | def check_for_update(version_url: str, current: str) -> int: 50 | # Check for the latest version of Whoogle 51 | try: 52 | update = bsoup(get(version_url).text, 'html.parser') 53 | latest = update.select_one('[class="Link--primary"]').string[1:] 54 | current = int(''.join(filter(str.isdigit, current))) 55 | latest = int(''.join(filter(str.isdigit, latest))) 56 | has_update = '' if current >= latest else latest 57 | except (exceptions.ConnectionError, AttributeError): 58 | # Ignore failures, assume current version is up to date 59 | has_update = '' 60 | 61 | return has_update 62 | 63 | 64 | def get_abs_url(url, page_url): 65 | # Creates a valid absolute URL using a partial or relative URL 66 | if url.startswith('//'): 67 | return f'https:{url}' 68 | elif url.startswith('/'): 69 | return f'{urlparse(page_url).netloc}{url}' 70 | elif url.startswith('./'): 71 | return f'{page_url}{url[2:]}' 72 | return url 73 | -------------------------------------------------------------------------------- /test/test_routes.py: -------------------------------------------------------------------------------- 1 | from app import app 2 | from app.models.endpoint import Endpoint 3 | 4 | import json 5 | 6 | from test.conftest import demo_config 7 | 8 | 9 | def test_main(client): 10 | rv = client.get('/') 11 | assert rv._status_code == 200 12 | 13 | 14 | def test_search(client): 15 | rv = client.get(f'/{Endpoint.search}?q=test') 16 | assert rv._status_code == 200 17 | 18 | 19 | def test_feeling_lucky(client): 20 | rv = client.get(f'/{Endpoint.search}?q=!%20test') 21 | assert rv._status_code == 303 22 | 23 | 24 | def test_ddg_bang(client): 25 | # Bang at beginning of query 26 | rv = client.get(f'/{Endpoint.search}?q=!gh%20whoogle') 27 | assert rv._status_code == 302 28 | assert rv.headers.get('Location').startswith('https://github.com') 29 | 30 | # Move bang to end of query 31 | rv = client.get(f'/{Endpoint.search}?q=github%20!w') 32 | assert rv._status_code == 302 33 | assert rv.headers.get('Location').startswith('https://en.wikipedia.org') 34 | 35 | # Move bang to middle of query 36 | rv = client.get(f'/{Endpoint.search}?q=big%20!r%20chungus') 37 | assert rv._status_code == 302 38 | assert rv.headers.get('Location').startswith('https://www.reddit.com') 39 | 40 | # Ensure bang is case insensitive 41 | rv = client.get(f'/{Endpoint.search}?q=!GH%20whoogle') 42 | assert rv._status_code == 302 43 | assert rv.headers.get('Location').startswith('https://github.com') 44 | 45 | # Ensure bang without a query still redirects to the result 46 | rv = client.get(f'/{Endpoint.search}?q=!gh') 47 | assert rv._status_code == 302 48 | assert rv.headers.get('Location').startswith('https://github.com') 49 | 50 | 51 | def test_config(client): 52 | rv = client.post(f'/{Endpoint.config}', data=demo_config) 53 | assert rv._status_code == 302 54 | 55 | rv = client.get(f'/{Endpoint.config}') 56 | assert rv._status_code == 200 57 | 58 | config = json.loads(rv.data) 59 | for key in demo_config.keys(): 60 | assert config[key] == demo_config[key] 61 | 62 | # Test disabling changing config from client 63 | app.config['CONFIG_DISABLE'] = 1 64 | dark_mod = not demo_config['dark'] 65 | demo_config['dark'] = dark_mod 66 | rv = client.post(f'/{Endpoint.config}', data=demo_config) 67 | assert rv._status_code == 403 68 | 69 | rv = client.get(f'/{Endpoint.config}') 70 | config = json.loads(rv.data) 71 | assert config['dark'] != dark_mod 72 | 73 | 74 | def test_opensearch(client): 75 | rv = client.get(f'/{Endpoint.opensearch}') 76 | assert rv._status_code == 200 77 | assert 'Whoogle' in str(rv.data) 78 | -------------------------------------------------------------------------------- /app/static/js/utils.js: -------------------------------------------------------------------------------- 1 | const checkForTracking = () => { 2 | const mainDiv = document.getElementById("main"); 3 | const searchBar = document.getElementById("search-bar"); 4 | // some pages (e.g. images) do not have these 5 | if (!mainDiv || !searchBar) 6 | return; 7 | const query = searchBar.value.replace(/\s+/g, ''); 8 | 9 | // Note: regex functions for checking for tracking queries were derived 10 | // from here -- https://stackoverflow.com/questions/619977 11 | const matchTracking = { 12 | "ups": { 13 | "link": `https://www.ups.com/track?tracknum=${query}`, 14 | "expr": [ 15 | /\b(1Z ?[0-9A-Z]{3} ?[0-9A-Z]{3} ?[0-9A-Z]{2} ?[0-9A-Z]{4} ?[0-9A-Z]{3} ?[0-9A-Z]|[\dT]\d\d\d ?\d\d\d\d ?\d\d\d)\b/ 16 | ] 17 | }, 18 | "usps": { 19 | "link": `https://tools.usps.com/go/TrackConfirmAction?tLabels=${query}`, 20 | "expr": [ 21 | /(\b\d{30}\b)|(\b91\d+\b)|(\b\d{20}\b)/, 22 | /^E\D{1}\d{9}\D{2}$|^9\d{15,21}$/, 23 | /^91[0-9]+$/, 24 | /^[A-Za-z]{2}[0-9]+US$/ 25 | ] 26 | }, 27 | "fedex": { 28 | "link": `https://www.fedex.com/apps/fedextrack/?tracknumbers=${query}`, 29 | "expr": [ 30 | /(\b96\d{20}\b)|(\b\d{15}\b)|(\b\d{12}\b)/, 31 | /\b((98\d\d\d\d\d?\d\d\d\d|98\d\d) ?\d\d\d\d ?\d\d\d\d( ?\d\d\d)?)\b/, 32 | /^[0-9]{15}$/ 33 | ] 34 | } 35 | }; 36 | 37 | // Creates a link to a UPS/USPS/FedEx tracking page 38 | const createTrackingLink = href => { 39 | let link = document.createElement("a"); 40 | link.className = "tracking-link"; 41 | link.innerHTML = "View Tracking Info"; 42 | link.href = href; 43 | mainDiv.prepend(link); 44 | }; 45 | 46 | // Compares the query against a set of regex patterns 47 | // for tracking numbers 48 | const compareQuery = provider => { 49 | provider.expr.some(regex => { 50 | if (query.match(regex)) { 51 | createTrackingLink(provider.link); 52 | return true; 53 | } 54 | }); 55 | }; 56 | 57 | for (const key of Object.keys(matchTracking)) { 58 | compareQuery(matchTracking[key]); 59 | } 60 | }; 61 | 62 | document.addEventListener("DOMContentLoaded", function() { 63 | checkForTracking(); 64 | 65 | // Clear input if reset button tapped 66 | const searchBar = document.getElementById("search-bar"); 67 | const resetBtn = document.getElementById("search-reset"); 68 | // some pages (e.g. images) do not have these 69 | if (!searchBar || !resetBtn) 70 | return; 71 | resetBtn.addEventListener("click", event => { 72 | event.preventDefault(); 73 | searchBar.value = ""; 74 | searchBar.focus(); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11.0a5-alpine as builder 2 | 3 | RUN apk --update add \ 4 | build-base \ 5 | libxml2-dev \ 6 | libxslt-dev \ 7 | openssl-dev \ 8 | libffi-dev 9 | 10 | COPY requirements.txt . 11 | 12 | RUN pip install --upgrade pip 13 | RUN pip install --prefix /install --no-warn-script-location --no-cache-dir -r requirements.txt 14 | 15 | FROM python:3.11.0a5-alpine 16 | 17 | RUN apk add --update --no-cache tor curl openrc libstdc++ 18 | # libcurl4-openssl-dev 19 | 20 | RUN apk -U upgrade 21 | 22 | ARG DOCKER_USER=whoogle 23 | ARG DOCKER_USERID=927 24 | ARG config_dir=/config 25 | RUN mkdir -p $config_dir 26 | RUN chmod a+w $config_dir 27 | VOLUME $config_dir 28 | 29 | ARG url_prefix='' 30 | ARG username='' 31 | ARG password='' 32 | ARG proxyuser='' 33 | ARG proxypass='' 34 | ARG proxytype='' 35 | ARG proxyloc='' 36 | ARG whoogle_dotenv='' 37 | ARG use_https='' 38 | ARG whoogle_port=5000 39 | ARG twitter_alt='farside.link/nitter' 40 | ARG youtube_alt='farside.link/invidious' 41 | ARG instagram_alt='farside.link/bibliogram/u' 42 | ARG reddit_alt='farside.link/libreddit' 43 | ARG medium_alt='farside.link/scribe' 44 | ARG translate_alt='farside.link/lingva' 45 | ARG imgur_alt='farside.link/rimgo' 46 | ARG wikipedia_alt='farside.link/wikiless' 47 | ARG imdb_alt='farside.link/libremdb' 48 | ARG quora_alt='farside.link/quetre' 49 | 50 | ENV CONFIG_VOLUME=$config_dir \ 51 | WHOOGLE_URL_PREFIX=$url_prefix \ 52 | WHOOGLE_USER=$username \ 53 | WHOOGLE_PASS=$password \ 54 | WHOOGLE_PROXY_USER=$proxyuser \ 55 | WHOOGLE_PROXY_PASS=$proxypass \ 56 | WHOOGLE_PROXY_TYPE=$proxytype \ 57 | WHOOGLE_PROXY_LOC=$proxyloc \ 58 | WHOOGLE_DOTENV=$whoogle_dotenv \ 59 | HTTPS_ONLY=$use_https \ 60 | EXPOSE_PORT=$whoogle_port \ 61 | WHOOGLE_ALT_TW=$twitter_alt \ 62 | WHOOGLE_ALT_YT=$youtube_alt \ 63 | WHOOGLE_ALT_IG=$instagram_alt \ 64 | WHOOGLE_ALT_RD=$reddit_alt \ 65 | WHOOGLE_ALT_MD=$medium_alt \ 66 | WHOOGLE_ALT_TL=$translate_alt \ 67 | WHOOGLE_ALT_IMG=$imgur_alt \ 68 | WHOOGLE_ALT_WIKI=$wikipedia_alt \ 69 | WHOOGLE_ALT_IMDB=$imdb_alt \ 70 | WHOOGLE_ALT_QUORA=$quora_alt 71 | 72 | WORKDIR /whoogle 73 | 74 | COPY --from=builder /install /usr/local 75 | COPY misc/tor/torrc /etc/tor/torrc 76 | COPY misc/tor/start-tor.sh misc/tor/start-tor.sh 77 | COPY app/ app/ 78 | COPY run . 79 | #COPY whoogle.env . 80 | 81 | # Create user/group to run as 82 | RUN adduser -D -g $DOCKER_USERID -u $DOCKER_USERID $DOCKER_USER 83 | 84 | # Fix ownership / permissions 85 | RUN chown -R ${DOCKER_USER}:${DOCKER_USER} /whoogle /var/lib/tor 86 | 87 | # Allow writing symlinks to build dir 88 | RUN chown $DOCKER_USERID:$DOCKER_USERID app/static/build 89 | 90 | USER $DOCKER_USER:$DOCKER_USER 91 | 92 | EXPOSE $EXPOSE_PORT 93 | 94 | HEALTHCHECK --interval=30s --timeout=5s \ 95 | CMD curl -f http://localhost:${EXPOSE_PORT}/healthz || exit 1 96 | 97 | CMD misc/tor/start-tor.sh & ./run 98 | -------------------------------------------------------------------------------- /app/utils/bangs.py: -------------------------------------------------------------------------------- 1 | import json 2 | import requests 3 | import urllib.parse as urlparse 4 | 5 | DDG_BANGS = 'https://duckduckgo.com/bang.v255.js' 6 | 7 | 8 | def gen_bangs_json(bangs_file: str) -> None: 9 | """Generates a json file from the DDG bangs list 10 | 11 | Args: 12 | bangs_file: The str path to the new DDG bangs json file 13 | 14 | Returns: 15 | None 16 | 17 | """ 18 | try: 19 | # Request full list from DDG 20 | r = requests.get(DDG_BANGS) 21 | r.raise_for_status() 22 | except requests.exceptions.HTTPError as err: 23 | raise SystemExit(err) 24 | 25 | # Convert to json 26 | data = json.loads(r.text) 27 | 28 | # Set up a json object (with better formatting) for all available bangs 29 | bangs_data = {} 30 | 31 | for row in data: 32 | bang_command = '!' + row['t'] 33 | bangs_data[bang_command] = { 34 | 'url': row['u'].replace('{{{s}}}', '{}'), 35 | 'suggestion': bang_command + ' (' + row['s'] + ')' 36 | } 37 | 38 | json.dump(bangs_data, open(bangs_file, 'w')) 39 | print('* Finished creating ddg bangs json') 40 | 41 | 42 | def resolve_bang(query: str, bangs_dict: dict) -> str: 43 | """Transform's a user's query to a bang search, if an operator is found 44 | 45 | Args: 46 | query: The search query 47 | bangs_dict: The dict of available bang operators, with corresponding 48 | format string search URLs 49 | (i.e. "!w": "https://en.wikipedia.org...?search={}") 50 | 51 | Returns: 52 | str: A formatted redirect for a bang search, or an empty str if there 53 | wasn't a match or didn't contain a bang operator 54 | 55 | """ 56 | 57 | #if ! not in query simply return (speed up processing) 58 | if '!' not in query: 59 | return '' 60 | 61 | split_query = query.strip().split(' ') 62 | 63 | # look for operator in query if one is found, list operator should be of 64 | # length 1, operator should not be case-sensitive here to remove it later 65 | operator = [ 66 | word 67 | for word in split_query 68 | if word.lower() in bangs_dict 69 | ] 70 | if len(operator) == 1: 71 | # get operator 72 | operator = operator[0] 73 | 74 | # removes operator from query 75 | split_query.remove(operator) 76 | 77 | # rebuild the query string 78 | bang_query = ' '.join(split_query).strip() 79 | 80 | # Check if operator is a key in bangs and get bang if exists 81 | bang = bangs_dict.get(operator.lower(), None) 82 | if bang: 83 | bang_url = bang['url'] 84 | 85 | if bang_query: 86 | return bang_url.replace('{}', bang_query, 1) 87 | else: 88 | parsed_url = urlparse.urlparse(bang_url) 89 | return f'{parsed_url.scheme}://{parsed_url.netloc}' 90 | return '' 91 | -------------------------------------------------------------------------------- /app/static/settings/languages.json: -------------------------------------------------------------------------------- 1 | [ 2 | {"name": "-------", "value": ""}, 3 | {"name": "English", "value": "lang_en"}, 4 | {"name": "Afrikaans (Afrikaans)", "value": "lang_af"}, 5 | {"name": "Arabic (عربى)", "value": "lang_ar"}, 6 | {"name": "Armenian (հայերեն)", "value": "lang_hy"}, 7 | {"name": "Belarusian (Беларуская)", "value": "lang_be"}, 8 | {"name": "Bulgarian (български)", "value": "lang_bg"}, 9 | {"name": "Catalan (Català)", "value": "lang_ca"}, 10 | {"name": "Chinese, Simplified (简体中文)", "value": "lang_zh-CN"}, 11 | {"name": "Chinese, Traditional (正體中文)", "value": "lang_zh-TW"}, 12 | {"name": "Croatian (Hrvatski)", "value": "lang_hr"}, 13 | {"name": "Czech (čeština)", "value": "lang_cs"}, 14 | {"name": "Danish (Dansk)", "value": "lang_da"}, 15 | {"name": "Dutch (Nederlands)", "value": "lang_nl"}, 16 | {"name": "Esperanto (Esperanto)", "value": "lang_eo"}, 17 | {"name": "Estonian (Eestlane)", "value": "lang_et"}, 18 | {"name": "Filipino (Pilipino)", "value": "lang_tl"}, 19 | {"name": "Finnish (Suomalainen)", "value": "lang_fi"}, 20 | {"name": "French (Français)", "value": "lang_fr"}, 21 | {"name": "German (Deutsch)", "value": "lang_de"}, 22 | {"name": "Greek (Ελληνικά)", "value": "lang_el"}, 23 | {"name": "Hebrew (עִברִית)", "value": "lang_iw"}, 24 | {"name": "Hindi (हिंदी)", "value": "lang_hi"}, 25 | {"name": "Hungarian (Magyar)", "value": "lang_hu"}, 26 | {"name": "Icelandic (Íslenska)", "value": "lang_is"}, 27 | {"name": "Indonesian (Indonesian)", "value": "lang_id"}, 28 | {"name": "Italian (Italiano)", "value": "lang_it"}, 29 | {"name": "Japanese (日本語)", "value": "lang_ja"}, 30 | {"name": "Korean (한국어)", "value": "lang_ko"}, 31 | {"name": "Kurdish (Kurdî)", "value": "lang_ku"}, 32 | {"name": "Latvian (Latvietis)", "value": "lang_lv"}, 33 | {"name": "Lithuanian (Lietuvis)", "value": "lang_lt"}, 34 | {"name": "Norwegian (Norwegian)", "value": "lang_no"}, 35 | {"name": "Persian (فارسی)", "value": "lang_fa"}, 36 | {"name": "Polish (Polskie)", "value": "lang_pl"}, 37 | {"name": "Portuguese (Português)", "value": "lang_pt"}, 38 | {"name": "Romanian (Română)", "value": "lang_ro"}, 39 | {"name": "Russian (русский)", "value": "lang_ru"}, 40 | {"name": "Serbian (Српски)", "value": "lang_sr"}, 41 | {"name": "Sinhala (සිංහල)", "value": "lang_si"}, 42 | {"name": "Slovak (Slovák)", "value": "lang_sk"}, 43 | {"name": "Slovenian (Slovenščina)", "value": "lang_sl"}, 44 | {"name": "Spanish (Español)", "value": "lang_es"}, 45 | {"name": "Swahili (Kiswahili)", "value": "lang_sw"}, 46 | {"name": "Swedish (Svenska)", "value": "lang_sv"}, 47 | {"name": "Thai (ไทย)", "value": "lang_th"}, 48 | {"name": "Turkish (Türk)", "value": "lang_tr"}, 49 | {"name": "Ukrainian (Український)", "value": "lang_uk"}, 50 | {"name": "Vietnamese (Tiếng Việt)", "value": "lang_vi"}, 51 | {"name": "Welsh (Cymraeg)", "value": "lang_cy"}, 52 | {"name": "Xhosa (isiXhosa)", "value": "lang_xh"}, 53 | {"name": "Zulu (isiZulu)", "value": "lang_zu"} 54 | ] 55 | -------------------------------------------------------------------------------- /app/static/js/controller.js: -------------------------------------------------------------------------------- 1 | const setupSearchLayout = () => { 2 | // Setup search field 3 | const searchBar = document.getElementById("search-bar"); 4 | const searchBtn = document.getElementById("search-submit"); 5 | const arrowKeys = [37, 38, 39, 40]; 6 | let searchValue = searchBar.value; 7 | 8 | // Automatically focus on search field 9 | searchBar.focus(); 10 | searchBar.select(); 11 | 12 | searchBar.addEventListener("keyup", function(event) { 13 | if (event.keyCode === 13) { 14 | event.preventDefault(); 15 | searchBtn.click(); 16 | } else if (searchBar.value !== searchValue && !arrowKeys.includes(event.keyCode)) { 17 | searchValue = searchBar.value; 18 | handleUserInput(); 19 | } 20 | }); 21 | }; 22 | 23 | const setupConfigLayout = () => { 24 | // Setup whoogle config 25 | const collapsible = document.getElementById("config-collapsible"); 26 | collapsible.addEventListener("click", function() { 27 | this.classList.toggle("active"); 28 | let content = this.nextElementSibling; 29 | if (content.style.maxHeight) { 30 | content.style.maxHeight = null; 31 | } else { 32 | content.style.maxHeight = "400px"; 33 | } 34 | 35 | content.classList.toggle("open"); 36 | }); 37 | }; 38 | 39 | const loadConfig = event => { 40 | event.preventDefault(); 41 | let config = prompt("Enter name of config:"); 42 | if (!config) { 43 | alert("Must specify a name for the config to load"); 44 | return; 45 | } 46 | 47 | let xhrPUT = new XMLHttpRequest(); 48 | xhrPUT.open("PUT", "config?name=" + config + ".conf"); 49 | xhrPUT.onload = function() { 50 | if (xhrPUT.readyState === 4 && xhrPUT.status !== 200) { 51 | alert("Error loading Whoogle config"); 52 | return; 53 | } 54 | 55 | location.reload(true); 56 | }; 57 | 58 | xhrPUT.send(); 59 | }; 60 | 61 | const saveConfig = event => { 62 | event.preventDefault(); 63 | let config = prompt("Enter name for this config:"); 64 | if (!config) { 65 | alert("Must specify a name for the config to save"); 66 | return; 67 | } 68 | 69 | let configForm = document.getElementById("config-form"); 70 | configForm.action = 'config?name=' + config + ".conf"; 71 | configForm.submit(); 72 | }; 73 | 74 | document.addEventListener("DOMContentLoaded", function() { 75 | setTimeout(function() { 76 | document.getElementById("main").style.display = "block"; 77 | }, 100); 78 | 79 | setupSearchLayout(); 80 | setupConfigLayout(); 81 | 82 | document.getElementById("config-load").addEventListener("click", loadConfig); 83 | document.getElementById("config-save").addEventListener("click", saveConfig); 84 | 85 | // Focusing on the search input field requires a delay for elements to finish 86 | // loading (seemingly only on FF) 87 | setTimeout(function() { document.getElementById("search-bar").focus(); }, 250); 88 | }); 89 | -------------------------------------------------------------------------------- /test/test_misc.py: -------------------------------------------------------------------------------- 1 | from cryptography.fernet import Fernet 2 | 3 | from app import app 4 | from app.models.endpoint import Endpoint 5 | from app.utils.session import generate_user_key, valid_user_session 6 | 7 | 8 | JAPAN_PREFS = 'uG-gGIJwHdqxl6DrS3mnu_511HlQcRpxYlG03Xs-' \ 9 | + '_znXNiJWI9nLOkRLkiiFwIpeUYMTGfUF5-t9fP5DGmzDLEt04DCx703j3nPf' \ 10 | + '29v_RWkU7gXw_44m2oAFIaKGmYlu4Z0bKyu9k5WXfL9Dy6YKKnpcR5CiaFsG' \ 11 | + 'rccNRkAPYm-eYGAFUV8M59f8StsGd_M-gHKGS9fLok7EhwBWjHxBJ2Kv8hsT' \ 12 | + '87zftP2gMJOevTdNnezw2Y5WOx-ZotgeheCW1BYCFcRqatlov21PHp22NGVG' \ 13 | + '8ZuBNAFW0bE99WSdyT7dUIvzeWCLJpbdSsq-3FUUZkxbRdFYlGd8vY1UgVAp' \ 14 | + 'OSie2uAmpgLFXygO-VfNBBZ68Q7gAap2QtzHCiKD5cFYwH3LPgVJ-DoZvJ6k' \ 15 | + 'alt34TaYiJphgiqFKV4SCeVmLWTkr0SF3xakSR78yYJU_d41D2ng-TojA9XZ' \ 16 | + 'uR2ZqjSvPKOWvjimu89YhFOgJxG1Po8Henj5h9OL9VXXvdvlJwBSAKw1E3FV' \ 17 | + '7UHWiglMxPblfxqou1cYckMYkFeIMCD2SBtju68mBiQh2k328XRPTsQ_ocby' \ 18 | + 'cgVKnleGperqbD6crRk3Z9xE5sVCjujn9JNVI-7mqOITMZ0kntq9uJ3R5n25' \ 19 | + 'Vec0TJ0P19nEtvjY0nJIrIjtnBg==' 20 | 21 | 22 | def test_generate_user_keys(): 23 | key = generate_user_key() 24 | assert Fernet(key) 25 | assert generate_user_key() != key 26 | 27 | 28 | def test_valid_session(client): 29 | assert not valid_user_session({'key': '', 'config': {}}) 30 | with client.session_transaction() as session: 31 | assert valid_user_session(session) 32 | 33 | 34 | def test_valid_translation_keys(client): 35 | valid_lang_keys = [_['value'] for _ in app.config['LANGUAGES']] 36 | en_keys = app.config['TRANSLATIONS']['lang_en'].keys() 37 | for translation_key in app.config['TRANSLATIONS']: 38 | # Ensure the translation is using a valid language value 39 | assert translation_key in valid_lang_keys 40 | 41 | # Ensure all translations match the same size/content of the original 42 | # English translation 43 | assert app.config['TRANSLATIONS'][translation_key].keys() == en_keys 44 | 45 | 46 | def test_query_decryption(client): 47 | # FIXME: Handle decryption errors in search.py and rewrite test 48 | # This previously was used to test swapping decryption keys between 49 | # queries. While this worked in theory and usually didn't cause problems, 50 | # they were tied to session IDs and those are really unreliable (meaning 51 | # that occasionally page navigation would break). 52 | rv = client.get('/') 53 | cookie = rv.headers['Set-Cookie'] 54 | 55 | rv = client.get(f'/{Endpoint.search}?q=test+1', headers={'Cookie': cookie}) 56 | assert rv._status_code == 200 57 | 58 | with client.session_transaction() as session: 59 | assert valid_user_session(session) 60 | 61 | rv = client.get(f'/{Endpoint.search}?q=test+2', headers={'Cookie': cookie}) 62 | assert rv._status_code == 200 63 | 64 | with client.session_transaction() as session: 65 | assert valid_user_session(session) 66 | 67 | 68 | def test_prefs_url(client): 69 | base_url = f'/{Endpoint.search}?q=wikipedia' 70 | rv = client.get(base_url) 71 | assert rv._status_code == 200 72 | assert b'wikipedia.org' in rv.data 73 | assert b'ja.wikipedia.org' not in rv.data 74 | 75 | rv = client.get(f'{base_url}&preferences={JAPAN_PREFS}') 76 | assert rv._status_code == 200 77 | assert b'ja.wikipedia.org' in rv.data 78 | 79 | -------------------------------------------------------------------------------- /docker-compose-traefik.yaml: -------------------------------------------------------------------------------- 1 | # can't use mem_limit in a 3.x docker-compose file in non swarm mode 2 | # see https://github.com/docker/compose/issues/4513 3 | version: "2.4" 4 | 5 | services: 6 | traefik: 7 | image: "traefik:v2.7" 8 | container_name: "traefik" 9 | command: 10 | #- "--log.level=DEBUG" 11 | - "--api.insecure=true" 12 | - "--providers.docker=true" 13 | - "--providers.docker.exposedbydefault=false" 14 | - "--entrypoints.websecure.address=:443" 15 | - "--certificatesresolvers.myresolver.acme.tlschallenge=true" 16 | #- "--certificatesresolvers.myresolver.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory" 17 | - "--certificatesresolvers.myresolver.acme.email=change@domain.name" 18 | - "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json" 19 | ports: 20 | - "443:443" 21 | - "8080:8080" 22 | volumes: 23 | - "./letsencrypt:/letsencrypt" 24 | - "/var/run/docker.sock:/var/run/docker.sock:ro" 25 | 26 | whoogle-search: 27 | labels: 28 | - "traefik.enable=true" 29 | - "traefik.http.routers.whoami.rule=Host(`change.host.name`)" 30 | - "traefik.http.routers.whoami.entrypoints=websecure" 31 | - "traefik.http.routers.whoami.tls.certresolver=myresolver" 32 | - "traefik.http.services.whoogle-search.loadbalancer.server.port=5000" 33 | image: ${WHOOGLE_IMAGE:-benbusby/whoogle-search} 34 | container_name: whoogle-search 35 | restart: unless-stopped 36 | pids_limit: 50 37 | mem_limit: 256mb 38 | memswap_limit: 256mb 39 | # user debian-tor from tor package 40 | user: whoogle 41 | security_opt: 42 | - no-new-privileges 43 | cap_drop: 44 | - ALL 45 | tmpfs: 46 | - /config/:size=10M,uid=927,gid=927,mode=1700 47 | - /var/lib/tor/:size=15M,uid=927,gid=927,mode=1700 48 | - /run/tor/:size=1M,uid=927,gid=927,mode=1700 49 | environment: # Uncomment to configure environment variables 50 | # Basic auth configuration, uncomment to enable 51 | #- WHOOGLE_USER= 52 | #- WHOOGLE_PASS= 53 | # Proxy configuration, uncomment to enable 54 | #- WHOOGLE_PROXY_USER= 55 | #- WHOOGLE_PROXY_PASS= 56 | #- WHOOGLE_PROXY_TYPE= 58 | # Site alternative configurations, uncomment to enable 59 | # Note: If not set, the feature will still be available 60 | # with default values. 61 | #- WHOOGLE_ALT_TW=farside.link/nitter 62 | #- WHOOGLE_ALT_YT=farside.link/invidious 63 | #- WHOOGLE_ALT_IG=farside.link/bibliogram/u 64 | #- WHOOGLE_ALT_RD=farside.link/libreddit 65 | #- WHOOGLE_ALT_MD=farside.link/scribe 66 | #- WHOOGLE_ALT_TL=farside.link/lingva 67 | #- WHOOGLE_ALT_IMG=farside.link/rimgo 68 | #- WHOOGLE_ALT_WIKI=farside.link/wikiless 69 | #- WHOOGLE_ALT_IMDB=farside.link/libremdb 70 | #- WHOOGLE_ALT_QUORA=farside.link/quetre 71 | # - WHOOGLE_CONFIG_DISABLE=1 72 | # - WHOOGLE_CONFIG_SEARCH_LANGUAGE=lang_en 73 | # - WHOOGLE_CONFIG_GET_ONLY=1 74 | # - WHOOGLE_CONFIG_COUNTRY=FR 75 | # - WHOOGLE_CONFIG_PREFERENCES_ENCRYPTED=1 76 | # - WHOOGLE_CONFIG_PREFERENCES_KEY="NEEDS_TO_BE_MODIFIED" 77 | #env_file: # Alternatively, load variables from whoogle.env 78 | #- whoogle.env 79 | ports: 80 | - 8000:5000 81 | -------------------------------------------------------------------------------- /whoogle.template.env: -------------------------------------------------------------------------------- 1 | # ---------------------------------- 2 | # Rename to "whoogle.env" before use 3 | # ---------------------------------- 4 | # You can set Whoogle environment variables here, but must 5 | # modify your deployment to enable these values: 6 | # - Local: Set WHOOGLE_DOTENV=1 7 | # - docker-compose: Uncomment the env_file option 8 | # - docker: Add "--env-file ./whoogle.env" to your build command 9 | 10 | #WHOOGLE_ALT_TW=farside.link/nitter 11 | #WHOOGLE_ALT_YT=farside.link/invidious 12 | #WHOOGLE_ALT_IG=farside.link/bibliogram/u 13 | #WHOOGLE_ALT_RD=farside.link/libreddit 14 | #WHOOGLE_ALT_MD=farside.link/scribe 15 | #WHOOGLE_ALT_TL=farside.link/lingva 16 | #WHOOGLE_ALT_IMG=farside.link/rimgo 17 | #WHOOGLE_ALT_WIKI=farside.link/wikiless 18 | #WHOOGLE_ALT_IMDB=farside.link/libremdb 19 | #WHOOGLE_ALT_QUORA=farside.link/quetre 20 | #WHOOGLE_USER="" 21 | #WHOOGLE_PASS="" 22 | #WHOOGLE_PROXY_USER="" 23 | #WHOOGLE_PROXY_PASS="" 24 | #WHOOGLE_PROXY_TYPE="" 25 | #WHOOGLE_PROXY_LOC="" 26 | #WHOOGLE_CSP=1 27 | #HTTPS_ONLY=1 28 | 29 | # The URL prefix to use for the whoogle instance (i.e. "/whoogle") 30 | #WHOOGLE_URL_PREFIX="" 31 | 32 | # Restrict results to only those near a particular city 33 | #WHOOGLE_CONFIG_NEAR=denver 34 | 35 | # See app/static/settings/countries.json for values 36 | #WHOOGLE_CONFIG_COUNTRY=US 37 | 38 | # See app/static/settings/languages.json for values 39 | #WHOOGLE_CONFIG_LANGUAGE=lang_en 40 | 41 | # See app/static/settings/languages.json for values 42 | #WHOOGLE_CONFIG_SEARCH_LANGUAGE=lang_en 43 | 44 | # Disable changing of config from client 45 | #WHOOGLE_CONFIG_DISABLE=1 46 | 47 | # Block websites from search results (comma-separated list) 48 | #WHOOGLE_CONFIG_BLOCK=pinterest.com,whitehouse.gov 49 | 50 | # Theme (light, dark, or system) 51 | #WHOOGLE_CONFIG_THEME=system 52 | 53 | # Safe search mode 54 | #WHOOGLE_CONFIG_SAFE=1 55 | 56 | # Use social media site alternatives (nitter, bibliogram, etc) 57 | #WHOOGLE_CONFIG_ALTS=1 58 | 59 | # Use Tor if available 60 | #WHOOGLE_CONFIG_TOR=1 61 | 62 | # Open results in new tab 63 | #WHOOGLE_CONFIG_NEW_TAB=1 64 | 65 | # Enable View Image option 66 | #WHOOGLE_CONFIG_VIEW_IMAGE=1 67 | 68 | # Search using GET requests only (exposes query in logs) 69 | #WHOOGLE_CONFIG_GET_ONLY=1 70 | 71 | # Remove everything except basic result cards from all search queries 72 | #WHOOGLE_MINIMAL=0 73 | 74 | # Set the number of results per page 75 | #WHOOGLE_RESULTS_PER_PAGE=10 76 | 77 | # Controls visibility of autocomplete/search suggestions 78 | #WHOOGLE_AUTOCOMPLETE=1 79 | 80 | # The port where Whoogle will be exposed 81 | #EXPOSE_PORT=5000 82 | 83 | # Set instance URL 84 | #WHOOGLE_CONFIG_URL=https:/// 85 | 86 | # Set custom CSS styling/theming 87 | #WHOOGLE_CONFIG_STYLE=":root { /* LIGHT THEME COLORS */ --whoogle-background: #d8dee9; --whoogle-accent: #2e3440; --whoogle-text: #3B4252; --whoogle-contrast-text: #eceff4; --whoogle-secondary-text: #70757a; --whoogle-result-bg: #fff; --whoogle-result-title: #4c566a; --whoogle-result-url: #81a1c1; --whoogle-result-visited: #a3be8c; /* DARK THEME COLORS */ --whoogle-dark-background: #222; --whoogle-dark-accent: #685e79; --whoogle-dark-text: #fff; --whoogle-dark-contrast-text: #000; --whoogle-dark-secondary-text: #bbb; --whoogle-dark-result-bg: #000; --whoogle-dark-result-title: #1967d2; --whoogle-dark-result-url: #4b11a8; --whoogle-dark-result-visited: #bbbbff; }" 88 | 89 | # Enable preferences encryption (requires key) 90 | #WHOOGLE_CONFIG_PREFERENCES_ENCRYPTED=1 91 | 92 | # Set Key to encode config in url 93 | #WHOOGLE_CONFIG_PREFERENCES_KEY="NEEDS_TO_BE_MODIFIED" -------------------------------------------------------------------------------- /app/static/css/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Avenir, Helvetica, Arial, sans-serif; 3 | } 4 | 5 | .logo { 6 | width: 80%; 7 | display: block; 8 | margin: auto; 9 | padding-bottom: 10px; 10 | } 11 | 12 | .logo-container { 13 | max-height: 500px; 14 | } 15 | 16 | .home-search { 17 | background: transparent !important; 18 | border: 3px solid; 19 | } 20 | 21 | .search-container { 22 | background: transparent !important; 23 | width: 80%; 24 | position: absolute; 25 | top: 50%; 26 | left: 50%; 27 | transform: translate(-50%, -50%); 28 | max-width: 600px; 29 | z-index: 15; 30 | } 31 | 32 | .search-items { 33 | width: 100%; 34 | position: relative; 35 | display: flex; 36 | } 37 | 38 | #search-bar { 39 | background: transparent !important; 40 | width: 100%; 41 | padding: 5px; 42 | height: 40px; 43 | outline: none; 44 | font-size: 24px; 45 | border-radius: 10px 10px 0 0; 46 | max-width: 600px; 47 | background: rgba(0, 0, 0, 0); 48 | } 49 | 50 | #search-submit { 51 | width: 100%; 52 | height: 40px; 53 | text-align: center; 54 | cursor: pointer; 55 | font-size: 20px; 56 | align-content: center; 57 | align-items: center; 58 | margin: auto; 59 | border-radius: 0 0 10px 10px; 60 | max-width: 600px; 61 | -webkit-appearance: none; 62 | } 63 | 64 | .config-options { 65 | max-height: 370px; 66 | overflow-y: scroll; 67 | } 68 | 69 | .config-buttons { 70 | max-height: 30px; 71 | } 72 | 73 | .config-div { 74 | padding: 5px; 75 | } 76 | 77 | button::-moz-focus-inner { 78 | border: 0; 79 | } 80 | 81 | .collapsible { 82 | outline: 0; 83 | background-color: rgba(0, 0, 0, 0); 84 | cursor: pointer; 85 | padding: 18px; 86 | width: 100%; 87 | border: none; 88 | text-align: left; 89 | outline: none; 90 | font-size: 15px; 91 | border-radius: 10px 10px 0 0; 92 | } 93 | 94 | .collapsible:after { 95 | content: '\002B'; 96 | font-weight: bold; 97 | float: right; 98 | margin-left: 5px; 99 | } 100 | 101 | .active:after { 102 | content: "\2212"; 103 | } 104 | 105 | .content { 106 | padding: 0 18px; 107 | max-height: 0; 108 | overflow: hidden; 109 | transition: max-height 0.2s ease-out; 110 | border-radius: 0 0 10px 10px; 111 | } 112 | 113 | .open { 114 | padding-bottom: 20px; 115 | } 116 | 117 | .hidden { 118 | display: none; 119 | } 120 | 121 | footer { 122 | position: fixed; 123 | bottom: 0%; 124 | text-align: center; 125 | width: 100%; 126 | z-index: 10; 127 | } 128 | 129 | .info-text { 130 | font-style: italic; 131 | font-size: 12px; 132 | } 133 | 134 | #config-style { 135 | resize: none; 136 | overflow-y: scroll; 137 | width: 100%; 138 | height: 100px; 139 | } 140 | 141 | .whoogle-logo { 142 | display: none; 143 | } 144 | 145 | .whoogle-svg { 146 | width: 80%; 147 | height: initial; 148 | display: block; 149 | margin: auto; 150 | padding-bottom: 10px; 151 | } 152 | 153 | .autocomplete { 154 | position: relative; 155 | display: inline-block; 156 | width: 100%; 157 | } 158 | 159 | .autocomplete-items { 160 | position: absolute; 161 | border-bottom: none; 162 | border-top: none; 163 | z-index: 99; 164 | 165 | /*position the autocomplete items to be the same width as the container:*/ 166 | top: 100%; 167 | left: 0; 168 | right: 0; 169 | } 170 | 171 | .autocomplete-items div { 172 | padding: 10px; 173 | cursor: pointer; 174 | } 175 | 176 | details summary { 177 | padding: 10px; 178 | font-weight: bold; 179 | } 180 | 181 | /* Mobile styles */ 182 | @media (max-width: 1000px) { 183 | select { 184 | width: 100%; 185 | } 186 | 187 | #search-bar { 188 | font-size: 20px; 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /test/test_results.py: -------------------------------------------------------------------------------- 1 | from bs4 import BeautifulSoup 2 | from app.filter import Filter 3 | from app.models.config import Config 4 | from app.models.endpoint import Endpoint 5 | from app.utils.session import generate_user_key 6 | from datetime import datetime 7 | from dateutil.parser import ParserError, parse 8 | from urllib.parse import urlparse 9 | 10 | from test.conftest import demo_config 11 | 12 | 13 | def get_search_results(data): 14 | secret_key = generate_user_key() 15 | soup = Filter(user_key=secret_key, config=Config(**demo_config)).clean( 16 | BeautifulSoup(data, 'html.parser')) 17 | 18 | main_divs = soup.find('div', {'id': 'main'}) 19 | assert len(main_divs) > 1 20 | 21 | result_divs = [] 22 | for div in main_divs: 23 | # Result divs should only have 1 inner div 24 | if (len(list(div.children)) != 1 25 | or not div.findChild() 26 | or 'div' not in div.findChild().name): 27 | continue 28 | 29 | result_divs.append(div) 30 | 31 | return result_divs 32 | 33 | 34 | def test_get_results(client): 35 | rv = client.get(f'/{Endpoint.search}?q=test') 36 | assert rv._status_code == 200 37 | 38 | # Depending on the search, there can be more 39 | # than 10 result divs 40 | results = get_search_results(rv.data) 41 | assert len(results) >= 10 42 | assert len(results) <= 15 43 | 44 | 45 | def test_post_results(client): 46 | rv = client.post(f'/{Endpoint.search}', data=dict(q='test')) 47 | assert rv._status_code == 200 48 | 49 | # Depending on the search, there can be more 50 | # than 10 result divs 51 | results = get_search_results(rv.data) 52 | assert len(results) >= 10 53 | assert len(results) <= 15 54 | 55 | 56 | def test_translate_search(client): 57 | rv = client.post(f'/{Endpoint.search}', data=dict(q='translate hola')) 58 | assert rv._status_code == 200 59 | 60 | # Pretty weak test, but better than nothing 61 | str_data = str(rv.data) 62 | assert 'iframe' in str_data 63 | assert '/auto/en/ hola' in str_data 64 | 65 | 66 | def test_block_results(client): 67 | rv = client.post(f'/{Endpoint.search}', data=dict(q='pinterest')) 68 | assert rv._status_code == 200 69 | 70 | has_pinterest = False 71 | for link in BeautifulSoup(rv.data, 'html.parser').find_all('a', href=True): 72 | if 'pinterest.com' in urlparse(link['href']).netloc: 73 | has_pinterest = True 74 | break 75 | 76 | assert has_pinterest 77 | 78 | demo_config['block'] = 'pinterest.com' 79 | rv = client.post(f'/{Endpoint.config}', data=demo_config) 80 | assert rv._status_code == 302 81 | 82 | rv = client.post(f'/{Endpoint.search}', data=dict(q='pinterest')) 83 | assert rv._status_code == 200 84 | 85 | for link in BeautifulSoup(rv.data, 'html.parser').find_all('a', href=True): 86 | result_site = urlparse(link['href']).netloc 87 | if not result_site: 88 | continue 89 | assert result_site not in 'pinterest.com' 90 | 91 | 92 | def test_view_my_ip(client): 93 | rv = client.post(f'/{Endpoint.search}', data=dict(q='my ip address')) 94 | assert rv._status_code == 200 95 | 96 | # Pretty weak test, but better than nothing 97 | str_data = str(rv.data) 98 | assert 'Your public IP address' in str_data 99 | assert '127.0.0.1' in str_data 100 | 101 | 102 | def test_recent_results(client): 103 | times = { 104 | 'past year': 365, 105 | 'past month': 31, 106 | 'past week': 7 107 | } 108 | 109 | for time, num_days in times.items(): 110 | rv = client.post(f'/{Endpoint.search}', data=dict(q='test :' + time)) 111 | result_divs = get_search_results(rv.data) 112 | 113 | current_date = datetime.now() 114 | for div in [_ for _ in result_divs if _.find('span')]: 115 | date_span = div.find('span').decode_contents() 116 | if not date_span or len(date_span) > 15 or len(date_span) < 7: 117 | continue 118 | 119 | try: 120 | date = parse(date_span) 121 | # Date can have a little bit of wiggle room 122 | assert (current_date - date).days <= (num_days + 5) 123 | except ParserError: 124 | pass 125 | 126 | 127 | def test_leading_slash_search(client): 128 | # Ensure searches with a leading slash are interpreted 129 | # correctly as queries and not endpoints 130 | q = '/test' 131 | rv = client.get(f'/{Endpoint.search}?q={q}') 132 | assert rv._status_code == 200 133 | 134 | soup = Filter( 135 | user_key=generate_user_key(), 136 | config=Config(**demo_config), 137 | query=q 138 | ).clean(BeautifulSoup(rv.data, 'html.parser')) 139 | 140 | for link in soup.find_all('a', href=True): 141 | if 'start=' not in link['href']: 142 | continue 143 | 144 | assert link['href'].startswith(f'{Endpoint.search}') 145 | -------------------------------------------------------------------------------- /app/static/css/light-theme.css: -------------------------------------------------------------------------------- 1 | html { 2 | background: var(--whoogle-page-bg) !important; 3 | } 4 | 5 | body { 6 | background: var(--whoogle-page-bg) !important; 7 | } 8 | 9 | div { 10 | color: var(--whoogle-text) !important; 11 | } 12 | 13 | label { 14 | color: var(--whoogle-contrast-text) !important; 15 | } 16 | 17 | li a { 18 | color: var(--whoogle-result-url) !important; 19 | } 20 | 21 | li { 22 | color: var(--whoogle-text) !important; 23 | } 24 | 25 | .anon-view { 26 | color: var(--whoogle-text) !important; 27 | text-decoration: underline; 28 | } 29 | 30 | textarea { 31 | background: var(--whoogle-page-bg) !important; 32 | color: var(--whoogle-text) !important; 33 | } 34 | 35 | select { 36 | background: var(--whoogle-page-bg) !important; 37 | color: var(--whoogle-text) !important; 38 | } 39 | 40 | .ZINbbc { 41 | overflow: hidden; 42 | background-color: var(--whoogle-result-bg) !important; 43 | margin-bottom: 10px !important; 44 | border-radius: 8px !important; 45 | box-shadow: 0 1px 6px rgba(32,33,36,0.28) !important; 46 | } 47 | 48 | .BVG0Nb { 49 | background-color: var(--whoogle-result-bg) !important; 50 | } 51 | 52 | .ZINbbc.luh4tb { 53 | background: var(--whoogle-result-bg) !important; 54 | margin-bottom: 24px !important; 55 | } 56 | 57 | .bRsWnc { 58 | background-color: var(--whoogle-result-bg) !important; 59 | } 60 | 61 | .x54gtf { 62 | background-color: var(--whoogle-divider) !important; 63 | } 64 | 65 | .Q0HXG { 66 | background-color: var(--whoogle-divider) !important; 67 | } 68 | 69 | .LKSyXe { 70 | background-color: var(--whoogle-divider) !important; 71 | } 72 | 73 | 74 | a:visited h3 div { 75 | color: var(--whoogle-result-visited) !important; 76 | } 77 | 78 | a:link h3 div { 79 | color: var(--whoogle-result-title) !important; 80 | } 81 | 82 | a:link div { 83 | color: var(--whoogle-result-url) !important; 84 | } 85 | 86 | div span { 87 | color: var(--whoogle-secondary-text) !important; 88 | } 89 | 90 | input { 91 | background-color: var(--whoogle-page-bg) !important; 92 | color: var(--whoogle-text) !important; 93 | } 94 | 95 | #search-bar { 96 | color: var(--whoogle-text) !important; 97 | background-color: var(--whoogle-page-bg); 98 | } 99 | 100 | .home-search { 101 | border-color: var(--whoogle-element-bg) !important; 102 | } 103 | 104 | .search-container { 105 | background-color: var(--whoogle-page-bg) !important; 106 | } 107 | 108 | #search-submit { 109 | border: 1px solid var(--whoogle-element-bg) !important; 110 | background: var(--whoogle-element-bg) !important; 111 | color: var(--whoogle-contrast-text) !important; 112 | } 113 | 114 | .info-text { 115 | color: var(--whoogle-contrast-text) !important; 116 | opacity: 75%; 117 | } 118 | 119 | .collapsible { 120 | color: var(--whoogle-text) !important; 121 | } 122 | 123 | .collapsible:after { 124 | color: var(--whoogle-text); 125 | } 126 | 127 | .active { 128 | background-color: var(--whoogle-element-bg) !important; 129 | color: var(--whoogle-contrast-text) !important; 130 | } 131 | 132 | .content, .result-config { 133 | background-color: var(--whoogle-element-bg) !important; 134 | color: var(--whoogle-contrast-text) !important; 135 | } 136 | 137 | .active:after { 138 | color: var(--whoogle-contrast-text); 139 | } 140 | 141 | .link { 142 | color: var(--whoogle-element-bg); 143 | } 144 | 145 | .link-color { 146 | color: var(--whoogle-result-url) !important; 147 | } 148 | 149 | .autocomplete-items { 150 | border: 1px solid var(--whoogle-element-bg); 151 | } 152 | 153 | .autocomplete-items div { 154 | background-color: var(--whoogle-page-bg); 155 | border-bottom: 1px solid var(--whoogle-element-bg); 156 | } 157 | 158 | .autocomplete-items div:hover { 159 | background-color: var(--whoogle-element-bg); 160 | color: var(--whoogle-contrast-text) !important; 161 | } 162 | 163 | .autocomplete-active { 164 | background-color: var(--whoogle-element-bg) !important; 165 | color: var(--whoogle-contrast-text) !important; 166 | } 167 | 168 | .footer { 169 | color: var(--whoogle-text); 170 | } 171 | 172 | path { 173 | fill: var(--whoogle-logo); 174 | } 175 | 176 | .header-div { 177 | background-color: var(--whoogle-result-bg) !important; 178 | } 179 | 180 | #search-reset { 181 | color: var(--whoogle-text) !important; 182 | } 183 | 184 | .mobile-search-bar { 185 | background-color: var(--whoogle-result-bg) !important; 186 | color: var(--whoogle-text) !important; 187 | } 188 | 189 | .search-bar-desktop { 190 | background-color: var(--whoogle-result-bg) !important; 191 | color: var(--whoogle-text); 192 | border-bottom: 0px; 193 | } 194 | 195 | .ip-text-div, .update_available, .cb_label, .cb { 196 | color: var(--whoogle-secondary-text) !important; 197 | } 198 | 199 | .cb:focus { 200 | color: var(--whoogle-text) !important; 201 | } 202 | 203 | .desktop-header, .mobile-header { 204 | background-color: var(--whoogle-result-bg) !important; 205 | } 206 | -------------------------------------------------------------------------------- /app/static/js/autocomplete.js: -------------------------------------------------------------------------------- 1 | let searchInput; 2 | let currentFocus; 3 | let originalSearch; 4 | let autocompleteResults; 5 | 6 | const handleUserInput = () => { 7 | let xhrRequest = new XMLHttpRequest(); 8 | xhrRequest.open("POST", "autocomplete"); 9 | xhrRequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); 10 | xhrRequest.onload = function () { 11 | if (xhrRequest.readyState === 4 && xhrRequest.status !== 200) { 12 | // Do nothing if failed to fetch autocomplete results 13 | return; 14 | } 15 | 16 | // Fill autocomplete with fetched results 17 | autocompleteResults = JSON.parse(xhrRequest.responseText)[1]; 18 | updateAutocompleteList(); 19 | }; 20 | 21 | xhrRequest.send('q=' + searchInput.value); 22 | }; 23 | 24 | const closeAllLists = el => { 25 | // Close all autocomplete suggestions 26 | let suggestions = document.getElementsByClassName("autocomplete-items"); 27 | for (let i = 0; i < suggestions.length; i++) { 28 | if (el !== suggestions[i] && el !== searchInput) { 29 | suggestions[i].parentNode.removeChild(suggestions[i]); 30 | } 31 | } 32 | }; 33 | 34 | const removeActive = suggestion => { 35 | // Remove "autocomplete-active" class from previously active suggestion 36 | for (let i = 0; i < suggestion.length; i++) { 37 | suggestion[i].classList.remove("autocomplete-active"); 38 | } 39 | }; 40 | 41 | const addActive = (suggestion) => { 42 | // Handle navigation outside of suggestion list 43 | if (!suggestion || !suggestion[currentFocus]) { 44 | if (currentFocus >= suggestion.length) { 45 | // Move selection back to the beginning 46 | currentFocus = 0; 47 | } else if (currentFocus < 0) { 48 | // Retrieve original search and remove active suggestion selection 49 | currentFocus = -1; 50 | searchInput.value = originalSearch; 51 | removeActive(suggestion); 52 | return; 53 | } else { 54 | return; 55 | } 56 | } 57 | 58 | removeActive(suggestion); 59 | suggestion[currentFocus].classList.add("autocomplete-active"); 60 | 61 | // Autofill search bar with suggestion content (minus the "bang name" if using a bang operator) 62 | let searchContent = suggestion[currentFocus].textContent; 63 | if (searchContent.indexOf('(') > 0) { 64 | searchInput.value = searchContent.substring(0, searchContent.indexOf('(')); 65 | } else { 66 | searchInput.value = searchContent; 67 | } 68 | 69 | searchInput.focus(); 70 | }; 71 | 72 | const autocompleteInput = (e) => { 73 | // Handle navigation between autocomplete suggestions 74 | let suggestion = document.getElementById(this.id + "-autocomplete-list"); 75 | if (suggestion) suggestion = suggestion.getElementsByTagName("div"); 76 | if (e.keyCode === 40) { // down 77 | e.preventDefault(); 78 | currentFocus++; 79 | addActive(suggestion); 80 | } else if (e.keyCode === 38) { //up 81 | e.preventDefault(); 82 | currentFocus--; 83 | addActive(suggestion); 84 | } else if (e.keyCode === 13) { // enter 85 | e.preventDefault(); 86 | if (currentFocus > -1) { 87 | if (suggestion) suggestion[currentFocus].click(); 88 | } 89 | } else { 90 | originalSearch = searchInput.value; 91 | } 92 | }; 93 | 94 | const updateAutocompleteList = () => { 95 | let autocompleteList, autocompleteItem, i; 96 | let val = originalSearch; 97 | closeAllLists(); 98 | 99 | if (!val || !autocompleteResults) { 100 | return false; 101 | } 102 | 103 | currentFocus = -1; 104 | autocompleteList = document.createElement("div"); 105 | autocompleteList.setAttribute("id", this.id + "-autocomplete-list"); 106 | autocompleteList.setAttribute("class", "autocomplete-items"); 107 | searchInput.parentNode.appendChild(autocompleteList); 108 | 109 | for (i = 0; i < autocompleteResults.length; i++) { 110 | if (autocompleteResults[i].substr(0, val.length).toUpperCase() === val.toUpperCase()) { 111 | autocompleteItem = document.createElement("div"); 112 | autocompleteItem.innerHTML = "" + autocompleteResults[i].substr(0, val.length) + ""; 113 | autocompleteItem.innerHTML += autocompleteResults[i].substr(val.length); 114 | autocompleteItem.innerHTML += ""; 115 | autocompleteItem.addEventListener("click", function () { 116 | searchInput.value = this.getElementsByTagName("input")[0].value; 117 | closeAllLists(); 118 | document.getElementById("search-form").submit(); 119 | }); 120 | autocompleteList.appendChild(autocompleteItem); 121 | } 122 | } 123 | }; 124 | 125 | document.addEventListener("DOMContentLoaded", function() { 126 | searchInput = document.getElementById("search-bar"); 127 | searchInput.addEventListener("keydown", (event) => autocompleteInput(event)); 128 | 129 | document.addEventListener("click", function (e) { 130 | closeAllLists(e.target); 131 | }); 132 | }); -------------------------------------------------------------------------------- /app/static/css/header.css: -------------------------------------------------------------------------------- 1 | header { 2 | font-family: Roboto,HelveticaNeue,Arial,sans-serif; 3 | font-size: 14px; 4 | line-height: 20px; 5 | color: #3C4043; 6 | word-wrap: break-word; 7 | } 8 | 9 | .logo-link, .logo-letter { 10 | text-decoration: none !important; 11 | letter-spacing: -1px; 12 | text-align: center; 13 | border-radius: 2px 0 0 0; 14 | } 15 | 16 | .result-config { 17 | margin-bottom: 10px; 18 | padding: 10px; 19 | border-radius: 8px; 20 | } 21 | 22 | .mobile-logo { 23 | font: 22px/36px Futura, Arial, sans-serif; 24 | padding-left: 5px; 25 | display: flex; 26 | justify-content: center; 27 | align-items: center; 28 | } 29 | 30 | .logo-div { 31 | letter-spacing: -1px; 32 | text-align: center; 33 | font: 22pt Futura, Arial, sans-serif; 34 | padding: 10px 0 5px 0; 35 | height: 37px; 36 | font-smoothing: antialiased; 37 | } 38 | 39 | .search-bar-desktop { 40 | border-radius: 8px 8px 0 0; 41 | height: 40px !important; 42 | } 43 | 44 | .search-div { 45 | border-radius: 8px 8px 0 0; 46 | box-shadow: 0 1px 6px rgba(32, 33, 36, 0.18); 47 | margin-top: 10px; 48 | } 49 | 50 | .search-form { 51 | height: 39px; 52 | display: flex; 53 | width: 100%; 54 | margin: 0px; 55 | } 56 | 57 | .search-input { 58 | background: none; 59 | margin: 2px 4px 2px 8px; 60 | display: block; 61 | font-size: 16px; 62 | padding: 0 0 0 8px; 63 | flex: 1; 64 | height: 35px; 65 | outline: none; 66 | border: none; 67 | width: 100%; 68 | -webkit-tap-highlight-color: rgba(0,0,0,0); 69 | overflow: hidden; 70 | } 71 | 72 | .tracking-link { 73 | font-size: large; 74 | text-align: center; 75 | margin: 15px; 76 | display: block; 77 | } 78 | 79 | #main>div:focus-within { 80 | border-radius: 8px; 81 | box-shadow: 0 0 6px 1px #2375e8; 82 | } 83 | 84 | #mobile-header-logo { 85 | height: 1.75em; 86 | } 87 | 88 | .mobile-input-div { 89 | width: 100%; 90 | } 91 | 92 | .mobile-search-bar { 93 | display: block; 94 | font-size: 16px; 95 | padding: 0 0 0 8px; 96 | padding-right: 0px; 97 | -webkit-box-flex: 1; 98 | height: 35px; 99 | outline: none; 100 | border: none; 101 | width: 100%; 102 | -webkit-tap-highlight-color: rgba(0,0,0,.00); 103 | overflow: hidden; 104 | border: 0px !important; 105 | } 106 | 107 | .autocomplete-mobile{ 108 | display: -webkit-box; 109 | width: 100%; 110 | } 111 | 112 | .desktop-header-logo { 113 | height: 1.65em; 114 | } 115 | 116 | .header-autocomplete { 117 | width: 100%; 118 | flex: 1 119 | } 120 | 121 | a { 122 | color: #1967D2; 123 | text-decoration: none; 124 | tap-highlight-color: rgba(0, 0, 0, .10); 125 | } 126 | 127 | .header-tab-div { 128 | border-radius: 0 0 8px 8px; 129 | box-shadow: 0 2px 3px rgba(32, 33, 36, 0.18); 130 | overflow: hidden; 131 | margin-bottom: 10px; 132 | } 133 | 134 | .header-tab-div-2 { 135 | border-top: 1px solid #dadce0; 136 | height: 39px; 137 | overflow: hidden; 138 | } 139 | 140 | .header-tab-div-3 { 141 | height: 51px; 142 | overflow-x: auto; 143 | overflow-y: hidden; 144 | } 145 | 146 | .desktop-header { 147 | height: 39px; 148 | display: box; 149 | display: flex; 150 | width: 100%; 151 | } 152 | 153 | .header-tab { 154 | box-pack: justify; 155 | font-size: 14px; 156 | line-height: 37px; 157 | justify-content: space-between; 158 | } 159 | 160 | .desktop-header a, .desktop-header span { 161 | color: #70757a; 162 | display: block; 163 | flex: none; 164 | padding: 0 16px; 165 | text-align: center; 166 | text-transform: uppercase; 167 | } 168 | 169 | span.header-tab-span { 170 | border-bottom: 2px solid #4285f4; 171 | color: #4285f4; 172 | font-weight: bold; 173 | } 174 | 175 | .mobile-header { 176 | height: 39px; 177 | display: box; 178 | display: flex; 179 | overflow-x: scroll; 180 | width: 100%; 181 | padding-left: 12px; 182 | } 183 | 184 | .mobile-header a, .mobile-header span { 185 | color: #70757a; 186 | text-decoration: none; 187 | display: inline-block; 188 | /* padding: 8px 12px 8px 12px; */ 189 | } 190 | 191 | span.mobile-tab-span { 192 | border-bottom: 2px solid #202124; 193 | color: #202124; 194 | height: 26px; 195 | /* margin: 0 12px; */ 196 | /* padding: 0; */ 197 | } 198 | 199 | .desktop-header input { 200 | margin: 2px 4px 2px 8px; 201 | } 202 | 203 | a.header-tab-a:visited { 204 | color: #70757a; 205 | } 206 | 207 | .header-tab-div-end { 208 | border-left: 1px solid rgba(0, 0, 0, 0.12); 209 | } 210 | 211 | .adv-search { 212 | font-size: 30px; 213 | } 214 | 215 | .adv-search:hover { 216 | cursor: pointer; 217 | } 218 | 219 | #adv-search-toggle { 220 | display: none; 221 | } 222 | 223 | .result-collapsible { 224 | max-height: 0px; 225 | overflow: hidden; 226 | transition: max-height .25s linear; 227 | } 228 | 229 | .search-bar-input { 230 | display: block; 231 | font-size: 16px; 232 | padding: 0 0 0 8px; 233 | flex: 1; 234 | height: 35px; 235 | outline: none; 236 | border: none; 237 | width: 100%; 238 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 239 | overflow: hidden; 240 | } 241 | 242 | #result-country { 243 | max-width: 200px; 244 | } 245 | 246 | @media (max-width: 801px) { 247 | .header-tab-div { 248 | margin-bottom: 10px !important 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /app/static/css/dark-theme.css: -------------------------------------------------------------------------------- 1 | html { 2 | background: var(--whoogle-dark-page-bg) !important; 3 | } 4 | 5 | body { 6 | background: var(--whoogle-dark-page-bg) !important; 7 | } 8 | 9 | div { 10 | color: var(--whoogle-dark-text) !important; 11 | } 12 | 13 | label { 14 | color: var(--whoogle-dark-contrast-text) !important; 15 | } 16 | 17 | li a { 18 | color: var(--whoogle-dark-result-url) !important; 19 | } 20 | 21 | li { 22 | color: var(--whoogle-dark-text) !important; 23 | } 24 | 25 | .anon-view { 26 | color: var(--whoogle-dark-text) !important; 27 | text-decoration: underline; 28 | } 29 | 30 | textarea { 31 | background: var(--whoogle-dark-page-bg) !important; 32 | color: var(--whoogle-dark-text) !important; 33 | } 34 | 35 | a:visited h3 div { 36 | color: var(--whoogle-dark-result-visited) !important; 37 | } 38 | 39 | a:link h3 div { 40 | color: var(--whoogle-dark-result-title) !important; 41 | } 42 | 43 | a:link div { 44 | color: var(--whoogle-dark-result-url) !important; 45 | } 46 | 47 | div span { 48 | color: var(--whoogle-dark-secondary-text) !important; 49 | } 50 | 51 | input { 52 | background-color: var(--whoogle-dark-page-bg) !important; 53 | color: var(--whoogle-dark-text) !important; 54 | } 55 | 56 | select { 57 | background: var(--whoogle-dark-page-bg) !important; 58 | color: var(--whoogle-dark-text) !important; 59 | } 60 | 61 | .search-container { 62 | background-color: var(--whoogle-dark-page-bg) !important; 63 | } 64 | 65 | .ZINbbc { 66 | overflow: hidden; 67 | box-shadow: 0 0 0 0 !important; 68 | background-color: var(--whoogle-dark-result-bg) !important; 69 | margin-bottom: 10px !important; 70 | border-radius: 8px !important; 71 | } 72 | 73 | .KP7LCb { 74 | box-shadow: 0 0 0 0 !important; 75 | } 76 | 77 | .BVG0Nb { 78 | box-shadow: 0 0 0 0 !important; 79 | background-color: var(--whoogle-dark-page-bg) !important; 80 | } 81 | 82 | .ZINbbc.luh4tb { 83 | background: var(--whoogle-dark-result-bg) !important; 84 | margin-bottom: 24px !important; 85 | } 86 | 87 | .bRsWnc { 88 | background-color: var(--whoogle-dark-result-bg) !important; 89 | } 90 | 91 | .x54gtf { 92 | background-color: var(--whoogle-dark-divider) !important; 93 | } 94 | 95 | .Q0HXG { 96 | background-color: var(--whoogle-dark-divider) !important; 97 | } 98 | 99 | .LKSyXe { 100 | background-color: var(--whoogle-dark-divider) !important; 101 | } 102 | 103 | .home-search { 104 | border-color: var(--whoogle-dark-element-bg) !important; 105 | } 106 | 107 | .sa1toc { 108 | background: var(--whoogle-dark-page-bg) !important; 109 | } 110 | 111 | #search-bar { 112 | border-color: var(--whoogle-dark-element-bg) !important; 113 | color: var(--whoogle-dark-text) !important; 114 | background-color: var(--whoogle-dark-result-bg) !important; 115 | border-bottom: 2px solid var(--whoogle-dark-element-bg); 116 | } 117 | 118 | #search-bar:focus { 119 | color: var(--whoogle-dark-text) !important; 120 | } 121 | 122 | #search-submit { 123 | border: 1px solid var(--whoogle-dark-element-bg) !important; 124 | background: var(--whoogle-dark-element-bg) !important; 125 | color: var(--whoogle-dark-contrast-text) !important; 126 | } 127 | 128 | .info-text { 129 | color: var(--whoogle-dark-contrast-text) !important; 130 | opacity: 75%; 131 | } 132 | 133 | .collapsible { 134 | color: var(--whoogle-dark-text) !important; 135 | } 136 | 137 | .collapsible:after { 138 | color: var(--whoogle-dark-text) !important; 139 | } 140 | 141 | .active { 142 | background-color: var(--whoogle-dark-element-bg) !important; 143 | color: var(--whoogle-dark-contrast-text) !important; 144 | } 145 | 146 | .content, .result-config { 147 | background-color: var(--whoogle-dark-element-bg) !important; 148 | color: var(--whoogle-contrast-text) !important; 149 | } 150 | 151 | .active:after { 152 | color: var(--whoogle-dark-contrast-text) !important; 153 | } 154 | 155 | .link { 156 | color: var(--whoogle-dark-contrast-text); 157 | } 158 | 159 | .link-color { 160 | color: var(--whoogle-dark-result-url) !important; 161 | } 162 | 163 | .autocomplete-items { 164 | border: 1px solid var(--whoogle-dark-element-bg); 165 | } 166 | 167 | .autocomplete-items div { 168 | color: var(--whoogle-dark-text); 169 | background-color: var(--whoogle-dark-page-bg); 170 | border-bottom: 1px solid var(--whoogle-dark-element-bg); 171 | } 172 | 173 | .autocomplete-items div:hover { 174 | background-color: var(--whoogle-dark-element-bg); 175 | color: var(--whoogle-dark-contrast-text) !important; 176 | } 177 | 178 | .autocomplete-active { 179 | background-color: var(--whoogle-dark-element-bg) !important; 180 | color: var(--whoogle-dark-contrast-text) !important; 181 | } 182 | 183 | .footer { 184 | color: var(--whoogle-dark-text); 185 | } 186 | 187 | path { 188 | fill: var(--whoogle-dark-logo); 189 | } 190 | 191 | .header-div { 192 | background-color: var(--whoogle-dark-result-bg) !important; 193 | } 194 | 195 | #search-reset { 196 | color: var(--whoogle-dark-text) !important; 197 | } 198 | 199 | .mobile-search-bar { 200 | background-color: var(--whoogle-dark-result-bg) !important; 201 | color: var(--whoogle-dark-text) !important; 202 | } 203 | 204 | .search-bar-desktop { 205 | color: var(--whoogle-dark-text) !important; 206 | } 207 | 208 | .ip-text-div, .update_available, .cb_label, .cb { 209 | color: var(--whoogle-dark-secondary-text) !important; 210 | } 211 | 212 | .cb:focus { 213 | color: var(--whoogle-dark-contrast-text) !important; 214 | } 215 | 216 | .desktop-header, .mobile-header { 217 | background-color: var(--whoogle-dark-result-bg) !important; 218 | } 219 | -------------------------------------------------------------------------------- /charts/whoogle/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for whoogle. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | nameOverride: "" 6 | fullnameOverride: "" 7 | 8 | replicaCount: 1 9 | image: 10 | repository: benbusby/whoogle-search 11 | pullPolicy: IfNotPresent 12 | # Overrides the image tag whose default is the chart appVersion. 13 | tag: "" 14 | pullSecrets: [] 15 | # - my-image-pull-secret 16 | 17 | serviceAccount: 18 | # Specifies whether a service account should be created 19 | create: true 20 | # Annotations to add to the service account 21 | annotations: {} 22 | # The name of the service account to use. 23 | # If not set and create is true, a name is generated using the fullname template 24 | name: "" 25 | 26 | conf: {} 27 | # WHOOGLE_URL_PREFIX: "" # The URL prefix to use for the whoogle instance (i.e. "/whoogle") 28 | # WHOOGLE_DOTENV: "" # Load environment variables in whoogle.env 29 | # WHOOGLE_USER: "" # The username for basic auth. WHOOGLE_PASS must also be set if used. 30 | # WHOOGLE_PASS: "" # The password for basic auth. WHOOGLE_USER must also be set if used. 31 | # WHOOGLE_PROXY_USER: "" # The username of the proxy server. 32 | # WHOOGLE_PROXY_PASS: "" # The password of the proxy server. 33 | # WHOOGLE_PROXY_TYPE: "" # The type of the proxy server. Can be "socks5", "socks4", or "http". 34 | # WHOOGLE_PROXY_LOC: "" # The location of the proxy server (host or ip). 35 | # EXPOSE_PORT: "" # The port where Whoogle will be exposed. (default 5000) 36 | # HTTPS_ONLY: "" # Enforce HTTPS. (See https://github.com/benbusby/whoogle-search#https-enforcement) 37 | # WHOOGLE_ALT_TW: "" # The twitter.com alternative to use when site alternatives are enabled in the config. 38 | # WHOOGLE_ALT_YT: "" # The youtube.com alternative to use when site alternatives are enabled in the config. 39 | # WHOOGLE_ALT_IG: "" # The instagram.com alternative to use when site alternatives are enabled in the config. 40 | # WHOOGLE_ALT_RD: "" # The reddit.com alternative to use when site alternatives are enabled in the config. 41 | # WHOOGLE_ALT_TL: "" # The Google Translate alternative to use. This is used for all "translate ____" searches. 42 | # WHOOGLE_ALT_MD: "" # The medium.com alternative to use when site alternatives are enabled in the config. 43 | # WHOOGLE_ALT_IMG: "" # The imgur.com alternative to use when site alternatives are enabled in the config. 44 | # WHOOGLE_ALT_WIKI: "" # The wikipedia.com alternative to use when site alternatives are enabled in the config. 45 | # WHOOGLE_ALT_IMDB: "" # The imdb.com alternative to use. Set to "" to continue using imdb.com when site alternatives are enabled. 46 | # WHOOGLE_ALT_QUORA: "" # The quora.com alternative to use. Set to "" to continue using quora.com when site alternatives are enabled. 47 | # WHOOGLE_AUTOCOMPLETE: "" # Controls visibility of autocomplete/search suggestions. Default on -- use '0' to disable 48 | # WHOOGLE_MINIMAL: "" # Remove everything except basic result cards from all search queries. 49 | 50 | # WHOOGLE_CONFIG_DISABLE: "" # Hide config from UI and disallow changes to config by client 51 | # WHOOGLE_CONFIG_COUNTRY: "" # Filter results by hosting country 52 | # WHOOGLE_CONFIG_LANGUAGE: "" # Set interface language 53 | # WHOOGLE_CONFIG_SEARCH_LANGUAGE: "" # Set search result language 54 | # WHOOGLE_CONFIG_BLOCK: "" # Block websites from search results (use comma-separated list) 55 | # WHOOGLE_CONFIG_THEME: "" # Set theme mode (light, dark, or system) 56 | # WHOOGLE_CONFIG_SAFE: "" # Enable safe searches 57 | # WHOOGLE_CONFIG_ALTS: "" # Use social media site alternatives (nitter, invidious, etc) 58 | # WHOOGLE_CONFIG_NEAR: "" # Restrict results to only those near a particular city 59 | # WHOOGLE_CONFIG_TOR: "" # Use Tor routing (if available) 60 | # WHOOGLE_CONFIG_NEW_TAB: "" # Always open results in new tab 61 | # WHOOGLE_CONFIG_VIEW_IMAGE: "" # Enable View Image option 62 | # WHOOGLE_CONFIG_GET_ONLY: "" # Search using GET requests only 63 | # WHOOGLE_CONFIG_URL: "" # The root url of the instance (https:///) 64 | # WHOOGLE_CONFIG_STYLE: "" # The custom CSS to use for styling (should be single line) 65 | # WHOOGLE_CONFIG_PREFERENCES_ENCRYPTED: "" # Encrypt preferences token, requires key 66 | # WHOOGLE_CONFIG_PREFERENCES_KEY: "" # Key to encrypt preferences in URL (REQUIRED to show url) 67 | 68 | podAnnotations: {} 69 | podSecurityContext: {} 70 | # fsGroup: 2000 71 | securityContext: 72 | runAsUser: 0 73 | # capabilities: 74 | # drop: 75 | # - ALL 76 | # readOnlyRootFilesystem: true 77 | 78 | service: 79 | type: ClusterIP 80 | port: 5000 81 | 82 | ingress: 83 | enabled: false 84 | className: "" 85 | annotations: {} 86 | # kubernetes.io/ingress.class: nginx 87 | # kubernetes.io/tls-acme: "true" 88 | hosts: 89 | - host: whoogle.example.com 90 | paths: 91 | - path: / 92 | pathType: ImplementationSpecific 93 | tls: [] 94 | # - secretName: chart-example-tls 95 | # hosts: 96 | # - whoogle.example.com 97 | 98 | resources: {} 99 | # requests: 100 | # cpu: 100m 101 | # memory: 128Mi 102 | # limits: 103 | # cpu: 100m 104 | # memory: 128Mi 105 | 106 | autoscaling: 107 | enabled: false 108 | minReplicas: 1 109 | maxReplicas: 100 110 | targetCPUUtilizationPercentage: 80 111 | # targetMemoryUtilizationPercentage: 80 112 | 113 | nodeSelector: {} 114 | tolerations: [] 115 | affinity: {} 116 | -------------------------------------------------------------------------------- /app/templates/opensearch.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | {% if not search_type %} 5 | Whoogle 6 | {% else %} 7 | Whoogle {{ search_name }} 8 | {% endif %} 9 | Whoogle: A lightweight, deployable Google search proxy for desktop/mobile that removes Javascript, AMP links, and ads 10 | 11 | UTF-8 12 | 13 | data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAYAAAA6/NlyAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAAASAAAAEgARslrPgAADS9JREFUaN7Nm2lsXNd1x3/nzQxnOMNdJEeUuYjUZu2Lh7IrWZZsy46btN7ioEgRSHZapEybLgHStPDSAqntNg7yIahdyAjgJg4aB66tJDBsy4ocW5QrWzYtUQslUdx3cV9nONt7tx/ekNSQQ84MOTLzB0hg3nbv/55zzzn33HOFm4Sqx58GEcEwHIALpXIQcaNUFmCJPKYQ8aJULyJDgBcRH4ZhHHn1+ZvSL0kZwX/9KTQ3g6bZgFKU2ghsBnYDxUAOkAnYAW2aMAQBLzAK9ALngLPANUQa8Psncbk48soP/jAIV33rBzA8DE5nHkp9CXggQnI14FjkZ8NAT4T4B8BvsNnaMQx15L//bXkIVz3+NFgsQihUDDwEPALcDriWOoizEAIuAb9B5KiIXFFK6Ud+/twXR7jq0JMAuRGSfwXsBGwpJjobBtACvAr8Ap+vhcxMkpV4UoSrDj8FIhpK7UKpfwK+DDhvMtHZ0IEziPwQOAYEk5F2QoSr/u4FGBkBkUyUOgz8A7DmCyY6G4PATxF5EcPowmLhyM+ejfuSJf53wbOhEqAA+Hfge0DhMpMFU7P2AFsQqUWpPs/2fdScP7U0wlWHngSRMuA54DCmW/lDgQBrgW3ANUpLOzwVO6iprV7whfhklXoR+Eq856eglPlPoeZvWASRlIUBAHWIfBvDOLWQes/bYsQSFwAvYEo2bu+UoUAg3elgRX4uK1cVkJuXjcNhR0QIhcJMjHvp7emn9/ogE+NewuEwmqbF+3Si+Bj4C5S6Mh/pmCSqDj8FkIlSLwB/CVgXJKoUIsLKogI2bV3Htp0bcRflk5HhxGqLftUwDPyTAUaGx7h2tYXaz+toberE7w+kSuLHEPlrTBfGbAs+p4Wqw0+B1SqEQn+DKd30eGRdGU7uPOBh710eCtwrkuq4zzvJlUuNHH+nmraWrlSRfgX4W8A3OyafY7Q82/eBUh7gR4A7Htni0iK+9udf4c4DlWRlZyTdYVuajVXFbtbdWk4oFKanqw9dN5ZKfC3Qgq5f8Ow6EGW5owjfEEH9B7BvoS8ahqJibQnf+ObDbNi0Bosl9jwMh3UCgSChYAgFaJoWk0xGposNGyswDIPWli4MXbEEznZgPZr2MXD9RsLTn6x64hkIBsFi+SbwIguoslKK4pIivn74QdasL5tzPxAI0t7aRXNDO91dfYyOjKHrBi5XOgWFeZRVFLN2XRnZuVlzyPt8kxz91TE+PvU5hjG/lU8Q/4WmfRcIThmwGYui62CxlADfikc2MyuDh752/xyyhmHQ0tjB74+fpv5KMxPjXpSa22m7PQ13UQF793vYvWc7TudMc05nOg9+9T7Gx7ycP3t5qar9VQzjdeBk1aEnOfLq86ZKVz3+NBgGiBwCnmBmvRoTB+69nbvu3o1oM53RdZ1TH3zG//7P2zQ1tBEKhqZ97ew/wzAYGR6n/koTA33DlK5ehdM1Q9ruSCNvRTaXLzbgn1yS9c4ADDTtXcCoOX8qQkwp0LQ8zNXPvC5IKcXKogLuuHMX2g1zVinFJx+d462jJxgaHJ13nt4ITRP0sM5nn5zn6K+OMToyHnV/dUUxO27btBTpTuEgSm2e+mGpeuIZU53hYaAKSJvvTRHhnvv3sLNycxShK5caefO1dxkbGUfTkpOGiHC9ux/RhPW3lk8HIZqmkZ7uSIWUs4FOLSPj1G2b7kDDMKbSMg9gqkBMKKVwudJZv7E8qvHJST8fnviYwYHhKBVPBqaGnOXa1Zao6yWrV7FmfVlMO5Ak/tjweotgZq6WApXxOlVYVMDKouiFUktjBw31rYgsPjwUEUZHxqk5cwE9rE9fT0uzUV5RkorQcwewA6XQUIpIwq083lv5Bbk4XdFpqsZrbUz6/EvxmdOkG6+1MTIyFnXdXVSA3Z4GSxOyC6V2EQ6jqXAYzOziggk3ESErOyNqtM3IqDcVKgfA+NgEY6MTUddy87KwO+wLrrwSxG5sNpcmNls6ZpYxLtLTo8dED+tzOrhYiJhWO+APRLfpdGCzJZSniIe1QL6GmWUsTuSNOZIUUr2mZfbcUIYiRQqUCxRrKJUb+RGHLUz6/FGXrFYruXlZKemNUgpbmhWnM1qLvN5JQqFwKprIQKlSDZFCFnBHM3wVw0Oj6PqMFbVaLZSUrUqJlJWCvBU5ZOdED+DgwDD+SX8q2nAgcosW2etJKE812D/MxLgv6tqadWVkZrlSYLgU6zaUk5UVPfa9PQMEg6ElDyhgQ6lcDXOJGNfRaaLR1ztId2dv1PXS1avYvG39ktyGUopCdz6Vf7QtKniZ9PlpbmhLmRcALIl7dDGjqrqL19B1Y2bY0mzcfd8eVpW4MQwj4c/dSNZms7L/3tspK4+2nU2N7TQ3daTUMGqYskl4CGs+uUhrc0fUtbLyW3j0zx6gwL0iKdJKKaxWC/vu3s3eA54oYsFAiE9P1zIx7kslYaUhMoG5ZRkXIsLw0CgnT3xCIBD9yuZt6/nGE4+wZp0Z+6oFFu9KKQzDICPTyZ88cpA/ffTgHB9/6UI9F89dTSVZHRizolQ/5v5sQhCBC+eusvHTi9xx523TblNEuHXzGvLyc/jow884//llhoZGCU4NjALEXAVlZWVQVlHMXffsZuOWtVitc1ek3Z29kUxmqvjiB7qtiAyi1GjihAW/P8BbR9/H7rCzq3JL1P1C9woefux+9t29m7bmTrq7+vBO+DAMA4fDTl5+Dqsriim6pRCHY37nsHe/h9bmTi7W1ie95JwHPkS6rJjS7U3mTRFhcGCEX7/+HjablS3bN0SpnmbRKCjMo6Awb/qaUiQlrdy8bB567D4mxr20NnUueul5A0aADkvl7oMhdH0TcbKUsUh7J3w01reaBN0rSEuzLfB88j3MzslkZVEBTQ1tTEws2XjVIvKKxbNlD5ih5aMk4I9nk56c9FN/uZn21i5cGU5y8rKxWBIL9pVS9PYMMDoyTlZ27GAvLz+HrOwM6i83E4zkyRaJ13E43rF4tu8DM4/1IGY6JCmICEop+q4PcrWukbbmTgKBoLmMFDN3NZXj0nWdgD/IyPAYrc0dnHz/DO/+9gPqLjRQvqaYrOzMmG0UuvPRRGht6iQcDi+GdBB4iXC4zhrRtQaU+hwoWezwaZowPublXE0dF8/Xk+Eypb0iPwdXhhNN0wj4zT2lwYFhxsa85lIwYr3feO1dvn7oQdxF+XO+bbVauOdLe/H7Axx/+9RiApxriJwFsHh27gfDCCNSBNxLgpvk80l7SuL+CLmerl7amrtoae6ks6OHgf4hfD4/RmQ7ZepvoG8I74SX9RsrYtoCi0WjuGQlA31DdHf1JivlX6JpbwCGpaa2Gs+Ou8AsE7oHKFos4VjkRQTRBE2Lzk3Hev56zwCGbrB2/Wos1rnjnmZPo2z1LfT1DtJ3fSBR0oPAsyjVhIgpTc+uA6jR0VGx293AAVJYsJYMDMOgq+M6TqeD0vLimP7X6UqnpGwVLU0djAyPJUL6OCL/iUjoyM+fMwnX1FZTufsgiAwC9wN58b5yMyAihMM67W095OZlsap4ZUx3lpnlIic3m4b61nhr5QngWUyXRM35UzPz1bNzP2K19mMYOcB+lknKIkLAH6S9rZvi0pXkF8Qe+0L3CjIznTQ3thPwB+cj/Q4iP0YkMLUxPk24prYaz9a9AO2YSb1FW+xUkPZ6J+nrHaSsojimjxYRClfm09PdT1fH9ViEe4F/AS7Z3W7OnD4GzA40LBYwjJZI0dfgchEG0801N7bz5mvvMDgwPOd+wB+k+vdnqL/chMxVxhDwEpr2O0T4yY/+fobijU/V1Fbj2bkfoBkzCNnDMqk2mFIc6B/G651kw8YKbJF6EZ/Pz9u/fp/33q7G552MJd33gGdQajxuyUPN+VN4tu8zEKkHtmDmc5cVvT39WKwWVlcUMzo8zltv/o6PPqwhHIoZddUh8o+I1E8ZqgUJA1TuOgC6PoZILbAVKGMZoesG7S1d+LyTfHSyhnM1ddOVQ7PQBnxn5I03Tjq2bk28bAmm6z0A9gIvY27HLDvmIQowgMj3gZ+hlJqvon7e1dGRV58HTYOXn/s/RL6NWfS17JiHbDsi30fkF4iohY4PLBg319RW4+nSQal24DRmBe0altGQxUAd8B3gKEoZ8UqJ4y4Uamqrqdx1AKXrA6Jpp4EsTEO23EWmIcyw8Xtit1ejlEqkbjq5AnFzXrswkwX/DKSkCGMR6AVeAo4A/RCZggkgadW84azDDsw6zMf44uqnJ4D3gZfRtBMoFUr27MPiD3mYxwHsGMYdwCHgIGY4ejPm9xBQDfwSkeNK10cdbjc/+fF3k/7Q0o/xPP40mBtVm1Hqy5jnIHaw9NMtQaABOAH8FpFPEfFiGAmr700hPE185gCIG7OA5DbMQpm1mEnCDMyyCuusdnXMJLkPM5XaDnwGnEXkLCLti1Hdm044irwpdUEpJ5APFKNUKSK3RDbgZ47iwRjQjUgX0AFcR2QcMBI5tJEs/h/GMBxGKn9DKwAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyMC0wNC0xMlQyMDoyMDo0OSswMDowME0is3UAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjAtMDQtMTJUMjA6MjA6NDkrMDA6MDA8fwvJAAAARnRFWHRzb2Z0d2FyZQBJbWFnZU1hZ2ljayA2LjcuOC05IDIwMTQtMDUtMTIgUTE2IGh0dHA6Ly93d3cuaW1hZ2VtYWdpY2sub3Jn3IbtAAAAABh0RVh0VGh1bWI6OkRvY3VtZW50OjpQYWdlcwAxp/+7LwAAABh0RVh0VGh1bWI6OkltYWdlOjpoZWlnaHQAMTkyDwByhQAAABd0RVh0VGh1bWI6OkltYWdlOjpXaWR0aAAxOTLTrCEIAAAAGXRFWHRUaHVtYjo6TWltZXR5cGUAaW1hZ2UvcG5nP7JWTgAAABd0RVh0VGh1bWI6Ok1UaW1lADE1ODY3MjI4NDlV2OpiAAAAD3RFWHRUaHVtYjo6U2l6ZQAwQkKUoj7sAAAAVnRFWHRUaHVtYjo6VVJJAGZpbGU6Ly8vbW50bG9nL2Zhdmljb25zLzIwMjAtMDQtMTIvNTdhMDYyNGFhNzAyYzk3ZWU1YTE5MjgwYWEwNTkwZDMuaWNvLnBuZ1EXWHMAAAAASUVORK5CYII= 14 | 15 | 16 | 17 | {% if search_type %} 18 | 19 | {% endif %} 20 | {% if preferences %} 21 | 22 | {% endif %} 23 | 24 | 25 | 26 | 27 | {{ main_url }}/search 28 | 29 | 30 | -------------------------------------------------------------------------------- /app/static/img/whoogle.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/templates/header.html: -------------------------------------------------------------------------------- 1 | {% if mobile %} 2 |
3 |
4 |
7 | 12 |
13 |
14 | {% if config.preferences %} 15 | 16 | {% endif %} 17 | 29 | 30 | 31 | 32 | 33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | {% for tab_id, tab_content in tabs.items() %} 44 | {% if tab_content['selected'] %} 45 | {{ tab_content['name'] }} 46 | {% else %} 47 | {{ tab_content['name'] }} 48 | {% endif %} 49 | {% endfor %} 50 | 51 | 52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 | {% else %} 61 |
62 | 69 |
70 |
74 |
75 |
76 | {% if config.preferences %} 77 | 78 | {% endif %} 79 | 90 | 91 | 92 | 93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 | {% for tab_id, tab_content in tabs.items() %} 105 | {% if tab_content['selected'] %} 106 | {{ tab_content['name'] }} 107 | {% else %} 108 | {{ tab_content['name'] }} 109 | {% endif %} 110 | {% endfor %} 111 | 112 | 113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 | {% endif %} 121 |
122 |
123 | 124 | 138 |
139 |
140 | 141 | 142 | -------------------------------------------------------------------------------- /app/utils/search.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | from typing import Any 4 | 5 | from app.filter import Filter 6 | from app.request import gen_query 7 | from app.utils.misc import get_proxy_host_url 8 | from app.utils.results import get_first_link 9 | from bs4 import BeautifulSoup as bsoup 10 | from cryptography.fernet import Fernet, InvalidToken 11 | from flask import g 12 | 13 | TOR_BANNER = '

You are using Tor


' 14 | CAPTCHA = 'div class="g-recaptcha"' 15 | 16 | 17 | def needs_https(url: str) -> bool: 18 | """Checks if the current instance needs to be upgraded to HTTPS 19 | 20 | Note that all Heroku instances are available by default over HTTPS, but 21 | do not automatically set up a redirect when visited over HTTP. 22 | 23 | Args: 24 | url: The instance url 25 | 26 | Returns: 27 | bool: True/False representing the need to upgrade 28 | 29 | """ 30 | https_only = bool(os.getenv('HTTPS_ONLY', 0)) 31 | is_heroku = url.endswith('.herokuapp.com') 32 | is_http = url.startswith('http://') 33 | 34 | return (is_heroku and is_http) or (https_only and is_http) 35 | 36 | 37 | def has_captcha(results: str) -> bool: 38 | """Checks to see if the search results are blocked by a captcha 39 | 40 | Args: 41 | results: The search page html as a string 42 | 43 | Returns: 44 | bool: True/False indicating if a captcha element was found 45 | 46 | """ 47 | return CAPTCHA in results 48 | 49 | 50 | class Search: 51 | """Search query preprocessor - used before submitting the query or 52 | redirecting to another site 53 | 54 | Attributes: 55 | request: the incoming flask request 56 | config: the current user config settings 57 | session_key: the flask user fernet key 58 | """ 59 | def __init__(self, request, config, session_key, cookies_disabled=False): 60 | method = request.method 61 | self.request = request 62 | self.request_params = request.args if method == 'GET' else request.form 63 | self.user_agent = request.headers.get('User-Agent') 64 | self.feeling_lucky = False 65 | self.config = config 66 | self.session_key = session_key 67 | self.query = '' 68 | self.cookies_disabled = cookies_disabled 69 | self.search_type = self.request_params.get( 70 | 'tbm') if 'tbm' in self.request_params else '' 71 | 72 | def __getitem__(self, name) -> Any: 73 | return getattr(self, name) 74 | 75 | def __setitem__(self, name, value) -> None: 76 | return setattr(self, name, value) 77 | 78 | def __delitem__(self, name) -> None: 79 | return delattr(self, name) 80 | 81 | def __contains__(self, name) -> bool: 82 | return hasattr(self, name) 83 | 84 | def new_search_query(self) -> str: 85 | """Parses a plaintext query into a valid string for submission 86 | 87 | Also decrypts the query string, if encrypted (in the case of 88 | paginated results). 89 | 90 | Returns: 91 | str: A valid query string 92 | 93 | """ 94 | q = self.request_params.get('q') 95 | 96 | if q is None or len(q) == 0: 97 | return '' 98 | else: 99 | # Attempt to decrypt if this is an internal link 100 | try: 101 | q = Fernet(self.session_key).decrypt(q.encode()).decode() 102 | except InvalidToken: 103 | pass 104 | 105 | # Strip leading '! ' for "feeling lucky" queries 106 | self.feeling_lucky = q.startswith('! ') 107 | self.query = q[2:] if self.feeling_lucky else q 108 | return self.query 109 | 110 | def generate_response(self) -> str: 111 | """Generates a response for the user's query 112 | 113 | Returns: 114 | str: A string response to the search query, in the form of a URL 115 | or string representation of HTML content. 116 | 117 | """ 118 | mobile = 'Android' in self.user_agent or 'iPhone' in self.user_agent 119 | # reconstruct url if X-Forwarded-Host header present 120 | root_url = get_proxy_host_url( 121 | self.request, 122 | self.request.url_root, 123 | root=True) 124 | 125 | content_filter = Filter(self.session_key, 126 | root_url=root_url, 127 | mobile=mobile, 128 | config=self.config, 129 | query=self.query) 130 | full_query = gen_query(self.query, 131 | self.request_params, 132 | self.config) 133 | self.full_query = full_query 134 | 135 | # force mobile search when view image is true and 136 | # the request is not already made by a mobile 137 | view_image = ('tbm=isch' in full_query 138 | and self.config.view_image 139 | and not g.user_request.mobile) 140 | 141 | get_body = g.user_request.send(query=full_query, 142 | force_mobile=view_image) 143 | 144 | # Produce cleanable html soup from response 145 | html_soup = bsoup(get_body.text, 'html.parser') 146 | 147 | # Replace current soup if view_image is active 148 | if view_image: 149 | html_soup = content_filter.view_image(html_soup) 150 | 151 | # Indicate whether or not a Tor connection is active 152 | if g.user_request.tor_valid: 153 | html_soup.insert(0, bsoup(TOR_BANNER, 'html.parser')) 154 | 155 | if self.feeling_lucky: 156 | return get_first_link(html_soup) 157 | else: 158 | formatted_results = content_filter.clean(html_soup) 159 | 160 | # Append user config to all search links, if available 161 | param_str = ''.join('&{}={}'.format(k, v) 162 | for k, v in 163 | self.request_params.to_dict(flat=True).items() 164 | if self.config.is_safe_key(k)) 165 | for link in formatted_results.find_all('a', href=True): 166 | link['rel'] = "nofollow noopener noreferrer" 167 | if 'search?' not in link['href'] or link['href'].index( 168 | 'search?') > 1: 169 | continue 170 | link['href'] += param_str 171 | 172 | return str(formatted_results) 173 | 174 | def check_kw_ip(self) -> re.Match: 175 | """Checks for keywords related to 'my ip' in the query 176 | 177 | Returns: 178 | bool 179 | 180 | """ 181 | return re.search("([^a-z0-9]|^)my *[^a-z0-9] *(ip|internet protocol)" + 182 | "($|( *[^a-z0-9] *(((addres|address|adres|" + 183 | "adress)|a)? *$)))", self.query.lower()) 184 | -------------------------------------------------------------------------------- /app/templates/logo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | from app.filter import clean_query 2 | from app.request import send_tor_signal 3 | from app.utils.session import generate_user_key 4 | from app.utils.bangs import gen_bangs_json 5 | from app.utils.misc import gen_file_hash, read_config_bool 6 | from base64 import b64encode 7 | from datetime import datetime, timedelta 8 | from flask import Flask 9 | import json 10 | import logging.config 11 | import os 12 | from stem import Signal 13 | import threading 14 | from dotenv import load_dotenv 15 | 16 | from werkzeug.middleware.proxy_fix import ProxyFix 17 | 18 | from app.utils.misc import read_config_bool 19 | from app.version import __version__ 20 | 21 | app = Flask(__name__, static_folder=os.path.dirname( 22 | os.path.abspath(__file__)) + '/static') 23 | 24 | app.wsgi_app = ProxyFix(app.wsgi_app) 25 | 26 | dot_env_path = ( 27 | os.path.join(os.path.dirname(os.path.abspath(__file__)), 28 | '../whoogle.env')) 29 | 30 | # Load .env file if enabled 31 | if read_config_bool('WHOOGLE_DOTENV'): 32 | load_dotenv(dot_env_path) 33 | 34 | app.default_key = generate_user_key() 35 | 36 | if read_config_bool('HTTPS_ONLY'): 37 | app.config['SESSION_COOKIE_NAME'] = '__Secure-session' 38 | app.config['SESSION_COOKIE_SECURE'] = True 39 | 40 | app.config['VERSION_NUMBER'] = __version__ 41 | app.config['APP_ROOT'] = os.getenv( 42 | 'APP_ROOT', 43 | os.path.dirname(os.path.abspath(__file__))) 44 | app.config['STATIC_FOLDER'] = os.getenv( 45 | 'STATIC_FOLDER', 46 | os.path.join(app.config['APP_ROOT'], 'static')) 47 | app.config['BUILD_FOLDER'] = os.path.join( 48 | app.config['STATIC_FOLDER'], 'build') 49 | app.config['CACHE_BUSTING_MAP'] = {} 50 | app.config['LANGUAGES'] = json.load(open( 51 | os.path.join(app.config['STATIC_FOLDER'], 'settings/languages.json'), 52 | encoding='utf-8')) 53 | app.config['COUNTRIES'] = json.load(open( 54 | os.path.join(app.config['STATIC_FOLDER'], 'settings/countries.json'), 55 | encoding='utf-8')) 56 | app.config['TRANSLATIONS'] = json.load(open( 57 | os.path.join(app.config['STATIC_FOLDER'], 'settings/translations.json'), 58 | encoding='utf-8')) 59 | app.config['THEMES'] = json.load(open( 60 | os.path.join(app.config['STATIC_FOLDER'], 'settings/themes.json'), 61 | encoding='utf-8')) 62 | app.config['HEADER_TABS'] = json.load(open( 63 | os.path.join(app.config['STATIC_FOLDER'], 'settings/header_tabs.json'), 64 | encoding='utf-8')) 65 | app.config['CONFIG_PATH'] = os.getenv( 66 | 'CONFIG_VOLUME', 67 | os.path.join(app.config['STATIC_FOLDER'], 'config')) 68 | app.config['DEFAULT_CONFIG'] = os.path.join( 69 | app.config['CONFIG_PATH'], 70 | 'config.json') 71 | app.config['CONFIG_DISABLE'] = read_config_bool('WHOOGLE_CONFIG_DISABLE') 72 | app.config['SESSION_FILE_DIR'] = os.path.join( 73 | app.config['CONFIG_PATH'], 74 | 'session') 75 | app.config['MAX_SESSION_SIZE'] = 4000 # Sessions won't exceed 4KB 76 | app.config['BANG_PATH'] = os.getenv( 77 | 'CONFIG_VOLUME', 78 | os.path.join(app.config['STATIC_FOLDER'], 'bangs')) 79 | app.config['BANG_FILE'] = os.path.join( 80 | app.config['BANG_PATH'], 81 | 'bangs.json') 82 | 83 | # Ensure all necessary directories exist 84 | if not os.path.exists(app.config['CONFIG_PATH']): 85 | os.makedirs(app.config['CONFIG_PATH']) 86 | 87 | if not os.path.exists(app.config['SESSION_FILE_DIR']): 88 | os.makedirs(app.config['SESSION_FILE_DIR']) 89 | 90 | if not os.path.exists(app.config['BANG_PATH']): 91 | os.makedirs(app.config['BANG_PATH']) 92 | 93 | if not os.path.exists(app.config['BUILD_FOLDER']): 94 | os.makedirs(app.config['BUILD_FOLDER']) 95 | 96 | # Session values 97 | app_key_path = os.path.join(app.config['CONFIG_PATH'], 'whoogle.key') 98 | if os.path.exists(app_key_path): 99 | app.config['SECRET_KEY'] = open(app_key_path, 'r').read() 100 | else: 101 | app.config['SECRET_KEY'] = str(b64encode(os.urandom(32))) 102 | with open(app_key_path, 'w') as key_file: 103 | key_file.write(app.config['SECRET_KEY']) 104 | key_file.close() 105 | app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=365) 106 | 107 | # NOTE: SESSION_COOKIE_SAMESITE must be set to 'lax' to allow the user's 108 | # previous session to persist when accessing the instance from an external 109 | # link. Setting this value to 'strict' causes Whoogle to revalidate a new 110 | # session, and fail, resulting in cookies being disabled. 111 | app.config['SESSION_COOKIE_SAMESITE'] = 'Lax' 112 | 113 | # Config fields that are used to check for updates 114 | app.config['RELEASES_URL'] = 'https://github.com/' \ 115 | 'benbusby/whoogle-search/releases' 116 | app.config['LAST_UPDATE_CHECK'] = datetime.now() - timedelta(hours=24) 117 | app.config['HAS_UPDATE'] = '' 118 | 119 | # The alternative to Google Translate is treated a bit differently than other 120 | # social media site alternatives, in that it is used for any translation 121 | # related searches. 122 | translate_url = os.getenv('WHOOGLE_ALT_TL', 'https://farside.link/lingva') 123 | if not translate_url.startswith('http'): 124 | translate_url = 'https://' + translate_url 125 | app.config['TRANSLATE_URL'] = translate_url 126 | 127 | app.config['CSP'] = 'default-src \'none\';' \ 128 | 'frame-src ' + translate_url + ';' \ 129 | 'manifest-src \'self\';' \ 130 | 'img-src \'self\' data:;' \ 131 | 'style-src \'self\' \'unsafe-inline\';' \ 132 | 'script-src \'self\';' \ 133 | 'media-src \'self\';' \ 134 | 'connect-src \'self\';' 135 | 136 | # Generate DDG bang filter 137 | if not os.path.exists(app.config['BANG_FILE']): 138 | json.dump({}, open(app.config['BANG_FILE'], 'w')) 139 | bangs_thread = threading.Thread( 140 | target=gen_bangs_json, 141 | args=(app.config['BANG_FILE'],)) 142 | bangs_thread.start() 143 | 144 | # Build new mapping of static files for cache busting 145 | cache_busting_dirs = ['css', 'js'] 146 | for cb_dir in cache_busting_dirs: 147 | full_cb_dir = os.path.join(app.config['STATIC_FOLDER'], cb_dir) 148 | for cb_file in os.listdir(full_cb_dir): 149 | # Create hash from current file state 150 | full_cb_path = os.path.join(full_cb_dir, cb_file) 151 | cb_file_link = gen_file_hash(full_cb_dir, cb_file) 152 | build_path = os.path.join(app.config['BUILD_FOLDER'], cb_file_link) 153 | 154 | try: 155 | os.symlink(full_cb_path, build_path) 156 | except FileExistsError: 157 | # Symlink hasn't changed, ignore 158 | pass 159 | 160 | # Create mapping for relative path urls 161 | map_path = build_path.replace(app.config['APP_ROOT'], '') 162 | if map_path.startswith('/'): 163 | map_path = map_path[1:] 164 | app.config['CACHE_BUSTING_MAP'][cb_file] = map_path 165 | 166 | # Templating functions 167 | app.jinja_env.globals.update(clean_query=clean_query) 168 | app.jinja_env.globals.update( 169 | cb_url=lambda f: app.config['CACHE_BUSTING_MAP'][f]) 170 | 171 | # Attempt to acquire tor identity, to determine if Tor config is available 172 | send_tor_signal(Signal.HEARTBEAT) 173 | 174 | from app import routes # noqa 175 | 176 | # Disable logging from imported modules 177 | logging.config.dictConfig({ 178 | 'version': 1, 179 | 'disable_existing_loggers': True, 180 | }) 181 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Whoogle Search", 3 | "description": "A lightweight, privacy-oriented, containerized Google search proxy for desktop/mobile that removes Javascript, AMP links, tracking, and ads/sponsored content", 4 | "repository": "https://github.com/benbusby/whoogle-search", 5 | "logo": "https://raw.githubusercontent.com/benbusby/whoogle-search/master/app/static/img/favicon/ms-icon-150x150.png", 6 | "keywords": [ 7 | "search", 8 | "metasearch", 9 | "flask", 10 | "docker", 11 | "heroku", 12 | "adblock", 13 | "degoogle", 14 | "privacy" 15 | ], 16 | "stack": "container", 17 | "env": { 18 | "WHOOGLE_URL_PREFIX": { 19 | "description": "The URL prefix to use for the whoogle instance (i.e. \"/whoogle\")", 20 | "value": "", 21 | "required": false 22 | }, 23 | "WHOOGLE_USER": { 24 | "description": "The username for basic auth. WHOOGLE_PASS must also be set if used. Leave empty to disable.", 25 | "value": "", 26 | "required": false 27 | }, 28 | "WHOOGLE_PASS": { 29 | "description": "The password for basic auth. WHOOGLE_USER must also be set if used. Leave empty to disable.", 30 | "value": "", 31 | "required": false 32 | }, 33 | "WHOOGLE_PROXY_USER": { 34 | "description": "The username of the proxy server. Leave empty to disable.", 35 | "value": "", 36 | "required": false 37 | }, 38 | "WHOOGLE_PROXY_PASS": { 39 | "description": "The password of the proxy server. Leave empty to disable.", 40 | "value": "", 41 | "required": false 42 | }, 43 | "WHOOGLE_PROXY_TYPE": { 44 | "description": "The type of the proxy server. For example \"socks5\". Leave empty to disable.", 45 | "value": "", 46 | "required": false 47 | }, 48 | "WHOOGLE_PROXY_LOC": { 49 | "description": "The location of the proxy server (host or ip). Leave empty to disable.", 50 | "value": "", 51 | "required": false 52 | }, 53 | "WHOOGLE_ALT_TW": { 54 | "description": "The site to use as a replacement for twitter.com when site alternatives are enabled in the config.", 55 | "value": "farside.link/nitter", 56 | "required": false 57 | }, 58 | "WHOOGLE_ALT_YT": { 59 | "description": "The site to use as a replacement for youtube.com when site alternatives are enabled in the config.", 60 | "value": "farside.link/invidious", 61 | "required": false 62 | }, 63 | "WHOOGLE_ALT_IG": { 64 | "description": "The site to use as a replacement for instagram.com when site alternatives are enabled in the config.", 65 | "value": "farside.link/bibliogram/u", 66 | "required": false 67 | }, 68 | "WHOOGLE_ALT_RD": { 69 | "description": "The site to use as a replacement for reddit.com when site alternatives are enabled in the config.", 70 | "value": "farside.link/libreddit", 71 | "required": false 72 | }, 73 | "WHOOGLE_ALT_MD": { 74 | "description": "The site to use as a replacement for medium.com when site alternatives are enabled in the config.", 75 | "value": "farside.link/scribe", 76 | "required": false 77 | }, 78 | "WHOOGLE_ALT_TL": { 79 | "description": "The Google Translate alternative to use for all searches following the 'translate ___' structure.", 80 | "value": "farside.link/lingva", 81 | "required": false 82 | }, 83 | "WHOOGLE_ALT_IMG": { 84 | "description": "The site to use as a replacement for imgur.com when site alternatives are enabled in the config.", 85 | "value": "farside.link/rimgo", 86 | "required": false 87 | }, 88 | "WHOOGLE_ALT_WIKI": { 89 | "description": "The site to use as a replacement for wikipedia.com when site alternatives are enabled in the config.", 90 | "value": "farside.link/wikiless", 91 | "required": false 92 | }, 93 | "WHOOGLE_ALT_IMDB": { 94 | "description": "The site to use as a replacement for imdb.com when site alternatives are enabled in the config.", 95 | "value": "farside.link/libremdb", 96 | "required": false 97 | }, 98 | "WHOOGLE_ALT_QUORA": { 99 | "description": "The site to use as a replacement for quora.com when site alternatives are enabled in the config.", 100 | "value": "farside.link/quetre", 101 | "required": false 102 | }, 103 | "WHOOGLE_MINIMAL": { 104 | "description": "Remove everything except basic result cards from all search queries (set to 1 or leave blank)", 105 | "value": "", 106 | "required": false 107 | }, 108 | "WHOOGLE_CONFIG_COUNTRY": { 109 | "description": "[CONFIG] The country to use for restricting search results (use values from https://raw.githubusercontent.com/benbusby/whoogle-search/develop/app/static/settings/countries.json)", 110 | "value": "", 111 | "required": false 112 | }, 113 | "WHOOGLE_CONFIG_LANGUAGE": { 114 | "description": "[CONFIG] The language to use for the interface (use values from https://raw.githubusercontent.com/benbusby/whoogle-search/develop/app/static/settings/languages.json)", 115 | "value": "", 116 | "required": false 117 | }, 118 | "WHOOGLE_CONFIG_SEARCH_LANGUAGE": { 119 | "description": "[CONFIG] The language to use for search results (use values from https://raw.githubusercontent.com/benbusby/whoogle-search/develop/app/static/settings/languages.json)", 120 | "value": "", 121 | "required": false 122 | }, 123 | "WHOOGLE_CONFIG_DISABLE": { 124 | "description": "[CONFIG] Disable ability for client to change config (set to 1 or leave blank)", 125 | "value": "", 126 | "required": false 127 | }, 128 | "WHOOGLE_CONFIG_BLOCK": { 129 | "description": "[CONFIG] Block websites from search results (comma-separated list)", 130 | "value": "", 131 | "required": false 132 | }, 133 | "WHOOGLE_CONFIG_THEME": { 134 | "description": "[CONFIG] Set theme to 'dark', 'light', or 'system'", 135 | "value": "system", 136 | "required": false 137 | }, 138 | "WHOOGLE_CONFIG_SAFE": { 139 | "description": "[CONFIG] Use safe mode for searches (set to 1 or leave blank)", 140 | "value": "", 141 | "required": false 142 | }, 143 | "WHOOGLE_CONFIG_ALTS": { 144 | "description": "[CONFIG] Use social media alternatives (set to 1 or leave blank)", 145 | "value": "", 146 | "required": false 147 | }, 148 | "WHOOGLE_CONFIG_NEAR": { 149 | "description": "[CONFIG] Restrict results to only those near a particular city", 150 | "value": "", 151 | "required": false 152 | }, 153 | "WHOOGLE_CONFIG_TOR": { 154 | "description": "[CONFIG] Use Tor, if available (set to 1 or leave blank)", 155 | "value": "", 156 | "required": false 157 | }, 158 | "WHOOGLE_CONFIG_NEW_TAB": { 159 | "description": "[CONFIG] Always open results in new tab (set to 1 or leave blank)", 160 | "value": "", 161 | "required": false 162 | }, 163 | "WHOOGLE_CONFIG_VIEW_IMAGE": { 164 | "description": "[CONFIG] Enable View Image option (set to 1 or leave blank)", 165 | "value": "", 166 | "required": false 167 | }, 168 | "WHOOGLE_CONFIG_GET_ONLY": { 169 | "description": "[CONFIG] Search using GET requests only (set to 1 or leave blank)", 170 | "value": "", 171 | "required": false 172 | }, 173 | "WHOOGLE_CONFIG_STYLE": { 174 | "description": "[CONFIG] Custom CSS styling (paste in CSS or leave blank)", 175 | "value": ":root { /* LIGHT THEME COLORS */ --whoogle-background: #d8dee9; --whoogle-accent: #2e3440; --whoogle-text: #3B4252; --whoogle-contrast-text: #eceff4; --whoogle-secondary-text: #70757a; --whoogle-result-bg: #fff; --whoogle-result-title: #4c566a; --whoogle-result-url: #81a1c1; --whoogle-result-visited: #a3be8c; /* DARK THEME COLORS */ --whoogle-dark-background: #222; --whoogle-dark-accent: #685e79; --whoogle-dark-text: #fff; --whoogle-dark-contrast-text: #000; --whoogle-dark-secondary-text: #bbb; --whoogle-dark-result-bg: #000; --whoogle-dark-result-title: #1967d2; --whoogle-dark-result-url: #4b11a8; --whoogle-dark-result-visited: #bbbbff; }", 176 | "required": false 177 | }, 178 | "WHOOGLE_CONFIG_PREFERENCES_ENCRYPTED": { 179 | "description": "[CONFIG] Encrypt preferences token, requires WHOOGLE_CONFIG_PREFERENCES_KEY to be set", 180 | "value": "", 181 | "required": false 182 | }, 183 | "WHOOGLE_CONFIG_PREFERENCES_KEY": { 184 | "description": "[CONFIG] Key to encrypt preferences", 185 | "value": "NEEDS_TO_BE_MODIFIED", 186 | "required": false 187 | } 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /app/models/config.py: -------------------------------------------------------------------------------- 1 | from inspect import Attribute 2 | from app.utils.misc import read_config_bool 3 | from flask import current_app 4 | import os 5 | import re 6 | from base64 import urlsafe_b64encode, urlsafe_b64decode 7 | import pickle 8 | from cryptography.fernet import Fernet 9 | import hashlib 10 | import brotli 11 | 12 | 13 | class Config: 14 | def __init__(self, **kwargs): 15 | app_config = current_app.config 16 | self.url = os.getenv('WHOOGLE_CONFIG_URL', '') 17 | self.lang_search = os.getenv('WHOOGLE_CONFIG_SEARCH_LANGUAGE', '') 18 | self.lang_interface = os.getenv('WHOOGLE_CONFIG_LANGUAGE', '') 19 | self.style = os.getenv( 20 | 'WHOOGLE_CONFIG_STYLE', 21 | open(os.path.join(app_config['STATIC_FOLDER'], 22 | 'css/variables.css')).read()) 23 | self.block = os.getenv('WHOOGLE_CONFIG_BLOCK', '') 24 | self.block_title = os.getenv('WHOOGLE_CONFIG_BLOCK_TITLE', '') 25 | self.block_url = os.getenv('WHOOGLE_CONFIG_BLOCK_URL', '') 26 | self.country = os.getenv('WHOOGLE_CONFIG_COUNTRY', '') 27 | self.theme = os.getenv('WHOOGLE_CONFIG_THEME', 'system') 28 | self.safe = read_config_bool('WHOOGLE_CONFIG_SAFE') 29 | self.dark = read_config_bool('WHOOGLE_CONFIG_DARK') # deprecated 30 | self.alts = read_config_bool('WHOOGLE_CONFIG_ALTS') 31 | self.nojs = read_config_bool('WHOOGLE_CONFIG_NOJS') 32 | self.tor = read_config_bool('WHOOGLE_CONFIG_TOR') 33 | self.near = os.getenv('WHOOGLE_CONFIG_NEAR', '') 34 | self.new_tab = read_config_bool('WHOOGLE_CONFIG_NEW_TAB') 35 | self.view_image = read_config_bool('WHOOGLE_CONFIG_VIEW_IMAGE') 36 | self.get_only = read_config_bool('WHOOGLE_CONFIG_GET_ONLY') 37 | self.anon_view = read_config_bool('WHOOGLE_CONFIG_ANON_VIEW') 38 | self.preferences_encrypted = read_config_bool('WHOOGLE_CONFIG_PREFERENCES_ENCRYPTED') 39 | self.preferences_key = os.getenv('WHOOGLE_CONFIG_PREFERENCES_KEY', '') 40 | 41 | self.accept_language = False 42 | 43 | self.safe_keys = [ 44 | 'lang_search', 45 | 'lang_interface', 46 | 'country', 47 | 'theme', 48 | 'alts', 49 | 'new_tab', 50 | 'view_image', 51 | 'block', 52 | 'safe', 53 | 'nojs', 54 | 'anon_view', 55 | 'preferences_encrypted' 56 | ] 57 | 58 | # Skip setting custom config if there isn't one 59 | if kwargs: 60 | mutable_attrs = self.get_mutable_attrs() 61 | for attr in mutable_attrs: 62 | if attr in kwargs.keys(): 63 | setattr(self, attr, kwargs[attr]) 64 | elif attr not in kwargs.keys() and mutable_attrs[attr] == bool: 65 | setattr(self, attr, False) 66 | 67 | def __getitem__(self, name): 68 | return getattr(self, name) 69 | 70 | def __setitem__(self, name, value): 71 | return setattr(self, name, value) 72 | 73 | def __delitem__(self, name): 74 | return delattr(self, name) 75 | 76 | def __contains__(self, name): 77 | return hasattr(self, name) 78 | 79 | def get_mutable_attrs(self): 80 | return {name: type(attr) for name, attr in self.__dict__.items() 81 | if not name.startswith("__") 82 | and (type(attr) is bool or type(attr) is str)} 83 | 84 | def get_attrs(self): 85 | return {name: attr for name, attr in self.__dict__.items() 86 | if not name.startswith("__") 87 | and (type(attr) is bool or type(attr) is str)} 88 | 89 | @property 90 | def preferences(self) -> str: 91 | # if encryption key is not set will uncheck preferences encryption 92 | if self.preferences_encrypted: 93 | self.preferences_encrypted = bool(self.preferences_key) 94 | 95 | # add a tag for visibility if preferences token startswith 'e' it means 96 | # the token is encrypted, 'u' means the token is unencrypted and can be 97 | # used by other whoogle instances 98 | encrypted_flag = "e" if self.preferences_encrypted else 'u' 99 | preferences_digest = self._encode_preferences() 100 | return f"{encrypted_flag}{preferences_digest}" 101 | 102 | def is_safe_key(self, key) -> bool: 103 | """Establishes a group of config options that are safe to set 104 | in the url. 105 | 106 | Args: 107 | key (str) -- the key to check against 108 | 109 | Returns: 110 | bool -- True/False depending on if the key is in the "safe" 111 | array 112 | """ 113 | 114 | return key in self.safe_keys 115 | 116 | def get_localization_lang(self): 117 | """Returns the correct language to use for localization, but falls 118 | back to english if not set. 119 | 120 | Returns: 121 | str -- the localization language string 122 | """ 123 | if (self.lang_interface and 124 | self.lang_interface in current_app.config['TRANSLATIONS']): 125 | return self.lang_interface 126 | 127 | return 'lang_en' 128 | 129 | def from_params(self, params) -> 'Config': 130 | """Modify user config with search parameters. This is primarily 131 | used for specifying configuration on a search-by-search basis on 132 | public instances. 133 | 134 | Args: 135 | params -- the url arguments (can be any deemed safe by is_safe()) 136 | 137 | Returns: 138 | Config -- a modified config object 139 | """ 140 | if 'preferences' in params: 141 | params_new = self._decode_preferences(params['preferences']) 142 | # if preferences leads to an empty dictionary it means preferences 143 | # parameter was not decrypted successfully 144 | if len(params_new): 145 | params = params_new 146 | 147 | for param_key in params.keys(): 148 | if not self.is_safe_key(param_key): 149 | continue 150 | param_val = params.get(param_key) 151 | 152 | if param_val == 'off': 153 | param_val = False 154 | elif isinstance(param_val, str): 155 | if param_val.isdigit(): 156 | param_val = int(param_val) 157 | 158 | self[param_key] = param_val 159 | return self 160 | 161 | def to_params(self, keys: list = []) -> str: 162 | """Generates a set of safe params for using in Whoogle URLs 163 | 164 | Args: 165 | keys (list) -- optional list of keys of URL parameters 166 | 167 | Returns: 168 | str -- a set of URL parameters 169 | """ 170 | if not len(keys): 171 | keys = self.safe_keys 172 | 173 | param_str = '' 174 | for safe_key in keys: 175 | if not self[safe_key]: 176 | continue 177 | param_str = param_str + f'&{safe_key}={self[safe_key]}' 178 | 179 | return param_str 180 | 181 | def _get_fernet_key(self, password: str) -> bytes: 182 | hash_object = hashlib.md5(password.encode()) 183 | key = urlsafe_b64encode(hash_object.hexdigest().encode()) 184 | return key 185 | 186 | def _encode_preferences(self) -> str: 187 | encoded_preferences = brotli.compress(pickle.dumps(self.get_attrs())) 188 | if self.preferences_encrypted: 189 | if self.preferences_key != '': 190 | key = self._get_fernet_key(self.preferences_key) 191 | encoded_preferences = Fernet(key).encrypt(encoded_preferences) 192 | encoded_preferences = brotli.compress(encoded_preferences) 193 | 194 | return urlsafe_b64encode(encoded_preferences).decode() 195 | 196 | def _decode_preferences(self, preferences: str) -> dict: 197 | mode = preferences[0] 198 | preferences = preferences[1:] 199 | if mode == 'e': # preferences are encrypted 200 | try: 201 | key = self._get_fernet_key(self.preferences_key) 202 | 203 | config = Fernet(key).decrypt( 204 | brotli.decompress(urlsafe_b64decode(preferences.encode())) 205 | ) 206 | 207 | config = pickle.loads(brotli.decompress(config)) 208 | except Exception: 209 | config = {} 210 | elif mode == 'u': # preferences are not encrypted 211 | config = pickle.loads( 212 | brotli.decompress(urlsafe_b64decode(preferences.encode())) 213 | ) 214 | else: # preferences are incorrectly formatted 215 | config = {} 216 | return config 217 | -------------------------------------------------------------------------------- /app/templates/imageresults.html: -------------------------------------------------------------------------------- 1 |
2 | 322 |
323 |
324 |
325 |
326 | 327 | 328 |
329 |
330 |
331 |
332 | 333 | {% for i in range((length // 4) + 1) %} 334 | 335 | {% for j in range([length - (i*4), 4]|min) %} 336 | 381 | {% endfor %} 382 | 383 | {% endfor %} 384 |
337 |
338 |
339 | 378 |
379 |
380 |
385 |
386 | 387 | 388 |
389 |
390 |
391 | -------------------------------------------------------------------------------- /app/static/settings/countries.json: -------------------------------------------------------------------------------- 1 | [ 2 | {"name": "-------", "value": ""}, 3 | {"name": "Afghanistan", "value": "AF"}, 4 | {"name": "Albania", "value": "AL"}, 5 | {"name": "Algeria", "value": "DZ"}, 6 | {"name": "American Samoa", "value": "AS"}, 7 | {"name": "Andorra", "value": "AD"}, 8 | {"name": "Angola", "value": "AO"}, 9 | {"name": "Anguilla", "value": "AI"}, 10 | {"name": "Antarctica", "value": "AQ"}, 11 | {"name": "Antigua and Barbuda", "value": "AG"}, 12 | {"name": "Argentina", "value": "AR"}, 13 | {"name": "Armenia", "value": "AM"}, 14 | {"name": "Aruba", "value": "AW"}, 15 | {"name": "Australia", "value": "AU"}, 16 | {"name": "Austria", "value": "AT"}, 17 | {"name": "Azerbaijan", "value": "AZ"}, 18 | {"name": "Bahamas", "value": "BS"}, 19 | {"name": "Bahrain", "value": "BH"}, 20 | {"name": "Bangladesh", "value": "BD"}, 21 | {"name": "Barbados", "value": "BB"}, 22 | {"name": "Belarus", "value": "BY"}, 23 | {"name": "Belgium", "value": "BE"}, 24 | {"name": "Belize", "value": "BZ"}, 25 | {"name": "Benin", "value": "BJ"}, 26 | {"name": "Bermuda", "value": "BM"}, 27 | {"name": "Bhutan", "value": "BT"}, 28 | {"name": "Bolivia", "value": "BO"}, 29 | {"name": "Bosnia and Herzegovina", "value": "BA"}, 30 | {"name": "Botswana", "value": "BW"}, 31 | {"name": "Bouvet Island", "value": "BV"}, 32 | {"name": "Brazil", "value": "BR"}, 33 | {"name": "British Indian Ocean Territory", "value": "IO"}, 34 | {"name": "Brunei Darussalam", "value": "BN"}, 35 | {"name": "Bulgaria", "value": "BG"}, 36 | {"name": "Burkina Faso", "value": "BF"}, 37 | {"name": "Burundi", "value": "BI"}, 38 | {"name": "Cambodia", "value": "KH"}, 39 | {"name": "Cameroon", "value": "CM"}, 40 | {"name": "Canada", "value": "CA"}, 41 | {"name": "Cape Verde", "value": "CV"}, 42 | {"name": "Cayman Islands", "value": "KY"}, 43 | {"name": "Central African Republic", "value": "CF"}, 44 | {"name": "Chad", "value": "TD"}, 45 | {"name": "Chile", "value": "CL"}, 46 | {"name": "China", "value": "CN"}, 47 | {"name": "Christmas Island", "value": "CX"}, 48 | {"name": "Cocos (Keeling) Islands", "value": "CC"}, 49 | {"name": "Colombia", "value": "CO"}, 50 | {"name": "Comoros", "value": "KM"}, 51 | {"name": "Congo", "value": "CG"}, 52 | {"name": "Congo, Democratic Republic of the", "value": "CD"}, 53 | {"name": "Cook Islands", "value": "CK"}, 54 | {"name": "Costa Rica", "value": "CR"}, 55 | {"name": "Cote D'ivoire", "value": "CI"}, 56 | {"name": "Croatia (Hrvatska)", "value": "HR"}, 57 | {"name": "Cuba", "value": "CU"}, 58 | {"name": "Cyprus", "value": "CY"}, 59 | {"name": "Czech Republic", "value": "CZ"}, 60 | {"name": "Denmark", "value": "DK"}, 61 | {"name": "Djibouti", "value": "DJ"}, 62 | {"name": "Dominica", "value": "DM"}, 63 | {"name": "Dominican Republic", "value": "DO"}, 64 | {"name": "East Timor", "value": "TP"}, 65 | {"name": "Ecuador", "value": "EC"}, 66 | {"name": "Egypt", "value": "EG"}, 67 | {"name": "El Salvador", "value": "SV"}, 68 | {"name": "Equatorial Guinea", "value": "GQ"}, 69 | {"name": "Eritrea", "value": "ER"}, 70 | {"name": "Estonia", "value": "EE"}, 71 | {"name": "Ethiopia", "value": "ET"}, 72 | {"name": "European Union", "value": "EU"}, 73 | {"name": "Falkland Islands (Malvinas)", "value": "FK"}, 74 | {"name": "Faroe Islands", "value": "FO"}, 75 | {"name": "Fiji", "value": "FJ"}, 76 | {"name": "Finland", "value": "FI"}, 77 | {"name": "France", "value": "FR"}, 78 | {"name": "France, Metropolitan", "value": "FX"}, 79 | {"name": "French Guiana", "value": "GF"}, 80 | {"name": "French Polynesia", "value": "PF"}, 81 | {"name": "French Southern Territories", "value": "TF"}, 82 | {"name": "Gabon", "value": "GA"}, 83 | {"name": "Gambia", "value": "GM"}, 84 | {"name": "Georgia", "value": "GE"}, 85 | {"name": "Germany", "value": "DE"}, 86 | {"name": "Ghana", "value": "GH"}, 87 | {"name": "Gibraltar", "value": "GI"}, 88 | {"name": "Greece", "value": "GR"}, 89 | {"name": "Greenland", "value": "GL"}, 90 | {"name": "Grenada", "value": "GD"}, 91 | {"name": "Guadeloupe", "value": "GP"}, 92 | {"name": "Guam", "value": "GU"}, 93 | {"name": "Guatemala", "value": "GT"}, 94 | {"name": "Guinea", "value": "GN"}, 95 | {"name": "Guinea-Bissau", "value": "GW"}, 96 | {"name": "Guyana", "value": "GY"}, 97 | {"name": "Haiti", "value": "HT"}, 98 | {"name": "Heard Island and Mcdonald Islands", "value": "HM"}, 99 | {"name": "Holy See (Vatican City State)", "value": "VA"}, 100 | {"name": "Honduras", "value": "HN"}, 101 | {"name": "Hong Kong", "value": "HK"}, 102 | {"name": "Hungary", "value": "HU"}, 103 | {"name": "Iceland", "value": "IS"}, 104 | {"name": "India", "value": "IN"}, 105 | {"name": "Indonesia", "value": "ID"}, 106 | {"name": "Iran, Islamic Republic of", "value": "IR"}, 107 | {"name": "Iraq", "value": "IQ"}, 108 | {"name": "Ireland", "value": "IE"}, 109 | {"name": "Israel", "value": "IL"}, 110 | {"name": "Italy", "value": "IT"}, 111 | {"name": "Jamaica", "value": "JM"}, 112 | {"name": "Japan", "value": "JP"}, 113 | {"name": "Jordan", "value": "JO"}, 114 | {"name": "Kazakhstan", "value": "KZ"}, 115 | {"name": "Kenya", "value": "KE"}, 116 | {"name": "Kiribati", "value": "KI"}, 117 | {"name": "Korea, Democratic People's Republic of", "value": "KP"}, 118 | {"name": "Korea, Republic of", "value": "KR"}, 119 | {"name": "Kuwait", "value": "KW"}, 120 | {"name": "Kyrgyzstan", "value": "KG"}, 121 | {"name": "Lao People's Democratic Republic", "value": "LA"}, 122 | {"name": "Latvia", "value": "LV"}, 123 | {"name": "Lebanon", "value": "LB"}, 124 | {"name": "Lesotho", "value": "LS"}, 125 | {"name": "Liberia", "value": "LR"}, 126 | {"name": "Libyan Arab Jamahiriya", "value": "LY"}, 127 | {"name": "Liechtenstein", "value": "LI"}, 128 | {"name": "Lithuania", "value": "LT"}, 129 | {"name": "Luxembourg", "value": "LU"}, 130 | {"name": "Macao", "value": "MO"}, 131 | {"name": "Macedonia, the Former Yugosalv Republic of", 132 | "value": "MK"}, 133 | {"name": "Madagascar", "value": "MG"}, 134 | {"name": "Malawi", "value": "MW"}, 135 | {"name": "Malaysia", "value": "MY"}, 136 | {"name": "Maldives", "value": "MV"}, 137 | {"name": "Mali", "value": "ML"}, 138 | {"name": "Malta", "value": "MT"}, 139 | {"name": "Marshall Islands", "value": "MH"}, 140 | {"name": "Martinique", "value": "MQ"}, 141 | {"name": "Mauritania", "value": "MR"}, 142 | {"name": "Mauritius", "value": "MU"}, 143 | {"name": "Mayotte", "value": "YT"}, 144 | {"name": "Mexico", "value": "MX"}, 145 | {"name": "Micronesia, Federated States of", "value": "FM"}, 146 | {"name": "Moldova, Republic of", "value": "MD"}, 147 | {"name": "Monaco", "value": "MC"}, 148 | {"name": "Mongolia", "value": "MN"}, 149 | {"name": "Montserrat", "value": "MS"}, 150 | {"name": "Morocco", "value": "MA"}, 151 | {"name": "Mozambique", "value": "MZ"}, 152 | {"name": "Myanmar", "value": "MM"}, 153 | {"name": "Namibia", "value": "NA"}, 154 | {"name": "Nauru", "value": "NR"}, 155 | {"name": "Nepal", "value": "NP"}, 156 | {"name": "Netherlands", "value": "NL"}, 157 | {"name": "Netherlands Antilles", "value": "AN"}, 158 | {"name": "New Caledonia", "value": "NC"}, 159 | {"name": "New Zealand", "value": "NZ"}, 160 | {"name": "Nicaragua", "value": "NI"}, 161 | {"name": "Niger", "value": "NE"}, 162 | {"name": "Nigeria", "value": "NG"}, 163 | {"name": "Niue", "value": "NU"}, 164 | {"name": "Norfolk Island", "value": "NF"}, 165 | {"name": "Northern Mariana Islands", "value": "MP"}, 166 | {"name": "Norway", "value": "NO"}, 167 | {"name": "Oman", "value": "OM"}, 168 | {"name": "Pakistan", "value": "PK"}, 169 | {"name": "Palau", "value": "PW"}, 170 | {"name": "Palestinian Territory", "value": "PS"}, 171 | {"name": "Panama", "value": "PA"}, 172 | {"name": "Papua New Guinea", "value": "PG"}, 173 | {"name": "Paraguay", "value": "PY"}, 174 | {"name": "Peru", "value": "PE"}, 175 | {"name": "Philippines", "value": "PH"}, 176 | {"name": "Pitcairn", "value": "PN"}, 177 | {"name": "Poland", "value": "PL"}, 178 | {"name": "Portugal", "value": "PT"}, 179 | {"name": "Puerto Rico", "value": "PR"}, 180 | {"name": "Qatar", "value": "QA"}, 181 | {"name": "Reunion", "value": "RE"}, 182 | {"name": "Romania", "value": "RO"}, 183 | {"name": "Russian Federation", "value": "RU"}, 184 | {"name": "Rwanda", "value": "RW"}, 185 | {"name": "Saint Helena", "value": "SH"}, 186 | {"name": "Saint Kitts and Nevis", "value": "KN"}, 187 | {"name": "Saint Lucia", "value": "LC"}, 188 | {"name": "Saint Pierre and Miquelon", "value": "PM"}, 189 | {"name": "Saint Vincent and the Grenadines", "value": "VC"}, 190 | {"name": "Samoa", "value": "WS"}, 191 | {"name": "San Marino", "value": "SM"}, 192 | {"name": "Sao Tome and Principe", "value": "ST"}, 193 | {"name": "Saudi Arabia", "value": "SA"}, 194 | {"name": "Senegal", "value": "SN"}, 195 | {"name": "Serbia and Montenegro", "value": "CS"}, 196 | {"name": "Seychelles", "value": "SC"}, 197 | {"name": "Sierra Leone", "value": "SL"}, 198 | {"name": "Singapore", "value": "SG"}, 199 | {"name": "Slovakia", "value": "SK"}, 200 | {"name": "Slovenia", "value": "SI"}, 201 | {"name": "Solomon Islands", "value": "SB"}, 202 | {"name": "Somalia", "value": "SO"}, 203 | {"name": "South Africa", "value": "ZA"}, 204 | {"name": "South Georgia and the South Sandwich Islands", 205 | "value": "GS"}, 206 | {"name": "Spain", "value": "ES"}, 207 | {"name": "Sri Lanka", "value": "LK"}, 208 | {"name": "Sudan", "value": "SD"}, 209 | {"name": "Suriname", "value": "SR"}, 210 | {"name": "Svalbard and Jan Mayen", "value": "SJ"}, 211 | {"name": "Swaziland", "value": "SZ"}, 212 | {"name": "Sweden", "value": "SE"}, 213 | {"name": "Switzerland", "value": "CH"}, 214 | {"name": "Syrian Arab Republic", "value": "SY"}, 215 | {"name": "Taiwan", "value": "TW"}, 216 | {"name": "Tajikistan", "value": "TJ"}, 217 | {"name": "Tanzania, United Republic of", "value": "TZ"}, 218 | {"name": "Thailand", "value": "TH"}, 219 | {"name": "Togo", "value": "TG"}, 220 | {"name": "Tokelau", "value": "TK"}, 221 | {"name": "Tonga", "value": "TO"}, 222 | {"name": "Trinidad and Tobago", "value": "TT"}, 223 | {"name": "Tunisia", "value": "TN"}, 224 | {"name": "Turkey", "value": "TR"}, 225 | {"name": "Turkmenistan", "value": "TM"}, 226 | {"name": "Turks and Caicos Islands", "value": "TC"}, 227 | {"name": "Tuvalu", "value": "TV"}, 228 | {"name": "Uganda", "value": "UG"}, 229 | {"name": "Ukraine", "value": "UA"}, 230 | {"name": "United Arab Emirates", "value": "AE"}, 231 | {"name": "United Kingdom", "value": "UK"}, 232 | {"name": "United States", "value": "US"}, 233 | {"name": "United States Minor Outlying Islands", "value": "UM"}, 234 | {"name": "Uruguay", "value": "UY"}, 235 | {"name": "Uzbekistan", "value": "UZ"}, 236 | {"name": "Vanuatu", "value": "VU"}, 237 | {"name": "Venezuela", "value": "VE"}, 238 | {"name": "Vietnam", "value": "VN"}, 239 | {"name": "Virgin Islands, British", "value": "VG"}, 240 | {"name": "Virgin Islands, U.S.", "value": "VI"}, 241 | {"name": "Wallis and Futuna", "value": "WF"}, 242 | {"name": "Western Sahara", "value": "EH"}, 243 | {"name": "Yemen", "value": "YE"}, 244 | {"name": "Yugoslavia", "value": "YU"}, 245 | {"name": "Zambia", "value": "ZM"}, 246 | {"name": "Zimbabwe", "value": "ZW"} 247 | ] 248 | -------------------------------------------------------------------------------- /app/request.py: -------------------------------------------------------------------------------- 1 | from app.models.config import Config 2 | from app.utils.misc import read_config_bool 3 | from datetime import datetime 4 | from defusedxml import ElementTree as ET 5 | import random 6 | import requests 7 | from requests import Response, ConnectionError 8 | import urllib.parse as urlparse 9 | import os 10 | from stem import Signal, SocketError 11 | from stem.connection import AuthenticationFailure 12 | from stem.control import Controller 13 | from stem.connection import authenticate_cookie, authenticate_password 14 | 15 | MAPS_URL = 'https://maps.google.com/maps' 16 | AUTOCOMPLETE_URL = ('https://suggestqueries.google.com/' 17 | 'complete/search?client=toolbar&') 18 | 19 | MOBILE_UA = '{}/5.0 (Android 0; Mobile; rv:54.0) Gecko/54.0 {}/59.0' 20 | DESKTOP_UA = '{}/5.0 (X11; {} x86_64; rv:75.0) Gecko/20100101 {}/75.0' 21 | 22 | # Valid query params 23 | VALID_PARAMS = ['tbs', 'tbm', 'start', 'near', 'source', 'nfpr'] 24 | 25 | 26 | class TorError(Exception): 27 | """Exception raised for errors in Tor requests. 28 | 29 | Attributes: 30 | message: a message describing the error that occurred 31 | disable: optionally disables Tor in the user config (note: 32 | this should only happen if the connection has been dropped 33 | altogether). 34 | """ 35 | 36 | def __init__(self, message, disable=False) -> None: 37 | self.message = message 38 | self.disable = disable 39 | super().__init__(message) 40 | 41 | 42 | def send_tor_signal(signal: Signal) -> bool: 43 | use_pass = read_config_bool('WHOOGLE_TOR_USE_PASS') 44 | 45 | confloc = './misc/tor/control.conf' 46 | # Check that the custom location of conf is real. 47 | temp = os.getenv('WHOOGLE_TOR_CONF', '') 48 | if os.path.isfile(temp): 49 | confloc = temp 50 | 51 | # Attempt to authenticate and send signal. 52 | try: 53 | with Controller.from_port(port=9051) as c: 54 | if use_pass: 55 | with open(confloc, "r") as conf: 56 | # Scan for the last line of the file. 57 | for line in conf: 58 | pass 59 | secret = line.strip('\n') 60 | authenticate_password(c, password=secret) 61 | else: 62 | cookie_path = '/var/lib/tor/control_auth_cookie' 63 | authenticate_cookie(c, cookie_path=cookie_path) 64 | c.signal(signal) 65 | os.environ['TOR_AVAILABLE'] = '1' 66 | return True 67 | except (SocketError, AuthenticationFailure, 68 | ConnectionRefusedError, ConnectionError): 69 | # TODO: Handle Tor authentication (password and cookie) 70 | os.environ['TOR_AVAILABLE'] = '0' 71 | 72 | return False 73 | 74 | 75 | def gen_user_agent(is_mobile) -> str: 76 | firefox = random.choice(['Choir', 'Squier', 'Higher', 'Wire']) + 'fox' 77 | linux = random.choice(['Win', 'Sin', 'Gin', 'Fin', 'Kin']) + 'ux' 78 | 79 | if is_mobile: 80 | return MOBILE_UA.format("Mozilla", firefox) 81 | 82 | return DESKTOP_UA.format("Mozilla", linux, firefox) 83 | 84 | 85 | def gen_query(query, args, config) -> str: 86 | param_dict = {key: '' for key in VALID_PARAMS} 87 | 88 | # Use :past(hour/day/week/month/year) if available 89 | # example search "new restaurants :past month" 90 | lang = '' 91 | if ':past' in query and 'tbs' not in args: 92 | time_range = str.strip(query.split(':past', 1)[-1]) 93 | param_dict['tbs'] = '&tbs=' + ('qdr:' + str.lower(time_range[0])) 94 | elif 'tbs' in args: 95 | result_tbs = args.get('tbs') 96 | param_dict['tbs'] = '&tbs=' + result_tbs 97 | 98 | # Occasionally the 'tbs' param provided by google also contains a 99 | # field for 'lr', but formatted strangely. This is a rough solution 100 | # for this. 101 | # 102 | # Example: 103 | # &tbs=qdr:h,lr:lang_1pl 104 | # -- the lr param needs to be extracted and remove the leading '1' 105 | result_params = [_ for _ in result_tbs.split(',') if 'lr:' in _] 106 | if len(result_params) > 0: 107 | result_param = result_params[0] 108 | lang = result_param[result_param.find('lr:') + 3:len(result_param)] 109 | 110 | # Ensure search query is parsable 111 | query = urlparse.quote(query) 112 | 113 | # Pass along type of results (news, images, books, etc) 114 | if 'tbm' in args: 115 | param_dict['tbm'] = '&tbm=' + args.get('tbm') 116 | 117 | # Get results page start value (10 per page, ie page 2 start val = 20) 118 | if 'start' in args: 119 | param_dict['start'] = '&start=' + args.get('start') 120 | 121 | # Search for results near a particular city, if available 122 | if config.near: 123 | param_dict['near'] = '&near=' + urlparse.quote(config.near) 124 | 125 | # Set language for results (lr) if source isn't set, otherwise use the 126 | # result language param provided in the results 127 | if 'source' in args: 128 | param_dict['source'] = '&source=' + args.get('source') 129 | param_dict['lr'] = ('&lr=' + ''.join( 130 | [_ for _ in lang if not _.isdigit()] 131 | )) if lang else '' 132 | else: 133 | param_dict['lr'] = ( 134 | '&lr=' + config.lang_search 135 | ) if config.lang_search else '' 136 | 137 | # 'nfpr' defines the exclusion of results from an auto-corrected query 138 | if 'nfpr' in args: 139 | param_dict['nfpr'] = '&nfpr=' + args.get('nfpr') 140 | 141 | # 'chips' is used in image tabs to pass the optional 'filter' to add to the 142 | # given search term 143 | if 'chips' in args: 144 | param_dict['chips'] = '&chips=' + args.get('chips') 145 | 146 | param_dict['gl'] = ( 147 | '&gl=' + config.country 148 | ) if config.country else '' 149 | param_dict['hl'] = ( 150 | '&hl=' + config.lang_interface.replace('lang_', '') 151 | ) if config.lang_interface else '' 152 | param_dict['safe'] = '&safe=' + ('active' if config.safe else 'off') 153 | 154 | # Block all sites specified in the user config 155 | unquoted_query = urlparse.unquote(query) 156 | for blocked_site in config.block.replace(' ', '').split(','): 157 | if not blocked_site: 158 | continue 159 | block = (' -site:' + blocked_site) 160 | query += block if block not in unquoted_query else '' 161 | 162 | for val in param_dict.values(): 163 | if not val: 164 | continue 165 | query += val 166 | 167 | return query 168 | 169 | 170 | class Request: 171 | """Class used for handling all outbound requests, including search queries, 172 | search suggestions, and loading of external content (images, audio, etc). 173 | 174 | Attributes: 175 | normal_ua: the user's current user agent 176 | root_path: the root path of the whoogle instance 177 | config: the user's current whoogle configuration 178 | """ 179 | 180 | def __init__(self, normal_ua, root_path, config: Config): 181 | self.search_url = 'https://www.google.com/search?gbv=1&num=' + str( 182 | os.getenv('WHOOGLE_RESULTS_PER_PAGE', 10)) + '&q=' 183 | # Send heartbeat to Tor, used in determining if the user can or cannot 184 | # enable Tor for future requests 185 | send_tor_signal(Signal.HEARTBEAT) 186 | 187 | self.language = ( 188 | config.lang_search if config.lang_search else '' 189 | ) 190 | 191 | self.country = config.country if config.country else '' 192 | 193 | # For setting Accept-language Header 194 | self.lang_interface = '' 195 | if config.accept_language: 196 | self.lang_interface = config.lang_interface 197 | 198 | self.mobile = bool(normal_ua) and ('Android' in normal_ua 199 | or 'iPhone' in normal_ua) 200 | self.modified_user_agent = gen_user_agent(self.mobile) 201 | if not self.mobile: 202 | self.modified_user_agent_mobile = gen_user_agent(True) 203 | 204 | # Set up proxy, if previously configured 205 | proxy_path = os.environ.get('WHOOGLE_PROXY_LOC', '') 206 | if proxy_path: 207 | proxy_type = os.environ.get('WHOOGLE_PROXY_TYPE', '') 208 | proxy_user = os.environ.get('WHOOGLE_PROXY_USER', '') 209 | proxy_pass = os.environ.get('WHOOGLE_PROXY_PASS', '') 210 | auth_str = '' 211 | if proxy_user: 212 | auth_str = proxy_user + ':' + proxy_pass 213 | self.proxies = { 214 | 'https': proxy_type + '://' + 215 | ((auth_str + '@') if auth_str else '') + proxy_path, 216 | } 217 | 218 | # Need to ensure both HTTP and HTTPS are in the proxy dict, 219 | # regardless of underlying protocol 220 | if proxy_type == 'https': 221 | self.proxies['http'] = self.proxies['https'].replace( 222 | 'https', 'http') 223 | else: 224 | self.proxies['http'] = self.proxies['https'] 225 | else: 226 | self.proxies = { 227 | 'http': 'socks5://127.0.0.1:9050', 228 | 'https': 'socks5://127.0.0.1:9050' 229 | } if config.tor else {} 230 | self.tor = config.tor 231 | self.tor_valid = False 232 | self.root_path = root_path 233 | 234 | def __getitem__(self, name): 235 | return getattr(self, name) 236 | 237 | def autocomplete(self, query) -> list: 238 | """Sends a query to Google's search suggestion service 239 | 240 | Args: 241 | query: The in-progress query to send 242 | 243 | Returns: 244 | list: The list of matches for possible search suggestions 245 | 246 | """ 247 | ac_query = dict(q=query) 248 | if self.language: 249 | ac_query['lr'] = self.language 250 | if self.country: 251 | ac_query['gl'] = self.country 252 | if self.lang_interface: 253 | ac_query['hl'] = self.lang_interface 254 | 255 | response = self.send(base_url=AUTOCOMPLETE_URL, 256 | query=urlparse.urlencode(ac_query)).text 257 | 258 | if not response: 259 | return [] 260 | 261 | try: 262 | root = ET.fromstring(response) 263 | return [_.attrib['data'] for _ in 264 | root.findall('.//suggestion/[@data]')] 265 | except ET.ParseError: 266 | # Malformed XML response 267 | return [] 268 | 269 | def send(self, base_url='', query='', attempt=0, 270 | force_mobile=False) -> Response: 271 | """Sends an outbound request to a URL. Optionally sends the request 272 | using Tor, if enabled by the user. 273 | 274 | Args: 275 | base_url: The URL to use in the request 276 | query: The optional query string for the request 277 | attempt: The number of attempts made for the request 278 | (used for cycling through Tor identities, if enabled) 279 | force_mobile: Optional flag to enable a mobile user agent 280 | (used for fetching full size images in search results) 281 | 282 | Returns: 283 | Response: The Response object returned by the requests call 284 | 285 | """ 286 | if force_mobile and not self.mobile: 287 | modified_user_agent = self.modified_user_agent_mobile 288 | else: 289 | modified_user_agent = self.modified_user_agent 290 | 291 | headers = { 292 | 'User-Agent': modified_user_agent 293 | } 294 | 295 | # Adding the Accept-Language to the Header if possible 296 | if self.lang_interface: 297 | headers.update({'Accept-Language': 298 | self.lang_interface.replace('lang_', '') 299 | + ';q=1.0'}) 300 | 301 | # view is suppressed correctly 302 | now = datetime.now() 303 | cookies = { 304 | 'CONSENT': 'YES+cb.{:d}{:02d}{:02d}-17-p0.de+F+678'.format( 305 | now.year, now.month, now.day 306 | ) 307 | } 308 | 309 | # Validate Tor conn and request new identity if the last one failed 310 | if self.tor and not send_tor_signal( 311 | Signal.NEWNYM if attempt > 0 else Signal.HEARTBEAT): 312 | raise TorError( 313 | "Tor was previously enabled, but the connection has been " 314 | "dropped. Please check your Tor configuration and try again.", 315 | disable=True) 316 | 317 | # Make sure that the tor connection is valid, if enabled 318 | if self.tor: 319 | try: 320 | tor_check = requests.get('https://check.torproject.org/', 321 | proxies=self.proxies, headers=headers) 322 | self.tor_valid = 'Congratulations' in tor_check.text 323 | 324 | if not self.tor_valid: 325 | raise TorError( 326 | "Tor connection succeeded, but the connection could " 327 | "not be validated by torproject.org", 328 | disable=True) 329 | except ConnectionError: 330 | raise TorError( 331 | "Error raised during Tor connection validation", 332 | disable=True) 333 | 334 | response = requests.get( 335 | (base_url or self.search_url) + query, 336 | proxies=self.proxies, 337 | headers=headers, 338 | cookies=cookies) 339 | 340 | # Retry query with new identity if using Tor (max 10 attempts) 341 | if 'form id="captcha-form"' in response.text and self.tor: 342 | attempt += 1 343 | if attempt > 10: 344 | raise TorError("Tor query failed -- max attempts exceeded 10") 345 | return self.send((base_url or self.search_url), query, attempt) 346 | 347 | return response 348 | --------------------------------------------------------------------------------