test_distrib.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464
  1. # Copyright (c) Twisted Matrix Laboratories.
  2. # See LICENSE for details.
  3. """
  4. Tests for L{twisted.web.distrib}.
  5. """
  6. from os.path import abspath
  7. from xml.dom.minidom import parseString
  8. try:
  9. import pwd
  10. except ImportError:
  11. pwd = None
  12. from zope.interface.verify import verifyObject
  13. from twisted.python import filepath
  14. from twisted.internet import reactor, defer
  15. from twisted.trial import unittest
  16. from twisted.spread import pb
  17. from twisted.spread.banana import SIZE_LIMIT
  18. from twisted.web import distrib, client, resource, static, server
  19. from twisted.web.test.test_web import DummyRequest
  20. from twisted.web.test._util import _render
  21. from twisted.test import proto_helpers
  22. from twisted.web.http_headers import Headers
  23. class MySite(server.Site):
  24. pass
  25. class PBServerFactory(pb.PBServerFactory):
  26. """
  27. A PB server factory which keeps track of the most recent protocol it
  28. created.
  29. @ivar proto: L{None} or the L{Broker} instance most recently returned
  30. from C{buildProtocol}.
  31. """
  32. proto = None
  33. def buildProtocol(self, addr):
  34. self.proto = pb.PBServerFactory.buildProtocol(self, addr)
  35. return self.proto
  36. class DistribTests(unittest.TestCase):
  37. port1 = None
  38. port2 = None
  39. sub = None
  40. f1 = None
  41. def tearDown(self):
  42. """
  43. Clean up all the event sources left behind by either directly by
  44. test methods or indirectly via some distrib API.
  45. """
  46. dl = [defer.Deferred(), defer.Deferred()]
  47. if self.f1 is not None and self.f1.proto is not None:
  48. self.f1.proto.notifyOnDisconnect(lambda: dl[0].callback(None))
  49. else:
  50. dl[0].callback(None)
  51. if self.sub is not None and self.sub.publisher is not None:
  52. self.sub.publisher.broker.notifyOnDisconnect(
  53. lambda: dl[1].callback(None))
  54. self.sub.publisher.broker.transport.loseConnection()
  55. else:
  56. dl[1].callback(None)
  57. if self.port1 is not None:
  58. dl.append(self.port1.stopListening())
  59. if self.port2 is not None:
  60. dl.append(self.port2.stopListening())
  61. return defer.gatherResults(dl)
  62. def testDistrib(self):
  63. # site1 is the publisher
  64. r1 = resource.Resource()
  65. r1.putChild(b"there", static.Data(b"root", "text/plain"))
  66. site1 = server.Site(r1)
  67. self.f1 = PBServerFactory(distrib.ResourcePublisher(site1))
  68. self.port1 = reactor.listenTCP(0, self.f1)
  69. self.sub = distrib.ResourceSubscription("127.0.0.1",
  70. self.port1.getHost().port)
  71. r2 = resource.Resource()
  72. r2.putChild(b"here", self.sub)
  73. f2 = MySite(r2)
  74. self.port2 = reactor.listenTCP(0, f2)
  75. agent = client.Agent(reactor)
  76. url = "http://127.0.0.1:{}/here/there".format(
  77. self.port2.getHost().port)
  78. url = url.encode("ascii")
  79. d = agent.request(b"GET", url)
  80. d.addCallback(client.readBody)
  81. d.addCallback(self.assertEqual, b'root')
  82. return d
  83. def _setupDistribServer(self, child):
  84. """
  85. Set up a resource on a distrib site using L{ResourcePublisher}.
  86. @param child: The resource to publish using distrib.
  87. @return: A tuple consisting of the host and port on which to contact
  88. the created site.
  89. """
  90. distribRoot = resource.Resource()
  91. distribRoot.putChild(b"child", child)
  92. distribSite = server.Site(distribRoot)
  93. self.f1 = distribFactory = PBServerFactory(
  94. distrib.ResourcePublisher(distribSite))
  95. distribPort = reactor.listenTCP(
  96. 0, distribFactory, interface="127.0.0.1")
  97. self.addCleanup(distribPort.stopListening)
  98. addr = distribPort.getHost()
  99. self.sub = mainRoot = distrib.ResourceSubscription(
  100. addr.host, addr.port)
  101. mainSite = server.Site(mainRoot)
  102. mainPort = reactor.listenTCP(0, mainSite, interface="127.0.0.1")
  103. self.addCleanup(mainPort.stopListening)
  104. mainAddr = mainPort.getHost()
  105. return mainPort, mainAddr
  106. def _requestTest(self, child, **kwargs):
  107. """
  108. Set up a resource on a distrib site using L{ResourcePublisher} and
  109. then retrieve it from a L{ResourceSubscription} via an HTTP client.
  110. @param child: The resource to publish using distrib.
  111. @param **kwargs: Extra keyword arguments to pass to L{Agent.request} when
  112. requesting the resource.
  113. @return: A L{Deferred} which fires with the result of the request.
  114. """
  115. mainPort, mainAddr = self._setupDistribServer(child)
  116. agent = client.Agent(reactor)
  117. url = "http://%s:%s/child" % (mainAddr.host, mainAddr.port)
  118. url = url.encode("ascii")
  119. d = agent.request(b"GET", url, **kwargs)
  120. d.addCallback(client.readBody)
  121. return d
  122. def _requestAgentTest(self, child, **kwargs):
  123. """
  124. Set up a resource on a distrib site using L{ResourcePublisher} and
  125. then retrieve it from a L{ResourceSubscription} via an HTTP client.
  126. @param child: The resource to publish using distrib.
  127. @param **kwargs: Extra keyword arguments to pass to L{Agent.request} when
  128. requesting the resource.
  129. @return: A L{Deferred} which fires with a tuple consisting of a
  130. L{twisted.test.proto_helpers.AccumulatingProtocol} containing the
  131. body of the response and an L{IResponse} with the response itself.
  132. """
  133. mainPort, mainAddr = self._setupDistribServer(child)
  134. url = "http://{}:{}/child".format(mainAddr.host, mainAddr.port)
  135. url = url.encode("ascii")
  136. d = client.Agent(reactor).request(b"GET", url, **kwargs)
  137. def cbCollectBody(response):
  138. protocol = proto_helpers.AccumulatingProtocol()
  139. response.deliverBody(protocol)
  140. d = protocol.closedDeferred = defer.Deferred()
  141. d.addCallback(lambda _: (protocol, response))
  142. return d
  143. d.addCallback(cbCollectBody)
  144. return d
  145. def test_requestHeaders(self):
  146. """
  147. The request headers are available on the request object passed to a
  148. distributed resource's C{render} method.
  149. """
  150. requestHeaders = {}
  151. class ReportRequestHeaders(resource.Resource):
  152. def render(self, request):
  153. requestHeaders.update(dict(
  154. request.requestHeaders.getAllRawHeaders()))
  155. return b""
  156. request = self._requestTest(
  157. ReportRequestHeaders(), headers=Headers({'foo': ['bar']}))
  158. def cbRequested(result):
  159. self.assertEqual(requestHeaders[b'Foo'], [b'bar'])
  160. request.addCallback(cbRequested)
  161. return request
  162. def test_requestResponseCode(self):
  163. """
  164. The response code can be set by the request object passed to a
  165. distributed resource's C{render} method.
  166. """
  167. class SetResponseCode(resource.Resource):
  168. def render(self, request):
  169. request.setResponseCode(200)
  170. return ""
  171. request = self._requestAgentTest(SetResponseCode())
  172. def cbRequested(result):
  173. self.assertEqual(result[0].data, b"")
  174. self.assertEqual(result[1].code, 200)
  175. self.assertEqual(result[1].phrase, b"OK")
  176. request.addCallback(cbRequested)
  177. return request
  178. def test_requestResponseCodeMessage(self):
  179. """
  180. The response code and message can be set by the request object passed to
  181. a distributed resource's C{render} method.
  182. """
  183. class SetResponseCode(resource.Resource):
  184. def render(self, request):
  185. request.setResponseCode(200, b"some-message")
  186. return ""
  187. request = self._requestAgentTest(SetResponseCode())
  188. def cbRequested(result):
  189. self.assertEqual(result[0].data, b"")
  190. self.assertEqual(result[1].code, 200)
  191. self.assertEqual(result[1].phrase, b"some-message")
  192. request.addCallback(cbRequested)
  193. return request
  194. def test_largeWrite(self):
  195. """
  196. If a string longer than the Banana size limit is passed to the
  197. L{distrib.Request} passed to the remote resource, it is broken into
  198. smaller strings to be transported over the PB connection.
  199. """
  200. class LargeWrite(resource.Resource):
  201. def render(self, request):
  202. request.write(b'x' * SIZE_LIMIT + b'y')
  203. request.finish()
  204. return server.NOT_DONE_YET
  205. request = self._requestTest(LargeWrite())
  206. request.addCallback(self.assertEqual, b'x' * SIZE_LIMIT + b'y')
  207. return request
  208. def test_largeReturn(self):
  209. """
  210. Like L{test_largeWrite}, but for the case where C{render} returns a
  211. long string rather than explicitly passing it to L{Request.write}.
  212. """
  213. class LargeReturn(resource.Resource):
  214. def render(self, request):
  215. return b'x' * SIZE_LIMIT + b'y'
  216. request = self._requestTest(LargeReturn())
  217. request.addCallback(self.assertEqual, b'x' * SIZE_LIMIT + b'y')
  218. return request
  219. def test_connectionLost(self):
  220. """
  221. If there is an error issuing the request to the remote publisher, an
  222. error response is returned.
  223. """
  224. # Using pb.Root as a publisher will cause request calls to fail with an
  225. # error every time. Just what we want to test.
  226. self.f1 = serverFactory = PBServerFactory(pb.Root())
  227. self.port1 = serverPort = reactor.listenTCP(0, serverFactory)
  228. self.sub = subscription = distrib.ResourceSubscription(
  229. "127.0.0.1", serverPort.getHost().port)
  230. request = DummyRequest([b''])
  231. d = _render(subscription, request)
  232. def cbRendered(ignored):
  233. self.assertEqual(request.responseCode, 500)
  234. # This is the error we caused the request to fail with. It should
  235. # have been logged.
  236. errors = self.flushLoggedErrors(pb.NoSuchMethod)
  237. self.assertEqual(len(errors), 1)
  238. # The error page is rendered as HTML.
  239. expected = [
  240. b'',
  241. b'<html>',
  242. b' <head><title>500 - Server Connection Lost</title></head>',
  243. b' <body>',
  244. b' <h1>Server Connection Lost</h1>',
  245. b' <p>Connection to distributed server lost:'
  246. b'<pre>'
  247. b'[Failure instance: Traceback from remote host -- '
  248. b'twisted.spread.flavors.NoSuchMethod: '
  249. b'No such method: remote_request',
  250. b']</pre></p>',
  251. b' </body>',
  252. b'</html>',
  253. b''
  254. ]
  255. self.assertEqual([b'\n'.join(expected)], request.written)
  256. d.addCallback(cbRendered)
  257. return d
  258. class _PasswordDatabase:
  259. def __init__(self, users):
  260. self._users = users
  261. def getpwall(self):
  262. return iter(self._users)
  263. def getpwnam(self, username):
  264. for user in self._users:
  265. if user[0] == username:
  266. return user
  267. raise KeyError()
  268. class UserDirectoryTests(unittest.TestCase):
  269. """
  270. Tests for L{UserDirectory}, a resource for listing all user resources
  271. available on a system.
  272. """
  273. def setUp(self):
  274. self.alice = ('alice', 'x', 123, 456, 'Alice,,,', self.mktemp(), '/bin/sh')
  275. self.bob = ('bob', 'x', 234, 567, 'Bob,,,', self.mktemp(), '/bin/sh')
  276. self.database = _PasswordDatabase([self.alice, self.bob])
  277. self.directory = distrib.UserDirectory(self.database)
  278. def test_interface(self):
  279. """
  280. L{UserDirectory} instances provide L{resource.IResource}.
  281. """
  282. self.assertTrue(verifyObject(resource.IResource, self.directory))
  283. def _404Test(self, name):
  284. """
  285. Verify that requesting the C{name} child of C{self.directory} results
  286. in a 404 response.
  287. """
  288. request = DummyRequest([name])
  289. result = self.directory.getChild(name, request)
  290. d = _render(result, request)
  291. def cbRendered(ignored):
  292. self.assertEqual(request.responseCode, 404)
  293. d.addCallback(cbRendered)
  294. return d
  295. def test_getInvalidUser(self):
  296. """
  297. L{UserDirectory.getChild} returns a resource which renders a 404
  298. response when passed a string which does not correspond to any known
  299. user.
  300. """
  301. return self._404Test('carol')
  302. def test_getUserWithoutResource(self):
  303. """
  304. L{UserDirectory.getChild} returns a resource which renders a 404
  305. response when passed a string which corresponds to a known user who has
  306. neither a user directory nor a user distrib socket.
  307. """
  308. return self._404Test('alice')
  309. def test_getPublicHTMLChild(self):
  310. """
  311. L{UserDirectory.getChild} returns a L{static.File} instance when passed
  312. the name of a user with a home directory containing a I{public_html}
  313. directory.
  314. """
  315. home = filepath.FilePath(self.bob[-2])
  316. public_html = home.child('public_html')
  317. public_html.makedirs()
  318. request = DummyRequest(['bob'])
  319. result = self.directory.getChild('bob', request)
  320. self.assertIsInstance(result, static.File)
  321. self.assertEqual(result.path, public_html.path)
  322. def test_getDistribChild(self):
  323. """
  324. L{UserDirectory.getChild} returns a L{ResourceSubscription} instance
  325. when passed the name of a user suffixed with C{".twistd"} who has a
  326. home directory containing a I{.twistd-web-pb} socket.
  327. """
  328. home = filepath.FilePath(self.bob[-2])
  329. home.makedirs()
  330. web = home.child('.twistd-web-pb')
  331. request = DummyRequest(['bob'])
  332. result = self.directory.getChild('bob.twistd', request)
  333. self.assertIsInstance(result, distrib.ResourceSubscription)
  334. self.assertEqual(result.host, 'unix')
  335. self.assertEqual(abspath(result.port), web.path)
  336. def test_invalidMethod(self):
  337. """
  338. L{UserDirectory.render} raises L{UnsupportedMethod} in response to a
  339. non-I{GET} request.
  340. """
  341. request = DummyRequest([''])
  342. request.method = 'POST'
  343. self.assertRaises(
  344. server.UnsupportedMethod, self.directory.render, request)
  345. def test_render(self):
  346. """
  347. L{UserDirectory} renders a list of links to available user content
  348. in response to a I{GET} request.
  349. """
  350. public_html = filepath.FilePath(self.alice[-2]).child('public_html')
  351. public_html.makedirs()
  352. web = filepath.FilePath(self.bob[-2])
  353. web.makedirs()
  354. # This really only works if it's a unix socket, but the implementation
  355. # doesn't currently check for that. It probably should someday, and
  356. # then skip users with non-sockets.
  357. web.child('.twistd-web-pb').setContent(b"")
  358. request = DummyRequest([''])
  359. result = _render(self.directory, request)
  360. def cbRendered(ignored):
  361. document = parseString(b''.join(request.written))
  362. # Each user should have an li with a link to their page.
  363. [alice, bob] = document.getElementsByTagName('li')
  364. self.assertEqual(alice.firstChild.tagName, 'a')
  365. self.assertEqual(alice.firstChild.getAttribute('href'), 'alice/')
  366. self.assertEqual(alice.firstChild.firstChild.data, 'Alice (file)')
  367. self.assertEqual(bob.firstChild.tagName, 'a')
  368. self.assertEqual(bob.firstChild.getAttribute('href'), 'bob.twistd/')
  369. self.assertEqual(bob.firstChild.firstChild.data, 'Bob (twistd)')
  370. result.addCallback(cbRendered)
  371. return result
  372. def test_passwordDatabase(self):
  373. """
  374. If L{UserDirectory} is instantiated with no arguments, it uses the
  375. L{pwd} module as its password database.
  376. """
  377. directory = distrib.UserDirectory()
  378. self.assertIdentical(directory._pwd, pwd)
  379. if pwd is None:
  380. test_passwordDatabase.skip = "pwd module required"