-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcore.py
357 lines (316 loc) · 11.2 KB
/
core.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
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
# -*- coding: utf-8 -*-
# @Author: FSOL
# @File : core.py
"""
core.py
========================
As its name, the core.py is the center of the whole system.
It is mainly in charge of connection management.(i.e. receive connection requests and messages and reply)
It has 2 modes(for now) to start: soldier and king.
(just as the slave and master model,
I name them like this just for fun and some possible update about connection model.)
To start in king mode, just type 'python core.py' and it will use the localhost in config to listen
preparing to receive connections or else.
To start in soldier mode, you should add target ip:host and password for server like this
'python core.py 127.0.0.1 9999 password'
and it will automatically connect to the server you said and try ONE time with the password,
If password is wrong, it will exit. Except for that, it will start to wait for order from target server.
(Both can use nohup or & or ...)
Be aware that I use fileno to identify a connection,
and connections are stored in the globalvar.py.
url to this system: https://github.com/sinnfashen/New_web
"""
import hashlib
import select
import socket
import time
import sys
import threading
import Work.globalvar as gv
import config as cf
from Work.log import Logger
# Initialize
import Function
import User
logger = Logger('connection', 'DEBUG')
inside = select.epoll()
outside = select.epoll()
password_list = dict(zip(cf.user_password.values(), cf.user_password.keys()))
self = socket.socket()
# punish_list is record of times of wrong password, being used to avoid someone try the password too many times.
# link_list is record of times of connection of one ip address, also being used to anti-attack.
punish_list = {}
link_list = {}
class Connection:
logger = Logger('connection', 'DEBUG')
def __init__(self, fileno, socket, time, level='Unidentified'):
self.fileno = fileno
self.socket = socket
self.level = level
self.time = time
def disconnect(self):
"""
Standard way to disconnect and clean all stuff without risk.
:param self:
:return:
"""
User.user_list[self.level]['leave'](self)
self.logger.info('{}:----{} disconnected'.format(self.fileno, self.level))
if self.level == 'Unidentified':
outside.unregister(self.fileno)
else:
inside.unregister(self.fileno)
self.socket.close()
gv.connections.pop(self.fileno)
punish_list[self.fileno] = 0
def upgrade(self, level):
"""
Give the connection level after received corresponding password.(Disconnect if it closed)
:param self:
:param level:
:return:
"""
link_list[self.socket.getpeername()[0]] -= 1
self.level = level
inside.register(self.fileno, select.EPOLLIN)
outside.unregister(self.fileno)
if self.save_send(cf.CONNECTSUCCESS) == 1:
return 1
try:
User.user_list[self.level]['entry'](self)
self.logger.info('{}: {}----{} connected'.format(
self.fileno, self.socket.getpeername()[0], self.level))
except Exception:
self.logger.warning("{}: {} unexcepted close.".format(self.fileno, self.level))
self.disconnect()
return 0
def save_send(self, message):
"""
Standard way to send message.
Avoid the risk of socket closed before send.(In that case, log and disconnect with it)
:param self:
:param message:
:return: 0 for normal and 1 for error.
"""
try:
self.socket.sendall(message)
except Exception:
self.logger.warning("{}: close before send.".format(self.fileno))
self.disconnect()
return 1
return 0
def save_receive(self):
"""
Standard way to receive a message.
:return:
"""
try:
message = self.socket.recv(2048)
except Exception:
message = ''
if message == '':
self.disconnect()
else:
return message
def process(self, message):
return User.user_list[self.level]['receive'](self, message)
def reloading():
"""
Reload files after update to keep all files are up-to-date.
(Won't reload globalvar, client and log)
:return:
"""
Function.function_list.clear()
User.user_list.clear()
try:
reload(Function)
reload(User)
reload(cf)
except Exception as e:
logger.error('Reload after update error!{}'.format(e))
finally:
gv.order_to_update = False
def encry(password):
"""
md5
:param password:
:return: md5(password)
"""
return hashlib.md5(password).hexdigest()
def punishment(conn):
"""
If fileno has given x times wrong password, it has to wait x^2 seconds to be heard again,
but if x is bigger than 10, just disconnect.
This function is operated in a single threading so that won't affect other requests.
:param conn: connection
:return:
"""
outside.unregister(conn.fileno)
if punish_list[conn.fileno] <= 10:
time.sleep(punish_list[conn.fileno]*punish_list[conn.fileno])
conn.save_send("WRONG PASSWORD!")
outside.register(conn.fileno, select.EPOLLIN)
else:
conn.save_send("Enough!")
conn.disconnect()
def add_count(number, the_list):
"""
Small function to make sure list start from 0.
:param number:
:param the_list:
:return:
"""
if number in the_list:
the_list[number] += 1
else:
the_list[number] = 1
def kill_out_time():
"""
Check all connections and disconnect with those being unidentified for too long(OUTTIME in config).
:return:
"""
for conn in gv.connections.values():
if conn.level == 'Unidentified' and time.time() - conn.time >= cf.OUTTIME:
conn.save_send("auto disconnect\n")
conn.disconnect()
def outside_listen():
"""
Listen from new and unidentified connections.
:return:
"""
while True:
events = outside.poll(20)
for fileno, event in events:
try:
if fileno == self.fileno():
con, conaddr = self.accept()
if conaddr not in link_list or link_list[conaddr[0]] <= 20:
add_count(conaddr[0], link_list)
logger.info(' '.join([str(conaddr), "Incoming Connection"]))
outside.register(con.fileno(), select.EPOLLIN)
gv.connections[con.fileno()] = Connection(con.fileno(), con, time.time())
else:
conn = gv.connections[fileno]
message = conn.save_receive()
if message:
if encry(message) in password_list:
conn.upgrade(password_list[encry(message)])
else:
add_count(fileno, punish_list)
temp = threading.Thread(target=punishment, args=[conn])
temp.setDaemon(True)
temp.start()
try:
logger.info('{}: {}----unidentified tried a wrong password'
.format(fileno, conn.socket.getpeername()))
except Exception:
logger.warning("{}: unexcepted close.".format(fileno))
conn.disconnect()
except Exception:
logger.error(logger.traceback())
def event_divider():
"""
Listen from identified connections.
Be aware that unlike soldier, king won't respond if there is nothing to say.
:return:
"""
events = inside.poll(20)
for fileno, event in events:
conn = gv.connections[fileno]
message = conn.save_receive()
if message:
conn.save_send(conn.process(message))
def king_server():
"""
Server starting in king mode will split one threading to receive unidentified connections
and handle the others itself.
How long will one loop be depends on any information received, which will cause a immediately flush.
But it will finished at least in 20s.
:return:
"""
socket.setdefaulttimeout(cf.timeout)
self.bind((cf.HOST, cf.PORT))
self.listen(10)
outside.register(self.fileno(), select.EPOLLIN)
tout = threading.Thread(target=outside_listen, args=[])
tout.setDaemon(True)
tout.start()
logger.info("--------------------------------\n MASTER SYSTEM STARTED")
while True:
if gv.order_to_close:
break
if gv.order_to_update:
reloading()
event_divider()
kill_out_time()
def soldier_server():
"""
Since soldier should only maintain one connection with king, there is no need for a object of connection or epoll.
If only password was passed in, connect default_king set in config.py.
Otherwise connect the ip:port passed in.
Soldier will stop trying only when the password was rejected.
Whether the order is correct(which should be in most cases), soldier will respond one time for one order, where
respond can be 'empty' for 'nothing to say'.
:return:
"""
global self
while True:
self = socket.socket()
try:
if len(sys.argv) == 2:
self.connect(cf.default_king)
else:
self.connect((sys.argv[2], int(sys.argv[3])))
logger.info("--------------------------------\n SOLDIER SYSTEM STARTED")
self.send(sys.argv[1])
message = self.recv(1024)
except Exception:
logger.error(logger.traceback())
logger.error("Lost connection, retry in 5min")
self.close()
time.sleep(300)
continue
logger.info(message)
if message != cf.CONNECTSUCCESS:
return 0
while True:
try:
if gv.order_to_close:
break
if gv.order_to_update:
reloading()
message = self.recv(1024)
respond = User.user_list['server']['receive'](self, message)
if respond:
self.send(respond)
else:
self.send("empty")
except Exception:
logger.error(logger.traceback())
logger.error("Lost connection, retry in 5min")
self.close()
time.sleep(300)
break
def main():
"""
Choose the mode.
The arguments are like this:
python core.py[0] password[1] ip[2] port[3]
:return:
"""
try:
if len(sys.argv) != 1:
soldier_server()
else:
king_server()
except Exception:
logger.critical(logger.traceback())
finally:
logger.info("Server is shutting down......")
temp = gv.connections.keys()
for fileno in temp:
gv.connections[fileno].disconnect()
self.close()
return 0
if __name__ == "__main__":
main()