-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathnoob.py
187 lines (139 loc) · 7.66 KB
/
noob.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
''' NOOB: Naive Objectionably Opaque Bank
A.K.A. Naieve Ontzettend Onveilige Bank
The NOOB is a pure clearing house:
- It doesn't store anything
- It doesn't charge anything
- It doesn't check anything
To get acquainted with how it works,
start several instances if the consumer bank emulator (NOOC),
each with its own bank code and currency conversion rate.
Note that while the NOOB is written in Python,
its clients may be written in any language,
as long as they speak JSON over WebSockets.
Clients are required to utilize the NOOB protocol:
Master role
===========
Remotely opening an ABNA account from bank INGB happens by sending to the NOOB:
["ABNA", "open", 300400, 1234, 0]
Remotely closing this ABNA account from bank INGB happens by sending to the NOOB:
["ABNA", "close", 300400, 1234, 0]
Deposing 1000 local coins on this ABNA account from bank INGB happens by sending to the NOOB:
["ABNA", "deposit", 300400, 1234, 1000]
Withdrawing 199.50 local coins from this ABNA account from bank INGB happens by sending to the NOOB:
["ABNA", "withdraw", 300400, 1234, 199.50]
The hack command allows inspecting all local accounts on a NOOC.
Slave role
==========
All commands received by the slave are the same as the ones sent by the master, only with the first (bank code) parameter omitted.
Replies
=======
All commands for both roles are replied upon:
[True] means succes, [False] means failure.
Happy banking!
'''
import sys
import asyncio
import websockets
import traceback
import bank
class Noob (bank.Bank):
version = '1.0.1'
timePerQueueCreationPoll = 0.1
exitServerMessage = '__exit_server__'
class RegistrationError (Exception):
pass
class ExitConnection (Exception):
pass
def __init__ (self):
super () .__init__ (self.centralBankCode)
self.print (f'Central bank {self.version} initiated (type q(uit) to exit)')
# Create message queues
self.commandQueues = {}
self.replyQueues = {}
# Start servers creator (as opposed to server) and run it until complete, which is never
serverFuture = websockets.serve (self.roleServer, self.centralHostName, self.centralPortNr)
asyncio.get_event_loop () .run_until_complete (serverFuture)
# Start command interpreter
asyncio.get_event_loop () .run_until_complete (self.commandInterpreter ())
# Prevent termination of event loop, since role servers subscribed to it
asyncio.get_event_loop () .run_forever ()
async def commandInterpreter (self):
while True:
command = await self.input ()
if self.match (command, 'quit'):
exit (0)
def reportUnknownBankCode (self, bankCode):
self.print (f'Error - Unknown bank code: {bankCode}')
async def created (self, bankCode, queues):
for iAttempt in range (round (1 / self.timePerQueueCreationPoll)): # Repeated attempts for 1 sec in total
if bankCode in queues:
break
await asyncio.sleep (self.timePerQueueCreationPoll) # Only a short sleep, so possibly fast reaction
else:
self.reportUnknownBankCode (bankCode)
async def roleServer (self, socket, path):
'''
Role communication handler
- Called once for each master and once for each slave
- Handles the socket belonging to the master or slave that it's called for
- Remains looping for that master or slave until connection is closed
- So several calls of this coroutine run concurrently, one per master and one per slave
'''
try:
self.print (f'Instantiated socket: {socket}')
command, role, bankCode = None, None, None
command, role, bankCode = await (self.recv (socket, role))
if command == 'register':
await self.send (socket, role, True)
if role == 'master':
self.commandQueues [bankCode] = asyncio.Queue () # This will also replace an abandoned queue by an empty one
await self.created (bankCode, self.replyQueues)
while True:
# Receive command from own master
message = await self.recv (socket, role)
# Obey exit request by own slave
if message == self.exitServerMessage:
raise self.ExitConnection ()
# Put it in the queue belonging to the right slave
try:
await self.commandQueues [message [0]] .put ([bankCode] + message [1:])
# Get reply of slave from own master queue and send it to master
# The master gives a command to only one slave at a time, so he knows who answered
await self.send (socket, role, await self.replyQueues [bankCode] .get ())
except KeyError:
self.reportUnknownBankCode (message [0])
await self.send (socket, role, False)
else:
self.replyQueues [bankCode] = asyncio.Queue () # This will also replace an abandoned queue by an empty one
await self.created (bankCode, self.commandQueues)
while True:
# Wait until command in own slave queue
message = await self.commandQueues [bankCode] .get ()
# Obey exit request by own master
if message == self.exitServerMessage:
raise self.ExitConnection ()
# Send it to own slave
await self.send (socket, role, message [1:])
# Get reply from own slave and put it in the right reply queue
try:
await self.replyQueues [message [0]] .put (await self.recv (socket, role))
except:
self.reportUnknownBankCode (message [0])
else:
raise self.RegistrationError ()
except self.RegistrationError:
try:
await socket.send (json.dumps (False), role) # Try to notify client
except:
pass # Escape if client closed connection
self.print (f'Error: registration expected, got command: {command}')
except websockets.exceptions.ConnectionClosed:
self.print (f'Error: connection closed by {bankCode} as {role}')
del (self.replyQueues if role == 'master' else self.commandQueues) [bankCode]
await (self.commandQueues if role == 'master' else self.replyQueues) [bankCode].put (self.exitServerMessage)
except self.ExitConnection:
del (self.replyQueues if role == 'master' else self.commandQueues) [bankCode]
self.print (f'Consequence: connection closed by central bank for {bankCode} as {role}')
except Exception:
self.print (f'Error: in serving {bankCode} as {role}\n{traceback.format_exc ()}')
noob = Noob ()