Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: post json support #998

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,11 +251,30 @@ Example response from running the `curl` above:

### + `request.post`

This is the same as `request.get` but it takes one more param:

| Parameter | Notes |
|-----------|--------------------------------------------------------------------------|
| postData | Must be a string with `application/x-www-form-urlencoded`. Eg: `a=b&c=d` |
| contentType | Supported types: `application/x-www-form-urlencoded`, `application/json`|
| postData | Encoded json object or a string with `application/x-www-form-urlencoded`. Eg: `a=b&c=d` |

Example request to post `application/json`:
```json
{
"cmd": "request.post",
"url":"http://example.com",
"contentType": "application/json",
"postData": "{\"key\":\"myKey\", \"value\": \"epicValue\"}"
}
```

Example request to post `application/x-www-form-urlencoded`:
```json
{
"cmd": "request.post",
"url":"http://example.com",
"contentType": "application/x-www-form-urlencoded",
"postData": "a=b&c=d"
}
```

## Environment variables

Expand Down
17 changes: 17 additions & 0 deletions docker-compose.local.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
version: "2.1"
services:
flaresolverr:
build:
context: .
dockerfile: Dockerfile
image: flaresolverr
container_name: flaresolverr
environment:
- LOG_LEVEL=${LOG_LEVEL:-info}
- LOG_HTML=${LOG_HTML:-false}
- CAPTCHA_SOLVER=${CAPTCHA_SOLVER:-none}
- TZ=Europe/London
ports:
- "${PORT:-8191}:8191"
restart: unless-stopped
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ websockets==11.0.3
xvfbwrapper==0.2.9; platform_system != "Windows"
# only required for windows
pefile==2023.2.7; platform_system == "Windows"
selenium_fetch
3 changes: 3 additions & 0 deletions src/dtos.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ class ChallengeResolutionT:
message: str = None
result: ChallengeResolutionResultT = None

response: str = None

def __init__(self, _dict):
self.__dict__.update(_dict)
if self.result is not None:
Expand All @@ -39,6 +41,7 @@ class V1RequestBase(object):

# V1Request
url: str = None
contentType: str = None
postData: str = None
returnOnlyCookies: bool = None
download: bool = None # deprecated v2.0.0, not used
Expand Down
84 changes: 51 additions & 33 deletions src/flaresolverr_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import sys
import time
from datetime import timedelta
import json
from urllib.parse import unquote

from func_timeout import FunctionTimedOut, func_timeout
Expand All @@ -19,6 +20,7 @@
ChallengeResolutionT, HealthResponse, IndexResponse,
V1RequestBase, V1ResponseBase)
from sessions import SessionsStorage
from selenium_fetch import fetch, Options, get_browser_user_agent

ACCESS_DENIED_TITLES = [
# Cloudflare
Expand Down Expand Up @@ -146,6 +148,8 @@ def _cmd_request_get(req: V1RequestBase) -> V1ResponseBase:
raise Exception("Request parameter 'url' is mandatory in 'request.get' command.")
if req.postData is not None:
raise Exception("Cannot use 'postBody' when sending a GET request.")
if req.contentType is not None:
raise Exception("Cannot use 'contentType' when sending a GET request.")
if req.returnRawHtml is not None:
logging.warning("Request parameter 'returnRawHtml' was removed in FlareSolverr v2.")
if req.download is not None:
Expand All @@ -163,6 +167,8 @@ def _cmd_request_post(req: V1RequestBase) -> V1ResponseBase:
# do some validations
if req.postData is None:
raise Exception("Request parameter 'postData' is mandatory in 'request.post' command.")
if req.contentType is None:
raise Exception("Request parameter 'contentType' is mandatory in 'request.post' command.")
if req.returnRawHtml is not None:
logging.warning("Request parameter 'returnRawHtml' was removed in FlareSolverr v2.")
if req.download is not None:
Expand Down Expand Up @@ -312,7 +318,7 @@ def _evil_logic(req: V1RequestBase, driver: WebDriver, method: str) -> Challenge
# navigate to the page
logging.debug(f'Navigating to... {req.url}')
if method == 'POST':
_post_request(req, driver)
res.response = _post_request(req, driver)
else:
access_page(driver, req.url)
driver = get_correct_window(driver)
Expand All @@ -325,7 +331,7 @@ def _evil_logic(req: V1RequestBase, driver: WebDriver, method: str) -> Challenge
driver.add_cookie(cookie)
# reload the page
if method == 'POST':
_post_request(req, driver)
res.response = _post_request(req, driver)
else:
access_page(driver, req.url)
driver = get_correct_window(driver)
Expand Down Expand Up @@ -412,41 +418,53 @@ def _evil_logic(req: V1RequestBase, driver: WebDriver, method: str) -> Challenge
challenge_res.userAgent = utils.get_user_agent(driver)

if not req.returnOnlyCookies:
content = driver.page_source
content = driver.find_element(By.TAG_NAME, "pre").text
parsed_json = json.loads(content)
challenge_res.headers = {} # todo: fix, selenium not provides this info
challenge_res.response = driver.page_source
challenge_res.response = res.response if res.response is not None else parsed_json

res.result = challenge_res
return res


def _post_request(req: V1RequestBase, driver: WebDriver):
post_form = f'<form id="hackForm" action="{req.url}" method="POST">'
query_string = req.postData if req.postData[0] != '?' else req.postData[1:]
pairs = query_string.split('&')
for pair in pairs:
parts = pair.split('=')
# noinspection PyBroadException
try:
name = unquote(parts[0])
except Exception:
name = parts[0]
if name == 'submit':
continue
# noinspection PyBroadException
try:
value = unquote(parts[1])
except Exception:
value = parts[1]
post_form += f'<input type="text" name="{name}" value="{value}"><br>'
post_form += '</form>'
html_content = f"""
<!DOCTYPE html>
<html>
<body>
{post_form}
<script>document.getElementById('hackForm').submit();</script>
</body>
</html>"""
driver.get("data:text/html;charset=utf-8," + html_content)
driver.start_session()
driver.start_session() # required to bypass Cloudflare
if req.contentType == 'application/x-www-form-urlencoded':
post_form = f'<form id="hackForm" action="{req.url}" method="POST">'
query_string = req.postData if req.postData[0] != '?' else req.postData[1:]
pairs = query_string.split('&')
for pair in pairs:
parts = pair.split('=')
# noinspection PyBroadException
try:
name = unquote(parts[0])
except Exception:
name = parts[0]
if name == 'submit':
continue
# noinspection PyBroadException
try:
value = unquote(parts[1])
except Exception:
value = parts[1]
post_form += f'<input type="text" name="{name}" value="{value}"><br>'
post_form += '</form>'
html_content = f"""
<!DOCTYPE html>
<html>
<body>
{post_form}
<script>document.getElementById('hackForm').submit();</script>
</body>
</html>"""
driver.get("data:text/html;charset=utf-8," + html_content)
driver.start_session() # required to bypass Cloudflare
return "Success"
elif req.contentType == 'application/json':
post_data = json.loads(unquote(req.postData))
options = Options(method="POST", body=post_data)
response = fetch(driver, req.url, options)
driver.start_session() # required to bypass Cloudflare
return response.text
else:
raise Exception(f"Request parameter 'contentType' = '{req.contentType}' is invalid.")