Added the tsuserver3 files necessary to support this custom client.

This commit is contained in:
Cerapter 2018-07-31 00:44:41 +02:00
parent f77381864e
commit 374e939ac4
16 changed files with 3021 additions and 0 deletions

2
.gitignore vendored
View File

@ -16,3 +16,5 @@ Makefile*
object_script*
/Attorney_Online_remake_resource.rc
/attorney_online_remake_plugin_import.cpp
server/__pycache__

0
server/__init__.py Normal file
View File

641
server/aoprotocol.py Normal file
View File

@ -0,0 +1,641 @@
# tsuserver3, an Attorney Online server
#
# Copyright (C) 2016 argoneus <argoneuscze@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import asyncio
import re
from time import localtime, strftime
from enum import Enum
from . import commands
from . import logger
from .exceptions import ClientError, AreaError, ArgumentError, ServerError
from .fantacrypt import fanta_decrypt
from .evidence import EvidenceList
from .websocket import WebSocket
class AOProtocol(asyncio.Protocol):
"""
The main class that deals with the AO protocol.
"""
class ArgType(Enum):
STR = 1,
STR_OR_EMPTY = 2,
INT = 3
def __init__(self, server):
super().__init__()
self.server = server
self.client = None
self.buffer = ''
self.ping_timeout = None
self.websocket = None
def data_received(self, data):
""" Handles any data received from the network.
Receives data, parses them into a command and passes it
to the command handler.
:param data: bytes of data
"""
if self.websocket is None:
self.websocket = WebSocket(self.client, self)
if not self.websocket.handshake(data):
self.websocket = False
else:
self.client.websocket = self.websocket
buf = data
if not self.client.is_checked and self.server.ban_manager.is_banned(self.client.ipid):
self.client.transport.close()
else:
self.client.is_checked = True
if self.websocket:
buf = self.websocket.handle(data)
if buf is None:
buf = b''
if not isinstance(buf, str):
# try to decode as utf-8, ignore any erroneous characters
self.buffer += buf.decode('utf-8', 'ignore')
else:
self.buffer = buf
if len(self.buffer) > 8192:
self.client.disconnect()
for msg in self.get_messages():
if len(msg) < 2:
self.client.disconnect()
return
# general netcode structure is not great
if msg[0] in ('#', '3', '4'):
if msg[0] == '#':
msg = msg[1:]
spl = msg.split('#', 1)
msg = '#'.join([fanta_decrypt(spl[0])] + spl[1:])
logger.log_debug('[INC][RAW]{}'.format(msg), self.client)
try:
cmd, *args = msg.split('#')
self.net_cmd_dispatcher[cmd](self, args)
except KeyError:
return
def connection_made(self, transport):
""" Called upon a new client connecting
:param transport: the transport object
"""
self.client = self.server.new_client(transport)
self.ping_timeout = asyncio.get_event_loop().call_later(self.server.config['timeout'], self.client.disconnect)
asyncio.get_event_loop().call_later(0.25, self.client.send_command, 'decryptor', 34) # just fantacrypt things)
def connection_lost(self, exc):
""" User disconnected
:param exc: reason
"""
self.server.remove_client(self.client)
self.ping_timeout.cancel()
def get_messages(self):
""" Parses out full messages from the buffer.
:return: yields messages
"""
while '#%' in self.buffer:
spl = self.buffer.split('#%', 1)
self.buffer = spl[1]
yield spl[0]
# exception because bad netcode
askchar2 = '#615810BC07D12A5A#'
if self.buffer == askchar2:
self.buffer = ''
yield askchar2
def validate_net_cmd(self, args, *types, needs_auth=True):
""" Makes sure the net command's arguments match expectations.
:param args: actual arguments to the net command
:param types: what kind of data types are expected
:param needs_auth: whether you need to have chosen a character
:return: returns True if message was validated
"""
if needs_auth and self.client.char_id == -1:
return False
if len(args) != len(types):
return False
for i, arg in enumerate(args):
if len(arg) == 0 and types[i] != self.ArgType.STR_OR_EMPTY:
return False
if types[i] == self.ArgType.INT:
try:
args[i] = int(arg)
except ValueError:
return False
return True
def net_cmd_hi(self, args):
""" Handshake.
HI#<hdid:string>#%
:param args: a list containing all the arguments
"""
if not self.validate_net_cmd(args, self.ArgType.STR, needs_auth=False):
return
self.client.hdid = args[0]
if self.client.hdid not in self.client.server.hdid_list:
self.client.server.hdid_list[self.client.hdid] = []
if self.client.ipid not in self.client.server.hdid_list[self.client.hdid]:
self.client.server.hdid_list[self.client.hdid].append(self.client.ipid)
self.client.server.dump_hdids()
for ipid in self.client.server.hdid_list[self.client.hdid]:
if self.server.ban_manager.is_banned(ipid):
self.client.disconnect()
return
logger.log_server('Connected. HDID: {}.'.format(self.client.hdid), self.client)
self.client.send_command('ID', self.client.id, self.server.software, self.server.get_version_string())
self.client.send_command('PN', self.server.get_player_count() - 1, self.server.config['playerlimit'])
def net_cmd_id(self, args):
""" Client version and PV
ID#<pv:int>#<software:string>#<version:string>#%
"""
self.client.is_ao2 = False
if len(args) < 2:
return
version_list = args[1].split('.')
if len(version_list) < 3:
return
release = int(version_list[0])
major = int(version_list[1])
minor = int(version_list[2])
if args[0] != 'AO2':
return
if release < 2:
return
elif release == 2:
if major < 2:
return
elif major == 2:
if minor < 5:
return
self.client.is_ao2 = True
self.client.send_command('FL', 'yellowtext', 'customobjections', 'flipping', 'fastloading', 'noencryption', 'deskmod', 'evidence')
def net_cmd_ch(self, _):
""" Periodically checks the connection.
CHECK#%
"""
self.client.send_command('CHECK')
self.ping_timeout.cancel()
self.ping_timeout = asyncio.get_event_loop().call_later(self.server.config['timeout'], self.client.disconnect)
def net_cmd_askchaa(self, _):
""" Ask for the counts of characters/evidence/music
askchaa#%
"""
char_cnt = len(self.server.char_list)
evi_cnt = 0
music_cnt = sum([len(x) for x in self.server.music_pages_ao1])
self.client.send_command('SI', char_cnt, evi_cnt, music_cnt)
def net_cmd_askchar2(self, _):
""" Asks for the character list.
askchar2#%
"""
self.client.send_command('CI', *self.server.char_pages_ao1[0])
def net_cmd_an(self, args):
""" Asks for specific pages of the character list.
AN#<page:int>#%
"""
if not self.validate_net_cmd(args, self.ArgType.INT, needs_auth=False):
return
if len(self.server.char_pages_ao1) > args[0] >= 0:
self.client.send_command('CI', *self.server.char_pages_ao1[args[0]])
else:
self.client.send_command('EM', *self.server.music_pages_ao1[0])
def net_cmd_ae(self, _):
""" Asks for specific pages of the evidence list.
AE#<page:int>#%
"""
pass # todo evidence maybe later
def net_cmd_am(self, args):
""" Asks for specific pages of the music list.
AM#<page:int>#%
"""
if not self.validate_net_cmd(args, self.ArgType.INT, needs_auth=False):
return
if len(self.server.music_pages_ao1) > args[0] >= 0:
self.client.send_command('EM', *self.server.music_pages_ao1[args[0]])
else:
self.client.send_done()
self.client.send_area_list()
self.client.send_motd()
def net_cmd_rc(self, _):
""" Asks for the whole character list(AO2)
AC#%
"""
self.client.send_command('SC', *self.server.char_list)
def net_cmd_rm(self, _):
""" Asks for the whole music list(AO2)
AM#%
"""
self.client.send_command('SM', *self.server.music_list_ao2)
def net_cmd_rd(self, _):
""" Asks for server metadata(charscheck, motd etc.) and a DONE#% signal(also best packet)
RD#%
"""
self.client.send_done()
self.client.send_area_list()
self.client.send_motd()
def net_cmd_cc(self, args):
""" Character selection.
CC#<client_id:int>#<char_id:int>#<hdid:string>#%
"""
if not self.validate_net_cmd(args, self.ArgType.INT, self.ArgType.INT, self.ArgType.STR, needs_auth=False):
return
cid = args[1]
try:
self.client.change_character(cid)
except ClientError:
return
def net_cmd_ms(self, args):
""" IC message.
Refer to the implementation for details.
"""
if self.client.is_muted: # Checks to see if the client has been muted by a mod
self.client.send_host_message("You have been muted by a moderator")
return
if not self.client.area.can_send_message(self.client):
return
if self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.STR_OR_EMPTY, self.ArgType.STR,
self.ArgType.STR,
self.ArgType.STR, self.ArgType.STR, self.ArgType.STR, self.ArgType.INT,
self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT,
self.ArgType.INT, self.ArgType.INT, self.ArgType.INT):
msg_type, pre, folder, anim, text, pos, sfx, anim_type, cid, sfx_delay, button, evidence, flip, ding, color = args
showname = self.client.get_char_name()
elif self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.STR_OR_EMPTY, self.ArgType.STR,
self.ArgType.STR,
self.ArgType.STR, self.ArgType.STR, self.ArgType.STR, self.ArgType.INT,
self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT,
self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.STR):
msg_type, pre, folder, anim, text, pos, sfx, anim_type, cid, sfx_delay, button, evidence, flip, ding, color, showname = args
if len(showname) > 0 and not self.client.area.showname_changes_allowed == "true":
self.client.send_host_message("Showname changes are forbidden in this area!")
return
else:
return
msg_type, pre, folder, anim, text, pos, sfx, anim_type, cid, sfx_delay, button, evidence, flip, ding, color = args
if self.client.area.is_iniswap(self.client, pre, anim, folder) and folder != self.client.get_char_name():
self.client.send_host_message("Iniswap is blocked in this area")
return
if msg_type not in ('chat', '0', '1'):
return
if anim_type not in (0, 1, 2, 5, 6):
return
if cid != self.client.char_id:
return
if sfx_delay < 0:
return
if button not in (0, 1, 2, 3, 4):
return
if evidence < 0:
return
if ding not in (0, 1):
return
if color not in (0, 1, 2, 3, 4, 5, 6, 7, 8):
return
if len(showname) > 15:
self.client.send_host_message("Your IC showname is way too long!")
return
if not self.client.area.shouts_allowed:
# Old clients communicate the objecting in anim_type.
if anim_type == 2:
anim_type = 1
elif anim_type == 6:
anim_type = 5
# New clients do it in a specific objection message area.
button = 0
# Turn off the ding.
ding = 0
if color == 2 and not self.client.is_mod:
color = 0
if color == 6:
text = re.sub(r'[^\x00-\x7F]+',' ', text) #remove all unicode to prevent redtext abuse
if len(text.strip( ' ' )) == 1:
color = 0
else:
if text.strip( ' ' ) in ('<num>', '<percent>', '<dollar>', '<and>'):
color = 0
if self.client.pos:
pos = self.client.pos
else:
if pos not in ('def', 'pro', 'hld', 'hlp', 'jud', 'wit'):
return
msg = text[:256]
if self.client.disemvowel:
msg = self.client.disemvowel_message(msg)
self.client.pos = pos
if evidence:
if self.client.area.evi_list.evidences[self.client.evi_list[evidence] - 1].pos != 'all':
self.client.area.evi_list.evidences[self.client.evi_list[evidence] - 1].pos = 'all'
self.client.area.broadcast_evidence_list()
self.client.area.send_command('MS', msg_type, pre, folder, anim, msg, pos, sfx, anim_type, cid,
sfx_delay, button, self.client.evi_list[evidence], flip, ding, color, showname)
self.client.area.set_next_msg_delay(len(msg))
logger.log_server('[IC][{}][{}]{}'.format(self.client.area.id, self.client.get_char_name(), msg), self.client)
if (self.client.area.is_recording):
self.client.area.recorded_messages.append(args)
def net_cmd_ct(self, args):
""" OOC Message
CT#<name:string>#<message:string>#%
"""
if self.client.is_ooc_muted: # Checks to see if the client has been muted by a mod
self.client.send_host_message("You have been muted by a moderator")
return
if not self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.STR):
return
if self.client.name != args[0] and self.client.fake_name != args[0]:
if self.client.is_valid_name(args[0]):
self.client.name = args[0]
self.client.fake_name = args[0]
else:
self.client.fake_name = args[0]
if self.client.name == '':
self.client.send_host_message('You must insert a name with at least one letter')
return
if len(self.client.name) > 30:
self.client.send_host_message('Your OOC name is too long! Limit it to 30 characters.')
return
if self.client.name.startswith(self.server.config['hostname']) or self.client.name.startswith('<dollar>G'):
self.client.send_host_message('That name is reserved!')
return
if args[1].startswith('/'):
spl = args[1][1:].split(' ', 1)
cmd = spl[0]
arg = ''
if len(spl) == 2:
arg = spl[1][:256]
try:
called_function = 'ooc_cmd_{}'.format(cmd)
getattr(commands, called_function)(self.client, arg)
except AttributeError:
print('Attribute error with ' + called_function)
self.client.send_host_message('Invalid command.')
except (ClientError, AreaError, ArgumentError, ServerError) as ex:
self.client.send_host_message(ex)
else:
if self.client.disemvowel:
args[1] = self.client.disemvowel_message(args[1])
self.client.area.send_command('CT', self.client.name, args[1])
logger.log_server(
'[OOC][{}][{}][{}]{}'.format(self.client.area.id, self.client.get_char_name(), self.client.name,
args[1]), self.client)
def net_cmd_mc(self, args):
""" Play music.
MC#<song_name:int>#<???:int>#%
"""
try:
area = self.server.area_manager.get_area_by_name(args[0])
self.client.change_area(area)
except AreaError:
if self.client.is_muted: # Checks to see if the client has been muted by a mod
self.client.send_host_message("You have been muted by a moderator")
return
if not self.client.is_dj:
self.client.send_host_message('You were blockdj\'d by a moderator.')
return
if not self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.INT):
return
if args[1] != self.client.char_id:
return
if self.client.change_music_cd():
self.client.send_host_message('You changed song too many times. Please try again after {} seconds.'.format(int(self.client.change_music_cd())))
return
try:
name, length = self.server.get_song_data(args[0])
self.client.area.play_music(name, self.client.char_id, length)
self.client.area.add_music_playing(self.client, name)
logger.log_server('[{}][{}]Changed music to {}.'
.format(self.client.area.id, self.client.get_char_name(), name), self.client)
except ServerError:
return
except ClientError as ex:
self.client.send_host_message(ex)
def net_cmd_rt(self, args):
""" Plays the Testimony/CE animation.
RT#<type:string>#%
"""
if self.client.is_muted: # Checks to see if the client has been muted by a mod
self.client.send_host_message("You have been muted by a moderator")
return
if not self.client.can_wtce:
self.client.send_host_message('You were blocked from using judge signs by a moderator.')
return
if not self.validate_net_cmd(args, self.ArgType.STR):
return
if args[0] == 'testimony1':
sign = 'WT'
elif args[0] == 'testimony2':
sign = 'CE'
else:
return
if self.client.wtce_mute():
self.client.send_host_message('You used witness testimony/cross examination signs too many times. Please try again after {} seconds.'.format(int(self.client.wtce_mute())))
return
self.client.area.send_command('RT', args[0])
self.client.area.add_to_judgelog(self.client, 'used {}'.format(sign))
logger.log_server("[{}]{} Used WT/CE".format(self.client.area.id, self.client.get_char_name()), self.client)
def net_cmd_hp(self, args):
""" Sets the penalty bar.
HP#<type:int>#<new_value:int>#%
"""
if self.client.is_muted: # Checks to see if the client has been muted by a mod
self.client.send_host_message("You have been muted by a moderator")
return
if not self.validate_net_cmd(args, self.ArgType.INT, self.ArgType.INT):
return
try:
self.client.area.change_hp(args[0], args[1])
self.client.area.add_to_judgelog(self.client, 'changed the penalties')
logger.log_server('[{}]{} changed HP ({}) to {}'
.format(self.client.area.id, self.client.get_char_name(), args[0], args[1]), self.client)
except AreaError:
return
def net_cmd_pe(self, args):
""" Adds a piece of evidence.
PE#<name: string>#<description: string>#<image: string>#%
"""
if len(args) < 3:
return
#evi = Evidence(args[0], args[1], args[2], self.client.pos)
self.client.area.evi_list.add_evidence(self.client, args[0], args[1], args[2], 'all')
self.client.area.broadcast_evidence_list()
def net_cmd_de(self, args):
""" Deletes a piece of evidence.
DE#<id: int>#%
"""
self.client.area.evi_list.del_evidence(self.client, self.client.evi_list[int(args[0])])
self.client.area.broadcast_evidence_list()
def net_cmd_ee(self, args):
""" Edits a piece of evidence.
EE#<id: int>#<name: string>#<description: string>#<image: string>#%
"""
if len(args) < 4:
return
evi = (args[1], args[2], args[3], 'all')
self.client.area.evi_list.edit_evidence(self.client, self.client.evi_list[int(args[0])], evi)
self.client.area.broadcast_evidence_list()
def net_cmd_zz(self, args):
""" Sent on mod call.
"""
if self.client.is_muted: # Checks to see if the client has been muted by a mod
self.client.send_host_message("You have been muted by a moderator")
return
if not self.client.can_call_mod():
self.client.send_host_message("You must wait 30 seconds between mod calls.")
return
current_time = strftime("%H:%M", localtime())
if len(args) < 1:
self.server.send_all_cmd_pred('ZZ', '[{}] {} ({}) in {} ({}) without reason (not using the Case Café client?)'
.format(current_time, self.client.get_char_name(), self.client.get_ip(), self.client.area.name,
self.client.area.id), pred=lambda c: c.is_mod)
self.client.set_mod_call_delay()
logger.log_server('[{}][{}]{} called a moderator.'.format(self.client.get_ip(), self.client.area.id, self.client.get_char_name()))
else:
self.server.send_all_cmd_pred('ZZ', '[{}] {} ({}) in {} ({}) with reason: {}'
.format(current_time, self.client.get_char_name(), self.client.get_ip(), self.client.area.name,
self.client.area.id, args[0]), pred=lambda c: c.is_mod)
self.client.set_mod_call_delay()
logger.log_server('[{}][{}]{} called a moderator: {}.'.format(self.client.get_ip(), self.client.area.id, self.client.get_char_name(), args[0]))
def net_cmd_opKICK(self, args):
self.net_cmd_ct(['opkick', '/kick {}'.format(args[0])])
def net_cmd_opBAN(self, args):
self.net_cmd_ct(['opban', '/ban {}'.format(args[0])])
net_cmd_dispatcher = {
'HI': net_cmd_hi, # handshake
'ID': net_cmd_id, # client version
'CH': net_cmd_ch, # keepalive
'askchaa': net_cmd_askchaa, # ask for list lengths
'askchar2': net_cmd_askchar2, # ask for list of characters
'AN': net_cmd_an, # character list
'AE': net_cmd_ae, # evidence list
'AM': net_cmd_am, # music list
'RC': net_cmd_rc, # AO2 character list
'RM': net_cmd_rm, # AO2 music list
'RD': net_cmd_rd, # AO2 done request, charscheck etc.
'CC': net_cmd_cc, # select character
'MS': net_cmd_ms, # IC message
'CT': net_cmd_ct, # OOC message
'MC': net_cmd_mc, # play song
'RT': net_cmd_rt, # WT/CE buttons
'HP': net_cmd_hp, # penalties
'PE': net_cmd_pe, # add evidence
'DE': net_cmd_de, # delete evidence
'EE': net_cmd_ee, # edit evidence
'ZZ': net_cmd_zz, # call mod button
'opKICK': net_cmd_opKICK, # /kick with guard on
'opBAN': net_cmd_opBAN, # /ban with guard on
}

