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.
@@ -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,8 @@
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
+
@@ -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])