Funktionierender Prototyp des Serious Games zur Vermittlung von Wissen zu Software-Engineering-Arbeitsmodellen.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

iosim.py 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559
  1. # -*- test-case-name: twisted.test.test_amp,twisted.test.test_iosim -*-
  2. # Copyright (c) Twisted Matrix Laboratories.
  3. # See LICENSE for details.
  4. """
  5. Utilities and helpers for simulating a network
  6. """
  7. import itertools
  8. try:
  9. from OpenSSL.SSL import Error as NativeOpenSSLError
  10. except ImportError:
  11. pass
  12. from zope.interface import directlyProvides, implementer
  13. from twisted.internet import error, interfaces
  14. from twisted.internet.endpoints import TCP4ClientEndpoint, TCP4ServerEndpoint
  15. from twisted.internet.error import ConnectionRefusedError
  16. from twisted.internet.protocol import Factory, Protocol
  17. from twisted.internet.testing import MemoryReactorClock
  18. from twisted.python.failure import Failure
  19. class TLSNegotiation:
  20. def __init__(self, obj, connectState):
  21. self.obj = obj
  22. self.connectState = connectState
  23. self.sent = False
  24. self.readyToSend = connectState
  25. def __repr__(self) -> str:
  26. return f"TLSNegotiation({self.obj!r})"
  27. def pretendToVerify(self, other, tpt):
  28. # Set the transport problems list here? disconnections?
  29. # hmmmmm... need some negative path tests.
  30. if not self.obj.iosimVerify(other.obj):
  31. tpt.disconnectReason = NativeOpenSSLError()
  32. tpt.loseConnection()
  33. @implementer(interfaces.IAddress)
  34. class FakeAddress:
  35. """
  36. The default address type for the host and peer of L{FakeTransport}
  37. connections.
  38. """
  39. @implementer(interfaces.ITransport, interfaces.ITLSTransport)
  40. class FakeTransport:
  41. """
  42. A wrapper around a file-like object to make it behave as a Transport.
  43. This doesn't actually stream the file to the attached protocol,
  44. and is thus useful mainly as a utility for debugging protocols.
  45. """
  46. _nextserial = staticmethod(lambda counter=itertools.count(): int(next(counter)))
  47. closed = 0
  48. disconnecting = 0
  49. disconnected = 0
  50. disconnectReason = error.ConnectionDone("Connection done")
  51. producer = None
  52. streamingProducer = 0
  53. tls = None
  54. def __init__(self, protocol, isServer, hostAddress=None, peerAddress=None):
  55. """
  56. @param protocol: This transport will deliver bytes to this protocol.
  57. @type protocol: L{IProtocol} provider
  58. @param isServer: C{True} if this is the accepting side of the
  59. connection, C{False} if it is the connecting side.
  60. @type isServer: L{bool}
  61. @param hostAddress: The value to return from C{getHost}. L{None}
  62. results in a new L{FakeAddress} being created to use as the value.
  63. @type hostAddress: L{IAddress} provider or L{None}
  64. @param peerAddress: The value to return from C{getPeer}. L{None}
  65. results in a new L{FakeAddress} being created to use as the value.
  66. @type peerAddress: L{IAddress} provider or L{None}
  67. """
  68. self.protocol = protocol
  69. self.isServer = isServer
  70. self.stream = []
  71. self.serial = self._nextserial()
  72. if hostAddress is None:
  73. hostAddress = FakeAddress()
  74. self.hostAddress = hostAddress
  75. if peerAddress is None:
  76. peerAddress = FakeAddress()
  77. self.peerAddress = peerAddress
  78. def __repr__(self) -> str:
  79. return "FakeTransport<{},{},{}>".format(
  80. self.isServer and "S" or "C",
  81. self.serial,
  82. self.protocol.__class__.__name__,
  83. )
  84. def write(self, data):
  85. # If transport is closed, we should accept writes but drop the data.
  86. if self.disconnecting:
  87. return
  88. if self.tls is not None:
  89. self.tlsbuf.append(data)
  90. else:
  91. self.stream.append(data)
  92. def _checkProducer(self):
  93. # Cheating; this is called at "idle" times to allow producers to be
  94. # found and dealt with
  95. if self.producer and not self.streamingProducer:
  96. self.producer.resumeProducing()
  97. def registerProducer(self, producer, streaming):
  98. """
  99. From abstract.FileDescriptor
  100. """
  101. self.producer = producer
  102. self.streamingProducer = streaming
  103. if not streaming:
  104. producer.resumeProducing()
  105. def unregisterProducer(self):
  106. self.producer = None
  107. def stopConsuming(self):
  108. self.unregisterProducer()
  109. self.loseConnection()
  110. def writeSequence(self, iovec):
  111. self.write(b"".join(iovec))
  112. def loseConnection(self):
  113. self.disconnecting = True
  114. def abortConnection(self):
  115. """
  116. For the time being, this is the same as loseConnection; no buffered
  117. data will be lost.
  118. """
  119. self.disconnecting = True
  120. def reportDisconnect(self):
  121. if self.tls is not None:
  122. # We were in the middle of negotiating! Must have been a TLS
  123. # problem.
  124. err = NativeOpenSSLError()
  125. else:
  126. err = self.disconnectReason
  127. self.protocol.connectionLost(Failure(err))
  128. def logPrefix(self):
  129. """
  130. Identify this transport/event source to the logging system.
  131. """
  132. return "iosim"
  133. def getPeer(self):
  134. return self.peerAddress
  135. def getHost(self):
  136. return self.hostAddress
  137. def resumeProducing(self):
  138. # Never sends data anyways
  139. pass
  140. def pauseProducing(self):
  141. # Never sends data anyways
  142. pass
  143. def stopProducing(self):
  144. self.loseConnection()
  145. def startTLS(self, contextFactory, beNormal=True):
  146. # Nothing's using this feature yet, but startTLS has an undocumented
  147. # second argument which defaults to true; if set to False, servers will
  148. # behave like clients and clients will behave like servers.
  149. connectState = self.isServer ^ beNormal
  150. self.tls = TLSNegotiation(contextFactory, connectState)
  151. self.tlsbuf = []
  152. def getOutBuffer(self):
  153. """
  154. Get the pending writes from this transport, clearing them from the
  155. pending buffer.
  156. @return: the bytes written with C{transport.write}
  157. @rtype: L{bytes}
  158. """
  159. S = self.stream
  160. if S:
  161. self.stream = []
  162. return b"".join(S)
  163. elif self.tls is not None:
  164. if self.tls.readyToSend:
  165. # Only _send_ the TLS negotiation "packet" if I'm ready to.
  166. self.tls.sent = True
  167. return self.tls
  168. else:
  169. return None
  170. else:
  171. return None
  172. def bufferReceived(self, buf):
  173. if isinstance(buf, TLSNegotiation):
  174. assert self.tls is not None # By the time you're receiving a
  175. # negotiation, you have to have called
  176. # startTLS already.
  177. if self.tls.sent:
  178. self.tls.pretendToVerify(buf, self)
  179. self.tls = None # We're done with the handshake if we've gotten
  180. # this far... although maybe it failed...?
  181. # TLS started! Unbuffer...
  182. b, self.tlsbuf = self.tlsbuf, None
  183. self.writeSequence(b)
  184. directlyProvides(self, interfaces.ISSLTransport)
  185. else:
  186. # We haven't sent our own TLS negotiation: time to do that!
  187. self.tls.readyToSend = True
  188. else:
  189. self.protocol.dataReceived(buf)
  190. def getTcpKeepAlive(self):
  191. # ITCPTransport.getTcpKeepAlive
  192. pass
  193. def getTcpNoDelay(self):
  194. # ITCPTransport.getTcpNoDelay
  195. pass
  196. def loseWriteConnection(self):
  197. # ITCPTransport.loseWriteConnection
  198. pass
  199. def setTcpKeepAlive(self, enabled):
  200. # ITCPTransport.setTcpKeepAlive
  201. pass
  202. def setTcpNoDelay(self, enabled):
  203. # ITCPTransport.setTcpNoDelay
  204. pass
  205. def makeFakeClient(clientProtocol):
  206. """
  207. Create and return a new in-memory transport hooked up to the given protocol.
  208. @param clientProtocol: The client protocol to use.
  209. @type clientProtocol: L{IProtocol} provider
  210. @return: The transport.
  211. @rtype: L{FakeTransport}
  212. """
  213. return FakeTransport(clientProtocol, isServer=False)
  214. def makeFakeServer(serverProtocol):
  215. """
  216. Create and return a new in-memory transport hooked up to the given protocol.
  217. @param serverProtocol: The server protocol to use.
  218. @type serverProtocol: L{IProtocol} provider
  219. @return: The transport.
  220. @rtype: L{FakeTransport}
  221. """
  222. return FakeTransport(serverProtocol, isServer=True)
  223. class IOPump:
  224. """
  225. Utility to pump data between clients and servers for protocol testing.
  226. Perhaps this is a utility worthy of being in protocol.py?
  227. """
  228. def __init__(self, client, server, clientIO, serverIO, debug):
  229. self.client = client
  230. self.server = server
  231. self.clientIO = clientIO
  232. self.serverIO = serverIO
  233. self.debug = debug
  234. def flush(self, debug=False):
  235. """
  236. Pump until there is no more input or output.
  237. Returns whether any data was moved.
  238. """
  239. result = False
  240. for x in range(1000):
  241. if self.pump(debug):
  242. result = True
  243. else:
  244. break
  245. else:
  246. assert 0, "Too long"
  247. return result
  248. def pump(self, debug=False):
  249. """
  250. Move data back and forth.
  251. Returns whether any data was moved.
  252. """
  253. if self.debug or debug:
  254. print("-- GLUG --")
  255. sData = self.serverIO.getOutBuffer()
  256. cData = self.clientIO.getOutBuffer()
  257. self.clientIO._checkProducer()
  258. self.serverIO._checkProducer()
  259. if self.debug or debug:
  260. print(".")
  261. # XXX slightly buggy in the face of incremental output
  262. if cData:
  263. print("C: " + repr(cData))
  264. if sData:
  265. print("S: " + repr(sData))
  266. if cData:
  267. self.serverIO.bufferReceived(cData)
  268. if sData:
  269. self.clientIO.bufferReceived(sData)
  270. if cData or sData:
  271. return True
  272. if self.serverIO.disconnecting and not self.serverIO.disconnected:
  273. if self.debug or debug:
  274. print("* C")
  275. self.serverIO.disconnected = True
  276. self.clientIO.disconnecting = True
  277. self.clientIO.reportDisconnect()
  278. return True
  279. if self.clientIO.disconnecting and not self.clientIO.disconnected:
  280. if self.debug or debug:
  281. print("* S")
  282. self.clientIO.disconnected = True
  283. self.serverIO.disconnecting = True
  284. self.serverIO.reportDisconnect()
  285. return True
  286. return False
  287. def connect(
  288. serverProtocol,
  289. serverTransport,
  290. clientProtocol,
  291. clientTransport,
  292. debug=False,
  293. greet=True,
  294. ):
  295. """
  296. Create a new L{IOPump} connecting two protocols.
  297. @param serverProtocol: The protocol to use on the accepting side of the
  298. connection.
  299. @type serverProtocol: L{IProtocol} provider
  300. @param serverTransport: The transport to associate with C{serverProtocol}.
  301. @type serverTransport: L{FakeTransport}
  302. @param clientProtocol: The protocol to use on the initiating side of the
  303. connection.
  304. @type clientProtocol: L{IProtocol} provider
  305. @param clientTransport: The transport to associate with C{clientProtocol}.
  306. @type clientTransport: L{FakeTransport}
  307. @param debug: A flag indicating whether to log information about what the
  308. L{IOPump} is doing.
  309. @type debug: L{bool}
  310. @param greet: Should the L{IOPump} be L{flushed <IOPump.flush>} once before
  311. returning to put the protocols into their post-handshake or
  312. post-server-greeting state?
  313. @type greet: L{bool}
  314. @return: An L{IOPump} which connects C{serverProtocol} and
  315. C{clientProtocol} and delivers bytes between them when it is pumped.
  316. @rtype: L{IOPump}
  317. """
  318. serverProtocol.makeConnection(serverTransport)
  319. clientProtocol.makeConnection(clientTransport)
  320. pump = IOPump(
  321. clientProtocol, serverProtocol, clientTransport, serverTransport, debug
  322. )
  323. if greet:
  324. # Kick off server greeting, etc
  325. pump.flush()
  326. return pump
  327. def connectedServerAndClient(
  328. ServerClass,
  329. ClientClass,
  330. clientTransportFactory=makeFakeClient,
  331. serverTransportFactory=makeFakeServer,
  332. debug=False,
  333. greet=True,
  334. ):
  335. """
  336. Connect a given server and client class to each other.
  337. @param ServerClass: a callable that produces the server-side protocol.
  338. @type ServerClass: 0-argument callable returning L{IProtocol} provider.
  339. @param ClientClass: like C{ServerClass} but for the other side of the
  340. connection.
  341. @type ClientClass: 0-argument callable returning L{IProtocol} provider.
  342. @param clientTransportFactory: a callable that produces the transport which
  343. will be attached to the protocol returned from C{ClientClass}.
  344. @type clientTransportFactory: callable taking (L{IProtocol}) and returning
  345. L{FakeTransport}
  346. @param serverTransportFactory: a callable that produces the transport which
  347. will be attached to the protocol returned from C{ServerClass}.
  348. @type serverTransportFactory: callable taking (L{IProtocol}) and returning
  349. L{FakeTransport}
  350. @param debug: Should this dump an escaped version of all traffic on this
  351. connection to stdout for inspection?
  352. @type debug: L{bool}
  353. @param greet: Should the L{IOPump} be L{flushed <IOPump.flush>} once before
  354. returning to put the protocols into their post-handshake or
  355. post-server-greeting state?
  356. @type greet: L{bool}
  357. @return: the client protocol, the server protocol, and an L{IOPump} which,
  358. when its C{pump} and C{flush} methods are called, will move data
  359. between the created client and server protocol instances.
  360. @rtype: 3-L{tuple} of L{IProtocol}, L{IProtocol}, L{IOPump}
  361. """
  362. c = ClientClass()
  363. s = ServerClass()
  364. cio = clientTransportFactory(c)
  365. sio = serverTransportFactory(s)
  366. return c, s, connect(s, sio, c, cio, debug, greet)
  367. def _factoriesShouldConnect(clientInfo, serverInfo):
  368. """
  369. Should the client and server described by the arguments be connected to
  370. each other, i.e. do their port numbers match?
  371. @param clientInfo: the args for connectTCP
  372. @type clientInfo: L{tuple}
  373. @param serverInfo: the args for listenTCP
  374. @type serverInfo: L{tuple}
  375. @return: If they do match, return factories for the client and server that
  376. should connect; otherwise return L{None}, indicating they shouldn't be
  377. connected.
  378. @rtype: L{None} or 2-L{tuple} of (L{ClientFactory},
  379. L{IProtocolFactory})
  380. """
  381. (
  382. clientHost,
  383. clientPort,
  384. clientFactory,
  385. clientTimeout,
  386. clientBindAddress,
  387. ) = clientInfo
  388. (serverPort, serverFactory, serverBacklog, serverInterface) = serverInfo
  389. if serverPort == clientPort:
  390. return clientFactory, serverFactory
  391. else:
  392. return None
  393. class ConnectionCompleter:
  394. """
  395. A L{ConnectionCompleter} can cause synthetic TCP connections established by
  396. L{MemoryReactor.connectTCP} and L{MemoryReactor.listenTCP} to succeed or
  397. fail.
  398. """
  399. def __init__(self, memoryReactor):
  400. """
  401. Create a L{ConnectionCompleter} from a L{MemoryReactor}.
  402. @param memoryReactor: The reactor to attach to.
  403. @type memoryReactor: L{MemoryReactor}
  404. """
  405. self._reactor = memoryReactor
  406. def succeedOnce(self, debug=False):
  407. """
  408. Complete a single TCP connection established on this
  409. L{ConnectionCompleter}'s L{MemoryReactor}.
  410. @param debug: A flag; whether to dump output from the established
  411. connection to stdout.
  412. @type debug: L{bool}
  413. @return: a pump for the connection, or L{None} if no connection could
  414. be established.
  415. @rtype: L{IOPump} or L{None}
  416. """
  417. memoryReactor = self._reactor
  418. for clientIdx, clientInfo in enumerate(memoryReactor.tcpClients):
  419. for serverInfo in memoryReactor.tcpServers:
  420. factories = _factoriesShouldConnect(clientInfo, serverInfo)
  421. if factories:
  422. memoryReactor.tcpClients.remove(clientInfo)
  423. memoryReactor.connectors.pop(clientIdx)
  424. clientFactory, serverFactory = factories
  425. clientProtocol = clientFactory.buildProtocol(None)
  426. serverProtocol = serverFactory.buildProtocol(None)
  427. serverTransport = makeFakeServer(serverProtocol)
  428. clientTransport = makeFakeClient(clientProtocol)
  429. return connect(
  430. serverProtocol,
  431. serverTransport,
  432. clientProtocol,
  433. clientTransport,
  434. debug,
  435. )
  436. def failOnce(self, reason=Failure(ConnectionRefusedError())):
  437. """
  438. Fail a single TCP connection established on this
  439. L{ConnectionCompleter}'s L{MemoryReactor}.
  440. @param reason: the reason to provide that the connection failed.
  441. @type reason: L{Failure}
  442. """
  443. self._reactor.tcpClients.pop(0)[2].clientConnectionFailed(
  444. self._reactor.connectors.pop(0), reason
  445. )
  446. def connectableEndpoint(debug=False):
  447. """
  448. Create an endpoint that can be fired on demand.
  449. @param debug: A flag; whether to dump output from the established
  450. connection to stdout.
  451. @type debug: L{bool}
  452. @return: A client endpoint, and an object that will cause one of the
  453. L{Deferred}s returned by that client endpoint.
  454. @rtype: 2-L{tuple} of (L{IStreamClientEndpoint}, L{ConnectionCompleter})
  455. """
  456. reactor = MemoryReactorClock()
  457. clientEndpoint = TCP4ClientEndpoint(reactor, "0.0.0.0", 4321)
  458. serverEndpoint = TCP4ServerEndpoint(reactor, 4321)
  459. serverEndpoint.listen(Factory.forProtocol(Protocol))
  460. return clientEndpoint, ConnectionCompleter(reactor)