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,224 @@
|
|
|
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
|
+
from datetime import datetime
|
|
11
|
+
|
|
12
|
+
import pytz
|
|
13
|
+
from twisted.web.resource import IResource, ErrorPage
|
|
14
|
+
from twisted.web.util import DeferredResource
|
|
15
|
+
from txhttputil.site.AuthCredentials import AuthCredentials
|
|
16
|
+
from txhttputil.site.AuthUserDetails import IUserSession
|
|
17
|
+
from txhttputil.site.BasicResource import BasicResource
|
|
18
|
+
from zope.interface import implementer
|
|
19
|
+
|
|
20
|
+
from .AuthResource import LoginResource, LoginSucceededResource
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(name=__name__)
|
|
23
|
+
|
|
24
|
+
__author__ = "Flying Ion Pty Ltd"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@implementer(IResource)
|
|
28
|
+
class FormBasedAuthSessionWrapper(object):
|
|
29
|
+
"""
|
|
30
|
+
Copied partly from C{twisted.web._auth.wrapper.HTTPAuthSessionWrapper}
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
isLeaf = False
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
siteRootResource: BasicResource,
|
|
38
|
+
credentialChecker: AuthCredentials,
|
|
39
|
+
allowedIpsList=None,
|
|
40
|
+
):
|
|
41
|
+
self._siteRootResource = siteRootResource
|
|
42
|
+
self._credentialChecker = credentialChecker
|
|
43
|
+
self._allowedIpsList = allowedIpsList
|
|
44
|
+
|
|
45
|
+
def _authorizedResource(self, request):
|
|
46
|
+
"""
|
|
47
|
+
Get the L{IResource} which the given request is authorized to receive.
|
|
48
|
+
If the proper authorization headers are present, the resource will be
|
|
49
|
+
requested from the portal. If not, an anonymous login_page attempt will be
|
|
50
|
+
made.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
# If the user has a logged in session, let them continue
|
|
54
|
+
# With out this, every HTTP request would result in a login screen.
|
|
55
|
+
userSession = IUserSession(request.getSession())
|
|
56
|
+
if userSession.userDetails.loggedIn:
|
|
57
|
+
request.getSession().touch()
|
|
58
|
+
return self._siteRootResource
|
|
59
|
+
|
|
60
|
+
# TRY Basic HTTP Authentication
|
|
61
|
+
# TODO, txHttpUtil is using FORM based authentication instead
|
|
62
|
+
# authheader = request.getHeader('authorization')
|
|
63
|
+
# if authheader:
|
|
64
|
+
# factory, respString = self._selectParseHeader(authheader)
|
|
65
|
+
# if factory is None:
|
|
66
|
+
# return LoginResource()
|
|
67
|
+
#
|
|
68
|
+
# try:
|
|
69
|
+
# credentials = factory.decode(respString, request)
|
|
70
|
+
# except error.LoginFailed:
|
|
71
|
+
# return LoginResource()
|
|
72
|
+
# except:
|
|
73
|
+
# logger.error("Unexpected failure from credentials factory")
|
|
74
|
+
# return ErrorPage(500, None, None)
|
|
75
|
+
# else:
|
|
76
|
+
# return DeferredResource(self._login(credentials))
|
|
77
|
+
|
|
78
|
+
# TRY form based authentication
|
|
79
|
+
|
|
80
|
+
a = request.args
|
|
81
|
+
if b"username" in a and b"password" in a:
|
|
82
|
+
user = a[b"username"][0]
|
|
83
|
+
pass_ = a[b"password"][0]
|
|
84
|
+
|
|
85
|
+
deferred = self._credentialChecker.check(user, pass_)
|
|
86
|
+
|
|
87
|
+
def checkResult(userDetails):
|
|
88
|
+
if not userDetails:
|
|
89
|
+
return LoginResource()
|
|
90
|
+
|
|
91
|
+
userDetails.loggedIn = True
|
|
92
|
+
userDetails.loginDate = datetime.now(pytz.utc)
|
|
93
|
+
|
|
94
|
+
userSession = IUserSession(request.getSession())
|
|
95
|
+
userSession.userDetails = userDetails
|
|
96
|
+
|
|
97
|
+
return LoginSucceededResource()
|
|
98
|
+
|
|
99
|
+
def error(failure):
|
|
100
|
+
logger.error("Unexpected failure from credentials factory")
|
|
101
|
+
logger.error(failure.value)
|
|
102
|
+
return ErrorPage(500, None, None)
|
|
103
|
+
|
|
104
|
+
deferred.addCallback(checkResult)
|
|
105
|
+
deferred.addErrback(error)
|
|
106
|
+
|
|
107
|
+
return DeferredResource(deferred)
|
|
108
|
+
|
|
109
|
+
# Return login_page form
|
|
110
|
+
return LoginResource()
|
|
111
|
+
|
|
112
|
+
def render(self, request):
|
|
113
|
+
"""
|
|
114
|
+
Find the L{IResource} avatar suitable for the given request, if
|
|
115
|
+
possible, and render it. Otherwise, perhaps render an error page
|
|
116
|
+
requiring authorization or describing an internal server failure.
|
|
117
|
+
"""
|
|
118
|
+
# Check IP filtering if configured
|
|
119
|
+
if self._allowedIpsList is not None:
|
|
120
|
+
clientIp = request.getClientAddress().host
|
|
121
|
+
if clientIp not in self._allowedIpsList:
|
|
122
|
+
request.setResponseCode(403)
|
|
123
|
+
return b"Forbidden: IP address not allowed"
|
|
124
|
+
|
|
125
|
+
return self._authorizedResource(request).render(request)
|
|
126
|
+
|
|
127
|
+
def getChildWithDefault(self, path, request):
|
|
128
|
+
"""
|
|
129
|
+
Inspect the Authorization HTTP header, and return a deferred which,
|
|
130
|
+
when fired after successful authentication, will return an authorized
|
|
131
|
+
C{Avatar}. On authentication failure, an C{UnauthorizedResource} will
|
|
132
|
+
be returned, essentially halting further dispatch on the wrapped
|
|
133
|
+
resource and all children
|
|
134
|
+
"""
|
|
135
|
+
# Check IP filtering if configured
|
|
136
|
+
if self._allowedIpsList is not None:
|
|
137
|
+
clientIp = request.getClientAddress().host
|
|
138
|
+
if clientIp not in self._allowedIpsList:
|
|
139
|
+
from twisted.web.resource import ErrorPage
|
|
140
|
+
|
|
141
|
+
return ErrorPage(403, b"Forbidden", b"IP address not allowed")
|
|
142
|
+
|
|
143
|
+
# Don't consume any segments of the request - this class should be
|
|
144
|
+
# transparent!
|
|
145
|
+
request.postpath.insert(0, request.prepath.pop())
|
|
146
|
+
return self._authorizedResource(request)
|
|
147
|
+
|
|
148
|
+
# def _login(self, credentials):
|
|
149
|
+
# """
|
|
150
|
+
# Get the L{IResource} avatar for the given credentials.
|
|
151
|
+
#
|
|
152
|
+
# @return: A L{Deferred} which will be called back with an L{IResource}
|
|
153
|
+
# avatar or which will errback if authentication fails.
|
|
154
|
+
# """
|
|
155
|
+
# d = self._portal.login(credentials, None, IResource)
|
|
156
|
+
# d.addCallbacks(self._loginSucceeded, self._loginFailed)
|
|
157
|
+
# return d
|
|
158
|
+
#
|
|
159
|
+
# def _loginSucceeded(self, (interface, avatar, logout)):
|
|
160
|
+
# """
|
|
161
|
+
# Handle login_page success by wrapping the resulting L{IResource} avatar
|
|
162
|
+
# so that the C{logout} callback will be invoked when rendering is
|
|
163
|
+
# complete.
|
|
164
|
+
# """
|
|
165
|
+
#
|
|
166
|
+
# class ResourceWrapper(proxyForInterface(IResource, 'resource')):
|
|
167
|
+
# """
|
|
168
|
+
# Wrap an L{IResource} so that whenever it or a child of it
|
|
169
|
+
# completes rendering, the cred logout hook will be invoked.
|
|
170
|
+
#
|
|
171
|
+
# An assumption is made here that exactly one L{IResource} from
|
|
172
|
+
# among C{avatar} and all of its children will be rendered. If
|
|
173
|
+
# more than one is rendered, C{logout} will be invoked multiple
|
|
174
|
+
# times and probably earlier than desired.
|
|
175
|
+
# """
|
|
176
|
+
#
|
|
177
|
+
# def getChildWithDefault(self, name, request):
|
|
178
|
+
# """
|
|
179
|
+
# Pass through the lookup to the wrapped resource, wrapping
|
|
180
|
+
# the result in L{ResourceWrapper} to ensure C{logout} is
|
|
181
|
+
# called when rendering of the child is complete.
|
|
182
|
+
# """
|
|
183
|
+
# return ResourceWrapper(self.resource.getChildWithDefault(name, request))
|
|
184
|
+
#
|
|
185
|
+
# def render(self, request):
|
|
186
|
+
# """
|
|
187
|
+
# Hook into response generation so that when rendering has
|
|
188
|
+
# finished completely (with or without error), C{logout} is
|
|
189
|
+
# called.
|
|
190
|
+
# """
|
|
191
|
+
# request.notifyFinish().addBoth(lambda ign: logout())
|
|
192
|
+
# return super(ResourceWrapper, self).render(request)
|
|
193
|
+
#
|
|
194
|
+
# return ResourceWrapper(avatar)
|
|
195
|
+
#
|
|
196
|
+
# def _loginFailed(self, result):
|
|
197
|
+
# """
|
|
198
|
+
# Handle login_page failure by presenting either another challenge (for
|
|
199
|
+
# expected authentication/authorization-related failures) or a server
|
|
200
|
+
# error page (for anything else).
|
|
201
|
+
# """
|
|
202
|
+
# if result.check(error.Unauthorized, error.LoginFailed):
|
|
203
|
+
# return UnauthorizedResource(self._credentialFactories)
|
|
204
|
+
# else:
|
|
205
|
+
# logger.error(str(result) +
|
|
206
|
+
# "HTTPAuthSessionWrapper.getChildWithDefault encountered "
|
|
207
|
+
# "unexpected error")
|
|
208
|
+
# return ErrorPage(500, None, None)
|
|
209
|
+
#
|
|
210
|
+
# def _selectParseHeader(self, header):
|
|
211
|
+
# """
|
|
212
|
+
# Choose an C{ICredentialFactory} from C{_credentialFactories}
|
|
213
|
+
# suitable to use to decode the given I{Authenticate} header.
|
|
214
|
+
#
|
|
215
|
+
# @return: A two-tuple of a factory and the remaining portion of the
|
|
216
|
+
# header value to be decoded or a two-tuple of C{None} if no
|
|
217
|
+
# factory can decode the header value.
|
|
218
|
+
# """
|
|
219
|
+
# elements = header.split(' ')
|
|
220
|
+
# scheme = elements[0].lower()
|
|
221
|
+
# for fact in self._credentialFactories:
|
|
222
|
+
# if fact.scheme == scheme:
|
|
223
|
+
# return (fact, ' '.join(elements[1:]))
|
|
224
|
+
# return (None, None)
|
|
@@ -0,0 +1,51 @@
|
|
|
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
|
+
from twisted.python.components import registerAdapter
|
|
10
|
+
from twisted.web.server import Session
|
|
11
|
+
from zope.interface import Attribute
|
|
12
|
+
from zope.interface import Interface
|
|
13
|
+
from zope.interface import implementer
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# TODO, Expand on this to allow restrictions to files, etc
|
|
17
|
+
class GroupDetails:
|
|
18
|
+
def __init__(self, name):
|
|
19
|
+
self.name = name
|
|
20
|
+
self.readOnly = False
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
everyoneGroup = GroupDetails("everyone")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# TODO, Expand on this to allow username, etc
|
|
27
|
+
class UserDetails:
|
|
28
|
+
def __init__(self):
|
|
29
|
+
self.loggedIn = False
|
|
30
|
+
self.loginDate = None
|
|
31
|
+
self.groupDetails = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class IUserSession(Interface):
|
|
35
|
+
userDetails = Attribute("The details of the user.")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@implementer(IUserSession)
|
|
39
|
+
class UserSession(object):
|
|
40
|
+
''' User Access
|
|
41
|
+
This class stores the details about the user that are required by the
|
|
42
|
+
elements and resources to be able to allow access.
|
|
43
|
+
'''
|
|
44
|
+
|
|
45
|
+
def __init__(self, session):
|
|
46
|
+
self.userDetails = UserDetails()
|
|
47
|
+
self.readOnly = True
|
|
48
|
+
self.groupId = None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
registerAdapter(UserSession, Session, IUserSession)
|
|
@@ -0,0 +1,155 @@
|
|
|
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.python.compat import nativeString
|
|
12
|
+
from twisted.web.error import UnsupportedMethod
|
|
13
|
+
from twisted.web.resource import EncodingResourceWrapper, IResource, NoResource, \
|
|
14
|
+
_computeAllowedMethods
|
|
15
|
+
from twisted.web.server import GzipEncoderFactory
|
|
16
|
+
from zope.interface import implementer
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
import mimetypes
|
|
21
|
+
|
|
22
|
+
mimetypes.init()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_extensions_for_type(general_type):
|
|
26
|
+
for ext in mimetypes.types_map:
|
|
27
|
+
if mimetypes.types_map[ext].split('/')[0] == general_type:
|
|
28
|
+
yield ext
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
IMAGE_EXTENSIONS = list(get_extensions_for_type('image'))
|
|
32
|
+
FONT_EXTENSIONS = list(get_extensions_for_type('font'))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@implementer(IResource)
|
|
36
|
+
class BasicResource:
|
|
37
|
+
""" Basic Resource
|
|
38
|
+
|
|
39
|
+
This class is a node for the resource tree, It's a slightly simpler version of
|
|
40
|
+
C{twisted.web.resource.Resource}
|
|
41
|
+
|
|
42
|
+
"""
|
|
43
|
+
isGzipped = False
|
|
44
|
+
entityType = IResource
|
|
45
|
+
server = None
|
|
46
|
+
isLeaf = False
|
|
47
|
+
|
|
48
|
+
def __init__(self, allowedIpsList=None):
|
|
49
|
+
"""
|
|
50
|
+
Initialize.
|
|
51
|
+
"""
|
|
52
|
+
self._children = {}
|
|
53
|
+
self._allowedIpsList = allowedIpsList
|
|
54
|
+
|
|
55
|
+
def getChildWithDefault(self, path, request):
|
|
56
|
+
""" Get Child With Default
|
|
57
|
+
|
|
58
|
+
This is the method queried by the site/server, if we implement this and then
|
|
59
|
+
only use getChild, we have greater control when something fails
|
|
60
|
+
@see C{FileUnderlayResource}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def getChildForRequest(resource, request):
|
|
64
|
+
# Traverse resource tree to find who will handle the request.
|
|
65
|
+
while request.postpath and not resource.isLeaf:
|
|
66
|
+
pathElement = request.postpath.pop(0)
|
|
67
|
+
request.prepath.append(pathElement)
|
|
68
|
+
resource = resource.getChildWithDefault(pathElement, request)
|
|
69
|
+
return resource
|
|
70
|
+
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
resource = self
|
|
74
|
+
|
|
75
|
+
from txhttputil.site.FileUnderlayResource import FileUnderlayResource
|
|
76
|
+
fileUnderlayResourceStack = []
|
|
77
|
+
|
|
78
|
+
if isinstance(self, FileUnderlayResource):
|
|
79
|
+
fileUnderlayResourceStack.append((resource, [path] + request.postpath))
|
|
80
|
+
|
|
81
|
+
while True:
|
|
82
|
+
resource = resource.getChild(path, request)
|
|
83
|
+
|
|
84
|
+
if isinstance(resource, FileUnderlayResource):
|
|
85
|
+
fileUnderlayResourceStack.append((resource, list(request.postpath)))
|
|
86
|
+
|
|
87
|
+
# If we've run into a dead end, return it.
|
|
88
|
+
if isinstance(resource, NoResource):
|
|
89
|
+
break
|
|
90
|
+
|
|
91
|
+
# If the resource is a leaf, this IS the resource we should render
|
|
92
|
+
if resource.isLeaf:
|
|
93
|
+
# Break before popping the path
|
|
94
|
+
break
|
|
95
|
+
|
|
96
|
+
# If there are no more paths to pop, this must be it
|
|
97
|
+
if not request.postpath:
|
|
98
|
+
break
|
|
99
|
+
|
|
100
|
+
path = request.postpath.pop(0)
|
|
101
|
+
request.prepath.append(path)
|
|
102
|
+
|
|
103
|
+
# Look back through the file resources and see if there are any matches
|
|
104
|
+
if isinstance(resource, NoResource) or isinstance(resource, FileUnderlayResource):
|
|
105
|
+
while fileUnderlayResourceStack:
|
|
106
|
+
resource, postPath = fileUnderlayResourceStack.pop()
|
|
107
|
+
fileResource = resource.getFileResource(postPath)
|
|
108
|
+
if not isinstance(fileResource, NoResource):
|
|
109
|
+
return fileResource
|
|
110
|
+
|
|
111
|
+
return resource
|
|
112
|
+
|
|
113
|
+
def getChild(self, path, request):
|
|
114
|
+
if path in self._children:
|
|
115
|
+
return self._children[path]
|
|
116
|
+
return NoResource()
|
|
117
|
+
|
|
118
|
+
def putChild(self, path: bytes, child):
|
|
119
|
+
if b'/' in path:
|
|
120
|
+
raise Exception("Path %s can not start or end with '/' ", path)
|
|
121
|
+
|
|
122
|
+
self._children[path] = child
|
|
123
|
+
child.server = self.server
|
|
124
|
+
|
|
125
|
+
def deleteChild(self, path: bytes):
|
|
126
|
+
if b'/' in path:
|
|
127
|
+
raise Exception("Path %s can not start or end with '/' ", path)
|
|
128
|
+
|
|
129
|
+
del self._children[path]
|
|
130
|
+
|
|
131
|
+
def render(self, request):
|
|
132
|
+
# Check IP filtering if configured
|
|
133
|
+
if self._allowedIpsList is not None:
|
|
134
|
+
clientIp = request.getClientAddress().host
|
|
135
|
+
if clientIp not in self._allowedIpsList:
|
|
136
|
+
request.setResponseCode(403)
|
|
137
|
+
return b'Forbidden: IP address not allowed'
|
|
138
|
+
|
|
139
|
+
# Optionally, Do some checking with userSession.userDetails.group
|
|
140
|
+
# userSession = IUserSession(request.getSession())
|
|
141
|
+
|
|
142
|
+
m = getattr(self, 'render_' + nativeString(request.method), None)
|
|
143
|
+
if not m:
|
|
144
|
+
raise UnsupportedMethod(_computeAllowedMethods(self))
|
|
145
|
+
return m(request)
|
|
146
|
+
|
|
147
|
+
def render_HEAD(self, request):
|
|
148
|
+
return self.render_GET(request)
|
|
149
|
+
|
|
150
|
+
def _gzipIfRequired(self, resource):
|
|
151
|
+
if (not isinstance(resource, EncodingResourceWrapper)
|
|
152
|
+
and hasattr(resource, 'isGzipped')
|
|
153
|
+
and resource.isGzipped):
|
|
154
|
+
return EncodingResourceWrapper(resource, [GzipEncoderFactory()])
|
|
155
|
+
return resource
|
|
@@ -0,0 +1,112 @@
|
|
|
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.trial import unittest
|
|
12
|
+
from twisted.web.resource import NoResource
|
|
13
|
+
from twisted.web.test.requesthelper import DummyRequest
|
|
14
|
+
from txhttputil.site.BasicResource import BasicResource
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class MockAddress:
|
|
20
|
+
def __init__(self, host):
|
|
21
|
+
self.host = host
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TestBasicResource(BasicResource):
|
|
25
|
+
def render_GET(self, request):
|
|
26
|
+
return b"GET response"
|
|
27
|
+
|
|
28
|
+
def render_POST(self, request):
|
|
29
|
+
return b"POST response"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class BasicResourceTest(unittest.TestCase):
|
|
33
|
+
|
|
34
|
+
def setUp(self):
|
|
35
|
+
self.resource = TestBasicResource()
|
|
36
|
+
|
|
37
|
+
def testBasicGetChild(self):
|
|
38
|
+
child = self.resource.getChild(b"nonexistent", None)
|
|
39
|
+
self.assertIsInstance(child, NoResource)
|
|
40
|
+
|
|
41
|
+
def testPutAndGetChild(self):
|
|
42
|
+
childResource = TestBasicResource()
|
|
43
|
+
self.resource.putChild(b"child", childResource)
|
|
44
|
+
|
|
45
|
+
retrievedChild = self.resource.getChild(b"child", None)
|
|
46
|
+
self.assertEqual(retrievedChild, childResource)
|
|
47
|
+
|
|
48
|
+
def testDeleteChild(self):
|
|
49
|
+
childResource = TestBasicResource()
|
|
50
|
+
self.resource.putChild(b"child", childResource)
|
|
51
|
+
|
|
52
|
+
self.resource.deleteChild(b"child")
|
|
53
|
+
|
|
54
|
+
retrievedChild = self.resource.getChild(b"child", None)
|
|
55
|
+
self.assertIsInstance(retrievedChild, NoResource)
|
|
56
|
+
|
|
57
|
+
def testRenderGetMethod(self):
|
|
58
|
+
request = DummyRequest([])
|
|
59
|
+
request.method = b"GET"
|
|
60
|
+
|
|
61
|
+
response = self.resource.render(request)
|
|
62
|
+
self.assertEqual(response, b"GET response")
|
|
63
|
+
|
|
64
|
+
def testRenderPostMethod(self):
|
|
65
|
+
request = DummyRequest([])
|
|
66
|
+
request.method = b"POST"
|
|
67
|
+
|
|
68
|
+
response = self.resource.render(request)
|
|
69
|
+
self.assertEqual(response, b"POST response")
|
|
70
|
+
|
|
71
|
+
def testIpFilteringAllowed(self):
|
|
72
|
+
allowedIps = ["127.0.0.1", "192.168.1.100"]
|
|
73
|
+
resource = TestBasicResource(allowedIpsList=allowedIps)
|
|
74
|
+
|
|
75
|
+
request = DummyRequest([])
|
|
76
|
+
request.method = b"GET"
|
|
77
|
+
request.getClientAddress = lambda: MockAddress("127.0.0.1")
|
|
78
|
+
|
|
79
|
+
response = resource.render(request)
|
|
80
|
+
self.assertEqual(response, b"GET response")
|
|
81
|
+
|
|
82
|
+
def testIpFilteringBlocked(self):
|
|
83
|
+
allowedIps = ["127.0.0.1", "192.168.1.100"]
|
|
84
|
+
resource = TestBasicResource(allowedIpsList=allowedIps)
|
|
85
|
+
|
|
86
|
+
request = DummyRequest([])
|
|
87
|
+
request.method = b"GET"
|
|
88
|
+
request.getClientAddress = lambda: MockAddress("10.0.0.1")
|
|
89
|
+
|
|
90
|
+
response = resource.render(request)
|
|
91
|
+
self.assertEqual(response, b"Forbidden: IP address not allowed")
|
|
92
|
+
self.assertEqual(request.responseCode, 403)
|
|
93
|
+
|
|
94
|
+
def testIpFilteringDisabled(self):
|
|
95
|
+
resource = TestBasicResource(allowedIpsList=None)
|
|
96
|
+
|
|
97
|
+
request = DummyRequest([])
|
|
98
|
+
request.method = b"GET"
|
|
99
|
+
request.getClientAddress = lambda: MockAddress("10.0.0.1")
|
|
100
|
+
|
|
101
|
+
response = resource.render(request)
|
|
102
|
+
self.assertEqual(response, b"GET response")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
if __name__ == "__main__":
|
|
106
|
+
from twisted.trial import runner, reporter
|
|
107
|
+
import sys
|
|
108
|
+
|
|
109
|
+
trialRunner = runner.TrialRunner(reporter.VerboseTextReporter)
|
|
110
|
+
suite = runner.TestLoader().loadClass(BasicResourceTest)
|
|
111
|
+
result = trialRunner.run(suite)
|
|
112
|
+
sys.exit(not result.wasSuccessful())
|