mirror of
https://github.com/searxng/searxng.git
synced 2026-09-11 16:56:05 +00:00
[fix] engines: duckduckgo web bypass botdetection
This commit is contained in:
@@ -14,11 +14,12 @@ can't build it ourselves and must scrape it from the HTML pages.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import typing as t
|
import typing as t
|
||||||
|
import re
|
||||||
|
|
||||||
from urllib.parse import quote_plus
|
from urllib.parse import quote_plus, urljoin
|
||||||
from lxml import html
|
from lxml import html
|
||||||
|
|
||||||
from searx.utils import html_to_text, gen_useragent, extract_text, eval_xpath
|
from searx.utils import html_to_text, extract_text, eval_xpath
|
||||||
from searx.result_types import EngineResults
|
from searx.result_types import EngineResults
|
||||||
from searx.enginelib import EngineCache
|
from searx.enginelib import EngineCache
|
||||||
from searx.network import get
|
from searx.network import get
|
||||||
@@ -38,7 +39,6 @@ about = {
|
|||||||
# engine dependent config
|
# engine dependent config
|
||||||
categories = ["general"]
|
categories = ["general"]
|
||||||
paging = True
|
paging = True
|
||||||
_HTTP_User_Agent: str = gen_useragent()
|
|
||||||
|
|
||||||
base_url = "https://duckduckgo.com"
|
base_url = "https://duckduckgo.com"
|
||||||
|
|
||||||
@@ -73,6 +73,8 @@ def _fetch_first_page_link(
|
|||||||
resp = get(
|
resp = get(
|
||||||
url=f"{base_url}/?q={quote_plus(query)}&t=h_&ia=web",
|
url=f"{base_url}/?q={quote_plus(query)}&t=h_&ia=web",
|
||||||
headers=headers,
|
headers=headers,
|
||||||
|
impersonate="firefox",
|
||||||
|
default_headers=False,
|
||||||
timeout=2,
|
timeout=2,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -96,6 +98,43 @@ def _cache_key(query: str, pageno: int) -> str:
|
|||||||
return f"nextpage_url|{query}|{pageno}"
|
return f"nextpage_url|{query}|{pageno}"
|
||||||
|
|
||||||
|
|
||||||
|
def _solve_jsa(resp: "SXNG_Response") -> "SXNG_Response":
|
||||||
|
"""Duckduckgo sometimes issues a challenge instead of json."""
|
||||||
|
|
||||||
|
# length that a real browser would report for where the broken snippet is
|
||||||
|
html_len = {
|
||||||
|
"<p><div></p><p></div": 32,
|
||||||
|
"<li><div></li><li></div": 29,
|
||||||
|
"<div><div></div><div></div": 33,
|
||||||
|
"<br><div></br><br></div": 23,
|
||||||
|
}
|
||||||
|
|
||||||
|
js = resp.text or ""
|
||||||
|
jsa_match = re.search(r"let jsa = (\d+);.*?DDG\.deep\.initialize\('([^']+)'", js, re.S)
|
||||||
|
if not jsa_match:
|
||||||
|
return resp
|
||||||
|
|
||||||
|
js_functions = dict(re.findall(r"let (\w+) = function\(num\) \{([^}]*)\};", js))
|
||||||
|
jsa = int(jsa_match.group(1))
|
||||||
|
try:
|
||||||
|
for name in re.findall(r"jsa = (\w+)\(jsa\);", js):
|
||||||
|
body = js_functions[name]
|
||||||
|
mul = re.search(r"num \* (\d+)", body)
|
||||||
|
jsa = jsa * int(mul.group(1)) if mul else jsa + html_len[re.search(r"`([^`]+)`", body).group(1)]
|
||||||
|
except (KeyError, AttributeError):
|
||||||
|
return resp
|
||||||
|
|
||||||
|
params = resp.search_params
|
||||||
|
follow = get(
|
||||||
|
urljoin("https://links.duckduckgo.com", jsa_match.group(2) + str(jsa)),
|
||||||
|
headers=params["headers"],
|
||||||
|
impersonate="firefox",
|
||||||
|
default_headers=False,
|
||||||
|
)
|
||||||
|
follow.search_params = params
|
||||||
|
return follow
|
||||||
|
|
||||||
|
|
||||||
def request(query: str, params: "OnlineParams") -> None:
|
def request(query: str, params: "OnlineParams") -> None:
|
||||||
|
|
||||||
if len(query) >= 500:
|
if len(query) >= 500:
|
||||||
@@ -103,25 +142,15 @@ def request(query: str, params: "OnlineParams") -> None:
|
|||||||
params["url"] = None
|
params["url"] = None
|
||||||
return
|
return
|
||||||
|
|
||||||
headers = params["headers"]
|
# firefox TLS only
|
||||||
|
params["impersonate"] = "firefox"
|
||||||
# The vqd value is generated from the query and the UA header. To be able
|
params["default_headers"] = False
|
||||||
# to reuse the vqd value, the UA header must be static.
|
|
||||||
headers["User-Agent"] = _HTTP_User_Agent
|
|
||||||
headers["Accept"] = "*/*"
|
|
||||||
headers["Referer"] = f"{base_url}/"
|
|
||||||
headers["Host"] = "duckduckgo.com"
|
|
||||||
|
|
||||||
# Sec-Fetch headers are required to not get blocked when sending a Firefox user agent
|
|
||||||
headers["Sec-Fetch-Dest"] = "script"
|
|
||||||
headers["Sec-Fetch-Mode"] = "no-cors"
|
|
||||||
headers["Sec-Fetch-Site"] = "same-site"
|
|
||||||
|
|
||||||
api_url = ""
|
api_url = ""
|
||||||
if params["pageno"] > 1:
|
if params["pageno"] > 1:
|
||||||
api_url = CACHE.get(_cache_key(query, params["pageno"]))
|
api_url = CACHE.get(_cache_key(query, params["pageno"]))
|
||||||
else:
|
else:
|
||||||
api_url = _fetch_first_page_link(query, headers)
|
api_url = _fetch_first_page_link(query, params["headers"])
|
||||||
|
|
||||||
if not api_url:
|
if not api_url:
|
||||||
params["url"] = None
|
params["url"] = None
|
||||||
@@ -129,14 +158,27 @@ def request(query: str, params: "OnlineParams") -> None:
|
|||||||
|
|
||||||
params["url"] = api_url.replace("/d.js?", "/d.js?o=json&")
|
params["url"] = api_url.replace("/d.js?", "/d.js?o=json&")
|
||||||
|
|
||||||
|
# loads as a script
|
||||||
|
headers = params["headers"]
|
||||||
|
headers["Accept"] = "*/*"
|
||||||
|
headers["Sec-Fetch-Dest"] = "script"
|
||||||
|
headers["Sec-Fetch-Mode"] = "no-cors"
|
||||||
|
headers["Sec-Fetch-Site"] = "same-site"
|
||||||
|
headers["Referer"] = f"{base_url}/"
|
||||||
|
|
||||||
# TODO: support safesearch, timerange and engine traits # pylint:disable=fixme
|
# TODO: support safesearch, timerange and engine traits # pylint:disable=fixme
|
||||||
|
|
||||||
|
|
||||||
def response(resp: "SXNG_Response"):
|
def response(resp: "SXNG_Response"):
|
||||||
res = EngineResults()
|
res = EngineResults()
|
||||||
res_json = resp.json()
|
|
||||||
|
|
||||||
for result in res_json["results"]:
|
# check if ddg returns a challenge
|
||||||
|
# e.g. 'site:github.com searxng'
|
||||||
|
if "let jsa =" in (resp.text or ""):
|
||||||
|
resp = _solve_jsa(resp)
|
||||||
|
|
||||||
|
results = resp.json()["results"]
|
||||||
|
for result in results:
|
||||||
if "u" not in result:
|
if "u" not in result:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -144,13 +186,13 @@ def response(resp: "SXNG_Response"):
|
|||||||
res.types.MainResult(url=result["u"], title=html_to_text(result["t"]), content=html_to_text(result["a"]))
|
res.types.MainResult(url=result["u"], title=html_to_text(result["t"]), content=html_to_text(result["a"]))
|
||||||
)
|
)
|
||||||
|
|
||||||
# link to next page
|
if results:
|
||||||
next_page_path = res_json["results"][-1].get("n")
|
next_page_path = results[-1].get("n")
|
||||||
if next_page_path:
|
if next_page_path:
|
||||||
CACHE.set(
|
CACHE.set(
|
||||||
_cache_key(resp.search_params["query"], resp.search_params["pageno"] + 1),
|
_cache_key(resp.search_params["query"], resp.search_params["pageno"] + 1),
|
||||||
base_url + next_page_path,
|
base_url + next_page_path,
|
||||||
expire=60 * 60,
|
expire=60 * 60,
|
||||||
)
|
)
|
||||||
|
|
||||||
return res
|
return res
|
||||||
|
|||||||
@@ -88,6 +88,9 @@ class HTTPParams(t.TypedDict):
|
|||||||
impersonate: t.NotRequired[str]
|
impersonate: t.NotRequired[str]
|
||||||
"""curl_cffi impersonate target. Default: ``chrome``."""
|
"""curl_cffi impersonate target. Default: ``chrome``."""
|
||||||
|
|
||||||
|
default_headers: t.NotRequired[bool]
|
||||||
|
"""Ask curl_cffi to skip setting the impersonated browser's default headers."""
|
||||||
|
|
||||||
curl_options: t.NotRequired[dict[int, t.Any]]
|
curl_options: t.NotRequired[dict[int, t.Any]]
|
||||||
"""Any extra libcurl options for the request."""
|
"""Any extra libcurl options for the request."""
|
||||||
|
|
||||||
@@ -169,8 +172,8 @@ class OnlineProcessor(EngineProcessor):
|
|||||||
"cookies": params["cookies"],
|
"cookies": params["cookies"],
|
||||||
"auth": params["auth"],
|
"auth": params["auth"],
|
||||||
}
|
}
|
||||||
for key in ("curl_options", "impersonate"):
|
for key in ("curl_options", "impersonate", "default_headers"):
|
||||||
if params.get(key):
|
if params.get(key) is not None:
|
||||||
request_args[key] = params[key]
|
request_args[key] = params[key]
|
||||||
|
|
||||||
verify = params.get("verify")
|
verify = params.get("verify")
|
||||||
|
|||||||
Reference in New Issue
Block a user