210
server/area_manager.py Normal file
View File

@ -0,0 +1,210 @@
# tsuserver3, an Attorney Online server
#
# Copyright (C) 2016 argoneus <argoneuscze@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import asyncio
import random
import time
import yaml
from server.exceptions import AreaError
from server.evidence import EvidenceList
class AreaManager:
class Area:
def __init__(self, area_id, server, name, background, bg_lock, evidence_mod = 'FFA', locking_allowed = False, iniswap_allowed = True, showname_changes_allowed = False, shouts_allowed = True):
self.iniswap_allowed = iniswap_allowed
self.clients = set()
self.invite_list = {}
self.id = area_id
self.name = name
self.background = background
self.bg_lock = bg_lock
self.server = server
self.music_looper = None
self.next_message_time = 0
self.hp_def = 10
self.hp_pro = 10
self.doc = 'No document.'
self.status = 'IDLE'
self.judgelog = []
self.current_music = ''
self.current_music_player = ''
self.evi_list = EvidenceList()
self.is_recording = False
self.recorded_messages = []
self.evidence_mod = evidence_mod
self.locking_allowed = locking_allowed
self.owned = False
self.cards = dict()
"""
#debug
self.evidence_list.append(Evidence("WOW", "desc", "1.png"))
self.evidence_list.append(Evidence("wewz", "desc2", "2.png"))
self.evidence_list.append(Evidence("weeeeeew", "desc3", "3.png"))
"""
self.is_locked = False
def new_client(self, client):
self.clients.add(client)
def remove_client(self, client):
self.clients.remove(client)
if client.is_cm:
client.is_cm = False
self.owned = False
if self.is_locked:
self.unlock()
def unlock(self):
self.is_locked = False
self.invite_list = {}
self.send_host_message('This area is open now.')
def is_char_available(self, char_id):
return char_id not in [x.char_id for x in self.clients]
def get_rand_avail_char_id(self):
avail_set = set(range(len(self.server.char_list))) - set([x.char_id for x in self.clients])
if len(avail_set) == 0:
raise AreaError('No available characters.')
return random.choice(tuple(avail_set))
def send_command(self, cmd, *args):
for c in self.clients:
c.send_command(cmd, *args)
def send_host_message(self, msg):
self.send_command('CT', self.server.config['hostname'], msg)
def set_next_msg_delay(self, msg_length):
delay = min(3000, 100 + 60 * msg_length)
self.next_message_time = round(time.time() * 1000.0 + delay)
def is_iniswap(self, client, anim1, anim2, char):
if self.iniswap_allowed:
return False
if '..' in anim1 or '..' in anim2:
return True
for char_link in self.server.allowed_iniswaps:
if client.get_char_name() in char_link and char in char_link:
return False
return True
def play_music(self, name, cid, length=-1):
self.send_command('MC', name, cid)
if self.music_looper:
self.music_looper.cancel()
if length > 0:
self.music_looper = asyncio.get_event_loop().call_later(length,
lambda: self.play_music(name, -1, length))
def can_send_message(self, client):
if self.is_locked and not client.is_mod and not client.ipid in self.invite_list:
client.send_host_message('This is a locked area - ask the CM to speak.')
return False
return (time.time() * 1000.0 - self.next_message_time) > 0
def change_hp(self, side, val):
if not 0 <= val <= 10:
raise AreaError('Invalid penalty value.')
if not 1 <= side <= 2:
raise AreaError('Invalid penalty side.')
if side == 1:
self.hp_def = val
elif side == 2:
self.hp_pro = val
self.send_command('HP', side, val)
def change_background(self, bg):
if bg.lower() not in (name.lower() for name in self.server.backgrounds):
raise AreaError('Invalid background name.')
self.background = bg
self.send_command('BN', self.background)
def change_status(self, value):
allowed_values = ('idle', 'building-open', 'building-full', 'casing-open', 'casing-full', 'recess')
if value.lower() not in allowed_values:
raise AreaError('Invalid status. Possible values: {}'.format(', '.join(allowed_values)))
self.status = value.upper()
def change_doc(self, doc='No document.'):
self.doc = doc
def add_to_judgelog(self, client, msg):
if len(self.judgelog) >= 10:
self.judgelog = self.judgelog[1:]
self.judgelog.append('{} ({}) {}.'.format(client.get_char_name(), client.get_ip(), msg))
def add_music_playing(self, client, name):
self.current_music_player = client.get_char_name()
self.current_music = name
def get_evidence_list(self, client):
client.evi_list, evi_list = self.evi_list.create_evi_list(client)
return evi_list
def broadcast_evidence_list(self):
"""
LE#<name>&<desc>&<img>#<name>
"""
for client in self.clients:
client.send_command('LE', *self.get_evidence_list(client))
def __init__(self, server):
self.server = server
self.cur_id = 0
self.areas = []
self.load_areas()
def load_areas(self):
with open('config/areas.yaml', 'r') as chars:
areas = yaml.load(chars)
for item in areas:
if 'evidence_mod' not in item:
item['evidence_mod'] = 'FFA'
if 'locking_allowed' not in item:
item['locking_allowed'] = False
if 'iniswap_allowed' not in item:
item['iniswap_allowed'] = True
if 'showname_changes_allowed' not in item:
item['showname_changes_allowed'] = False
if 'shouts_allowed' not in item:
item['shouts_allowed'] = True
self.areas.append(
self.Area(self.cur_id, self.server, item['area'], item['background'], item['bglock'], item['evidence_mod'], item['locking_allowed'], item['iniswap_allowed'], item['showname_changes_allowed'], item['shouts_allowed']))
self.cur_id += 1
def default_area(self):
return self.areas[0]
def get_area_by_name(self, name):
for area in self.areas:
if area.name == name:
return area
raise AreaError('Area not found.')
def get_area_by_id(self, num):
for area in self.areas:
if area.id == num:
return area
raise AreaError('Area not found.')

