txhttputil 1.4.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- txhttputil/__init__.py +9 -0
- txhttputil/downloader/HttpFileDownloader.py +102 -0
- txhttputil/downloader/HttpResourceProxy.py +151 -0
- txhttputil/downloader/__init__.py +8 -0
- txhttputil/login_page/LoginElement.py +45 -0
- txhttputil/login_page/LoginTemplate.xml +63 -0
- txhttputil/login_page/__init__.py +8 -0
- txhttputil/site/AuthCredentials.py +38 -0
- txhttputil/site/AuthResource.py +58 -0
- txhttputil/site/AuthSessionWrapper.py +224 -0
- txhttputil/site/AuthUserDetails.py +51 -0
- txhttputil/site/BasicResource.py +155 -0
- txhttputil/site/BasicResourceTest.py +112 -0
- txhttputil/site/FileUnderlayResource.py +188 -0
- txhttputil/site/FileUnderlayResourceTest.py +108 -0
- txhttputil/site/FileUploadRequest.py +250 -0
- txhttputil/site/RedirectToHttpsResource.py +43 -0
- txhttputil/site/RedirectionRule.py +24 -0
- txhttputil/site/ResourceCreator.py +12 -0
- txhttputil/site/RootResource.py +29 -0
- txhttputil/site/RootSiteTest.py +8 -0
- txhttputil/site/SiteUtil.py +157 -0
- txhttputil/site/StaticFileResource.py +163 -0
- txhttputil/site/__init__.py +8 -0
- txhttputil/util/DeferUtil.py +54 -0
- txhttputil/util/ModuleUtil.py +23 -0
- txhttputil/util/PemUtil.py +750 -0
- txhttputil/util/SslUtil.py +396 -0
- txhttputil/util/__init__.py +8 -0
- txhttputil-1.4.9.dist-info/LICENSE +22 -0
- txhttputil-1.4.9.dist-info/METADATA +82 -0
- txhttputil-1.4.9.dist-info/RECORD +34 -0
- txhttputil-1.4.9.dist-info/WHEEL +5 -0
- txhttputil-1.4.9.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# Copyright (c) 2016-2026 Flying Ion Pty Ltd
|
|
2
|
+
#
|
|
3
|
+
# This software is open source; the MIT license applies.
|
|
4
|
+
# See LICENSE for the full license text.
|
|
5
|
+
#
|
|
6
|
+
# Website : https://flyingion.com
|
|
7
|
+
# Support : contact@flyingion.com
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import platform
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
from twisted.internet import reactor
|
|
14
|
+
from twisted.internet._sslverify import trustRootFromCertificates
|
|
15
|
+
from twisted.web import server
|
|
16
|
+
from txhttputil.login_page.LoginElement import LoginElement
|
|
17
|
+
from txhttputil.site.AuthCredentials import (
|
|
18
|
+
AllowAllAuthCredentials,
|
|
19
|
+
AuthCredentials,
|
|
20
|
+
)
|
|
21
|
+
from txhttputil.site.AuthSessionWrapper import FormBasedAuthSessionWrapper
|
|
22
|
+
from txhttputil.site.FileUploadRequest import FileUploadRequest
|
|
23
|
+
from txhttputil.site.RedirectToHttpsResource import RedirectToHttpsResource
|
|
24
|
+
from txhttputil.util.PemUtil import (
|
|
25
|
+
parseTrustRootFromBundle,
|
|
26
|
+
parsePemBundleForServer,
|
|
27
|
+
parsePemBundleForTrustedPeers,
|
|
28
|
+
parseDiffieHellmanParameter,
|
|
29
|
+
)
|
|
30
|
+
from txhttputil.util.SslUtil import (
|
|
31
|
+
buildCertificateOptionsForTwisted,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
from txwebsocket.txws import WebSocketUpgradeHTTPChannel
|
|
35
|
+
|
|
36
|
+
logger = logging.getLogger(__name__)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def setupSite(
|
|
40
|
+
name: str,
|
|
41
|
+
rootResource,
|
|
42
|
+
portNum: int = 8000,
|
|
43
|
+
credentialChecker: AuthCredentials = AllowAllAuthCredentials(),
|
|
44
|
+
enableLogin=True,
|
|
45
|
+
SiteProtocol=WebSocketUpgradeHTTPChannel,
|
|
46
|
+
redirectFromHttpPort: Optional[int] = None,
|
|
47
|
+
enableSsl: Optional[bool] = False,
|
|
48
|
+
sslEnableMutualTLS: Optional[bool] = False,
|
|
49
|
+
sslBundleFilePath: Optional[str] = None,
|
|
50
|
+
sslMutualTLSCertificateAuthorityBundleFilePath: Optional[str] = None,
|
|
51
|
+
sslMutualTLSTrustedPeerCertificateBundleFilePath: Optional[str] = None,
|
|
52
|
+
dhParamPemFilePath: Optional[str] = None,
|
|
53
|
+
):
|
|
54
|
+
"""Setup Site
|
|
55
|
+
Sets up the web site to listen for connections and serve the site.
|
|
56
|
+
Supports customisation of resources based on user details
|
|
57
|
+
|
|
58
|
+
@return: Port object
|
|
59
|
+
"""
|
|
60
|
+
assert (
|
|
61
|
+
not sslEnableMutualTLS or enableSsl and sslEnableMutualTLS
|
|
62
|
+
), "Mutual TLS only works if the server is using TLS"
|
|
63
|
+
|
|
64
|
+
logMsg = f"setting up http server '{name}' on port {portNum}, "
|
|
65
|
+
logMsg += f"with ssl '{'on' if enableSsl else 'off'}', "
|
|
66
|
+
if enableSsl:
|
|
67
|
+
logMsg += f"with ssl PEM bundle from '{sslBundleFilePath}', "
|
|
68
|
+
logMsg += f"with mTLS '{'on' if sslEnableMutualTLS else 'off'}', "
|
|
69
|
+
if sslEnableMutualTLS:
|
|
70
|
+
logMsg += (
|
|
71
|
+
f"with mTLS CA bundle from"
|
|
72
|
+
f" '{sslMutualTLSCertificateAuthorityBundleFilePath}', "
|
|
73
|
+
)
|
|
74
|
+
logMsg += (
|
|
75
|
+
f"with mTLS trusted peer bundle from"
|
|
76
|
+
f" '{sslMutualTLSTrustedPeerCertificateBundleFilePath}', "
|
|
77
|
+
)
|
|
78
|
+
logMsg += (
|
|
79
|
+
f"with Diffie-Hellman parameter from '{dhParamPemFilePath}'"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
logger.info(logMsg)
|
|
83
|
+
if redirectFromHttpPort is not None:
|
|
84
|
+
setupSite(
|
|
85
|
+
name="%s https redirect" % name,
|
|
86
|
+
rootResource=RedirectToHttpsResource(portNum),
|
|
87
|
+
portNum=redirectFromHttpPort,
|
|
88
|
+
enableLogin=False,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
LoginElement.siteName = name
|
|
92
|
+
|
|
93
|
+
if enableLogin:
|
|
94
|
+
protectedResource = FormBasedAuthSessionWrapper(
|
|
95
|
+
rootResource, credentialChecker
|
|
96
|
+
)
|
|
97
|
+
else:
|
|
98
|
+
logger.critical("Resource protection disabled NO LOGIN REQUIRED")
|
|
99
|
+
protectedResource = rootResource
|
|
100
|
+
|
|
101
|
+
site = server.Site(protectedResource)
|
|
102
|
+
site.protocol = SiteProtocol
|
|
103
|
+
site.requestFactory = FileUploadRequest
|
|
104
|
+
|
|
105
|
+
if enableSsl:
|
|
106
|
+
proto = "https"
|
|
107
|
+
|
|
108
|
+
trustedCertificateAuthorities = None
|
|
109
|
+
if sslEnableMutualTLS:
|
|
110
|
+
trustedCertificateAuthorities = parseTrustRootFromBundle(
|
|
111
|
+
sslMutualTLSCertificateAuthorityBundleFilePath
|
|
112
|
+
)
|
|
113
|
+
trustedCertificateAuthorities = trustRootFromCertificates(
|
|
114
|
+
trustedCertificateAuthorities
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
privateKeyWithFullChain = parsePemBundleForServer(sslBundleFilePath)
|
|
118
|
+
|
|
119
|
+
trustedPeerCertificates = None
|
|
120
|
+
if sslMutualTLSTrustedPeerCertificateBundleFilePath is not None:
|
|
121
|
+
trustedPeerCertificates = parsePemBundleForTrustedPeers(
|
|
122
|
+
sslMutualTLSTrustedPeerCertificateBundleFilePath
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
dhParameters = None
|
|
126
|
+
if dhParamPemFilePath is not None:
|
|
127
|
+
dhParameters = parseDiffieHellmanParameter(dhParamPemFilePath)
|
|
128
|
+
|
|
129
|
+
contextFactory = buildCertificateOptionsForTwisted(
|
|
130
|
+
privateKeyWithFullChain,
|
|
131
|
+
trustRoot=trustedCertificateAuthorities,
|
|
132
|
+
trustedPeerCertificates=trustedPeerCertificates,
|
|
133
|
+
dhParameters=dhParameters,
|
|
134
|
+
)
|
|
135
|
+
sitePort = reactor.listenSSL(portNum, site, contextFactory)
|
|
136
|
+
|
|
137
|
+
else:
|
|
138
|
+
proto = "http"
|
|
139
|
+
sitePort = reactor.listenTCP(portNum, site)
|
|
140
|
+
|
|
141
|
+
if platform.system() == "Linux":
|
|
142
|
+
import subprocess
|
|
143
|
+
|
|
144
|
+
ip = (
|
|
145
|
+
subprocess.getoutput("/sbin/ifconfig").split("\n")[1].split()[1][5:]
|
|
146
|
+
)
|
|
147
|
+
else:
|
|
148
|
+
ip = "0.0.0.0"
|
|
149
|
+
|
|
150
|
+
logger.info(
|
|
151
|
+
"%s is alive and listening on %s://%s:%s",
|
|
152
|
+
name,
|
|
153
|
+
proto,
|
|
154
|
+
ip,
|
|
155
|
+
sitePort.port,
|
|
156
|
+
)
|
|
157
|
+
return sitePort
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# Copyright (c) 2016-2026 Flying Ion Pty Ltd
|
|
2
|
+
#
|
|
3
|
+
# This software is open source; the MIT license applies.
|
|
4
|
+
# See LICENSE for the full license text.
|
|
5
|
+
#
|
|
6
|
+
# Website : https://flyingion.com
|
|
7
|
+
# Support : contact@flyingion.com
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import mimetypes
|
|
11
|
+
import os
|
|
12
|
+
from collections import namedtuple
|
|
13
|
+
from datetime import date, timedelta
|
|
14
|
+
from time import mktime
|
|
15
|
+
from urllib.request import pathname2url
|
|
16
|
+
from wsgiref.handlers import format_date_time
|
|
17
|
+
|
|
18
|
+
from filetype import filetype
|
|
19
|
+
from twisted.internet.task import cooperate
|
|
20
|
+
from twisted.web.server import NOT_DONE_YET
|
|
21
|
+
from txhttputil.site.BasicResource import BasicResource
|
|
22
|
+
from txhttputil.util.DeferUtil import deferToThreadWrap
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
FileData = namedtuple("FileData", ["fobj", "size", "cacheControl", "expires"])
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class StaticFileResource(BasicResource):
|
|
30
|
+
""" """
|
|
31
|
+
|
|
32
|
+
isLeaf = True
|
|
33
|
+
|
|
34
|
+
__STATIC_MIME_TYPES = [
|
|
35
|
+
(".js.map", "application/json"),
|
|
36
|
+
(".css.map", "application/json"),
|
|
37
|
+
(".js", "application/javascript"),
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self, filePath: str, expireMinutes: int = 30, chunkSize: int = 128000, allowedIpsList=None
|
|
42
|
+
):
|
|
43
|
+
BasicResource.__init__(self, allowedIpsList)
|
|
44
|
+
self._filePath = filePath
|
|
45
|
+
self.cancelDownload = False
|
|
46
|
+
self.expireMinutes = expireMinutes
|
|
47
|
+
self.chunkSize = chunkSize
|
|
48
|
+
|
|
49
|
+
# Set the MIME Type
|
|
50
|
+
for extension, mimetype in self.__STATIC_MIME_TYPES:
|
|
51
|
+
if filePath.endswith(extension):
|
|
52
|
+
self._mimetype = mimetype
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
fileTypeGuess = filetype.guess(filePath)
|
|
56
|
+
if fileTypeGuess:
|
|
57
|
+
self._mimetype = fileTypeGuess.mime
|
|
58
|
+
|
|
59
|
+
else:
|
|
60
|
+
self._mimetype = mimetypes.guess_type(
|
|
61
|
+
pathname2url(filePath), strict=False
|
|
62
|
+
)[0]
|
|
63
|
+
|
|
64
|
+
if self._mimetype is None:
|
|
65
|
+
# fallback to arbitrary binary data
|
|
66
|
+
self._mimetype = "application/octet-stream"
|
|
67
|
+
|
|
68
|
+
assert self._mimetype, "Unknown mime type for: %s" % filePath
|
|
69
|
+
|
|
70
|
+
def render_GET(self, request):
|
|
71
|
+
request.responseHeaders.setRawHeaders(b"content-type", [self._mimetype])
|
|
72
|
+
return self.serveStaticFileWithCache(request)
|
|
73
|
+
|
|
74
|
+
def serveStaticFileWithCache(self, request):
|
|
75
|
+
"""Resource Create And Serve Static File
|
|
76
|
+
|
|
77
|
+
This should probably be a class now.
|
|
78
|
+
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
d = self.loadFileInThread(request)
|
|
82
|
+
d.addCallback(self._setHeaders, request)
|
|
83
|
+
d.addCallback(self._writeData, request)
|
|
84
|
+
d.addErrback(self._fileFailed, request)
|
|
85
|
+
|
|
86
|
+
request.notifyFinish().addErrback(self._closedError)
|
|
87
|
+
|
|
88
|
+
return NOT_DONE_YET
|
|
89
|
+
|
|
90
|
+
@deferToThreadWrap
|
|
91
|
+
def loadFileInThread(self, request):
|
|
92
|
+
requestPath = request.path
|
|
93
|
+
if not self._filePath or not os.path.exists(self._filePath):
|
|
94
|
+
raise Exception(
|
|
95
|
+
"File %s doesn't exist for resource %s"
|
|
96
|
+
% (self._filePath, requestPath)
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
size = os.stat(self._filePath).st_size
|
|
100
|
+
fobj = open(self._filePath, "rb")
|
|
101
|
+
|
|
102
|
+
expiry = (date.today() + timedelta(self.expireMinutes)).timetuple()
|
|
103
|
+
expiresTime = format_date_time(mktime(expiry))
|
|
104
|
+
|
|
105
|
+
cacheControl = (
|
|
106
|
+
b"max-age=" + str(self.expireMinutes * 60).encode()
|
|
107
|
+
) # In Seconds
|
|
108
|
+
cacheControl += b", private"
|
|
109
|
+
|
|
110
|
+
return FileData(
|
|
111
|
+
fobj=fobj, size=size, cacheControl=cacheControl, expires=expiresTime
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
def _setHeaders(self, fileData, request):
|
|
115
|
+
# DISABLED
|
|
116
|
+
# Cache control is disabled for gziped resources as they are chunk-encoded
|
|
117
|
+
# request.setHeader("Cache-Control", fileData.cacheControl)
|
|
118
|
+
# request.setHeader("Expires", fileData.expires)
|
|
119
|
+
request.setHeader(b"Content-Length", str(fileData.size))
|
|
120
|
+
return fileData
|
|
121
|
+
|
|
122
|
+
def _writeData(self, fileData, request):
|
|
123
|
+
def writer():
|
|
124
|
+
try:
|
|
125
|
+
data = fileData.fobj.read(self.chunkSize)
|
|
126
|
+
while data and not self.cancelDownload:
|
|
127
|
+
request.write(data)
|
|
128
|
+
yield None # Yield to the reactor for a bit
|
|
129
|
+
data = fileData.fobj.read(self.chunkSize)
|
|
130
|
+
yield None # Allow any "NotifyFinish" updates
|
|
131
|
+
|
|
132
|
+
if not self.cancelDownload:
|
|
133
|
+
request.finish()
|
|
134
|
+
|
|
135
|
+
fileData.fobj.close()
|
|
136
|
+
|
|
137
|
+
except Exception as e:
|
|
138
|
+
logger.error(
|
|
139
|
+
"An error occured loading and sending the file"
|
|
140
|
+
" data for file %s for resource %s",
|
|
141
|
+
self._filePath,
|
|
142
|
+
request.path,
|
|
143
|
+
)
|
|
144
|
+
logger.exception(e)
|
|
145
|
+
|
|
146
|
+
return cooperate(writer())
|
|
147
|
+
|
|
148
|
+
def _fileFailed(self, failure, request):
|
|
149
|
+
self.cancelDownload = True
|
|
150
|
+
request.setResponseCode(404)
|
|
151
|
+
logger.error(str(failure.value))
|
|
152
|
+
request.finish()
|
|
153
|
+
|
|
154
|
+
logger.error(
|
|
155
|
+
"Failed to send file %s for resource %s",
|
|
156
|
+
self._filePath,
|
|
157
|
+
request.path,
|
|
158
|
+
)
|
|
159
|
+
logger.exception(failure.value)
|
|
160
|
+
|
|
161
|
+
def _closedError(self, failure):
|
|
162
|
+
self.cancelDownload = True
|
|
163
|
+
logger.error("Got closedError %s" % failure)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Copyright (c) 2016-2026 Flying Ion Pty Ltd
|
|
2
|
+
#
|
|
3
|
+
# This software is open source; the MIT license applies.
|
|
4
|
+
# See LICENSE for the full license text.
|
|
5
|
+
#
|
|
6
|
+
# Website : https://flyingion.com
|
|
7
|
+
# Support : contact@flyingion.com
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
|
|
11
|
+
from twisted.internet.defer import maybeDeferred
|
|
12
|
+
from twisted.internet.threads import deferToThread
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger()
|
|
15
|
+
|
|
16
|
+
__author__ = 'Flying Ion Pty Ltd'
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def vortexLogFailure(failure, loggerArg, consumeError=False, successValue=True):
|
|
20
|
+
try:
|
|
21
|
+
if not hasattr(failure, '_vortexLogged'):
|
|
22
|
+
if failure.getTraceback():
|
|
23
|
+
loggerArg.error(failure.getTraceback())
|
|
24
|
+
failure._vortexFailureLogged = True
|
|
25
|
+
return successValue if consumeError else failure
|
|
26
|
+
except Exception as e:
|
|
27
|
+
logger.exception(e)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def printFailure(failure):
|
|
31
|
+
vortexLogFailure(failure, logger)
|
|
32
|
+
return failure
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def deferToThreadWrap(funcToWrap):
|
|
36
|
+
def func(*args, **kwargs):
|
|
37
|
+
d = deferToThread(funcToWrap, *args, **kwargs)
|
|
38
|
+
d.addErrback(printFailure)
|
|
39
|
+
return d
|
|
40
|
+
|
|
41
|
+
return func
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def maybeDeferredWrap(funcToWrap):
|
|
45
|
+
""" Maybe Deferred Wrap
|
|
46
|
+
|
|
47
|
+
A decorator that ensures a function will return a deferred.
|
|
48
|
+
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def func(*args, **kwargs):
|
|
52
|
+
return maybeDeferred(funcToWrap, *args, **kwargs)
|
|
53
|
+
|
|
54
|
+
return func
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Copyright (c) 2016-2026 Flying Ion Pty Ltd
|
|
2
|
+
#
|
|
3
|
+
# This software is open source; the MIT license applies.
|
|
4
|
+
# See LICENSE for the full license text.
|
|
5
|
+
#
|
|
6
|
+
# Website : https://flyingion.com
|
|
7
|
+
# Support : contact@flyingion.com
|
|
8
|
+
|
|
9
|
+
def filterModules(name_:str, file_: str) -> [str]:
|
|
10
|
+
import os.path
|
|
11
|
+
|
|
12
|
+
for module in os.listdir(os.path.dirname(file_)):
|
|
13
|
+
modName, modExt = module.rsplit('.', 1) if '.' in module else [module, '']
|
|
14
|
+
if modExt not in ('py', 'pyc'):
|
|
15
|
+
continue
|
|
16
|
+
|
|
17
|
+
if modName == '__init__':
|
|
18
|
+
continue
|
|
19
|
+
|
|
20
|
+
if modName.endswith("Test"):
|
|
21
|
+
continue
|
|
22
|
+
|
|
23
|
+
yield '%s.%s' % (name_, module.rsplit('.', 1)[0])
|