101 |
119 |
120 |
121 | {% endif %}
122 |
123 |
124 |
125 |
139 |
140 |
141 |
155 |
156 |
157 |
158 |
159 |
--------------------------------------------------------------------------------
/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_key
4 | from app.utils.bangs import gen_bangs_json, load_all_bangs
5 | from app.utils.misc import gen_file_hash, read_config_bool
6 | from base64 import b64encode
7 | from bs4 import MarkupResemblesLocatorWarning
8 | from datetime import datetime, timedelta
9 | from dotenv import load_dotenv
10 | from flask import Flask
11 | import json
12 | import logging.config
13 | import os
14 | from stem import Signal
15 | import threading
16 | import warnings
17 |
18 | from werkzeug.middleware.proxy_fix import ProxyFix
19 |
20 | from app.utils.misc import read_config_bool
21 | from app.version import __version__
22 |
23 | app = Flask(__name__, static_folder=os.path.dirname(
24 | os.path.abspath(__file__)) + '/static')
25 |
26 | app.wsgi_app = ProxyFix(app.wsgi_app)
27 |
28 | # look for WHOOGLE_ENV, else look in parent directory
29 | dot_env_path = os.getenv(
30 | "WHOOGLE_DOTENV_PATH",
31 | os.path.join(os.path.dirname(os.path.abspath(__file__)), "../whoogle.env"))
32 |
33 | # Load .env file if enabled
34 | if os.path.exists(dot_env_path):
35 | load_dotenv(dot_env_path)
36 |
37 | app.enc_key = generate_key()
38 |
39 | if read_config_bool('HTTPS_ONLY'):
40 | app.config['SESSION_COOKIE_NAME'] = '__Secure-session'
41 | app.config['SESSION_COOKIE_SECURE'] = True
42 |
43 | app.config['VERSION_NUMBER'] = __version__
44 | app.config['APP_ROOT'] = os.getenv(
45 | 'APP_ROOT',
46 | os.path.dirname(os.path.abspath(__file__)))
47 | app.config['STATIC_FOLDER'] = os.getenv(
48 | 'STATIC_FOLDER',
49 | os.path.join(app.config['APP_ROOT'], 'static'))
50 | app.config['BUILD_FOLDER'] = os.path.join(
51 | app.config['STATIC_FOLDER'], 'build')
52 | app.config['CACHE_BUSTING_MAP'] = {}
53 | app.config['LANGUAGES'] = json.load(open(
54 | os.path.join(app.config['STATIC_FOLDER'], 'settings/languages.json'),
55 | encoding='utf-8'))
56 | app.config['COUNTRIES'] = json.load(open(
57 | os.path.join(app.config['STATIC_FOLDER'], 'settings/countries.json'),
58 | encoding='utf-8'))
59 | app.config['TIME_PERIODS'] = json.load(open(
60 | os.path.join(app.config['STATIC_FOLDER'], 'settings/time_periods.json'),
61 | encoding='utf-8'))
62 | app.config['TRANSLATIONS'] = json.load(open(
63 | os.path.join(app.config['STATIC_FOLDER'], 'settings/translations.json'),
64 | encoding='utf-8'))
65 | app.config['THEMES'] = json.load(open(
66 | os.path.join(app.config['STATIC_FOLDER'], 'settings/themes.json'),
67 | encoding='utf-8'))
68 | app.config['HEADER_TABS'] = json.load(open(
69 | os.path.join(app.config['STATIC_FOLDER'], 'settings/header_tabs.json'),
70 | encoding='utf-8'))
71 | app.config['CONFIG_PATH'] = os.getenv(
72 | 'CONFIG_VOLUME',
73 | os.path.join(app.config['STATIC_FOLDER'], 'config'))
74 | app.config['DEFAULT_CONFIG'] = os.path.join(
75 | app.config['CONFIG_PATH'],
76 | 'config.json')
77 | app.config['CONFIG_DISABLE'] = read_config_bool('WHOOGLE_CONFIG_DISABLE')
78 | app.config['SESSION_FILE_DIR'] = os.path.join(
79 | app.config['CONFIG_PATH'],
80 | 'session')
81 | app.config['MAX_SESSION_SIZE'] = 4000 # Sessions won't exceed 4KB
82 | app.config['BANG_PATH'] = os.getenv(
83 | 'CONFIG_VOLUME',
84 | os.path.join(app.config['STATIC_FOLDER'], 'bangs'))
85 | app.config['BANG_FILE'] = os.path.join(
86 | app.config['BANG_PATH'],
87 | 'bangs.json')
88 |
89 | # Ensure all necessary directories exist
90 | if not os.path.exists(app.config['CONFIG_PATH']):
91 | os.makedirs(app.config['CONFIG_PATH'])
92 |
93 | if not os.path.exists(app.config['SESSION_FILE_DIR']):
94 | os.makedirs(app.config['SESSION_FILE_DIR'])
95 |
96 | if not os.path.exists(app.config['BANG_PATH']):
97 | os.makedirs(app.config['BANG_PATH'])
98 |
99 | if not os.path.exists(app.config['BUILD_FOLDER']):
100 | os.makedirs(app.config['BUILD_FOLDER'])
101 |
102 | # Session values
103 | app_key_path = os.path.join(app.config['CONFIG_PATH'], 'whoogle.key')
104 | if os.path.exists(app_key_path):
105 | try:
106 | app.config['SECRET_KEY'] = open(app_key_path, 'r').read()
107 | except PermissionError:
108 | app.config['SECRET_KEY'] = str(b64encode(os.urandom(32)))
109 | else:
110 | app.config['SECRET_KEY'] = str(b64encode(os.urandom(32)))
111 | with open(app_key_path, 'w') as key_file:
112 | key_file.write(app.config['SECRET_KEY'])
113 | key_file.close()
114 | app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=365)
115 |
116 | # NOTE: SESSION_COOKIE_SAMESITE must be set to 'lax' to allow the user's
117 | # previous session to persist when accessing the instance from an external
118 | # link. Setting this value to 'strict' causes Whoogle to revalidate a new
119 | # session, and fail, resulting in cookies being disabled.
120 | app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
121 |
122 | # Config fields that are used to check for updates
123 | app.config['RELEASES_URL'] = 'https://github.com/' \
124 | 'benbusby/whoogle-search/releases'
125 | app.config['LAST_UPDATE_CHECK'] = datetime.now() - timedelta(hours=24)
126 | app.config['HAS_UPDATE'] = ''
127 |
128 | # The alternative to Google Translate is treated a bit differently than other
129 | # social media site alternatives, in that it is used for any translation
130 | # related searches.
131 | translate_url = os.getenv('WHOOGLE_ALT_TL', 'https://farside.link/lingva')
132 | if not translate_url.startswith('http'):
133 | translate_url = 'https://' + translate_url
134 | app.config['TRANSLATE_URL'] = translate_url
135 |
136 | app.config['CSP'] = 'default-src \'none\';' \
137 | 'frame-src ' + translate_url + ';' \
138 | 'manifest-src \'self\';' \
139 | 'img-src \'self\' data:;' \
140 | 'style-src \'self\' \'unsafe-inline\';' \
141 | 'script-src \'self\';' \
142 | 'media-src \'self\';' \
143 | 'connect-src \'self\';'
144 |
145 | # Generate DDG bang filter
146 | generating_bangs = False
147 | if not os.path.exists(app.config['BANG_FILE']):
148 | generating_bangs = True
149 | json.dump({}, open(app.config['BANG_FILE'], 'w'))
150 | bangs_thread = threading.Thread(
151 | target=gen_bangs_json,
152 | args=(app.config['BANG_FILE'],))
153 | bangs_thread.start()
154 |
155 | # Build new mapping of static files for cache busting
156 | cache_busting_dirs = ['css', 'js']
157 | for cb_dir in cache_busting_dirs:
158 | full_cb_dir = os.path.join(app.config['STATIC_FOLDER'], cb_dir)
159 | for cb_file in os.listdir(full_cb_dir):
160 | # Create hash from current file state
161 | full_cb_path = os.path.join(full_cb_dir, cb_file)
162 | cb_file_link = gen_file_hash(full_cb_dir, cb_file)
163 | build_path = os.path.join(app.config['BUILD_FOLDER'], cb_file_link)
164 |
165 | try:
166 | os.symlink(full_cb_path, build_path)
167 | except FileExistsError:
168 | # Symlink hasn't changed, ignore
169 | pass
170 |
171 | # Create mapping for relative path urls
172 | map_path = build_path.replace(app.config['APP_ROOT'], '')
173 | if map_path.startswith('/'):
174 | map_path = map_path[1:]
175 | app.config['CACHE_BUSTING_MAP'][cb_file] = map_path
176 |
177 | # Templating functions
178 | app.jinja_env.globals.update(clean_query=clean_query)
179 | app.jinja_env.globals.update(
180 | cb_url=lambda f: app.config['CACHE_BUSTING_MAP'][f.lower()])
181 |
182 | # Attempt to acquire tor identity, to determine if Tor config is available
183 | send_tor_signal(Signal.HEARTBEAT)
184 |
185 | # Suppress spurious warnings from BeautifulSoup
186 | warnings.simplefilter('ignore', MarkupResemblesLocatorWarning)
187 |
188 | from app import routes # noqa
189 |
190 | # The gen_bangs_json function takes care of loading bangs, so skip it here if
191 | # it's already being loaded
192 | if not generating_bangs:
193 | load_all_bangs(app.config['BANG_FILE'])
194 |
195 | # Disable logging from imported modules
196 | logging.config.dictConfig({
197 | 'version': 1,
198 | 'disable_existing_loggers': True,
199 | })
200 |
--------------------------------------------------------------------------------
/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_RD": {
64 | "description": "The site to use as a replacement for reddit.com when site alternatives are enabled in the config.",
65 | "value": "farside.link/libreddit",
66 | "required": false
67 | },
68 | "WHOOGLE_ALT_MD": {
69 | "description": "The site to use as a replacement for medium.com when site alternatives are enabled in the config.",
70 | "value": "farside.link/scribe",
71 | "required": false
72 | },
73 | "WHOOGLE_ALT_TL": {
74 | "description": "The Google Translate alternative to use for all searches following the 'translate ___' structure.",
75 | "value": "farside.link/lingva",
76 | "required": false
77 | },
78 | "WHOOGLE_ALT_IMG": {
79 | "description": "The site to use as a replacement for imgur.com when site alternatives are enabled in the config.",
80 | "value": "farside.link/rimgo",
81 | "required": false
82 | },
83 | "WHOOGLE_ALT_WIKI": {
84 | "description": "The site to use as a replacement for wikipedia.com when site alternatives are enabled in the config.",
85 | "value": "farside.link/wikiless",
86 | "required": false
87 | },
88 | "WHOOGLE_ALT_IMDB": {
89 | "description": "The site to use as a replacement for imdb.com when site alternatives are enabled in the config.",
90 | "value": "farside.link/libremdb",
91 | "required": false
92 | },
93 | "WHOOGLE_ALT_QUORA": {
94 | "description": "The site to use as a replacement for quora.com when site alternatives are enabled in the config.",
95 | "value": "farside.link/quetre",
96 | "required": false
97 | },
98 | "WHOOGLE_ALT_SO": {
99 | "description": "The site to use as a replacement for stackoverflow.com when site alternatives are enabled in the config.",
100 | "value": "farside.link/anonymousoverflow",
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_TIME_PERIOD" : {
114 | "description": "[CONFIG] The time period to use for restricting search results",
115 | "value": "",
116 | "required": false
117 | },
118 | "WHOOGLE_CONFIG_LANGUAGE": {
119 | "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)",
120 | "value": "",
121 | "required": false
122 | },
123 | "WHOOGLE_CONFIG_SEARCH_LANGUAGE": {
124 | "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)",
125 | "value": "",
126 | "required": false
127 | },
128 | "WHOOGLE_CONFIG_DISABLE": {
129 | "description": "[CONFIG] Disable ability for client to change config (set to 1 or leave blank)",
130 | "value": "",
131 | "required": false
132 | },
133 | "WHOOGLE_CONFIG_BLOCK": {
134 | "description": "[CONFIG] Block websites from search results (comma-separated list)",
135 | "value": "",
136 | "required": false
137 | },
138 | "WHOOGLE_CONFIG_THEME": {
139 | "description": "[CONFIG] Set theme to 'dark', 'light', or 'system'",
140 | "value": "system",
141 | "required": false
142 | },
143 | "WHOOGLE_CONFIG_SAFE": {
144 | "description": "[CONFIG] Use safe mode for searches (set to 1 or leave blank)",
145 | "value": "",
146 | "required": false
147 | },
148 | "WHOOGLE_CONFIG_ALTS": {
149 | "description": "[CONFIG] Use social media alternatives (set to 1 or leave blank)",
150 | "value": "",
151 | "required": false
152 | },
153 | "WHOOGLE_CONFIG_NEAR": {
154 | "description": "[CONFIG] Restrict results to only those near a particular city",
155 | "value": "",
156 | "required": false
157 | },
158 | "WHOOGLE_CONFIG_TOR": {
159 | "description": "[CONFIG] Use Tor, if available (set to 1 or leave blank)",
160 | "value": "",
161 | "required": false
162 | },
163 | "WHOOGLE_CONFIG_NEW_TAB": {
164 | "description": "[CONFIG] Always open results in new tab (set to 1 or leave blank)",
165 | "value": "",
166 | "required": false
167 | },
168 | "WHOOGLE_CONFIG_VIEW_IMAGE": {
169 | "description": "[CONFIG] Enable View Image option (set to 1 or leave blank)",
170 | "value": "",
171 | "required": false
172 | },
173 | "WHOOGLE_CONFIG_GET_ONLY": {
174 | "description": "[CONFIG] Search using GET requests only (set to 1 or leave blank)",
175 | "value": "",
176 | "required": false
177 | },
178 | "WHOOGLE_CONFIG_STYLE": {
179 | "description": "[CONFIG] Custom CSS styling (paste in CSS or leave blank)",
180 | "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; }",
181 | "required": false
182 | },
183 | "WHOOGLE_CONFIG_PREFERENCES_ENCRYPTED": {
184 | "description": "[CONFIG] Encrypt preferences token, requires WHOOGLE_CONFIG_PREFERENCES_KEY to be set",
185 | "value": "",
186 | "required": false
187 | },
188 | "WHOOGLE_CONFIG_PREFERENCES_KEY": {
189 | "description": "[CONFIG] Key to encrypt preferences",
190 | "value": "NEEDS_TO_BE_MODIFIED",
191 | "required": false
192 | }
193 | }
194 | }
195 |
--------------------------------------------------------------------------------
/app/static/widgets/calculator.html:
--------------------------------------------------------------------------------
1 |
6 |
7 |
78 |
79 |
114 |
261 |
--------------------------------------------------------------------------------
/app/templates/imageresults.html:
--------------------------------------------------------------------------------
1 |
2 |
322 |
323 |
332 |
333 | {% for i in range((length // 4) + 1) %}
334 |
335 | {% for j in range([length - (i*4), 4]|min) %}
336 | |
337 |
380 | |
381 | {% endfor %}
382 |
383 | {% endfor %}
384 |
385 |
386 |
389 |
390 |
391 |
--------------------------------------------------------------------------------