54
server/ban_manager.py Normal file
View File

@ -0,0 +1,54 @@
# tsuserver3, an Attorney Online server
#
# Copyright (C) 2016 argoneus <argoneuscze@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import json
from server.exceptions import ServerError
class BanManager:
def __init__(self):
self.bans = []
self.load_banlist()
def load_banlist(self):
try:
with open('storage/banlist.json', 'r') as banlist_file:
self.bans = json.load(banlist_file)
except FileNotFoundError:
return
def write_banlist(self):
with open('storage/banlist.json', 'w') as banlist_file:
json.dump(self.bans, banlist_file)
def add_ban(self, ip):
if ip not in self.bans:
self.bans.append(ip)
else:
raise ServerError('This IPID is already banned.')
self.write_banlist()
def remove_ban(self, ip):
if ip in self.bans:
self.bans.remove(ip)
else:
raise ServerError('This IPID is not banned.')
self.write_banlist()
def is_banned(self, ipid):
return (ipid in self.bans)

380
server/client_manager.py Normal file
View File

@ -0,0 +1,380 @@
# tsuserver3, an Attorney Online server
#
# Copyright (C) 2016 argoneus <argoneuscze@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from server import fantacrypt
from server import logger
from server.exceptions import ClientError, AreaError
from enum import Enum
from server.constants import TargetType
from heapq import heappop, heappush
import time
import re
class ClientManager:
class Client:
def __init__(self, server, transport, user_id, ipid):
self.is_checked = False
self.transport = transport
self.hdid = ''
self.pm_mute = False
self.id = user_id
self.char_id = -1
self.area = server.area_manager.default_area()
self.server = server
self.name = ''
self.fake_name = ''
self.is_mod = False
self.is_dj = True
self.can_wtce = True
self.pos = ''
self.is_cm = False
self.evi_list = []
self.disemvowel = False
self.muted_global = False
self.muted_adverts = False
self.is_muted = False
self.is_ooc_muted = False
self.pm_mute = False
self.mod_call_time = 0
self.in_rp = False
self.ipid = ipid
self.websocket = None
#flood-guard stuff
self.mus_counter = 0
self.mus_mute_time = 0
self.mus_change_time = [x * self.server.config['music_change_floodguard']['interval_length'] for x in range(self.server.config['music_change_floodguard']['times_per_interval'])]
self.wtce_counter = 0
self.wtce_mute_time = 0
self.wtce_time = [x * self.server.config['wtce_floodguard']['interval_length'] for x in range(self.server.config['wtce_floodguard']['times_per_interval'])]
def send_raw_message(self, msg):
if self.websocket:
self.websocket.send_text(msg.encode('utf-8'))
else:
self.transport.write(msg.encode('utf-8'))
def send_command(self, command, *args):
if args:
if command == 'MS':
for evi_num in range(len(self.evi_list)):
if self.evi_list[evi_num] == args[11]:
lst = list(args)
lst[11] = evi_num
args = tuple(lst)
break
self.send_raw_message('{}#{}#%'.format(command, '#'.join([str(x) for x in args])))
else:
self.send_raw_message('{}#%'.format(command))
def send_host_message(self, msg):
self.send_command('CT', self.server.config['hostname'], msg)
def send_motd(self):
self.send_host_message('=== MOTD ===\r\n{}\r\n============='.format(self.server.config['motd']))
def send_player_count(self):
self.send_host_message('{}/{} players online.'.format(
self.server.get_player_count(),
self.server.config['playerlimit']))
def is_valid_name(self, name):
name_ws = name.replace(' ', '')
if not name_ws or name_ws.isdigit():
return False
for client in self.server.client_manager.clients:
print(client.name == name)
if client.name == name:
return False
return True
def disconnect(self):
self.transport.close()
def change_character(self, char_id, force=False):
if not self.server.is_valid_char_id(char_id):
raise ClientError('Invalid Character ID.')
if not self.area.is_char_available(char_id):
if force:
for client in self.area.clients:
if client.char_id == char_id:
client.char_select()
else:
raise ClientError('Character not available.')
old_char = self.get_char_name()
self.char_id = char_id
self.pos = ''
self.send_command('PV', self.id, 'CID', self.char_id)
logger.log_server('[{}]Changed character from {} to {}.'
.format(self.area.id, old_char, self.get_char_name()), self)
def change_music_cd(self):
if self.is_mod or self.is_cm:
return 0
if self.mus_mute_time:
if time.time() - self.mus_mute_time < self.server.config['music_change_floodguard']['mute_length']:
return self.server.config['music_change_floodguard']['mute_length'] - (time.time() - self.mus_mute_time)
else:
self.mus_mute_time = 0
times_per_interval = self.server.config['music_change_floodguard']['times_per_interval']
interval_length = self.server.config['music_change_floodguard']['interval_length']
if time.time() - self.mus_change_time[(self.mus_counter - times_per_interval + 1) % times_per_interval] < interval_length:
self.mus_mute_time = time.time()
return self.server.config['music_change_floodguard']['mute_length']
self.mus_counter = (self.mus_counter + 1) % times_per_interval
self.mus_change_time[self.mus_counter] = time.time()
return 0
def wtce_mute(self):
if self.is_mod or self.is_cm:
return 0
if self.wtce_mute_time:
if time.time() - self.wtce_mute_time < self.server.config['wtce_floodguard']['mute_length']:
return self.server.config['wtce_floodguard']['mute_length'] - (time.time() - self.wtce_mute_time)
else:
self.wtce_mute_time = 0
times_per_interval = self.server.config['wtce_floodguard']['times_per_interval']
interval_length = self.server.config['wtce_floodguard']['interval_length']
if time.time() - self.wtce_time[(self.wtce_counter - times_per_interval + 1) % times_per_interval] < interval_length:
self.wtce_mute_time = time.time()
return self.server.config['music_change_floodguard']['mute_length']
self.wtce_counter = (self.wtce_counter + 1) % times_per_interval
self.wtce_time[self.wtce_counter] = time.time()
return 0
def reload_character(self):
try:
self.change_character(self.char_id, True)
except ClientError:
raise
def change_area(self, area):
if self.area == area:
raise ClientError('User already in specified area.')
if area.is_locked and not self.is_mod and not self.ipid in area.invite_list:
#self.send_host_message('This area is locked - you will be unable to send messages ICly.')
raise ClientError("That area is locked!")
old_area = self.area
if not area.is_char_available(self.char_id):
try:
new_char_id = area.get_rand_avail_char_id()
except AreaError:
raise ClientError('No available characters in that area.')
self.change_character(new_char_id)
self.send_host_message('Character taken, switched to {}.'.format(self.get_char_name()))
self.area.remove_client(self)
self.area = area
area.new_client(self)
self.send_host_message('Changed area to {}.[{}]'.format(area.name, self.area.status))
logger.log_server(
'[{}]Changed area from {} ({}) to {} ({}).'.format(self.get_char_name(), old_area.name, old_area.id,
self.area.name, self.area.id), self)
self.send_command('HP', 1, self.area.hp_def)
self.send_command('HP', 2, self.area.hp_pro)
self.send_command('BN', self.area.background)
self.send_command('LE', *self.area.get_evidence_list(self))
def send_area_list(self):
msg = '=== Areas ==='
lock = {True: '[LOCKED]', False: ''}
for i, area in enumerate(self.server.area_manager.areas):
owner = 'FREE'
if area.owned:
for client in [x for x in area.clients if x.is_cm]:
owner = 'MASTER: {}'.format(client.get_char_name())
break
msg += '\r\nArea {}: {} (users: {}) [{}][{}]{}'.format(i, area.name, len(area.clients), area.status, owner, lock[area.is_locked])
if self.area == area:
msg += ' [*]'
self.send_host_message(msg)
def get_area_info(self, area_id, mods):
info = ''
try:
area = self.server.area_manager.get_area_by_id(area_id)
except AreaError:
raise
info += '= Area {}: {} =='.format(area.id, area.name)
sorted_clients = []
for client in area.clients:
if (not mods) or client.is_mod:
sorted_clients.append(client)
sorted_clients = sorted(sorted_clients, key=lambda x: x.get_char_name())
for c in sorted_clients:
info += '\r\n[{}] {}'.format(c.id, c.get_char_name())
if self.is_mod:
info += ' ({})'.format(c.ipid)
info += ': {}'.format(c.name)
return info
def send_area_info(self, area_id, mods):
#if area_id is -1 then return all areas. If mods is True then return only mods
info = ''
if area_id == -1:
# all areas info
cnt = 0
info = '\n== Area List =='
for i in range(len(self.server.area_manager.areas)):
if len(self.server.area_manager.areas[i].clients) > 0:
cnt += len(self.server.area_manager.areas[i].clients)
info += '\r\n{}'.format(self.get_area_info(i, mods))
info = 'Current online: {}'.format(cnt) + info
else:
try:
info = 'People in this area: {}\n'.format(len(self.server.area_manager.areas[area_id].clients)) + self.get_area_info(area_id, mods)
except AreaError:
raise
self.send_host_message(info)
def send_area_hdid(self, area_id):
try:
info = self.get_area_hdid(area_id)
except AreaError:
raise
self.send_host_message(info)
def send_all_area_hdid(self):
info = '== HDID List =='
for i in range (len(self.server.area_manager.areas)):
if len(self.server.area_manager.areas[i].clients) > 0:
info += '\r\n{}'.format(self.get_area_hdid(i))
self.send_host_message(info)
def send_all_area_ip(self):
info = '== IP List =='
for i in range (len(self.server.area_manager.areas)):
if len(self.server.area_manager.areas[i].clients) > 0:
info += '\r\n{}'.format(self.get_area_ip(i))
self.send_host_message(info)
def send_done(self):
avail_char_ids = set(range(len(self.server.char_list))) - set([x.char_id for x in self.area.clients])
char_list = [-1] * len(self.server.char_list)
for x in avail_char_ids:
char_list[x] = 0
self.send_command('CharsCheck', *char_list)
self.send_command('HP', 1, self.area.hp_def)
self.send_command('HP', 2, self.area.hp_pro)
self.send_command('BN', self.area.background)
self.send_command('LE', *self.area.get_evidence_list(self))
self.send_command('MM', 1)
self.send_command('DONE')
def char_select(self):
self.char_id = -1
self.send_done()
def auth_mod(self, password):
if self.is_mod:
raise ClientError('Already logged in.')
if password == self.server.config['modpass']:
self.is_mod = True
else:
raise ClientError('Invalid password.')
def get_ip(self):
return self.ipid
def get_char_name(self):
if self.char_id == -1:
return 'CHAR_SELECT'
return self.server.char_list[self.char_id]
def change_position(self, pos=''):
if pos not in ('', 'def', 'pro', 'hld', 'hlp', 'jud', 'wit'):
raise ClientError('Invalid position. Possible values: def, pro, hld, hlp, jud, wit.')
self.pos = pos
def set_mod_call_delay(self):
self.mod_call_time = round(time.time() * 1000.0 + 30000)
def can_call_mod(self):
return (time.time() * 1000.0 - self.mod_call_time) > 0
def disemvowel_message(self, message):
message = re.sub("[aeiou]", "", message, flags=re.IGNORECASE)
return re.sub(r"\s+", " ", message)
def __init__(self, server):
self.clients = set()
self.server = server
self.cur_id = [i for i in range(self.server.config['playerlimit'])]
self.clients_list = []
def new_client(self, transport):
c = self.Client(self.server, transport, heappop(self.cur_id), self.server.get_ipid(transport.get_extra_info('peername')[0]))
self.clients.add(c)
return c
def remove_client(self, client):
heappush(self.cur_id, client.id)
self.clients.remove(client)
def get_targets(self, client, key, value, local = False):
#possible keys: ip, OOC, id, cname, ipid, hdid
areas = None
if local:
areas = [client.area]
else:
areas = client.server.area_manager.areas
targets = []
if key == TargetType.ALL:
for nkey in range(6):
targets += self.get_targets(client, nkey, value, local)
for area in areas:
for client in area.clients:
if key == TargetType.IP:
if value.lower().startswith(client.get_ip().lower()):
targets.append(client)
elif key == TargetType.OOC_NAME:
if value.lower().startswith(client.name.lower()) and client.name:
targets.append(client)
elif key == TargetType.CHAR_NAME:
if value.lower().startswith(client.get_char_name().lower()):
targets.append(client)
elif key == TargetType.ID:
if client.id == value:
targets.append(client)
elif key == TargetType.IPID:
if client.ipid == value:
targets.append(client)
return targets
def get_muted_clients(self):
clients = []
for client in self.clients:
if client.is_muted:
clients.append(client)
return clients
def get_ooc_muted_clients(self):
clients = []
for client in self.clients:
if client.is_ooc_muted:
clients.append(client)
return clients

