Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
maurosoria
GitHub Repository: maurosoria/dirsearch
Path: blob/master/lib/connection/response.py
896 views
1
# -*- coding: utf-8 -*-
2
# This program is free software; you can redistribute it and/or modify
3
# it under the terms of the GNU General Public License as published by
4
# the Free Software Foundation; either version 2 of the License, or
5
# (at your option) any later version.
6
#
7
# This program is distributed in the hope that it will be useful,
8
# but WITHOUT ANY WARRANTY; without even the implied warranty of
9
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10
# GNU General Public License for more details.
11
#
12
# You should have received a copy of the GNU General Public License
13
# along with this program; if not, write to the Free Software
14
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
15
# MA 02110-1301, USA.
16
#
17
# Author: Mauro Soria
18
19
from __future__ import annotations
20
21
from typing import Any
22
23
import time
24
import httpx
25
import requests
26
27
from lib.core.settings import (
28
DEFAULT_ENCODING,
29
ITER_CHUNK_SIZE,
30
MAX_RESPONSE_SIZE,
31
UNKNOWN,
32
)
33
from lib.parse.url import clean_path, parse_path
34
from lib.utils.common import get_readable_size, is_binary, replace_path
35
36
37
class BaseResponse:
38
def __init__(self, url, response: requests.Response | httpx.Response) -> None:
39
self.datetime = time.strftime("%Y-%m-%d %H:%M:%S")
40
self.url = url
41
self.full_path = parse_path(self.url)
42
self.path = clean_path(self.full_path)
43
self.status = response.status_code
44
self.headers = response.headers
45
self.redirect = self.headers.get("location", "")
46
self.history = [str(res.url) for res in response.history]
47
self.content = ""
48
self.body = b""
49
50
@property
51
def type(self) -> str:
52
if ct := self.headers.get("content-type"):
53
return ct.split(";")[0]
54
55
return UNKNOWN
56
57
@property
58
def length(self) -> int:
59
if cl := self.headers.get("content-length"):
60
return int(cl)
61
62
return len(self.body)
63
64
@property
65
def size(self) -> str:
66
return get_readable_size(self.length)
67
68
def __hash__(self) -> int:
69
# Hash the static parts of the response only.
70
# See https://github.com/maurosoria/dirsearch/pull/1436#issuecomment-2476390956
71
body = replace_path(self.content, self.full_path.split("#")[0], "") if self.content else self.body
72
return hash((self.status, body))
73
74
def __eq__(self, other: Any) -> bool:
75
return (self.status, self.body, self.redirect) == (
76
other.status,
77
other.body,
78
other.redirect,
79
)
80
81
82
class Response(BaseResponse):
83
def __init__(self, url, response: requests.Response) -> None:
84
super().__init__(url, response)
85
86
for chunk in response.iter_content(chunk_size=ITER_CHUNK_SIZE):
87
self.body += chunk
88
89
if len(self.body) >= MAX_RESPONSE_SIZE or (
90
"content-length" in self.headers and is_binary(self.body)
91
):
92
break
93
94
if not is_binary(self.body):
95
try:
96
self.content = self.body.decode(
97
response.encoding or DEFAULT_ENCODING, errors="ignore"
98
)
99
except LookupError:
100
self.content = self.body.decode(DEFAULT_ENCODING, errors="ignore")
101
102
103
class AsyncResponse(BaseResponse):
104
@classmethod
105
async def create(cls, url, response: httpx.Response) -> AsyncResponse:
106
self = cls(url, response)
107
async for chunk in response.aiter_bytes(chunk_size=ITER_CHUNK_SIZE):
108
self.body += chunk
109
110
if len(self.body) >= MAX_RESPONSE_SIZE or (
111
"content-length" in self.headers and is_binary(self.body)
112
):
113
break
114
115
if not is_binary(self.body):
116
try:
117
self.content = self.body.decode(
118
response.encoding or DEFAULT_ENCODING, errors="ignore"
119
)
120
except LookupError:
121
self.content = self.body.decode(DEFAULT_ENCODING, errors="ignore")
122
123
return self
124
125