Sessions with auto-creation on fetch request and TTL (#736)

* Add support for sessions

* Add tests for sessions

* Missing return type

* Don't re-create an existing session

* Return success in case of session doesn't exists on destroy

* Create session if necessary on get request

* Add session TTL to the request.get method

When fetching some webpage with a predefined session id,
FlareSorverr is using existing instance of WebDriver.
That allows user to not manage cookies explicitly
and rely on WebDriver to maintain the session. However,
if session has been created long time ago, CloudFlare might
stop accepting the requests, so we want to recreate the session
time to time. From the user perspective the easiest way of doing it
is to define their expectation on the session duration.

These changes add an option to define Time-to-live (TTL) for the session
and FlareSorverr takes care about rotating the sessions.

* Update message for session destroy in tests

---------

Co-authored-by: Michel Roux <xefir@crystalyx.net>
This commit is contained in:
Artemiy Ryabinkov
2023-03-20 15:25:48 +00:00
committed by GitHub
parent f6879c70de
commit 49fd1aacfc
4 changed files with 243 additions and 21 deletions

View File

@@ -2,19 +2,25 @@ import logging
import platform
import sys
import time
from datetime import timedelta
from urllib.parse import unquote
from uuid import uuid1
from func_timeout import func_timeout, FunctionTimedOut
from func_timeout import FunctionTimedOut, func_timeout
from selenium.common import TimeoutException
from selenium.webdriver.chrome.webdriver import WebDriver
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.by import By
from selenium.webdriver.support.expected_conditions import (
presence_of_element_located, staleness_of, title_is)
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support.expected_conditions import presence_of_element_located, staleness_of, title_is
from dtos import V1RequestBase, V1ResponseBase, ChallengeResolutionT, ChallengeResolutionResultT, IndexResponse, \
HealthResponse, STATUS_OK, STATUS_ERROR
import utils
from dtos import (STATUS_ERROR, STATUS_OK, ChallengeResolutionResultT,
ChallengeResolutionT, HealthResponse, IndexResponse,
V1RequestBase, V1ResponseBase)
from sessions import SessionsStorage
ACCESS_DENIED_TITLES = [
# Cloudflare
@@ -44,6 +50,7 @@ CHALLENGE_SELECTORS = [
]
SHORT_TIMEOUT = 10
SESSIONS_STORAGE = SessionsStorage()
def test_browser_installation():
logging.info("Testing web browser installation...")
@@ -120,11 +127,11 @@ def _controller_v1_handler(req: V1RequestBase) -> V1ResponseBase:
# execute the command
res: V1ResponseBase
if req.cmd == 'sessions.create':
raise Exception("Not implemented yet.")
res = _cmd_sessions_create(req)
elif req.cmd == 'sessions.list':
raise Exception("Not implemented yet.")
res = _cmd_sessions_list(req)
elif req.cmd == 'sessions.destroy':
raise Exception("Not implemented yet.")
res = _cmd_sessions_destroy(req)
elif req.cmd == 'request.get':
res = _cmd_request_get(req)
elif req.cmd == 'request.post':
@@ -171,19 +178,79 @@ def _cmd_request_post(req: V1RequestBase) -> V1ResponseBase:
return res
def _cmd_sessions_create(req: V1RequestBase) -> V1ResponseBase:
logging.debug("Creating new session...")
session, fresh = SESSIONS_STORAGE.create()
session_id = session.session_id
if not fresh:
return V1ResponseBase({
"status": STATUS_OK,
"message": "Session already exists.",
"session": session_id
})
return V1ResponseBase({
"status": STATUS_OK,
"message": "Session created successfully.",
"session": session_id
})
def _cmd_sessions_list(req: V1RequestBase) -> V1ResponseBase:
session_ids = SESSIONS_STORAGE.session_ids()
return V1ResponseBase({
"status": STATUS_OK,
"message": "",
"sessions": session_ids
})
def _cmd_sessions_destroy(req: V1RequestBase) -> V1ResponseBase:
session_id = req.session
existed = SESSIONS_STORAGE.destroy(session_id)
if not existed:
return V1ResponseBase({
"status": STATUS_OK,
"message": "The session doesn't exists."
})
return V1ResponseBase({
"status": STATUS_OK,
"message": "The session has been removed."
})
def _resolve_challenge(req: V1RequestBase, method: str) -> ChallengeResolutionT:
timeout = req.maxTimeout / 1000
driver = None
try:
driver = utils.get_webdriver()
if req.session:
session_id = req.session
ttl = timedelta(minutes=req.session_ttl_minutes) if req.session_ttl_minutes else None
session, fresh = SESSIONS_STORAGE.get(session_id, ttl)
if fresh:
logging.debug(f"new session created to perform the request (session_id={session_id})")
else:
logging.debug(f"existing session is used to perform the request (session_id={session_id}, lifetime={str(session.lifetime())}, ttl={str(ttl)})")
driver = session.driver
else:
driver = utils.get_webdriver()
logging.debug('New instance of webdriver has been created to perform the request')
return func_timeout(timeout, _evil_logic, (req, driver, method))
except FunctionTimedOut:
raise Exception(f'Error solving the challenge. Timeout after {timeout} seconds.')
except Exception as e:
raise Exception('Error solving the challenge. ' + str(e))
finally:
if driver is not None:
if not req.session and driver is not None:
driver.quit()
logging.debug('A used instance of webdriver has been destroyed')
def click_verify(driver: WebDriver):