848
server/commands.py Normal file
View File

@ -0,0 +1,848 @@
# tsuserver3, an Attorney Online server
#
# Copyright (C) 2016 argoneus <argoneuscze@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#possible keys: ip, OOC, id, cname, ipid, hdid
import random
import hashlib
import string
from server.constants import TargetType
from server import logger
from server.exceptions import ClientError, ServerError, ArgumentError, AreaError
def ooc_cmd_switch(client, arg):
if len(arg) == 0:
raise ArgumentError('You must specify a character name.')
try:
cid = client.server.get_char_id_by_name(arg)
except ServerError:
raise
try:
client.change_character(cid, client.is_mod)
except ClientError:
raise
client.send_host_message('Character changed.')
def ooc_cmd_bg(client, arg):
if len(arg) == 0:
raise ArgumentError('You must specify a name. Use /bg <background>.')
if not client.is_mod and client.area.bg_lock == "true":
raise AreaError("This area's background is locked")
try:
client.area.change_background(arg)
except AreaError:
raise
client.area.send_host_message('{} changed the background to {}.'.format(client.get_char_name(), arg))
logger.log_server('[{}][{}]Changed background to {}'.format(client.area.id, client.get_char_name(), arg), client)
def ooc_cmd_bglock(client,arg):
if not client.is_mod:
raise ClientError('You must be authorized to do that.')
if len(arg) != 0:
raise ArgumentError('This command has no arguments.')
if client.area.bg_lock == "true":
client.area.bg_lock = "false"
else:
client.area.bg_lock = "true"
client.area.send_host_message('A mod has set the background lock to {}.'.format(client.area.bg_lock))
logger.log_server('[{}][{}]Changed bglock to {}'.format(client.area.id, client.get_char_name(), client.area.bg_lock), client)
def ooc_cmd_evidence_mod(client, arg):
if not client.is_mod:
raise ClientError('You must be authorized to do that.')
if not arg:
client.send_host_message('current evidence mod: {}'.format(client.area.evidence_mod))
return
if arg in ['FFA', 'Mods', 'CM', 'HiddenCM']:
if arg == client.area.evidence_mod:
client.send_host_message('current evidence mod: {}'.format(client.area.evidence_mod))
return
if client.area.evidence_mod == 'HiddenCM':
for i in range(len(client.area.evi_list.evidences)):
client.area.evi_list.evidences[i].pos = 'all'
client.area.evidence_mod = arg
client.send_host_message('current evidence mod: {}'.format(client.area.evidence_mod))
return
else:
raise ArgumentError('Wrong Argument. Use /evidence_mod <MOD>. Possible values: FFA, CM, Mods, HiddenCM')
return
def ooc_cmd_allow_iniswap(client, arg):
if not client.is_mod:
raise ClientError('You must be authorized to do that.')
client.area.iniswap_allowed = not client.area.iniswap_allowed
answer = {True: 'allowed', False: 'forbidden'}
client.send_host_message('iniswap is {}.'.format(answer[client.area.iniswap_allowed]))
return
def ooc_cmd_roll(client, arg):
roll_max = 11037
if len(arg) != 0:
try:
val = list(map(int, arg.split(' ')))
if not 1 <= val[0] <= roll_max:
raise ArgumentError('Roll value must be between 1 and {}.'.format(roll_max))
except ValueError:
raise ArgumentError('Wrong argument. Use /roll [<max>] [<num of rolls>]')
else:
val = [6]
if len(val) == 1:
val.append(1)
if len(val) > 2:
raise ArgumentError('Too many arguments. Use /roll [<max>] [<num of rolls>]')
if val[1] > 20 or val[1] < 1:
raise ArgumentError('Num of rolls must be between 1 and 20')
roll = ''
for i in range(val[1]):
roll += str(random.randint(1, val[0])) + ', '
roll = roll[:-2]
if val[1] > 1:
roll = '(' + roll + ')'
client.area.send_host_message('{} rolled {} out of {}.'.format(client.get_char_name(), roll, val[0]))
logger.log_server(
'[{}][{}]Used /roll and got {} out of {}.'.format(client.area.id, client.get_char_name(), roll, val[0]))
def ooc_cmd_rollp(client, arg):
roll_max = 11037
if len(arg) != 0:
try:
val = list(map(int, arg.split(' ')))
if not 1 <= val[0] <= roll_max:
raise ArgumentError('Roll value must be between 1 and {}.'.format(roll_max))
except ValueError:
raise ArgumentError('Wrong argument. Use /roll [<max>] [<num of rolls>]')
else:
val = [6]
if len(val) == 1:
val.append(1)
if len(val) > 2:
raise ArgumentError('Too many arguments. Use /roll [<max>] [<num of rolls>]')
if val[1] > 20 or val[1] < 1:
raise ArgumentError('Num of rolls must be between 1 and 20')
roll = ''
for i in range(val[1]):
roll += str(random.randint(1, val[0])) + ', '
roll = roll[:-2]
if val[1] > 1:
roll = '(' + roll + ')'
client.send_host_message('{} rolled {} out of {}.'.format(client.get_char_name(), roll, val[0]))
client.area.send_host_message('{} rolled.'.format(client.get_char_name(), roll, val[0]))
SALT = ''.join(random.choices(string.ascii_uppercase + string.digits, k=16))
logger.log_server(
'[{}][{}]Used /roll and got {} out of {}.'.format(client.area.id, client.get_char_name(), hashlib.sha1((str(roll) + SALT).encode('utf-8')).hexdigest() + '|' + SALT, val[0]))
def ooc_cmd_currentmusic(client, arg):
if len(arg) != 0:
raise ArgumentError('This command has no arguments.')
if client.area.current_music == '':
raise ClientError('There is no music currently playing.')
client.send_host_message('The current music is {} and was played by {}.'.format(client.area.current_music,
client.area.current_music_player))
def ooc_cmd_coinflip(client, arg):
if len(arg) != 0:
raise ArgumentError('This command has no arguments.')
coin = ['heads', 'tails']
flip = random.choice(coin)
client.area.send_host_message('{} flipped a coin and got {}.'.format(client.get_char_name(), flip))
logger.log_server(
'[{}][{}]Used /coinflip and got {}.'.format(client.area.id, client.get_char_name(), flip))
def ooc_cmd_motd(client, arg):
if len(arg) != 0:
raise ArgumentError("This command doesn't take any arguments")
client.send_motd()
def ooc_cmd_pos(client, arg):
if len(arg) == 0:
client.change_position()
client.send_host_message('Position reset.')
else:
try:
client.change_position(arg)
except ClientError:
raise
client.area.broadcast_evidence_list()
client.send_host_message('Position changed.')
def ooc_cmd_forcepos(client, arg):
if not client.is_cm and not client.is_mod:
raise ClientError('You must be authorized to do that.')
args = arg.split()
if len(args) < 1:
raise ArgumentError(
'Not enough arguments. Use /forcepos <pos> <target>. Target should be ID, OOC-name or char-name. Use /getarea for getting info like "[ID] char-name".')
targets = []
pos = args[0]
if len(args) > 1:
targets = client.server.client_manager.get_targets(
client, TargetType.CHAR_NAME, " ".join(args[1:]), True)
if len(targets) == 0 and args[1].isdigit():
targets = client.server.client_manager.get_targets(
client, TargetType.ID, int(arg[1]), True)
if len(targets) == 0:
targets = client.server.client_manager.get_targets(
client, TargetType.OOC_NAME, " ".join(args[1:]), True)
if len(targets) == 0:
raise ArgumentError('No targets found.')
else:
for c in client.area.clients:
targets.append(c)
for t in targets:
try:
t.change_position(pos)
t.area.broadcast_evidence_list()
t.send_host_message('Forced into /pos {}.'.format(pos))
except ClientError:
raise
client.area.send_host_message(
'{} forced {} client(s) into /pos {}.'.format(client.get_char_name(), len(targets), pos))
logger.log_server(
'[{}][{}]Used /forcepos {} for {} client(s).'.format(client.area.id, client.get_char_name(), pos, len(targets)))
def ooc_cmd_help(client, arg):
if len(arg) != 0:
raise ArgumentError('This command has no arguments.')
help_url = 'https://github.com/AttorneyOnline/tsuserver3/blob/master/README.md'
help_msg = 'Available commands, source code and issues can be found here: {}'.format(help_url)
client.send_host_message(help_msg)
def ooc_cmd_kick(client, arg):
if not client.is_mod:
raise ClientError('You must be authorized to do that.')
if len(arg) == 0:
raise ArgumentError('You must specify a target. Use /kick <ipid>.')
targets = client.server.client_manager.get_targets(client, TargetType.IPID, int(arg), False)
if targets:
for c in targets:
logger.log_server('Kicked {}.'.format(c.ipid), client)
client.send_host_message("{} was kicked.".format(c.get_char_name()))
c.disconnect()
else:
client.send_host_message("No targets found.")
def ooc_cmd_ban(client, arg):
if not client.is_mod:
raise ClientError('You must be authorized to do that.')
try:
ipid = int(arg.strip())
except:
raise ClientError('You must specify ipid')
try:
client.server.ban_manager.add_ban(ipid)
except ServerError:
raise
if ipid != None:
targets = client.server.client_manager.get_targets(client, TargetType.IPID, ipid, False)
if targets:
for c in targets:
c.disconnect()
client.send_host_message('{} clients was kicked.'.format(len(targets)))
client.send_host_message('{} was banned.'.format(ipid))
logger.log_server('Banned {}.'.format(ipid), client)
def ooc_cmd_unban(client, arg):
if not client.is_mod:
raise ClientError('You must be authorized to do that.')
try:
client.server.ban_manager.remove_ban(int(arg.strip()))
except:
raise ClientError('You must specify \'hdid\'')
logger.log_server('Unbanned {}.'.format(arg), client)
client.send_host_message('Unbanned {}'.format(arg))
def ooc_cmd_play(client, arg):
if not client.is_mod:
raise ClientError('You must be authorized to do that.')
if len(arg) == 0:
raise ArgumentError('You must specify a song.')
client.area.play_music(arg, client.char_id, -1)
client.area.add_music_playing(client, arg)
logger.log_server('[{}][{}]Changed music to {}.'.format(client.area.id, client.get_char_name(), arg), client)
def ooc_cmd_mute(client, arg):
if not client.is_mod:
raise ClientError('You must be authorized to do that.')
if len(arg) == 0:
raise ArgumentError('You must specify a target.')
try:
c = client.server.client_manager.get_targets(client, TargetType.IPID, int(arg), False)[0]
c.is_muted = True
client.send_host_message('{} existing client(s).'.format(c.get_char_name()))
except:
client.send_host_message("No targets found. Use /mute <id> for mute")
def ooc_cmd_unmute(client, arg):
if not client.is_mod:
raise ClientError('You must be authorized to do that.')
if len(arg) == 0:
raise ArgumentError('You must specify a target.')
try:
c = client.server.client_manager.get_targets(client, TargetType.IPID, int(arg), False)[0]
c.is_muted = False
client.send_host_message('{} existing client(s).'.format(c.get_char_name()))
except:
client.send_host_message("No targets found. Use /mute <id> for mute")
def ooc_cmd_login(client, arg):
if len(arg) == 0:
raise ArgumentError('You must specify the password.')
try:
client.auth_mod(arg)
except ClientError:
raise
if client.area.evidence_mod == 'HiddenCM':
client.area.broadcast_evidence_list()
client.send_host_message('Logged in as a moderator.')
logger.log_server('Logged in as moderator.', client)
def ooc_cmd_g(client, arg):
if client.muted_global:
raise ClientError('Global chat toggled off.')
if len(arg) == 0:
raise ArgumentError("You can't send an empty message.")
client.server.broadcast_global(client, arg)
logger.log_server('[{}][{}][GLOBAL]{}.'.format(client.area.id, client.get_char_name(), arg), client)
def ooc_cmd_gm(client, arg):
if not client.is_mod:
raise ClientError('You must be authorized to do that.')
if client.muted_global:
raise ClientError('You have the global chat muted.')
if len(arg) == 0:
raise ArgumentError("Can't send an empty message.")
client.server.broadcast_global(client, arg, True)
logger.log_server('[{}][{}][GLOBAL-MOD]{}.'.format(client.area.id, client.get_char_name(), arg), client)
def ooc_cmd_lm(client, arg):
if not client.is_mod:
raise ClientError('You must be authorized to do that.')
if len(arg) == 0:
raise ArgumentError("Can't send an empty message.")
client.area.send_command('CT', '{}[MOD][{}]'
.format(client.server.config['hostname'], client.get_char_name()), arg)
logger.log_server('[{}][{}][LOCAL-MOD]{}.'.format(client.area.id, client.get_char_name(), arg), client)
def ooc_cmd_announce(client, arg):
if not client.is_mod:
raise ClientError('You must be authorized to do that.')
if len(arg) == 0:
raise ArgumentError("Can't send an empty message.")
client.server.send_all_cmd_pred('CT', '{}'.format(client.server.config['hostname']),
'=== Announcement ===\r\n{}\r\n=================='.format(arg))
logger.log_server('[{}][{}][ANNOUNCEMENT]{}.'.format(client.area.id, client.get_char_name(), arg), client)
def ooc_cmd_toggleglobal(client, arg):
if len(arg) != 0:
raise ArgumentError("This command doesn't take any arguments")
client.muted_global = not client.muted_global
glob_stat = 'on'
if client.muted_global:
glob_stat = 'off'
client.send_host_message('Global chat turned {}.'.format(glob_stat))
def ooc_cmd_need(client, arg):
if client.muted_adverts:
raise ClientError('You have advertisements muted.')
if len(arg) == 0:
raise ArgumentError("You must specify what you need.")
client.server.broadcast_need(client, arg)
logger.log_server('[{}][{}][NEED]{}.'.format(client.area.id, client.get_char_name(), arg), client)
def ooc_cmd_toggleadverts(client, arg):
if len(arg) != 0:
raise ArgumentError("This command doesn't take any arguments")
client.muted_adverts = not client.muted_adverts
adv_stat = 'on'
if client.muted_adverts:
adv_stat = 'off'
client.send_host_message('Advertisements turned {}.'.format(adv_stat))
def ooc_cmd_doc(client, arg):
if len(arg) == 0:
client.send_host_message('Document: {}'.format(client.area.doc))
logger.log_server(
'[{}][{}]Requested document. Link: {}'.format(client.area.id, client.get_char_name(), client.area.doc))
else:
client.area.change_doc(arg)
client.area.send_host_message('{} changed the doc link.'.format(client.get_char_name()))
logger.log_server('[{}][{}]Changed document to: {}'.format(client.area.id, client.get_char_name(), arg))
def ooc_cmd_cleardoc(client, arg):
if len(arg) != 0:
raise ArgumentError('This command has no arguments.')
client.area.send_host_message('{} cleared the doc link.'.format(client.get_char_name()))
logger.log_server('[{}][{}]Cleared document. Old link: {}'
.format(client.area.id, client.get_char_name(), client.area.doc))
client.area.change_doc()
def ooc_cmd_status(client, arg):
if len(arg) == 0:
client.send_host_message('Current status: {}'.format(client.area.status))
else:
try:
client.area.change_status(arg)
client.area.send_host_message('{} changed status to {}.'.format(client.get_char_name(), client.area.status))
logger.log_server(
'[{}][{}]Changed status to {}'.format(client.area.id, client.get_char_name(), client.area.status))
except AreaError:
raise
def ooc_cmd_online(client, _):
client.send_player_count()
def ooc_cmd_area(client, arg):
args = arg.split()
if len(args) == 0:
client.send_area_list()
elif len(args) == 1:
try:
area = client.server.area_manager.get_area_by_id(int(args[0]))
client.change_area(area)
except ValueError:
raise ArgumentError('Area ID must be a number.')
except (AreaError, ClientError):
raise
else:
raise ArgumentError('Too many arguments. Use /area <id>.')
def ooc_cmd_pm(client, arg):
args = arg.split()
key = ''
msg = None
if len(args) < 2:
raise ArgumentError('Not enough arguments. use /pm <target> <message>. Target should be ID, OOC-name or char-name. Use /getarea for getting info like "[ID] char-name".')
targets = client.server.client_manager.get_targets(client, TargetType.CHAR_NAME, arg, True)
key = TargetType.CHAR_NAME
if len(targets) == 0 and args[0].isdigit():
targets = client.server.client_manager.get_targets(client, TargetType.ID, int(args[0]), False)
key = TargetType.ID
if len(targets) == 0:
targets = client.server.client_manager.get_targets(client, TargetType.OOC_NAME, arg, True)
key = TargetType.OOC_NAME
if len(targets) == 0:
raise ArgumentError('No targets found.')
try:
if key == TargetType.ID:
msg = ' '.join(args[1:])
else:
if key == TargetType.CHAR_NAME:
msg = arg[len(targets[0].get_char_name()) + 1:]
if key == TargetType.OOC_NAME:
msg = arg[len(targets[0].name) + 1:]
except:
raise ArgumentError('Not enough arguments. Use /pm <target> <message>.')
c = targets[0]
if c.pm_mute:
raise ClientError('This user muted all pm conversation')
else:
c.send_host_message('PM from {} in {} ({}): {}'.format(client.name, client.area.name, client.get_char_name(), msg))
client.send_host_message('PM sent to {}. Message: {}'.format(args[0], msg))
def ooc_cmd_mutepm(client, arg):
if len(arg) != 0:
raise ArgumentError("This command doesn't take any arguments")
client.pm_mute = not client.pm_mute
client.send_host_message({True:'You stopped receiving PMs', False:'You are now receiving PMs'}[client.pm_mute])
def ooc_cmd_charselect(client, arg):
if not arg:
client.char_select()
else:
if client.is_mod:
try:
client.server.client_manager.get_targets(client, TargetType.ID, int(arg), False)[0].char_select()
except:
raise ArgumentError('Wrong arguments. Use /charselect <target\'s id>')
def ooc_cmd_reload(client, arg):
if len(arg) != 0:
raise ArgumentError("This command doesn't take any arguments")
try:
client.reload_character()
except ClientError:
raise
client.send_host_message('Character reloaded.')
def ooc_cmd_randomchar(client, arg):
if len(arg) != 0:
raise ArgumentError('This command has no arguments.')
try:
free_id = client.area.get_rand_avail_char_id()
except AreaError:
raise
try:
client.change_character(free_id)
except ClientError:
raise
client.send_host_message('Randomly switched to {}'.format(client.get_char_name()))
def ooc_cmd_getarea(client, arg):
client.send_area_info(client.area.id, False)
def ooc_cmd_getareas(client, arg):
client.send_area_info(-1, False)
def ooc_cmd_mods(client, arg):
client.send_area_info(-1, True)
def ooc_cmd_evi_swap(client, arg):
args = list(arg.split(' '))
if len(args) != 2:
raise ClientError("you must specify 2 numbers")
try:
client.area.evi_list.evidence_swap(client, int(args[0]), int(args[1]))
client.area.broadcast_evidence_list()
except:
raise ClientError("you must specify 2 numbers")
def ooc_cmd_cm(client, arg):
if 'CM' not in client.area.evidence_mod:
raise ClientError('You can\'t become a CM in this area')
if client.area.owned == False:
client.area.owned = True
client.is_cm = True
if client.area.evidence_mod == 'HiddenCM':
client.area.broadcast_evidence_list()
client.area.send_host_message('{} is CM in this area now.'.format(client.get_char_name()))
def ooc_cmd_unmod(client, arg):
client.is_mod = False
if client.area.evidence_mod == 'HiddenCM':
client.area.broadcast_evidence_list()
client.send_host_message('you\'re not a mod now')
def ooc_cmd_area_lock(client, arg):
if not client.area.locking_allowed:
client.send_host_message('Area locking is disabled in this area.')
return
if client.area.is_locked:
client.send_host_message('Area is already locked.')
if client.is_cm:
client.area.is_locked = True
client.area.send_host_message('Area is locked.')
for i in client.area.clients:
client.area.invite_list[i.ipid] = None
return
else:
raise ClientError('Only CM can lock the area.')
def ooc_cmd_area_unlock(client, arg):
if not client.area.is_locked:
raise ClientError('Area is already unlocked.')
if not client.is_cm:
raise ClientError('Only CM can unlock area.')
client.area.unlock()
client.send_host_message('Area is unlocked.')
def ooc_cmd_invite(client, arg):
if not arg:
raise ClientError('You must specify a target. Use /invite <id>')
if not client.area.is_locked:
raise ClientError('Area isn\'t locked.')
if not client.is_cm and not client.is_mod:
raise ClientError('You must be authorized to do that.')
try:
c = client.server.client_manager.get_targets(client, TargetType.ID, int(arg), False)[0]
client.area.invite_list[c.ipid] = None
client.send_host_message('{} is invited to your area.'.format(c.get_char_name()))
c.send_host_message('You were invited and given access to area {}.'.format(client.area.id))
except:
raise ClientError('You must specify a target. Use /invite <id>')
def ooc_cmd_uninvite(client, arg):
if not client.is_cm and not client.is_mod:
raise ClientError('You must be authorized to do that.')
if not client.area.is_locked and not client.is_mod:
raise ClientError('Area isn\'t locked.')
if not arg:
raise ClientError('You must specify a target. Use /uninvite <id>')
arg = arg.split(' ')
targets = client.server.client_manager.get_targets(client, TargetType.ID, int(arg[0]), True)
if targets:
try:
for c in targets:
client.send_host_message("You have removed {} from the whitelist.".format(c.get_char_name()))
c.send_host_message("You were removed from the area whitelist.")
if client.area.is_locked:
client.area.invite_list.pop(c.ipid)
except AreaError:
raise
except ClientError:
raise
else:
client.send_host_message("No targets found.")
def ooc_cmd_area_kick(client, arg):
if not client.is_mod:
raise ClientError('You must be authorized to do that.')
if not client.area.is_locked and not client.is_mod:
raise ClientError('Area isn\'t locked.')
if not arg:
raise ClientError('You must specify a target. Use /area_kick <id> [destination #]')
arg = arg.split(' ')
targets = client.server.client_manager.get_targets(client, TargetType.ID, int(arg[0]), False)
if targets:
try:
for c in targets:
if len(arg) == 1:
area = client.server.area_manager.get_area_by_id(int(0))
output = 0
else:
try:
area = client.server.area_manager.get_area_by_id(int(arg[1]))
output = arg[1]
except AreaError:
raise
client.send_host_message("Attempting to kick {} to area {}.".format(c.get_char_name(), output))
c.change_area(area)
c.send_host_message("You were kicked from the area to area {}.".format(output))
if client.area.is_locked:
client.area.invite_list.pop(c.ipid)
except AreaError:
raise
except ClientError:
raise
else:
client.send_host_message("No targets found.")
def ooc_cmd_ooc_mute(client, arg):
if not client.is_mod:
raise ClientError('You must be authorized to do that.')
if len(arg) == 0:
raise ArgumentError('You must specify a target. Use /ooc_mute <OOC-name>.')
targets = client.server.client_manager.get_targets(client, TargetType.OOC_NAME, arg, False)
if not targets:
raise ArgumentError('Targets not found. Use /ooc_mute <OOC-name>.')
for target in targets:
target.is_ooc_muted = True
client.send_host_message('Muted {} existing client(s).'.format(len(targets)))
def ooc_cmd_ooc_unmute(client, arg):
if not client.is_mod:
raise ClientError('You must be authorized to do that.')
if len(arg) == 0:
raise ArgumentError('You must specify a target. Use /ooc_mute <OOC-name>.')
targets = client.server.client_manager.get_targets(client, TargetType.ID, arg, False)
if not targets:
raise ArgumentError('Target not found. Use /ooc_mute <OOC-name>.')
for target in targets:
target.is_ooc_muted = False
client.send_host_message('Unmuted {} existing client(s).'.format(len(targets)))
def ooc_cmd_disemvowel(client, arg):
if not client.is_mod:
raise ClientError('You must be authorized to do that.')
elif len(arg) == 0:
raise ArgumentError('You must specify a target.')
try:
targets = client.server.client_manager.get_targets(client, TargetType.ID, int(arg), False)
except:
raise ArgumentError('You must specify a target. Use /disemvowel <id>.')
if targets:
for c in targets:
logger.log_server('Disemvowelling {}.'.format(c.get_ip()), client)
c.disemvowel = True
client.send_host_message('Disemvowelled {} existing client(s).'.format(len(targets)))
else:
client.send_host_message('No targets found.')
def ooc_cmd_undisemvowel(client, arg):
if not client.is_mod:
raise ClientError('You must be authorized to do that.')
elif len(arg) == 0:
raise ArgumentError('You must specify a target.')
try:
targets = client.server.client_manager.get_targets(client, TargetType.ID, int(arg), False)
except:
raise ArgumentError('You must specify a target. Use /disemvowel <id>.')
if targets:
for c in targets:
logger.log_server('Undisemvowelling {}.'.format(c.get_ip()), client)
c.disemvowel = False
client.send_host_message('Undisemvowelled {} existing client(s).'.format(len(targets)))
else:
client.send_host_message('No targets found.')
def ooc_cmd_blockdj(client, arg):
if not client.is_mod:
raise ClientError('You must be authorized to do that.')
if len(arg) == 0:
raise ArgumentError('You must specify a target. Use /blockdj <id>.')
try:
targets = client.server.client_manager.get_targets(client, TargetType.ID, int(arg), False)
except:
raise ArgumentError('You must enter a number. Use /blockdj <id>.')
if not targets:
raise ArgumentError('Target not found. Use /blockdj <id>.')
for target in targets:
target.is_dj = False
target.send_host_message('A moderator muted you from changing the music.')
client.send_host_message('blockdj\'d {}.'.format(targets[0].get_char_name()))
def ooc_cmd_unblockdj(client, arg):
if not client.is_mod:
raise ClientError('You must be authorized to do that.')
if len(arg) == 0:
raise ArgumentError('You must specify a target. Use /unblockdj <id>.')
try:
targets = client.server.client_manager.get_targets(client, TargetType.ID, int(arg), False)
except:
raise ArgumentError('You must enter a number. Use /unblockdj <id>.')
if not targets:
raise ArgumentError('Target not found. Use /blockdj <id>.')
for target in targets:
target.is_dj = True
target.send_host_message('A moderator unmuted you from changing the music.')
client.send_host_message('Unblockdj\'d {}.'.format(targets[0].get_char_name()))
def ooc_cmd_blockwtce(client, arg):
if not client.is_mod:
raise ClientError('You must be authorized to do that.')
if len(arg) == 0:
raise ArgumentError('You must specify a target. Use /blockwtce <id>.')
try:
targets = client.server.client_manager.get_targets(client, TargetType.ID, int(arg), False)
except:
raise ArgumentError('You must enter a number. Use /blockwtce <id>.')
if not targets:
raise ArgumentError('Target not found. Use /blockwtce <id>.')
for target in targets:
target.can_wtce = False
target.send_host_message('A moderator blocked you from using judge signs.')
client.send_host_message('blockwtce\'d {}.'.format(targets[0].get_char_name()))
def ooc_cmd_unblockwtce(client, arg):
if not client.is_mod:
raise ClientError('You must be authorized to do that.')
if len(arg) == 0:
raise ArgumentError('You must specify a target. Use /unblockwtce <id>.')
try:
targets = client.server.client_manager.get_targets(client, TargetType.ID, int(arg), False)
except:
raise ArgumentError('You must enter a number. Use /unblockwtce <id>.')
if not targets:
raise ArgumentError('Target not found. Use /unblockwtce <id>.')
for target in targets:
target.can_wtce = True
target.send_host_message('A moderator unblocked you from using judge signs.')
client.send_host_message('unblockwtce\'d {}.'.format(targets[0].get_char_name()))
def ooc_cmd_notecard(client, arg):
if len(arg) == 0:
raise ArgumentError('You must specify the contents of the note card.')
client.area.cards[client.get_char_name()] = arg
client.area.send_host_message('{} wrote a note card.'.format(client.get_char_name()))
def ooc_cmd_notecard_clear(client, arg):
try:
del client.area.cards[client.get_char_name()]
client.area.send_host_message('{} erased their note card.'.format(client.get_char_name()))
except KeyError:
raise ClientError('You do not have a note card.')
def ooc_cmd_notecard_reveal(client, arg):
if not client.is_cm and not client.is_mod:
raise ClientError('You must be a CM or moderator to reveal cards.')
if len(client.area.cards) == 0:
raise ClientError('There are no cards to reveal in this area.')
msg = 'Note cards have been revealed.\n'
for card_owner, card_msg in client.area.cards.items():
msg += '{}: {}\n'.format(card_owner, card_msg)
client.area.cards.clear()
client.area.send_host_message(msg)
def ooc_cmd_rolla_reload(client, arg):
if not client.is_mod:
raise ClientError('You must be a moderator to load the ability dice configuration.')
rolla_reload(client.area)
client.send_host_message('Reloaded ability dice configuration.')
def rolla_reload(area):
try:
import yaml
with open('config/dice.yaml', 'r') as dice:
area.ability_dice = yaml.load(dice)
except:
raise ServerError('There was an error parsing the ability dice configuration. Check your syntax.')
def ooc_cmd_rolla_set(client, arg):
if not hasattr(client.area, 'ability_dice'):
rolla_reload(client.area)
available_sets = client.area.ability_dice.keys()
if len(arg) == 0:
raise ArgumentError('You must specify the ability set name.\nAvailable sets: {}'.format(available_sets))
if arg in client.area.ability_dice:
client.ability_dice_set = arg
client.send_host_message("Set ability set to {}.".format(arg))
else:
raise ArgumentError('Invalid ability set \'{}\'.\nAvailable sets: {}'.format(arg, available_sets))
def ooc_cmd_rolla(client, arg):
if not hasattr(client.area, 'ability_dice'):
rolla_reload(client.area)
if not hasattr(client, 'ability_dice_set'):
raise ClientError('You must set your ability set using /rolla_set <name>.')
ability_dice = client.area.ability_dice[client.ability_dice_set]
max_roll = ability_dice['max'] if 'max' in ability_dice else 6
roll = random.randint(1, max_roll)
ability = ability_dice[roll] if roll in ability_dice else "Nothing happens"
client.area.send_host_message(
'{} rolled a {} (out of {}): {}.'.format(client.get_char_name(), roll, max_roll, ability))
def ooc_cmd_refresh(client, arg):
if not client.is_mod:
raise ClientError('You must be authorized to do that.')
if len (arg) > 0:
raise ClientError('This command does not take in any arguments!')
else:
try:
client.server.refresh()
client.send_host_message('You have reloaded the server.')
except ServerError:
raise
def ooc_cmd_judgelog(client, arg):
if not client.is_mod:
raise ClientError('You must be authorized to do that.')
if len(arg) != 0:
raise ArgumentError('This command does not take any arguments.')
jlog = client.area.judgelog
if len(jlog) > 0:
jlog_msg = '== Judge Log =='
for x in jlog:
jlog_msg += '\r\n{}'.format(x)
client.send_host_message(jlog_msg)
else:
raise ServerError('There have been no judge actions in this area since start of session.')

