forked from zaclys/searxng
commit
38e0b9360b
|
@ -5,6 +5,7 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import threading
|
import threading
|
||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
|
from types import MethodType
|
||||||
from timeit import default_timer
|
from timeit import default_timer
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
@ -161,19 +162,32 @@ def patch(url, data=None, **kwargs):
|
||||||
def delete(url, **kwargs):
|
def delete(url, **kwargs):
|
||||||
return request('delete', url, **kwargs)
|
return request('delete', url, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
async def stream_chunk_to_queue(network, queue, method, url, **kwargs):
|
async def stream_chunk_to_queue(network, queue, method, url, **kwargs):
|
||||||
try:
|
try:
|
||||||
async with network.stream(method, url, **kwargs) as response:
|
async with network.stream(method, url, **kwargs) as response:
|
||||||
queue.put(response)
|
queue.put(response)
|
||||||
async for chunk in response.aiter_bytes(65536):
|
# aiter_raw: access the raw bytes on the response without applying any HTTP content decoding
|
||||||
|
# https://www.python-httpx.org/quickstart/#streaming-responses
|
||||||
|
async for chunk in response.aiter_raw(65536):
|
||||||
if len(chunk) > 0:
|
if len(chunk) > 0:
|
||||||
queue.put(chunk)
|
queue.put(chunk)
|
||||||
|
except httpx.ResponseClosed as e:
|
||||||
|
# the response was closed
|
||||||
|
pass
|
||||||
except (httpx.HTTPError, OSError, h2.exceptions.ProtocolError) as e:
|
except (httpx.HTTPError, OSError, h2.exceptions.ProtocolError) as e:
|
||||||
queue.put(e)
|
queue.put(e)
|
||||||
finally:
|
finally:
|
||||||
queue.put(None)
|
queue.put(None)
|
||||||
|
|
||||||
|
|
||||||
|
def _close_response_method(self):
|
||||||
|
asyncio.run_coroutine_threadsafe(
|
||||||
|
self.aclose(),
|
||||||
|
get_loop()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def stream(method, url, **kwargs):
|
def stream(method, url, **kwargs):
|
||||||
"""Replace httpx.stream.
|
"""Replace httpx.stream.
|
||||||
|
|
||||||
|
@ -191,10 +205,19 @@ def stream(method, url, **kwargs):
|
||||||
stream_chunk_to_queue(get_network(), queue, method, url, **kwargs),
|
stream_chunk_to_queue(get_network(), queue, method, url, **kwargs),
|
||||||
get_loop()
|
get_loop()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# yield response
|
||||||
|
response = queue.get()
|
||||||
|
if isinstance(response, Exception):
|
||||||
|
raise response
|
||||||
|
response.close = MethodType(_close_response_method, response)
|
||||||
|
yield response
|
||||||
|
|
||||||
|
# yield chunks
|
||||||
chunk_or_exception = queue.get()
|
chunk_or_exception = queue.get()
|
||||||
while chunk_or_exception is not None:
|
while chunk_or_exception is not None:
|
||||||
if isinstance(chunk_or_exception, Exception):
|
if isinstance(chunk_or_exception, Exception):
|
||||||
raise chunk_or_exception
|
raise chunk_or_exception
|
||||||
yield chunk_or_exception
|
yield chunk_or_exception
|
||||||
chunk_or_exception = queue.get()
|
chunk_or_exception = queue.get()
|
||||||
return future.result()
|
future.result()
|
||||||
|
|
|
@ -289,6 +289,14 @@ def initialize(settings_engines=None, settings_outgoing=None):
|
||||||
if isinstance(network, str):
|
if isinstance(network, str):
|
||||||
NETWORKS[engine_name] = NETWORKS[network]
|
NETWORKS[engine_name] = NETWORKS[network]
|
||||||
|
|
||||||
|
# the /image_proxy endpoint has a dedicated network.
|
||||||
|
# same parameters than the default network, but HTTP/2 is disabled.
|
||||||
|
# It decreases the CPU load average, and the total time is more or less the same
|
||||||
|
if 'image_proxy' not in NETWORKS:
|
||||||
|
image_proxy_params = default_params.copy()
|
||||||
|
image_proxy_params['enable_http2'] = False
|
||||||
|
NETWORKS['image_proxy'] = new_network(image_proxy_params)
|
||||||
|
|
||||||
|
|
||||||
@atexit.register
|
@atexit.register
|
||||||
def done():
|
def done():
|
||||||
|
|
|
@ -262,11 +262,7 @@ def dict_subset(d, properties):
|
||||||
>>> >> dict_subset({'A': 'a', 'B': 'b', 'C': 'c'}, ['A', 'D'])
|
>>> >> dict_subset({'A': 'a', 'B': 'b', 'C': 'c'}, ['A', 'D'])
|
||||||
{'A': 'a'}
|
{'A': 'a'}
|
||||||
"""
|
"""
|
||||||
result = {}
|
return {k: d[k] for k in properties if k in d}
|
||||||
for k in properties:
|
|
||||||
if k in d:
|
|
||||||
result[k] = d[k]
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def get_torrent_size(filesize, filesize_multiplier):
|
def get_torrent_size(filesize, filesize_multiplier):
|
||||||
|
|
|
@ -108,7 +108,7 @@ from searx.autocomplete import search_autocomplete, backends as autocomplete_bac
|
||||||
from searx.languages import language_codes as languages
|
from searx.languages import language_codes as languages
|
||||||
from searx.locales import LOCALE_NAMES, UI_LOCALE_CODES, RTL_LOCALES
|
from searx.locales import LOCALE_NAMES, UI_LOCALE_CODES, RTL_LOCALES
|
||||||
from searx.search import SearchWithPlugins, initialize as search_initialize
|
from searx.search import SearchWithPlugins, initialize as search_initialize
|
||||||
from searx.network import stream as http_stream
|
from searx.network import stream as http_stream, set_context_network_name
|
||||||
from searx.search.checker import get_result as checker_get_result
|
from searx.search.checker import get_result as checker_get_result
|
||||||
from searx.settings_loader import get_default_settings_path
|
from searx.settings_loader import get_default_settings_path
|
||||||
|
|
||||||
|
@ -1065,7 +1065,7 @@ def _is_selected_language_supported(engine, preferences): # pylint: disable=red
|
||||||
|
|
||||||
@app.route('/image_proxy', methods=['GET'])
|
@app.route('/image_proxy', methods=['GET'])
|
||||||
def image_proxy():
|
def image_proxy():
|
||||||
# pylint: disable=too-many-return-statements
|
# pylint: disable=too-many-return-statements, too-many-branches
|
||||||
|
|
||||||
url = request.args.get('url')
|
url = request.args.get('url')
|
||||||
if not url:
|
if not url:
|
||||||
|
@ -1076,17 +1076,21 @@ def image_proxy():
|
||||||
return '', 400
|
return '', 400
|
||||||
|
|
||||||
maximum_size = 5 * 1024 * 1024
|
maximum_size = 5 * 1024 * 1024
|
||||||
|
forward_resp = False
|
||||||
|
resp = None
|
||||||
try:
|
try:
|
||||||
headers = dict_subset(request.headers, {'If-Modified-Since', 'If-None-Match'})
|
request_headers = {
|
||||||
headers['User-Agent'] = gen_useragent()
|
'User-Agent': gen_useragent(),
|
||||||
|
'Accept': 'image/webp,*/*',
|
||||||
|
'Accept-Encoding': 'gzip, deflate',
|
||||||
|
'Sec-GPC': '1',
|
||||||
|
'DNT': '1',
|
||||||
|
}
|
||||||
|
set_context_network_name('image_proxy')
|
||||||
stream = http_stream(
|
stream = http_stream(
|
||||||
method = 'GET',
|
method = 'GET',
|
||||||
url = url,
|
url = url,
|
||||||
headers = headers,
|
headers = request_headers
|
||||||
timeout = settings['outgoing']['request_timeout'],
|
|
||||||
allow_redirects = True,
|
|
||||||
max_redirects = 20
|
|
||||||
)
|
)
|
||||||
resp = next(stream)
|
resp = next(stream)
|
||||||
content_length = resp.headers.get('Content-Length')
|
content_length = resp.headers.get('Content-Length')
|
||||||
|
@ -1095,32 +1099,37 @@ def image_proxy():
|
||||||
and int(content_length) > maximum_size ):
|
and int(content_length) > maximum_size ):
|
||||||
return 'Max size', 400
|
return 'Max size', 400
|
||||||
|
|
||||||
if resp.status_code == 304:
|
|
||||||
return '', resp.status_code
|
|
||||||
|
|
||||||
if resp.status_code != 200:
|
if resp.status_code != 200:
|
||||||
logger.debug(
|
logger.debug('image-proxy: wrong response code: %i', resp.status_code)
|
||||||
'image-proxy: wrong response code: {0}'.format(
|
|
||||||
resp.status_code))
|
|
||||||
if resp.status_code >= 400:
|
if resp.status_code >= 400:
|
||||||
return '', resp.status_code
|
return '', resp.status_code
|
||||||
return '', 400
|
return '', 400
|
||||||
|
|
||||||
if not resp.headers.get('content-type', '').startswith('image/'):
|
if not resp.headers.get('Content-Type', '').startswith('image/'):
|
||||||
logger.debug(
|
logger.debug('image-proxy: wrong content-type: %s', resp.headers.get('Content-Type', ''))
|
||||||
'image-proxy: wrong content-type: {0}'.format(
|
|
||||||
resp.headers.get('content-type')))
|
|
||||||
return '', 400
|
return '', 400
|
||||||
|
|
||||||
|
forward_resp = True
|
||||||
|
except httpx.HTTPError:
|
||||||
|
logger.exception('HTTP error')
|
||||||
|
return '', 400
|
||||||
|
finally:
|
||||||
|
if resp and not forward_resp:
|
||||||
|
# the code is about to return an HTTP 400 error to the browser
|
||||||
|
# we make sure to close the response between searxng and the HTTP server
|
||||||
|
try:
|
||||||
|
resp.close()
|
||||||
|
except httpx.HTTPError:
|
||||||
|
logger.exception('HTTP error on closing')
|
||||||
|
|
||||||
|
try:
|
||||||
headers = dict_subset(
|
headers = dict_subset(
|
||||||
resp.headers,
|
resp.headers,
|
||||||
{'Content-Length', 'Length', 'Date', 'Last-Modified', 'Expires', 'Etag'}
|
{'Content-Type', 'Content-Encoding', 'Content-Length', 'Length'}
|
||||||
)
|
)
|
||||||
|
|
||||||
total_length = 0
|
|
||||||
|
|
||||||
def forward_chunk():
|
def forward_chunk():
|
||||||
nonlocal total_length
|
total_length = 0
|
||||||
for chunk in stream:
|
for chunk in stream:
|
||||||
total_length += len(chunk)
|
total_length += len(chunk)
|
||||||
if total_length > maximum_size:
|
if total_length > maximum_size:
|
||||||
|
|
Loading…
Reference in New Issue