11
server/constants.py Normal file
View File

@ -0,0 +1,11 @@
from enum import Enum
class TargetType(Enum):
#possible keys: ip, OOC, id, cname, ipid, hdid
IP = 0
OOC_NAME = 1
ID = 2
CHAR_NAME = 3
IPID = 4
HDID = 5
ALL = 6

79
server/districtclient.py Normal file
View File

@ -0,0 +1,79 @@
# tsuserver3, an Attorney Online server
#
# Copyright (C) 2016 argoneus <argoneuscze@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import asyncio
from server import logger
class DistrictClient:
def __init__(self, server):
self.server = server
self.reader = None
self.writer = None
self.message_queue = []
async def connect(self):
loop = asyncio.get_event_loop()
while True:
try:
self.reader, self.writer = await asyncio.open_connection(self.server.config['district_ip'],
self.server.config['district_port'], loop=loop)
await self.handle_connection()
except (ConnectionRefusedError, TimeoutError):
pass
except (ConnectionResetError, asyncio.IncompleteReadError):
self.writer = None
self.reader = None
finally:
logger.log_debug("Couldn't connect to the district, retrying in 30 seconds.")
await asyncio.sleep(30)
async def handle_connection(self):
logger.log_debug('District connected.')
self.send_raw_message('AUTH#{}'.format(self.server.config['district_password']))
while True:
data = await self.reader.readuntil(b'\r\n')
if not data:
return
raw_msg = data.decode()[:-2]
logger.log_debug('[DISTRICT][INC][RAW]{}'.format(raw_msg))
cmd, *args = raw_msg.split('#')
if cmd == 'GLOBAL':
glob_name = '{}[{}:{}][{}]'.format('<dollar>G', args[1], args[2], args[3])
if args[0] == '1':
glob_name += '[M]'
self.server.send_all_cmd_pred('CT', glob_name, args[4], pred=lambda x: not x.muted_global)
elif cmd == 'NEED':
need_msg = '=== Cross Advert ===\r\n{} at {} in {} [{}] needs {}\r\n====================' \
.format(args[1], args[0], args[2], args[3], args[4])
self.server.send_all_cmd_pred('CT', '{}'.format(self.server.config['hostname']), need_msg,
pred=lambda x: not x.muted_adverts)
async def write_queue(self):
while self.message_queue:
msg = self.message_queue.pop(0)
try:
self.writer.write(msg)
await self.writer.drain()
except ConnectionResetError:
return
def send_raw_message(self, msg):
if not self.writer:
return
self.message_queue.append('{}\r\n'.format(msg).encode())
asyncio.ensure_future(self.write_queue(), loop=asyncio.get_event_loop())

91
server/evidence.py Normal file
View File

@ -0,0 +1,91 @@
class EvidenceList:
limit = 35
class Evidence:
def __init__(self, name, desc, image, pos):
self.name = name
self.desc = desc
self.image = image
self.public = False
self.pos = pos
def set_name(self, name):
self.name = name
def set_desc(self, desc):
self.desc = desc
def set_image(self, image):
self.image = image
def to_string(self):
sequence = (self.name, self.desc, self.image)
return '&'.join(sequence)
def __init__(self):
self.evidences = []
self.poses = {'def':['def', 'hld'], 'pro':['pro', 'hlp'], 'wit':['wit'], 'hlp':['hlp', 'pro'], 'hld':['hld', 'def'], 'jud':['jud'], 'all':['hlp', 'hld', 'wit', 'jud', 'pro', 'def', ''], 'pos':[]}
def login(self, client):
if client.area.evidence_mod == 'FFA':
pass
if client.area.evidence_mod == 'Mods':
if not client.is_cm:
return False
if client.area.evidence_mod == 'CM':
if not client.is_cm and not client.is_mod:
return False
if client.area.evidence_mod == 'HiddenCM':
if not client.is_cm and not client.is_mod:
return False
return True
def correct_format(self, client, desc):
if client.area.evidence_mod != 'HiddenCM':
return True
else:
#correct format: <owner = pos>\ndesc
if desc[:9] == '<owner = ' and desc[9:12] in self.poses and desc[12:14] == '>\n':
return True
return False
def add_evidence(self, client, name, description, image, pos = 'all'):
if self.login(client):
if client.area.evidence_mod == 'HiddenCM':
pos = 'pos'
if len(self.evidences) >= self.limit:
client.send_host_message('You can\'t have more than {} evidence items at a time.'.format(self.limit))
else:
self.evidences.append(self.Evidence(name, description, image, pos))
def evidence_swap(self, client, id1, id2):
if self.login(client):
self.evidences[id1], self.evidences[id2] = self.evidences[id2], self.evidences[id1]
def create_evi_list(self, client):
evi_list = []
nums_list = [0]
for i in range(len(self.evidences)):
if client.area.evidence_mod == 'HiddenCM' and self.login(client):
nums_list.append(i + 1)
evi = self.evidences[i]
evi_list.append(self.Evidence(evi.name, '<owner = {}>\n{}'.format(evi.pos, evi.desc), evi.image, evi.pos).to_string())
elif client.pos in self.poses[self.evidences[i].pos]:
nums_list.append(i + 1)
evi_list.append(self.evidences[i].to_string())
return nums_list, evi_list
def del_evidence(self, client, id):
if self.login(client):
self.evidences.pop(id)
def edit_evidence(self, client, id, arg):
if self.login(client):
if client.area.evidence_mod == 'HiddenCM' and self.correct_format(client, arg[1]):
self.evidences[id] = self.Evidence(arg[0], arg[1][14:], arg[2], arg[1][9:12])
return
if client.area.evidence_mod == 'HiddenCM':
client.send_host_message('You entered a wrong pos.')
return
self.evidences[id] = self.Evidence(arg[0], arg[1], arg[2], arg[3])

32
server/exceptions.py Normal file
View File

@ -0,0 +1,32 @@
# tsuserver3, an Attorney Online server
#
# Copyright (C) 2016 argoneus <argoneuscze@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
class ClientError(Exception):
pass
class AreaError(Exception):
pass
class ArgumentError(Exception):
pass
class ServerError(Exception):
pass

45
server/fantacrypt.py Normal file
View File

@ -0,0 +1,45 @@
# tsuserver3, an Attorney Online server
#
# Copyright (C) 2016 argoneus <argoneuscze@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# fantacrypt was a mistake, just hardcoding some numbers is good enough
import binascii
CRYPT_CONST_1 = 53761
CRYPT_CONST_2 = 32618
CRYPT_KEY = 5
def fanta_decrypt(data):
data_bytes = [int(data[x:x + 2], 16) for x in range(0, len(data), 2)]
key = CRYPT_KEY
ret = ''
for byte in data_bytes:
val = byte ^ ((key & 0xffff) >> 8)
ret += chr(val)
key = ((byte + key) * CRYPT_CONST_1) + CRYPT_CONST_2
return ret
def fanta_encrypt(data):
key = CRYPT_KEY
ret = ''
for char in data:
val = ord(char) ^ ((key & 0xffff) >> 8)
ret += binascii.hexlify(val.to_bytes(1, byteorder='big')).decode().upper()
key = ((val + key) * CRYPT_CONST_1) + CRYPT_CONST_2
return ret

64
server/logger.py Normal file
View File

@ -0,0 +1,64 @@
# tsuserver3, an Attorney Online server
#
# Copyright (C) 2016 argoneus <argoneuscze@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
import time
def setup_logger(debug):
logging.Formatter.converter = time.gmtime
debug_formatter = logging.Formatter('[%(asctime)s UTC]%(message)s')
srv_formatter = logging.Formatter('[%(asctime)s UTC]%(message)s')
debug_log = logging.getLogger('debug')
debug_log.setLevel(logging.DEBUG)
debug_handler = logging.FileHandler('logs/debug.log', encoding='utf-8')
debug_handler.setLevel(logging.DEBUG)
debug_handler.setFormatter(debug_formatter)
debug_log.addHandler(debug_handler)
if not debug:
debug_log.disabled = True
server_log = logging.getLogger('server')
server_log.setLevel(logging.INFO)
server_handler = logging.FileHandler('logs/server.log', encoding='utf-8')
server_handler.setLevel(logging.INFO)
server_handler.setFormatter(srv_formatter)
server_log.addHandler(server_handler)
def log_debug(msg, client=None):
msg = parse_client_info(client) + msg
logging.getLogger('debug').debug(msg)
def log_server(msg, client=None):
msg = parse_client_info(client) + msg
logging.getLogger('server').info(msg)
def parse_client_info(client):
if client is None:
return ''
info = client.get_ip()
if client.is_mod:
return '[{:<15}][{}][MOD]'.format(info, client.id)
return '[{:<15}][{}]'.format(info, client.id)

View File

@ -0,0 +1,89 @@
# tsuserver3, an Attorney Online server
#
# Copyright (C) 2016 argoneus <argoneuscze@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import asyncio
import time
from server import logger
class MasterServerClient:
def __init__(self, server):
self.server = server
self.reader = None
self.writer = None
async def connect(self):
loop = asyncio.get_event_loop()
while True:
try:
self.reader, self.writer = await asyncio.open_connection(self.server.config['masterserver_ip'],
self.server.config['masterserver_port'],
loop=loop)
await self.handle_connection()
except (ConnectionRefusedError, TimeoutError):
pass
except (ConnectionResetError, asyncio.IncompleteReadError):
self.writer = None
self.reader = None
finally:
logger.log_debug("Couldn't connect to the master server, retrying in 30 seconds.")
print("Couldn't connect to the master server, retrying in 30 seconds.")
await asyncio.sleep(30)
async def handle_connection(self):
logger.log_debug('Master server connected.')
await self.send_server_info()
fl = False
lastping = time.time() - 20
while True:
self.reader.feed_data(b'END')
full_data = await self.reader.readuntil(b'END')
full_data = full_data[:-3]
if len(full_data) > 0:
data_list = list(full_data.split(b'#%'))[:-1]
for data in data_list:
raw_msg = data.decode()
cmd, *args = raw_msg.split('#')
if cmd != 'CHECK' and cmd != 'PONG':
logger.log_debug('[MASTERSERVER][INC][RAW]{}'.format(raw_msg))
elif cmd == 'CHECK':
await self.send_raw_message('PING#%')
elif cmd == 'PONG':
fl = False
elif cmd == 'NOSERV':
await self.send_server_info()
if time.time() - lastping > 5:
if fl:
return
lastping = time.time()
fl = True
await self.send_raw_message('PING#%')
await asyncio.sleep(1)
async def send_server_info(self):
cfg = self.server.config
msg = 'SCC#{}#{}#{}#{}#%'.format(cfg['port'], cfg['masterserver_name'], cfg['masterserver_description'],
self.server.software)
await self.send_raw_message(msg)
async def send_raw_message(self, msg):
try:
self.writer.write(msg.encode())
await self.writer.drain()
except ConnectionResetError:
return

263
server/tsuserver.py Normal file
View File

@ -0,0 +1,263 @@
# tsuserver3, an Attorney Online server
#
# Copyright (C) 2016 argoneus <argoneuscze@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import asyncio
import yaml
import json
from server import logger
from server.aoprotocol import AOProtocol
from server.area_manager import AreaManager
from server.ban_manager import BanManager
from server.client_manager import ClientManager
from server.districtclient import DistrictClient
from server.exceptions import ServerError
from server.masterserverclient import MasterServerClient
class TsuServer3:
def __init__(self):
self.config = None
self.allowed_iniswaps = None
self.load_config()
self.load_iniswaps()
self.client_manager = ClientManager(self)
self.area_manager = AreaManager(self)
self.ban_manager = BanManager()
self.software = 'tsuserver3'
self.version = 'tsuserver3dev'
self.release = 3
self.major_version = 1
self.minor_version = 1
self.ipid_list = {}
self.hdid_list = {}
self.char_list = None
self.char_pages_ao1 = None
self.music_list = None
self.music_list_ao2 = None
self.music_pages_ao1 = None
self.backgrounds = None
self.load_characters()
self.load_music()
self.load_backgrounds()
self.load_ids()
self.district_client = None
self.ms_client = None
self.rp_mode = False
logger.setup_logger(debug=self.config['debug'])
def start(self):
loop = asyncio.get_event_loop()
bound_ip = '0.0.0.0'
if self.config['local']:
bound_ip = '127.0.0.1'
ao_server_crt = loop.create_server(lambda: AOProtocol(self), bound_ip, self.config['port'])
ao_server = loop.run_until_complete(ao_server_crt)
if self.config['use_district']:
self.district_client = DistrictClient(self)
asyncio.ensure_future(self.district_client.connect(), loop=loop)
if self.config['use_masterserver']:
self.ms_client = MasterServerClient(self)
asyncio.ensure_future(self.ms_client.connect(), loop=loop)
logger.log_debug('Server started.')
try:
loop.run_forever()
except KeyboardInterrupt:
pass
logger.log_debug('Server shutting down.')
ao_server.close()
loop.run_until_complete(ao_server.wait_closed())
loop.close()
def get_version_string(self):
return str(self.release) + '.' + str(self.major_version) + '.' + str(self.minor_version)
def new_client(self, transport):
c = self.client_manager.new_client(transport)
if self.rp_mode:
c.in_rp = True
c.server = self
c.area = self.area_manager.default_area()
c.area.new_client(c)
return c
def remove_client(self, client):
client.area.remove_client(client)
self.client_manager.remove_client(client)
def get_player_count(self):
return len(self.client_manager.clients)
def load_config(self):
with open('config/config.yaml', 'r', encoding = 'utf-8') as cfg:
self.config = yaml.load(cfg)
self.config['motd'] = self.config['motd'].replace('\\n', ' \n')
if 'music_change_floodguard' not in self.config:
self.config['music_change_floodguard'] = {'times_per_interval': 1, 'interval_length': 0, 'mute_length': 0}
if 'wtce_floodguard' not in self.config:
self.config['wtce_floodguard'] = {'times_per_interval': 1, 'interval_length': 0, 'mute_length': 0}
def load_characters(self):
with open('config/characters.yaml', 'r', encoding = 'utf-8') as chars:
self.char_list = yaml.load(chars)
self.build_char_pages_ao1()
def load_music(self):
with open('config/music.yaml', 'r', encoding = 'utf-8') as music:
self.music_list = yaml.load(music)
self.build_music_pages_ao1()
self.build_music_list_ao2()
def load_ids(self):
self.ipid_list = {}
self.hdid_list = {}
#load ipids
try:
with open('storage/ip_ids.json', 'r', encoding = 'utf-8') as whole_list:
self.ipid_list = json.loads(whole_list.read())
except:
logger.log_debug('Failed to load ip_ids.json from ./storage. If ip_ids.json is exist then remove it.')
#load hdids
try:
with open('storage/hd_ids.json', 'r', encoding = 'utf-8') as whole_list:
self.hdid_list = json.loads(whole_list.read())
except:
logger.log_debug('Failed to load hd_ids.json from ./storage. If hd_ids.json is exist then remove it.')
def dump_ipids(self):
with open('storage/ip_ids.json', 'w') as whole_list:
json.dump(self.ipid_list, whole_list)
def dump_hdids(self):
with open('storage/hd_ids.json', 'w') as whole_list:
json.dump(self.hdid_list, whole_list)
def get_ipid(self, ip):
if not (ip in self.ipid_list):
self.ipid_list[ip] = len(self.ipid_list)
self.dump_ipids()
return self.ipid_list[ip]
def load_backgrounds(self):
with open('config/backgrounds.yaml', 'r', encoding = 'utf-8') as bgs:
self.backgrounds = yaml.load(bgs)
def load_iniswaps(self):
try:
with open('config/iniswaps.yaml', 'r', encoding = 'utf-8') as iniswaps:
self.allowed_iniswaps = yaml.load(iniswaps)
except:
logger.log_debug('cannot find iniswaps.yaml')
def build_char_pages_ao1(self):
self.char_pages_ao1 = [self.char_list[x:x + 10] for x in range(0, len(self.char_list), 10)]
for i in range(len(self.char_list)):
self.char_pages_ao1[i // 10][i % 10] = '{}#{}&&0&&&0&'.format(i, self.char_list[i])
def build_music_pages_ao1(self):
self.music_pages_ao1 = []
index = 0
# add areas first
for area in self.area_manager.areas:
self.music_pages_ao1.append('{}#{}'.format(index, area.name))
index += 1
# then add music
for item in self.music_list:
self.music_pages_ao1.append('{}#{}'.format(index, item['category']))
index += 1
for song in item['songs']:
self.music_pages_ao1.append('{}#{}'.format(index, song['name']))
index += 1
self.music_pages_ao1 = [self.music_pages_ao1[x:x + 10] for x in range(0, len(self.music_pages_ao1), 10)]
def build_music_list_ao2(self):
self.music_list_ao2 = []
# add areas first
for area in self.area_manager.areas:
self.music_list_ao2.append(area.name)
# then add music
for item in self.music_list:
self.music_list_ao2.append(item['category'])
for song in item['songs']:
self.music_list_ao2.append(song['name'])
def is_valid_char_id(self, char_id):
return len(self.char_list) > char_id >= 0
def get_char_id_by_name(self, name):
for i, ch in enumerate(self.char_list):
if ch.lower() == name.lower():
return i
raise ServerError('Character not found.')
def get_song_data(self, music):
for item in self.music_list:
if item['category'] == music:
return item['category'], -1
for song in item['songs']:
if song['name'] == music:
try:
return song['name'], song['length']
except KeyError:
return song['name'], -1
raise ServerError('Music not found.')
def send_all_cmd_pred(self, cmd, *args, pred=lambda x: True):
for client in self.client_manager.clients:
if pred(client):
client.send_command(cmd, *args)
def broadcast_global(self, client, msg, as_mod=False):
char_name = client.get_char_name()
ooc_name = '{}[{}][{}]'.format('<dollar>G', client.area.id, char_name)
if as_mod:
ooc_name += '[M]'
self.send_all_cmd_pred('CT', ooc_name, msg, pred=lambda x: not x.muted_global)
if self.config['use_district']:
self.district_client.send_raw_message(
'GLOBAL#{}#{}#{}#{}'.format(int(as_mod), client.area.id, char_name, msg))
def broadcast_need(self, client, msg):
char_name = client.get_char_name()
area_name = client.area.name
area_id = client.area.id
self.send_all_cmd_pred('CT', '{}'.format(self.config['hostname']),
'=== Advert ===\r\n{} in {} [{}] needs {}\r\n==============='
.format(char_name, area_name, area_id, msg), pred=lambda x: not x.muted_adverts)
if self.config['use_district']:
self.district_client.send_raw_message('NEED#{}#{}#{}#{}'.format(char_name, area_name, area_id, msg))
def refresh(self):
with open('config/config.yaml', 'r') as cfg:
self.config['motd'] = yaml.load(cfg)['motd'].replace('\\n', ' \n')
with open('config/characters.yaml', 'r') as chars:
self.char_list = yaml.load(chars)
with open('config/music.yaml', 'r') as music:
self.music_list = yaml.load(music)
self.build_music_pages_ao1()
self.build_music_list_ao2()
with open('config/backgrounds.yaml', 'r') as bgs:
self.backgrounds = yaml.load(bgs)

212
server/websocket.py Normal file
View File

@ -0,0 +1,212 @@
# tsuserver3, an Attorney Online server
#
# Copyright (C) 2017 argoneus <argoneuscze@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# Partly authored by Johan Hanssen Seferidis (MIT license):
# https://github.com/Pithikos/python-websocket-server
import asyncio
import re
import struct
from base64 import b64encode
from hashlib import sha1
from server import logger
class Bitmasks:
FIN = 0x80
OPCODE = 0x0f
MASKED = 0x80
PAYLOAD_LEN = 0x7f
PAYLOAD_LEN_EXT16 = 0x7e
PAYLOAD_LEN_EXT64 = 0x7f
class Opcode:
CONTINUATION = 0x0
TEXT = 0x1
BINARY = 0x2
CLOSE_CONN = 0x8
PING = 0x9
PONG = 0xA
class WebSocket:
"""
State data for clients that are connected via a WebSocket that wraps
over a conventional TCP connection.
"""
def __init__(self, client, protocol):
self.client = client
self.transport = client.transport
self.protocol = protocol
self.keep_alive = True
self.handshake_done = False
self.valid = False
def handle(self, data):
if not self.handshake_done:
return self.handshake(data)
return self.parse(data)
def parse(self, data):
b1, b2 = 0, 0
if len(data) >= 2:
b1, b2 = data[0], data[1]
fin = b1 & Bitmasks.FIN
opcode = b1 & Bitmasks.OPCODE
masked = b2 & Bitmasks.MASKED
payload_length = b2 & Bitmasks.PAYLOAD_LEN
if not b1:
# Connection closed
self.keep_alive = 0
return
if opcode == Opcode.CLOSE_CONN:
# Connection close requested
self.keep_alive = 0
return
if not masked:
# Client was not masked (spec violation)
logger.log_debug("ws: client was not masked.", self.client)
self.keep_alive = 0
print(data)
return
if opcode == Opcode.CONTINUATION:
# No continuation frames supported
logger.log_debug("ws: client tried to send continuation frame.", self.client)
return
elif opcode == Opcode.BINARY:
# No binary frames supported
logger.log_debug("ws: client tried to send binary frame.", self.client)
return
elif opcode == Opcode.TEXT:
def opcode_handler(s, msg):
return msg
elif opcode == Opcode.PING:
opcode_handler = self.send_pong
elif opcode == Opcode.PONG:
opcode_handler = lambda s, msg: None
else:
# Unknown opcode
logger.log_debug("ws: unknown opcode!", self.client)
self.keep_alive = 0
return
if payload_length == 126:
payload_length = struct.unpack(">H", data[2:4])[0]
elif payload_length == 127:
payload_length = struct.unpack(">Q", data[2:10])[0]
masks = data[2:6]
decoded = ""
for char in data[6:payload_length + 6]:
char ^= masks[len(decoded) % 4]
decoded += chr(char)
return opcode_handler(self, decoded)
def send_message(self, message):
self.send_text(message)
def send_pong(self, message):
self.send_text(message, Opcode.PONG)
def send_text(self, message, opcode=Opcode.TEXT):
"""
Important: Fragmented (continuation) messages are not supported since
their usage cases are limited - when we don't know the payload length.
"""
# Validate message
if isinstance(message, bytes):
message = message.decode("utf-8")
elif isinstance(message, str):
pass
else:
raise TypeError("Message must be either str or bytes")
header = bytearray()
payload = message.encode("utf-8")
payload_length = len(payload)
# Normal payload
if payload_length <= 125:
header.append(Bitmasks.FIN | opcode)
header.append(payload_length)
# Extended payload
elif payload_length >= 126 and payload_length <= 65535:
header.append(Bitmasks.FIN | opcode)
header.append(Bitmasks.PAYLOAD_LEN_EXT16)
header.extend(struct.pack(">H", payload_length))
# Huge extended payload
elif payload_length < (1 << 64):
header.append(Bitmasks.FIN | opcode)
header.append(Bitmasks.PAYLOAD_LEN_EXT64)
header.extend(struct.pack(">Q", payload_length))
else:
raise Exception("Message is too big")
self.transport.write(header + payload)
def handshake(self, data):
try:
message = data[0:1024].decode().strip()
except UnicodeDecodeError:
return False
upgrade = re.search('\nupgrade[\s]*:[\s]*websocket', message.lower())
if not upgrade:
self.keep_alive = False
return False
key = re.search('\n[sS]ec-[wW]eb[sS]ocket-[kK]ey[\s]*:[\s]*(.*)\r\n', message)
if key:
key = key.group(1)
else:
logger.log_debug("Client tried to connect but was missing a key", self.client)
self.keep_alive = False
return False
response = self.make_handshake_response(key)
print(response.encode())
self.transport.write(response.encode())
self.handshake_done = True
self.valid = True
return True
def make_handshake_response(self, key):
return \
'HTTP/1.1 101 Switching Protocols\r\n'\
'Upgrade: websocket\r\n' \
'Connection: Upgrade\r\n' \
'Sec-WebSocket-Accept: %s\r\n' \
'\r\n' % self.calculate_response_key(key)
def calculate_response_key(self, key):
GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
hash = sha1(key.encode() + GUID.encode())
response_key = b64encode(hash.digest()).strip()
return response_key.decode('ASCII')
def finish(self):
self.protocol.connection_lost(self)