Ejemplo n.º 1
0
 def test_read_make_list(self):
     ban_list = [
         ["GOD", "177.47.27.223", ": esp hacker", 1511717871.435394],
         ["Tragon700", "187.16.185.94", ": AIMBOT", 1511718148.115572],
         ["Bradley", "94.193.254.118", ": ESP", 1511716740.165356],
         ["GOD", "189.24.61.113", ": ANT BAN", 1511718023.381783],
         ["RUszaj SIe", "188.146.106.109",
          ": Sle auto-headshot auto-kill", 1511718234.179913],
         ["Dj_Hazel_PL", "185.46.170.234", ": Hack", 1511715860.348984],
         ["(unknown)", "178.63.171.105", ": Hack", 1511716248.032651],
         ["Ken Kaneki", "177.222.250.65", ": kaneki afk", 1511718856.598152],
         ["panic-recover", "172.17.0.1", ": 10", 1512040137.320614],
     ]
     networkdict = NetworkDict()
     networkdict.read_list(ban_list)
     self.assertEqual(ban_list, networkdict.make_list())
Ejemplo n.º 2
0
class FeatureProtocol(ServerProtocol):
    connection_class = FeatureConnection
    bans = None
    ban_publish = None
    ban_manager = None
    everyone_is_admin = False
    player_memory = None
    irc_relay = None
    balanced_teams = None
    timestamps = None
    building = True
    killing = True
    global_chat = True
    remote_console = None
    debug_log = None
    advance_call = None
    master_reconnect_call = None
    master = False
    ip = None
    identifier = None

    planned_map = None

    map_info = None
    spawns = None
    user_blocks = None
    god_blocks = None

    last_time = None
    interface = None

    team_class = FeatureTeam

    game_mode = None  # default to None so we can check
    time_announce_schedule = None

    default_fog = (128, 232, 255)

    def __init__(self, interface: bytes, config_dict: Dict[str, Any]) -> None:
        # logfile path relative to config dir if not abs path
        log_filename = logfile.get()
        if log_filename.strip():  # catches empty filename
            if not os.path.isabs(log_filename):
                log_filename = os.path.join(config.config_dir, log_filename)
            ensure_dir_exists(log_filename)
            if logging_rotate_daily.get():
                logging_file = DailyLogFile(log_filename, '.')
            else:
                logging_file = open(log_filename, 'a')
            predicate = LogLevelFilterPredicate(
                LogLevel.levelWithName(loglevel.get()))
            observers = [
                FilteringLogObserver(textFileLogObserver(sys.stderr),
                                     [predicate]),
                FilteringLogObserver(textFileLogObserver(logging_file),
                                     [predicate])
            ]
            globalLogBeginner.beginLoggingTo(observers)
            log.info('piqueserver started on %s' % time.strftime('%c'))

        self.config = config_dict
        if random_rotation.get():
            self.map_rotator_type = random_choice_cycle
        else:
            self.map_rotator_type = itertools.cycle
        self.default_time_limit = default_time_limit.get()
        self.default_cap_limit = cap_limit.get()
        self.advance_on_win = int(advance_on_win.get())
        self.win_count = itertools.count(1)
        self.bans = NetworkDict()

        # attempt to load a saved bans list
        try:
            with open(os.path.join(config.config_dir, bans_file.get()),
                      'r') as f:
                self.bans.read_list(json.load(f))
            log.debug("loaded {count} bans", count=len(self.bans))
        except FileNotFoundError:
            log.debug("skip loading bans: file unavailable",
                      count=len(self.bans))
        except IOError as e:
            log.error('Could not read bans.txt: {}'.format(e))
        except ValueError as e:
            log.error('Could not parse bans.txt: {}'.format(e))

        self.hard_bans = set()  # possible DDoS'ers are added here
        self.player_memory = deque(maxlen=100)
        if len(self.name) > MAX_SERVER_NAME_SIZE:
            log.warn('(server name too long; it will be truncated to "%s")' %
                     (self.name[:MAX_SERVER_NAME_SIZE]))
        self.respawn_time = respawn_time_option.get()
        self.respawn_waves = respawn_waves.get()

        # since AoS only supports CTF and TC at a protocol level, we need to get
        # the base game mode if we are using a custom game mode.
        game_mode_name = game_mode.get()
        if game_mode_name == 'ctf':
            self.game_mode = CTF_MODE
        elif game_mode.get() == 'tc':
            self.game_mode = TC_MODE
        elif self.game_mode not in [CTF_MODE, TC_MODE]:
            raise ValueError(
                'invalid game mode: custom game mode "{}" does not set '
                'protocol.game_mode to one of TC_MODE or CTF_MODE. Are '
                'you sure the thing you have specified is a game mode?'.format(
                    game_mode_name))

        self.game_mode_name = game_mode.get().split('.')[-1]
        self.team1_name = team1_name.get()[:9]
        self.team2_name = team2_name.get()[:9]
        self.team1_color = tuple(team1_color.get())
        self.team2_color = tuple(team2_color.get())
        self.friendly_fire = friendly_fire.get()
        self.friendly_fire_on_grief = friendly_fire_on_grief.get()
        self.friendly_fire_time = grief_friendly_fire_time.get()
        self.spade_teamkills_on_grief = spade_teamkills_on_grief.get()
        self.fall_damage = fall_damage.get()
        self.teamswitch_interval = teamswitch_interval.get()
        self.teamswitch_allowed = teamswitch_allowed.get()
        self.max_players = max_players.get()
        self.melee_damage = melee_damage.get()
        self.max_connections_per_ip = max_connections_per_ip.get()
        self.passwords = passwords.get()
        self.server_prefix = server_prefix.get()
        self.time_announcements = time_announcements.get()
        self.balanced_teams = balanced_teams.get()
        self.login_retries = login_retries.get()

        # voting configuration
        self.default_ban_time = default_ban_duration.get()

        self.speedhack_detect = speedhack_detect.get()
        self.rubberband_distance = rubberband_distance.get()
        if user_blocks_only.get():
            self.user_blocks = set()
        self.set_god_build = set_god_build.get()
        self.debug_log = debug_log_enabled.get()
        if self.debug_log:
            # TODO: make this configurable
            pyspades.debug.open_debug_log(
                os.path.join(config.config_dir, 'debug.log'))
        if ssh_enabled.get():
            from piqueserver.ssh import RemoteConsole
            self.remote_console = RemoteConsole(self)
        irc = irc_options.get()
        if irc.get('enabled', False):
            from piqueserver.irc import IRCRelay
            self.irc_relay = IRCRelay(self, irc)
        if status_server_enabled.get():
            from piqueserver.statusserver import StatusServer
            self.status_server = StatusServer(self)
            ensureDeferred(self.status_server.listen())
        if ban_publish.get():
            from piqueserver.banpublish import PublishServer
            self.ban_publish = PublishServer(self, ban_publish_port.get())
        if bans_urls.get():
            from piqueserver import bansubscribe
            self.ban_manager = bansubscribe.BanManager(self)
        self.start_time = time.time()
        self.end_calls = []
        # TODO: why is this here?
        create_console(self)

        for user_type, func_names in rights.get().items():
            for func_name in func_names:
                commands.add_rights(user_type, func_name)

        self.port = port_option.get()
        ServerProtocol.__init__(self, self.port, interface)
        self.host.intercept = self.receive_callback

        try:
            self.set_map_rotation(self.config['rotation'])
        except MapNotFound as e:
            log.critical('Invalid map in map rotation (%s), exiting.' % e.map)
            raise SystemExit

        map_load_d = self.advance_rotation()
        # discard the result of the map advance for now
        map_load_d.addCallback(lambda x: self._post_init())

        ip_getter = ip_getter_option.get()
        if ip_getter:
            ensureDeferred(as_deferred(self.get_external_ip(ip_getter)))

        self.new_release = None
        notify_new_releases = config.option("release_notifications",
                                            default=True)
        if notify_new_releases.get():
            ensureDeferred(as_deferred(self.watch_for_releases()))

        self.vacuum_loop = LoopingCall(self.vacuum_bans)
        # Run the vacuum every 6 hours, and kick it off it right now
        self.vacuum_loop.start(60 * 60 * 6, True)

        reactor.addSystemEventTrigger('before', 'shutdown',
                                      lambda: ensureDeferred(self.shutdown()))

    def _post_init(self):
        """called after the map has been loaded"""
        self.update_format()
        self.tip_frequency = tip_frequency.get()
        if self.tips is not None and self.tip_frequency > 0:
            reactor.callLater(self.tip_frequency * 60, self.send_tip)

        self.master = register_master_option.get()
        self.set_master()

    async def get_external_ip(self, ip_getter: str) -> Iterator[Deferred]:
        log.info(
            'Retrieving external IP from {!r} to generate server identifier.'.
            format(ip_getter))
        try:
            async with aiohttp.ClientSession() as session:
                async with session.get(ip_getter) as response:
                    ip = await response.text()
                    ip = IPv4Address(ip.strip())
        except AddressValueError as e:
            log.warn('External IP getter service returned invalid data.\n'
                     'Please check the "ip_getter" setting in your config.')
            return
        except Exception as e:  # pylint: disable=broad-except
            log.warn("Getting external IP failed: {reason}", reason=e)
            return

        self.ip = ip
        self.identifier = make_server_identifier(ip, self.port)
        log.info('Server public ip address: {}:{}'.format(ip, self.port))
        log.info('Public aos identifier: {}'.format(self.identifier))

    def set_time_limit(self,
                       time_limit: Optional[int] = None,
                       additive: bool = False) -> Optional[int]:
        advance_call = self.advance_call
        add_time = 0.0
        if advance_call is not None:
            add_time = ((advance_call.getTime() - reactor.seconds()) / 60.0)
            advance_call.cancel()
            self.advance_call = None
        time_limit = time_limit or self.default_time_limit
        if not time_limit:
            for call in self.end_calls[:]:
                call.set(None)
            return None

        if additive:
            time_limit = min(time_limit + add_time, self.default_time_limit)

        seconds = time_limit * 60
        self.advance_call = reactor.callLater(seconds, self._time_up)

        for call in self.end_calls[:]:
            call.set(seconds)

        if self.time_announce_schedule is not None:
            self.time_announce_schedule.reset()
        self.time_announce_schedule = Scheduler(self)
        for seconds in self.time_announcements:
            self.time_announce_schedule.call_end(seconds,
                                                 self._next_time_announce)

        return time_limit

    def _next_time_announce(self):
        remaining = self.advance_call.getTime() - reactor.seconds()
        if remaining < 60.001:
            if remaining < 10.001:
                self.send_chat('%s...' % int(round(remaining)))
            else:
                self.send_chat('%s seconds remaining.' % int(round(remaining)))
        else:
            self.send_chat('%s minutes remaining.' %
                           int(round(remaining / 60)))

    def _time_up(self):
        self.advance_call = None
        self.advance_rotation('Time up!')

    def advance_rotation(self, message: Optional[str] = None) -> Deferred:
        """
        Advances to the next map in the rotation. If message is provided
        it will send it to the chat, waits for 10 seconds and then advances.

        Returns:
            Deferred that fires when the map has been loaded
        """
        self.set_time_limit(False)
        if self.planned_map is None:
            self.planned_map = next(self.map_rotator)
        planned_map = self.planned_map
        self.planned_map = None
        self.on_advance(planned_map)

        async def do_advance():
            if message is not None:
                log.info("advancing to map '{name}' ({reason}) in 10 seconds",
                         name=planned_map.full_name,
                         reason=message)
                self.send_chat('{} Next map: {}.'.format(
                    message, planned_map.full_name),
                               irc=True)
                await sleep(10)
            else:
                log.info("advancing to map '{name}'",
                         name=planned_map.full_name)

            await self.set_map_name(planned_map)

        return ensureDeferred(do_advance())

    def get_mode_name(self) -> str:
        return self.game_mode_name

    async def set_map_name(self, rot_info: RotationInfo) -> None:
        """
        Sets the map by its name.
        """
        map_info = await self.make_map(rot_info)
        if self.map_info:
            self.on_map_leave()
        self.map_info = map_info
        self.max_score = self.map_info.cap_limit or self.default_cap_limit
        self.set_map(self.map_info.data)
        self.set_time_limit(self.map_info.time_limit)
        self.update_format()

    def set_server_name(self, name: str) -> None:
        name_option.set(name)
        self.update_format()

    def make_map(self, rot_info: RotationInfo) -> Deferred:
        """
        Creates and returns a Map object from rotation info in a new thread

        Returns:
            Deferred that resolves to a `Map` object.
        """
        # we must do this in a new thread, since map generation might take so
        # long that clients time out.
        return threads.deferToThread(Map, rot_info,
                                     os.path.join(config.config_dir, 'maps'))

    def set_map_rotation(self, maps: List[str]) -> None:
        """
        Over-writes the current map rotation with provided one.
        `FeatureProtocol.advance_rotation` still needs to be called to actually
        change the map,
        """
        maps = check_rotation(maps, os.path.join(config.config_dir, 'maps'))
        self.maps = maps
        self.map_rotator = self.map_rotator_type(maps)

    def get_map_rotation(self):
        return [map_item.full_name for map_item in self.maps]

    def is_indestructable(self, x: int, y: int, z: int) -> bool:
        if self.user_blocks is not None:
            if (x, y, z) not in self.user_blocks:
                return True
        if self.god_blocks is not None:
            if (x, y, z) in self.god_blocks:  # pylint: disable=unsupported-membership-test
                return True
        map_is_indestructable = self.map_info.is_indestructable
        if map_is_indestructable is not None:
            if map_is_indestructable(self, x, y, z):
                return True
        return False

    def update_format(self) -> None:
        """
        Called when the map (or other variables) have been updated
        """
        self.name = self.format(name_option.get())
        self.motd = self.format_lines(motd_option.get())
        self.help = self.format_lines(help_option.get())
        self.tips = self.format_lines(tips_option.get())
        self.rules = self.format_lines(rules_option.get())
        if self.master_connection is not None:
            self.master_connection.send_server()

    def format(self,
               value: str,
               extra: Optional[Dict[str, str]] = None) -> str:
        map_info = self.map_info
        format_dict = {
            'map_name': map_info.name,
            'map_author': map_info.author,
            'map_description': map_info.description,
            'game_mode': self.get_mode_name(),
            'server_name': self.name,
        }

        if extra:
            format_dict.update(extra)
        # format with both old-style and new string formatting to stay
        # compatible with older configs
        return value.format(**format_dict) % format_dict

    def format_lines(self, value: List[str]) -> List[str]:
        if value is None:
            return
        return [self.format(line) for line in value]

    def got_master_connection(self, client):
        log.info('Master connection established.')
        ServerProtocol.got_master_connection(self, client)

    def master_disconnected(self, client=None):
        ServerProtocol.master_disconnected(self, client)
        if self.master and self.master_reconnect_call is None:
            if client:
                message = 'Master connection could not be established'
            else:
                message = 'Master connection lost'
            log.info('%s, reconnecting in 60 seconds...' % message)
            self.master_reconnect_call = reactor.callLater(
                60, self.reconnect_master)

    def reconnect_master(self):
        self.master_reconnect_call = None
        self.set_master()

    def set_master_state(self, value):
        if value == self.master:
            return
        self.master = value
        has_connection = self.master_connection is not None
        has_reconnect = self.master_reconnect_call is not None
        if value:
            if not has_connection and not has_reconnect:
                self.set_master()
        else:
            if has_reconnect:
                self.master_reconnect_call.cancel()
                self.master_reconnect_call = None
            if has_connection:
                self.master_connection.disconnect()

    async def shutdown(self):
        """
        Notifies players and disconnects them before a shutdown.
        """
        if not self.connections:
            # exit instantly if nobody is connected anyway
            return

        # send shutdown notification
        log.info("disconnecting players")
        self.broadcast_chat("Server shutting down in 3sec.")
        for i in range(3, 0, -1):
            self.broadcast_chat(str(i) + "...")
            await sleep(1)

        # disconnect all players
        for connection in list(self.connections.values()):
            connection.disconnect(ERROR_SHUTDOWN)

        # give the connections some time to terminate
        await sleep(0.2)

    def add_ban(self, ip, reason, duration, name=None):
        """
        Ban an ip with an optional reason and duration in seconds. If duration
        is None, ban is permanent.
        """
        network = ip_network(str(ip), strict=False)
        for connection in list(self.connections.values()):
            if ip_address(connection.address[0]) in network:
                name = connection.name
                connection.kick(silent=True)
        if duration:
            duration = reactor.seconds() + duration
        else:
            duration = None
        self.bans[ip] = (name or '(unknown)', reason, duration)
        self.save_bans()

    def remove_ban(self, ip):
        results = self.bans.remove(ip)
        log.info('Removing ban: {ip} {results}', ip=ip, results=results)
        self.save_bans()

    async def watch_for_releases(self):
        """Starts a loop for `check_for_releases` and updates `self.new_release`."""
        while True:
            self.new_release = await check_for_releases()
            if self.new_release:
                log.info("#" * 60)
                log.info(format_release(self.new_release))
                log.info("#" * 60)
            await asyncio.sleep(86400)  # 24 hrs

    def vacuum_bans(self):
        """remove any bans that might have expired. This takes a while, so it is
        split up over the event loop"""
        def do_vacuum_bans():
            """do the actual clearing of bans"""

            bans_count = len(self.bans)
            log.info("starting ban vacuum with {count} bans", count=bans_count)
            start_time = time.time()

            # create a copy of the items, so we don't have issues modifying
            # while iteraing
            for ban in list(self.bans.iteritems()):
                ban_exipry = ban[1][2]
                if ban_exipry is None:
                    # entry never expires
                    continue
                if ban[1][2] < start_time:
                    # expired
                    del self.bans[ban[0]]
                yield
            log.debug(
                "ban vacuum took {time:.2f} seconds, removed {count} bans",
                count=bans_count - len(self.bans),
                time=time.time() - start_time)
            self.save_bans()

        # TODO: use cooperate() here instead, once you figure out why it's
        # swallowing errors. Perhaps try add an errback?
        coiterate(do_vacuum_bans())

    def undo_last_ban(self):
        result = self.bans.pop()
        self.save_bans()
        return result

    def save_bans(self):
        ban_file = os.path.join(config.config_dir, 'bans.txt')
        ensure_dir_exists(ban_file)

        start_time = reactor.seconds()
        with open(ban_file, 'w') as f:
            json.dump(self.bans.make_list(), f, indent=2)
        log.debug("saving {count} bans took {time:.2f} seconds",
                  count=len(self.bans),
                  time=reactor.seconds() - start_time)

        if self.ban_publish is not None:
            self.ban_publish.update()

    def receive_callback(self, address: Address, data: bytes) -> int:
        """This hook receives the raw UDP data before it is processed by enet"""

        # exceptions get swallowed in the pyenet C stuff, so we catch anything
        # for now. This should ideally get fixed in pyenet instead.
        try:
            # reply to ASCII HELLO messages with HI so that clients can measure the
            # connection latency
            if data == b'HELLO':
                self.host.socket.send(address, b'HI')
                return 1
            # reply to ASCII HELLOLAN messages with server data for LAN discovery
            elif data == b'HELLOLAN':
                # we might receive a HELLOLAN before the map has been loaded
                # if so, return a dummy string instead
                if self.map_info:
                    map_name = self.map_info.short_name
                else:
                    map_name = "loading..."
                entry = {
                    "name": self.name,
                    "players_current": self.get_player_count(),
                    "players_max": self.max_players,
                    "map": map_name,
                    "game_mode": self.get_mode_name(),
                    "game_version": "0.75"
                }
                payload = json.dumps(entry).encode()
                self.host.socket.send(address, payload)
                return 1

            # This drop the connection of any ip in hard_bans
            if address.host in self.hard_bans:
                return 1
        except Exception:
            import traceback
            traceback.print_exc()

        return 0

    def data_received(self, peer: Peer, packet: Packet) -> None:
        ip = peer.address.host
        current_time = reactor.seconds()
        try:
            ServerProtocol.data_received(self, peer, packet)
        except (NoDataLeft, ValueError):
            import traceback
            traceback.print_exc()
            log.info(
                'IP %s was hardbanned for invalid data or possibly DDoS.' % ip)
            self.hard_bans.add(ip)
            return
        dt = reactor.seconds() - current_time
        if dt > 1.0:
            log.warn('processing {!r} from {} took {}'.format(
                packet.data, ip, dt))

    def irc_say(self, msg: str, me: bool = False) -> None:
        if self.irc_relay:
            if me:
                self.irc_relay.me(msg, do_filter=True)
            else:
                self.irc_relay.send(msg, do_filter=True)

    def send_tip(self):
        line = self.tips[random.randrange(len(self.tips))]
        self.send_chat(line)
        reactor.callLater(self.tip_frequency * 60, self.send_tip)

    # pylint: disable=arguments-differ
    def broadcast_chat(self,
                       value,
                       global_message=True,
                       sender=None,
                       team=None,
                       irc=False):
        """
        Send a chat message to many users
        """
        if irc:
            self.irc_say('* %s' % value)
        ServerProtocol.send_chat(self, value, global_message, sender, team)

    # backwards compatability
    send_chat = broadcast_chat

    # log high CPU usage

    def update_world(self):
        last_time = self.last_time
        current_time = reactor.seconds()
        if last_time is not None:
            dt = current_time - last_time
            if dt > 1.0:
                log.warn('high CPU usage detected - %s' % dt)
        self.last_time = current_time
        ServerProtocol.update_world(self)
        time_taken = reactor.seconds() - current_time
        if time_taken > 1.0:
            log.warn('World update iteration took %s, objects: %s' %
                     (time_taken, self.world.objects))

    # events

    def on_map_change(self, the_map: VXLData) -> None:
        self.set_fog_color(getattr(self.map_info.info, 'fog',
                                   self.default_fog))

        map_on_map_change = self.map_info.on_map_change
        if map_on_map_change is not None:
            map_on_map_change(self, the_map)

    def on_map_leave(self):
        map_on_map_leave = self.map_info.on_map_leave
        if map_on_map_leave is not None:
            map_on_map_leave(self)

    def on_game_end(self):
        if self.advance_on_win <= 0:
            self.irc_say('Round ended!', me=True)
        elif next(self.win_count) % self.advance_on_win == 0:
            self.advance_rotation('Game finished!')

    def on_advance(self, map_name: str) -> None:
        pass

    def on_ban_attempt(self, connection, reason, duration):
        return True

    def on_ban(self, connection, reason, duration):
        pass

    # voting

    def cancel_vote(self, connection=None):
        return 'No vote in progress.'

    # useful twisted wrappers

    def listenTCP(self, *arg, **kw) -> Port:
        return reactor.listenTCP(*arg, interface=network_interface.get(), **kw)

    def connectTCP(self, *arg, **kw):
        return reactor.connectTCP(*arg,
                                  bindAddress=(network_interface.get(), 0),
                                  **kw)

    # before-end calls

    def call_end(self, delay: int, func: Callable, *arg, **kw) -> EndCall:
        call = EndCall(self, delay, func, *arg, **kw)
        call.set(self.get_advance_time())
        return call

    def get_advance_time(self) -> float:
        if not self.advance_call:
            return None
        return self.advance_call.getTime() - self.advance_call.seconds()
Ejemplo n.º 3
0
class FeatureProtocol(ServerProtocol):
    connection_class = FeatureConnection
    bans = None
    ban_publish = None
    ban_manager = None
    everyone_is_admin = False
    player_memory = None
    irc_relay = None
    balanced_teams = None
    timestamps = None
    building = True
    killing = True
    global_chat = True
    remote_console = None
    debug_log = None
    advance_call = None
    master_reconnect_call = None
    master = False
    ip = None
    identifier = None

    planned_map = None

    map_info = None
    spawns = None
    user_blocks = None
    god_blocks = None

    last_time = None
    interface = None

    team_class = FeatureTeam

    game_mode = None  # default to None so we can check
    time_announce_schedule = None

    server_version = cfg.server_version

    default_fog = (128, 232, 255)

    def __init__(self, interface, config):
        self.config = config
        if config.get('random_rotation', False):
            self.map_rotator_type = random_choice_cycle
        else:
            self.map_rotator_type = itertools.cycle  # pylint: disable=redefined-variable-type
        self.default_time_limit = config.get('default_time_limit', 20.0)
        self.default_cap_limit = config.get('cap_limit', 10.0)
        self.advance_on_win = int(config.get('advance_on_win', False))
        self.win_count = itertools.count(1)
        self.bans = NetworkDict()
        # TODO: check if this is actually working and not silently failing
        try:
            self.bans.read_list(
                json.load(open(os.path.join(cfg.config_dir, 'bans.txt'), 'rb')))
        except IOError:
            pass
        self.hard_bans = set()  # possible DDoS'ers are added here
        self.player_memory = deque(maxlen=100)
        self.config = config
        if len(self.name) > MAX_SERVER_NAME_SIZE:
            print('(server name too long; it will be truncated to "%s")' % (
                self.name[:MAX_SERVER_NAME_SIZE]))
        self.respawn_time = config.get('respawn_time', 8)
        self.respawn_waves = config.get('respawn_waves', False)
        game_mode = config.get('game_mode', 'ctf')
        if game_mode == 'ctf':
            self.game_mode = CTF_MODE
        elif game_mode == 'tc':
            self.game_mode = TC_MODE
        elif self.game_mode is None:
            raise NotImplementedError('invalid game mode: %s' % game_mode)
        self.game_mode_name = game_mode
        team1 = config.get('team1', {})
        team2 = config.get('team2', {})
        self.team1_name = team1.get('name', 'Blue')
        self.team2_name = team2.get('name', 'Green')
        self.team1_color = tuple(team1.get('color', (0, 0, 196)))
        self.team2_color = tuple(team2.get('color', (0, 196, 0)))
        self.friendly_fire = config.get('friendly_fire', True)
        self.friendly_fire_time = config.get('grief_friendly_fire_time', 2.0)
        self.spade_teamkills_on_grief = config.get('spade_teamkills_on_grief',
                                                   False)
        self.fall_damage = config.get('fall_damage', True)
        self.teamswitch_interval = config.get('teamswitch_interval', 0)
        self.max_players = config.get('max_players', 20)
        self.melee_damage = config.get('melee_damage', 100)
        self.max_connections_per_ip = config.get('max_connections_per_ip', 0)
        self.passwords = config.get('passwords', {})
        self.server_prefix = encode(config.get('server_prefix', '[*]'))
        self.time_announcements = config.get('time_announcements',
                                             [1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
                                              30, 60, 120, 180, 240, 300, 600,
                                              900, 1200, 1800, 2400, 3000])
        self.balanced_teams = config.get('balanced_teams', None)
        self.login_retries = config.get('login_retries', 1)

        # voting configuration
        self.default_ban_time = config.get('default_ban_duration', 24 * 60)

        self.speedhack_detect = config.get('speedhack_detect', True)
        if config.get('user_blocks_only', False):
            self.user_blocks = set()
        self.set_god_build = config.get('set_god_build', False)
        self.debug_log = config.get('debug_log', False)
        if self.debug_log:
            pyspades.debug.open_debug_log(
                os.path.join(cfg.config_dir, 'debug.log'))
        ssh = config.get('ssh', {})
        if ssh.get('enabled', False):
            from piqueserver.ssh import RemoteConsole
            self.remote_console = RemoteConsole(self, ssh)
        irc = config.get('irc', {})
        if irc.get('enabled', False):
            from piqueserver.irc import IRCRelay
            self.irc_relay = IRCRelay(self, irc)
        status = config.get('status_server', {})
        if status.get('enabled', False):
            from piqueserver.statusserver import StatusServerFactory
            self.status_server = StatusServerFactory(self, status)
        publish = config.get('ban_publish', {})
        if publish.get('enabled', False):
            from piqueserver.banpublish import PublishServer
            self.ban_publish = PublishServer(self, publish)
        ban_subscribe = config.get('ban_subscribe', {})
        if ban_subscribe.get('enabled', True):
            from piqueserver import bansubscribe
            self.ban_manager = bansubscribe.BanManager(self, ban_subscribe)
        # logfile path relative to config dir if not abs path
        logfile = config.get('logfile', '')
        if not os.path.isabs(logfile):
            logfile = os.path.join(cfg.config_dir, logfile)
        if logfile.strip():  # catches empty filename
            if config.get('rotate_daily', False):
                create_filename_path(logfile)
                logging_file = DailyLogFile(logfile, '.')
            else:
                logging_file = open_create(logfile, 'a')
            log.addObserver(log.FileLogObserver(logging_file).emit)
            log.msg('pyspades server started on %s' % time.strftime('%c'))
        log.startLogging(sys.stdout)  # force twisted logging

        self.start_time = reactor.seconds()
        self.end_calls = []
        # TODO: why is this here?
        create_console(self)

        # check for default password usage
        for group, passwords in self.passwords.items():
            if group in DEFAULT_PASSWORDS:
                for password in passwords:
                    if password in DEFAULT_PASSWORDS[group]:
                        print(("WARNING: FOUND DEFAULT PASSWORD '%s'"
                               " IN GROUP '%s'" % (password, group)))

        for password in self.passwords.get('admin', []):
            if not password:
                self.everyone_is_admin = True

        for user_type, func_names in config.get('rights', {}).items():
            for func_name in func_names:
                commands.add_rights(user_type, func_name)

        port = self.port = config.get('port', 32887)
        ServerProtocol.__init__(self, port, interface)
        self.host.intercept = self.receive_callback
        ret = self.set_map_rotation(config['maps'])
        if not ret:
            print('Invalid map in map rotation (%s), exiting.' % ret.map)
            raise SystemExit

        self.update_format()
        self.tip_frequency = config.get('tip_frequency', 0)
        if self.tips is not None and self.tip_frequency > 0:
            reactor.callLater(self.tip_frequency * 60, self.send_tip)

        self.master = config.get('master', True)
        self.set_master()

        get_external_ip(config.get('network_interface', '').encode()).addCallback(
            self.got_external_ip)

    def got_external_ip(self, ip):
        self.ip = ip
        self.identifier = make_server_identifier(ip, self.port)
        print('Server identifier is %s' % self.identifier)

    def set_time_limit(self, time_limit=None, additive=False):
        advance_call = self.advance_call
        add_time = 0.0
        if advance_call is not None:
            add_time = ((advance_call.getTime() - reactor.seconds()) / 60.0)
            advance_call.cancel()
            self.advance_call = None
        time_limit = time_limit or self.default_time_limit
        if time_limit == False:
            for call in self.end_calls[:]:
                call.set(None)
            return

        if additive:
            time_limit = min(time_limit + add_time, self.default_time_limit)

        seconds = time_limit * 60.0
        self.advance_call = reactor.callLater(seconds, self._time_up)

        for call in self.end_calls[:]:
            call.set(seconds)

        if self.time_announce_schedule is not None:
            self.time_announce_schedule.reset()
        self.time_announce_schedule = Scheduler(self)
        for seconds in self.time_announcements:
            self.time_announce_schedule.call_end(seconds,
                                                 self._next_time_announce)

        return time_limit

    def _next_time_announce(self):
        remaining = self.advance_call.getTime() - reactor.seconds()
        if remaining < 60.001:
            if remaining < 10.001:
                self.send_chat('%s...' % int(round(remaining)))
            else:
                self.send_chat('%s seconds remaining.' % int(round(remaining)))
        else:
            self.send_chat('%s minutes remaining.' %
                           int(round(remaining / 60)))

    def _time_up(self):
        self.advance_call = None
        self.advance_rotation('Time up!')

    def advance_rotation(self, message=None):
        self.set_time_limit(False)
        if self.planned_map is None:
            self.planned_map = next(self.map_rotator)
        planned_map = self.planned_map
        self.planned_map = None
        self.on_advance(map)
        if message is None:
            self.set_map_name(planned_map)
        else:
            self.send_chat(
                '%s Next map: %s.' % (message, planned_map.full_name),
                irc=True)
            reactor.callLater(10, self.set_map_name, planned_map)

    def get_mode_name(self):
        return self.game_mode_name

    def set_map_name(self, rot_info):
        try:
            map_info = self.get_map(rot_info)
        except MapNotFound as e:
            return e
        if self.map_info:
            self.on_map_leave()
        self.map_info = map_info
        self.max_score = self.map_info.cap_limit or self.default_cap_limit
        self.set_map(self.map_info.data)
        self.set_time_limit(self.map_info.time_limit)
        self.update_format()
        return True

    def get_map(self, rot_info):
        return Map(rot_info, os.path.join(cfg.config_dir, 'maps'))

    def set_map_rotation(self, maps, now=True):
        try:
            maps = check_rotation(maps, os.path.join(cfg.config_dir, 'maps'))
        except MapNotFound as e:
            return e
        self.maps = maps
        self.map_rotator = self.map_rotator_type(maps)
        if now:
            self.advance_rotation()
        return True

    def get_map_rotation(self):
        return [map_item.full_name for map_item in self.maps]

    def is_indestructable(self, x, y, z):
        if self.user_blocks is not None:
            if (x, y, z) not in self.user_blocks:
                return True
        if self.god_blocks is not None:
            if (x, y, z) in self.god_blocks:  # pylint: disable=unsupported-membership-test
                return True
        map_is_indestructable = self.map_info.is_indestructable
        if map_is_indestructable is not None:
            if map_is_indestructable(self, x, y, z) == True:
                return True
        return False

    def update_format(self):
        """
        Called when the map (or other variables) have been updated
        """
        config = self.config
        default_name = 'pyspades server %s' % random.randrange(0, 2000)
        self.name = self.format(config.get('name', default_name))
        self.motd = self.format_lines(config.get('motd', None))
        self.help = self.format_lines(config.get('help', None))
        self.tips = self.format_lines(config.get('tips', None))
        self.rules = self.format_lines(config.get('rules', None))
        if self.master_connection is not None:
            self.master_connection.send_server()

    def format(self, value, extra=None):
        if extra is None:
            extra = {}

        map_info = self.map_info
        format_dict = {
            'map_name': map_info.name,
            'map_author': map_info.author,
            'map_description': map_info.description,
            'game_mode': self.get_mode_name()
        }
        format_dict.update(extra)
        return value % format_dict

    def format_lines(self, value):
        if value is None:
            return
        lines = []
        extra = {'server_name': self.name}
        for line in value:
            lines.append(self.format(line, extra))
        return lines

    def got_master_connection(self, client):
        print('Master connection established.')
        ServerProtocol.got_master_connection(self, client)

    def master_disconnected(self, client=None):
        ServerProtocol.master_disconnected(self, client)
        if self.master and self.master_reconnect_call is None:
            if client:
                message = 'Master connection could not be established'
            else:
                message = 'Master connection lost'
            print('%s, reconnecting in 60 seconds...' % message)
            self.master_reconnect_call = reactor.callLater(60,
                                                           self.reconnect_master)

    def reconnect_master(self):
        self.master_reconnect_call = None
        self.set_master()

    def set_master_state(self, value):
        if value == self.master:
            return
        self.master = value
        has_connection = self.master_connection is not None
        has_reconnect = self.master_reconnect_call is not None
        if value:
            if not has_connection and not has_reconnect:
                self.set_master()
        else:
            if has_reconnect:
                self.master_reconnect_call.cancel()
                self.master_reconnect_call = None
            if has_connection:
                self.master_connection.disconnect()

    def add_ban(self, ip, reason, duration, name=None):
        """
        Ban an ip with an optional reason and duration in minutes. If duration
        is None, ban is permanent.
        """
        network = get_network(ip)
        for connection in self.connections.values():
            if get_network(connection.address[0]) in network:
                name = connection.name
                connection.kick(silent=True)
        if duration:
            duration = reactor.seconds() + duration * 60
        else:
            duration = None
        self.bans[ip] = (name or '(unknown)', reason, duration)
        self.save_bans()

    def remove_ban(self, ip):
        results = self.bans.remove(ip)
        print('Removing ban:', ip, results)
        self.save_bans()

    def undo_last_ban(self):
        result = self.bans.pop()
        self.save_bans()
        return result

    def save_bans(self):
        json.dump(self.bans.make_list(), open_create(
            os.path.join(cfg.config_dir, 'bans.txt'), 'wb'))
        if self.ban_publish is not None:
            self.ban_publish.update()

    def receive_callback(self, address, data):
        if data == b'HELLO':
            print("test")
            self.host.socket.send(address, b'HI')
            return 1
        if address.host in self.hard_bans:
            return 1

    def data_received(self, peer, packet):
        ip = peer.address.host
        current_time = reactor.seconds()
        try:
            ServerProtocol.data_received(self, peer, packet)
        except (NoDataLeft, ValueError):
            import traceback
            traceback.print_exc()
            print(
                'IP %s was hardbanned for invalid data or possibly DDoS.' % ip)
            self.hard_bans.add(ip)
            return
        dt = reactor.seconds() - current_time
        if dt > 1.0:
            print('(warning: processing %r from %s took %s)' % (
                packet.data, ip, dt))

    def irc_say(self, msg, me=False):
        if self.irc_relay:
            if me:
                self.irc_relay.me(msg, filter=True)
            else:
                self.irc_relay.send(msg, filter=True)

    def send_tip(self):
        line = self.tips[random.randrange(len(self.tips))]
        self.send_chat(line)
        reactor.callLater(self.tip_frequency * 60, self.send_tip)

    # pylint: disable=arguments-differ
    def broadcast_chat(self, value, global_message=True, sender=None,
                  team=None, irc=False):
        """
        Send a chat message to many users
        """
        if irc:
            self.irc_say('* %s' % value)
        ServerProtocol.send_chat(self, value, global_message, sender, team)

    # backwards compatability
    send_chat = broadcast_chat

    # log high CPU usage

    def update_world(self):
        last_time = self.last_time
        current_time = reactor.seconds()
        if last_time is not None:
            dt = current_time - last_time
            if dt > 1.0:
                print('(warning: high CPU usage detected - %s)' % dt)
        self.last_time = current_time
        ServerProtocol.update_world(self)
        time_taken = reactor.seconds() - current_time
        if time_taken > 1.0:
            print('World update iteration took %s, objects: %s' % (time_taken,
                                                                   self.world.objects))

    # events

    def on_map_change(self, the_map):
        self.set_fog_color(
            getattr(self.map_info.info, 'fog', self.default_fog)
        )

        map_on_map_change = self.map_info.on_map_change
        if map_on_map_change is not None:
            map_on_map_change(self, the_map)

    def on_map_leave(self):
        map_on_map_leave = self.map_info.on_map_leave
        if map_on_map_leave is not None:
            map_on_map_leave(self)

    def on_game_end(self):
        if self.advance_on_win <= 0:
            self.irc_say('Round ended!', me=True)
        elif next(self.win_count) % self.advance_on_win == 0:
            self.advance_rotation('Game finished!')

    def on_advance(self, map_name):
        pass

    def on_ban_attempt(self, connection, reason, duration):
        return True

    def on_ban(self, connection, reason, duration):
        pass

    # voting

    def cancel_vote(self, connection=None):
        return 'No vote in progress.'

    # useful twisted wrappers

    def listenTCP(self, *arg, **kw):
        return reactor.listenTCP(*arg,
                                 interface=self.config.get('network_interface', ''), **kw)

    def connectTCP(self, *arg, **kw):
        return reactor.connectTCP(*arg,
                                  bindAddress=(self.config.get('network_interface', ''), 0), **kw)

    def getPage(self, *arg, **kw):
        return getPage(*arg,
                       bindAddress=(self.config.get('network_interface', ''), 0), **kw)

    # before-end calls

    def call_end(self, delay, func, *arg, **kw):
        call = EndCall(self, delay, func, *arg, **kw)
        call.set(self.get_advance_time())
        return call

    def get_advance_time(self):
        if not self.advance_call:
            return None
        return self.advance_call.getTime() - self.advance_call.seconds()
Ejemplo n.º 4
0
class FeatureProtocol(ServerProtocol):
    connection_class = FeatureConnection
    bans = None
    ban_publish = None
    ban_manager = None
    everyone_is_admin = False
    player_memory = None
    irc_relay = None
    balanced_teams = None
    timestamps = None
    building = True
    killing = True
    global_chat = True
    remote_console = None
    debug_log = None
    advance_call = None
    master_reconnect_call = None
    master = False
    ip = None
    identifier = None

    planned_map = None

    map_info = None
    spawns = None
    user_blocks = None
    god_blocks = None

    last_time = None
    interface = None

    team_class = FeatureTeam

    game_mode = None  # default to None so we can check
    time_announce_schedule = None

    server_version = '{} - {}'.format(sys.platform, piqueserver.__version__)

    default_fog = (128, 232, 255)

    def __init__(self, interface: bytes, config_dict: Dict[str, Any]) -> None:
        # logfile path relative to config dir if not abs path
        log_filename = logfile.get()
        if log_filename.strip():  # catches empty filename
            if not os.path.isabs(log_filename):
                log_filename = os.path.join(config.config_dir, log_filename)
            ensure_dir_exists(log_filename)
            if logging_rotate_daily.get():
                logging_file = DailyLogFile(log_filename, '.')
            else:
                logging_file = open(log_filename, 'a')
            globalLogPublisher.addObserver(textFileLogObserver(logging_file))
            globalLogPublisher.addObserver(textFileLogObserver(sys.stderr))
            log.info('piqueserver started on %s' % time.strftime('%c'))

        self.config = config_dict
        if random_rotation:
            self.map_rotator_type = random_choice_cycle
        else:
            self.map_rotator_type = itertools.cycle  # pylint: disable=redefined-variable-type
        self.default_time_limit = default_time_limit.get()
        self.default_cap_limit = cap_limit.get()
        self.advance_on_win = int(advance_on_win.get())
        self.win_count = itertools.count(1)
        self.bans = NetworkDict()

        # attempt to load a saved bans list
        try:
            with open(os.path.join(config.config_dir, bans_file.get()),
                      'r') as f:
                self.bans.read_list(json.load(f))
            log.debug("loaded {count} bans", count=len(self.bans))
        except FileNotFoundError:
            log.debug("skip loading bans: file unavailable",
                      count=len(self.bans))
        except IOError as e:
            log.error('Could not read bans.txt: {}'.format(e))
        except ValueError as e:
            log.error('Could not parse bans.txt: {}'.format(e))

        self.hard_bans = set()  # possible DDoS'ers are added here
        self.player_memory = deque(maxlen=100)
        if len(self.name) > MAX_SERVER_NAME_SIZE:
            log.warn('(server name too long; it will be truncated to "%s")' %
                     (self.name[:MAX_SERVER_NAME_SIZE]))
        self.respawn_time = respawn_time_option.get()
        self.respawn_waves = respawn_waves.get()
        if game_mode.get() == 'ctf':
            self.game_mode = CTF_MODE
        elif game_mode.get() == 'tc':
            self.game_mode = TC_MODE
        elif self.game_mode is None:
            raise NotImplementedError('invalid game mode: %s' % game_mode)
        self.game_mode_name = game_mode.get().split('.')[-1]
        self.team1_name = team1_name.get()
        self.team2_name = team2_name.get()
        self.team1_color = tuple(team1_color.get())
        self.team2_color = tuple(team2_color.get())
        self.friendly_fire = friendly_fire.get()
        self.friendly_fire_on_grief = friendly_fire_on_grief.get()
        self.friendly_fire_time = grief_friendly_fire_time.get()
        self.spade_teamkills_on_grief = spade_teamkills_on_grief.get()
        self.fall_damage = fall_damage.get()
        self.teamswitch_interval = teamswitch_interval.get()
        self.teamswitch_allowed = teamswitch_allowed.get()
        self.max_players = max_players.get()
        self.melee_damage = melee_damage.get()
        self.max_connections_per_ip = max_connections_per_ip.get()
        self.passwords = passwords.get()
        self.server_prefix = server_prefix.get()
        self.time_announcements = time_announcements.get()
        self.balanced_teams = balanced_teams.get()
        self.login_retries = login_retries.get()

        # voting configuration
        self.default_ban_time = default_ban_duration.get()

        self.speedhack_detect = speedhack_detect.get()
        if user_blocks_only.get():
            self.user_blocks = set()
        self.set_god_build = set_god_build.get()
        self.debug_log = debug_log_enabled.get()
        if self.debug_log:
            # TODO: make this configurable
            pyspades.debug.open_debug_log(
                os.path.join(config.config_dir, 'debug.log'))
        if ssh_enabled.get():
            from piqueserver.ssh import RemoteConsole
            self.remote_console = RemoteConsole(self)
        irc = irc_options.get()
        if irc.get('enabled', False):
            from piqueserver.irc import IRCRelay
            self.irc_relay = IRCRelay(self, irc)
        if status_server_enabled.get():
            from piqueserver.statusserver import StatusServerFactory
            self.status_server = StatusServerFactory(self)
        if ban_publish.get():
            from piqueserver.banpublish import PublishServer
            self.ban_publish = PublishServer(self, ban_publish_port.get())
        if bans_urls.get():
            from piqueserver import bansubscribe
            self.ban_manager = bansubscribe.BanManager(self)
        self.start_time = reactor.seconds()
        self.end_calls = []
        # TODO: why is this here?
        create_console(self)

        for user_type, func_names in rights.get().items():
            for func_name in func_names:
                commands.add_rights(user_type, func_name)

        port = self.port = port_option.get()
        ServerProtocol.__init__(self, port, interface)
        self.host.intercept = self.receive_callback
        try:
            self.set_map_rotation(self.config['rotation'])
        except MapNotFound as e:
            log.critical('Invalid map in map rotation (%s), exiting.' % e.map)
            raise SystemExit

        self.update_format()
        self.tip_frequency = tip_frequency.get()
        if self.tips is not None and self.tip_frequency > 0:
            reactor.callLater(self.tip_frequency * 60, self.send_tip)

        self.master = register_master_option.get()
        self.set_master()

        self.http_agent = web_client.Agent(reactor)

        ip_getter = ip_getter_option.get()
        if ip_getter:
            self.get_external_ip(ip_getter)

    @inlineCallbacks
    def get_external_ip(self, ip_getter: str) -> Iterator[Deferred]:
        log.info(
            'Retrieving external IP from {!r} to generate server identifier.'.
            format(ip_getter))
        try:
            ip = yield self.getPage(ip_getter)
            ip = IPv4Address(ip.strip())
        except AddressValueError as e:
            log.warn('External IP getter service returned invalid data.\n'
                     'Please check the "ip_getter" setting in your config.')
            return
        except Exception as e:
            log.warn("Getting external IP failed: {reason}", reason=e)
            return

        self.ip = ip
        self.identifier = make_server_identifier(ip, self.port)
        log.info('Server public ip address: {}:{}'.format(ip, self.port))
        log.info('Public aos identifier: {}'.format(self.identifier))

    def set_time_limit(self,
                       time_limit: Optional[bool] = None,
                       additive: bool = False) -> int:
        advance_call = self.advance_call
        add_time = 0.0
        if advance_call is not None:
            add_time = ((advance_call.getTime() - reactor.seconds()) / 60.0)
            advance_call.cancel()
            self.advance_call = None
        time_limit = time_limit or self.default_time_limit
        if not time_limit:
            for call in self.end_calls[:]:
                call.set(None)
            return

        if additive:
            time_limit = min(time_limit + add_time, self.default_time_limit)

        seconds = time_limit * 60.0
        self.advance_call = reactor.callLater(seconds, self._time_up)

        for call in self.end_calls[:]:
            call.set(seconds)

        if self.time_announce_schedule is not None:
            self.time_announce_schedule.reset()
        self.time_announce_schedule = Scheduler(self)
        for seconds in self.time_announcements:
            self.time_announce_schedule.call_end(seconds,
                                                 self._next_time_announce)

        return time_limit

    def _next_time_announce(self):
        remaining = self.advance_call.getTime() - reactor.seconds()
        if remaining < 60.001:
            if remaining < 10.001:
                self.send_chat('%s...' % int(round(remaining)))
            else:
                self.send_chat('%s seconds remaining.' % int(round(remaining)))
        else:
            self.send_chat('%s minutes remaining.' %
                           int(round(remaining / 60)))

    def _time_up(self):
        self.advance_call = None
        self.advance_rotation('Time up!')

    def advance_rotation(self, message: None = None) -> None:
        """
        Advances to the next map in the rotation. If message is provided
        it will send it to the chat, waits for 10 seconds and then advances.
        """
        self.set_time_limit(False)
        if self.planned_map is None:
            self.planned_map = next(self.map_rotator)
        planned_map = self.planned_map
        self.planned_map = None
        self.on_advance(planned_map)
        if message is None:
            self.set_map_name(planned_map)
        else:
            self.send_chat('{} Next map: {}.'.format(message,
                                                     planned_map.full_name),
                           irc=True)
            reactor.callLater(10, self.set_map_name, planned_map)

    def get_mode_name(self) -> str:
        return self.game_mode_name

    def set_map_name(self, rot_info: RotationInfo) -> None:
        """
        Sets the map by its name.
        """
        map_info = self.get_map(rot_info)
        if self.map_info:
            self.on_map_leave()
        self.map_info = map_info
        self.max_score = self.map_info.cap_limit or self.default_cap_limit
        self.set_map(self.map_info.data)
        self.set_time_limit(self.map_info.time_limit)
        self.update_format()

    def set_server_name(self, name: str) -> None:
        name_option.set(name)
        self.update_format()

    def get_map(self, rot_info: RotationInfo) -> Map:
        """
        Creates and returns a Map object from rotation info
        """
        return Map(rot_info, os.path.join(config.config_dir, 'maps'))

    def set_map_rotation(self, maps: List[str], now: bool = True) -> None:
        """
        Over-writes the current map rotation with provided one.
        And advances immediately with the new rotation by default.
        """
        maps = check_rotation(maps, os.path.join(config.config_dir, 'maps'))
        self.maps = maps
        self.map_rotator = self.map_rotator_type(maps)
        if now:
            self.advance_rotation()

    def get_map_rotation(self):
        return [map_item.full_name for map_item in self.maps]

    def is_indestructable(self, x: int, y: int, z: int) -> bool:
        if self.user_blocks is not None:
            if (x, y, z) not in self.user_blocks:
                return True
        if self.god_blocks is not None:
            if (x, y, z) in self.god_blocks:  # pylint: disable=unsupported-membership-test
                return True
        map_is_indestructable = self.map_info.is_indestructable
        if map_is_indestructable is not None:
            if map_is_indestructable(self, x, y, z):
                return True
        return False

    def update_format(self) -> None:
        """
        Called when the map (or other variables) have been updated
        """
        self.name = self.format(name_option.get())
        self.motd = self.format_lines(motd_option.get())
        self.help = self.format_lines(help_option.get())
        self.tips = self.format_lines(tips_option.get())
        self.rules = self.format_lines(rules_option.get())
        if self.master_connection is not None:
            self.master_connection.send_server()

    def format(self,
               value: str,
               extra: Optional[Dict[str, str]] = None) -> str:
        if extra is None:
            extra = {}

        map_info = self.map_info
        format_dict = {
            'map_name': map_info.name,
            'map_author': map_info.author,
            'map_description': map_info.description,
            'game_mode': self.get_mode_name()
        }
        format_dict.update(extra)
        return value % format_dict

    def format_lines(self, value: List[str]) -> List[str]:
        if value is None:
            return
        lines = []
        extra = {'server_name': self.name}
        for line in value:
            lines.append(self.format(line, extra))
        return lines

    def got_master_connection(self, client):
        log.info('Master connection established.')
        ServerProtocol.got_master_connection(self, client)

    def master_disconnected(self, client=None):
        ServerProtocol.master_disconnected(self, client)
        if self.master and self.master_reconnect_call is None:
            if client:
                message = 'Master connection could not be established'
            else:
                message = 'Master connection lost'
            log.info('%s, reconnecting in 60 seconds...' % message)
            self.master_reconnect_call = reactor.callLater(
                60, self.reconnect_master)

    def reconnect_master(self):
        self.master_reconnect_call = None
        self.set_master()

    def set_master_state(self, value):
        if value == self.master:
            return
        self.master = value
        has_connection = self.master_connection is not None
        has_reconnect = self.master_reconnect_call is not None
        if value:
            if not has_connection and not has_reconnect:
                self.set_master()
        else:
            if has_reconnect:
                self.master_reconnect_call.cancel()
                self.master_reconnect_call = None
            if has_connection:
                self.master_connection.disconnect()

    def add_ban(self, ip, reason, duration, name=None):
        """
        Ban an ip with an optional reason and duration in minutes. If duration
        is None, ban is permanent.
        """
        network = ip_network(str(ip), strict=False)
        for connection in list(self.connections.values()):
            if ip_address(connection.address[0]) in network:
                name = connection.name
                connection.kick(silent=True)
        if duration:
            duration = reactor.seconds() + duration * 60
        else:
            duration = None
        self.bans[ip] = (name or '(unknown)', reason, duration)
        self.save_bans()

    def remove_ban(self, ip):
        results = self.bans.remove(ip)
        log.info('Removing ban: {ip} {results}', ip=ip, results=results)
        self.save_bans()

    def undo_last_ban(self):
        result = self.bans.pop()
        self.save_bans()
        return result

    def save_bans(self):
        ban_file = os.path.join(config.config_dir, 'bans.txt')
        ensure_dir_exists(ban_file)

        start_time = reactor.seconds()
        with open(ban_file, 'w') as f:
            json.dump(self.bans.make_list(), f, indent=2)
        log.debug("saving {count} bans took {time} seconds",
                  count=len(self.bans),
                  time=reactor.seconds() - start_time)

        if self.ban_publish is not None:
            self.ban_publish.update()

    def receive_callback(self, address: Address, data: bytes) -> None:
        """This hook recieves the raw UDP data before it is processed by enet"""

        # reply to ASCII HELLO messages with HI so that clients can measure the
        # connection latency
        if data == b'HELLO':
            self.host.socket.send(address, b'HI')
            return 1

        # This drop the connection of any ip in hard_bans
        if address.host in self.hard_bans:
            return 1

    def data_received(self, peer: Peer, packet: Packet) -> None:
        ip = peer.address.host
        current_time = reactor.seconds()
        try:
            ServerProtocol.data_received(self, peer, packet)
        except (NoDataLeft, ValueError):
            import traceback
            traceback.print_exc()
            log.info(
                'IP %s was hardbanned for invalid data or possibly DDoS.' % ip)
            self.hard_bans.add(ip)
            return
        dt = reactor.seconds() - current_time
        if dt > 1.0:
            log.warn('processing {!r} from {} took {}'.format(
                packet.data, ip, dt))

    def irc_say(self, msg: str, me: bool = False) -> None:
        if self.irc_relay:
            if me:
                self.irc_relay.me(msg, do_filter=True)
            else:
                self.irc_relay.send(msg, do_filter=True)

    def send_tip(self):
        line = self.tips[random.randrange(len(self.tips))]
        self.send_chat(line)
        reactor.callLater(self.tip_frequency * 60, self.send_tip)

    # pylint: disable=arguments-differ
    def broadcast_chat(self,
                       value,
                       global_message=True,
                       sender=None,
                       team=None,
                       irc=False):
        """
        Send a chat message to many users
        """
        if irc:
            self.irc_say('* %s' % value)
        ServerProtocol.send_chat(self, value, global_message, sender, team)

    # backwards compatability
    send_chat = broadcast_chat

    # log high CPU usage

    def update_world(self):
        last_time = self.last_time
        current_time = reactor.seconds()
        if last_time is not None:
            dt = current_time - last_time
            if dt > 1.0:
                log.warn('high CPU usage detected - %s' % dt)
        self.last_time = current_time
        ServerProtocol.update_world(self)
        time_taken = reactor.seconds() - current_time
        if time_taken > 1.0:
            log.warn('World update iteration took %s, objects: %s' %
                     (time_taken, self.world.objects))

    # events

    def on_map_change(self, the_map: VXLData) -> None:
        self.set_fog_color(getattr(self.map_info.info, 'fog',
                                   self.default_fog))

        map_on_map_change = self.map_info.on_map_change
        if map_on_map_change is not None:
            map_on_map_change(self, the_map)

    def on_map_leave(self):
        map_on_map_leave = self.map_info.on_map_leave
        if map_on_map_leave is not None:
            map_on_map_leave(self)

    def on_game_end(self):
        if self.advance_on_win <= 0:
            self.irc_say('Round ended!', me=True)
        elif next(self.win_count) % self.advance_on_win == 0:
            self.advance_rotation('Game finished!')

    def on_advance(self, map_name: str) -> None:
        pass

    def on_ban_attempt(self, connection, reason, duration):
        return True

    def on_ban(self, connection, reason, duration):
        pass

    # voting

    def cancel_vote(self, connection=None):
        return 'No vote in progress.'

    # useful twisted wrappers

    def listenTCP(self, *arg, **kw) -> Port:
        return reactor.listenTCP(*arg, interface=network_interface.get(), **kw)

    def connectTCP(self, *arg, **kw):
        return reactor.connectTCP(*arg,
                                  bindAddress=(network_interface.get(), 0),
                                  **kw)

    @inlineCallbacks
    def getPage(self, url: str) -> Iterator[Deferred]:
        resp = yield self.http_agent.request(b'GET', url.encode())
        body = yield web_client.readBody(resp)
        return body.decode()

    # before-end calls

    def call_end(self, delay: int, func: Callable, *arg, **kw) -> EndCall:
        call = EndCall(self, delay, func, *arg, **kw)
        call.set(self.get_advance_time())
        return call

    def get_advance_time(self) -> float:
        if not self.advance_call:
            return None
        return self.advance_call.getTime() - self.advance_call.seconds()
Ejemplo n.º 5
0
class FeatureProtocol(ServerProtocol):
    connection_class = FeatureConnection
    bans = None
    ban_publish = None
    ban_manager = None
    everyone_is_admin = False
    player_memory = None
    irc_relay = None
    balanced_teams = None
    timestamps = None
    building = True
    killing = True
    global_chat = True
    remote_console = None
    debug_log = None
    advance_call = None
    master_reconnect_call = None
    master = False
    ip = None
    identifier = None

    planned_map = None

    map_info = None
    spawns = None
    user_blocks = None
    god_blocks = None

    last_time = None
    interface = None

    team_class = FeatureTeam

    game_mode = None  # default to None so we can check
    time_announce_schedule = None

    server_version = cfg.server_version

    def __init__(self, interface, config):
        self.config = config
        if config.get('random_rotation', False):
            self.map_rotator_type = random_choice_cycle
        else:
            self.map_rotator_type = itertools.cycle # pylint: disable=redefined-variable-type
        self.default_time_limit = config.get('default_time_limit', 20.0)
        self.default_cap_limit = config.get('cap_limit', 10.0)
        self.advance_on_win = int(config.get('advance_on_win', False))
        self.win_count = itertools.count(1)
        self.bans = NetworkDict()
        # TODO: check if this is actually working and not silently failing
        try:
            self.bans.read_list(json.load(open(os.path.join(cfg.config_dir, 'bans.txt'), 'rb')))
        except IOError:
            pass
        self.hard_bans = set()  # possible DDoS'ers are added here
        self.player_memory = deque(maxlen=100)
        self.config = config
        if len(self.name) > MAX_SERVER_NAME_SIZE:
            print('(server name too long; it will be truncated to "%s")' % (
                self.name[:MAX_SERVER_NAME_SIZE]))
        self.respawn_time = config.get('respawn_time', 8)
        self.respawn_waves = config.get('respawn_waves', False)
        game_mode = config.get('game_mode', 'ctf')
        if game_mode == 'ctf':
            self.game_mode = CTF_MODE
        elif game_mode == 'tc':
            self.game_mode = TC_MODE
        elif self.game_mode is None:
            raise NotImplementedError('invalid game mode: %s' % game_mode)
        self.game_mode_name = game_mode
        team1 = config.get('team1', {})
        team2 = config.get('team2', {})
        self.team1_name = team1.get('name', 'Blue')
        self.team2_name = team2.get('name', 'Green')
        self.team1_color = tuple(team1.get('color', (0, 0, 196)))
        self.team2_color = tuple(team2.get('color', (0, 196, 0)))
        self.friendly_fire = config.get('friendly_fire', True)
        self.friendly_fire_time = config.get('grief_friendly_fire_time', 2.0)
        self.spade_teamkills_on_grief = config.get('spade_teamkills_on_grief',
                                                   False)
        self.fall_damage = config.get('fall_damage', True)
        self.teamswitch_interval = config.get('teamswitch_interval', 0)
        self.max_players = config.get('max_players', 20)
        self.melee_damage = config.get('melee_damage', 100)
        self.max_connections_per_ip = config.get('max_connections_per_ip', 0)
        self.passwords = config.get('passwords', {})
        self.server_prefix = encode(config.get('server_prefix', '[*]'))
        self.time_announcements = config.get('time_announcements',
                                             [1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
                                              30, 60, 120, 180, 240, 300, 600,
                                              900, 1200, 1800, 2400, 3000])
        self.balanced_teams = config.get('balanced_teams', None)
        self.login_retries = config.get('login_retries', 1)

        # voting configuration
        self.default_ban_time = config.get('default_ban_duration', 24 * 60)

        self.speedhack_detect = config.get('speedhack_detect', True)
        if config.get('user_blocks_only', False):
            self.user_blocks = set()
        self.set_god_build = config.get('set_god_build', False)
        self.debug_log = config.get('debug_log', False)
        if self.debug_log:
            pyspades.debug.open_debug_log(os.path.join(cfg.config_dir, 'debug.log'))
        ssh = config.get('ssh', {})
        if ssh.get('enabled', False):
            from piqueserver.ssh import RemoteConsole
            self.remote_console = RemoteConsole(self, ssh)
        irc = config.get('irc', {})
        if irc.get('enabled', False):
            from piqueserver.irc import IRCRelay
            self.irc_relay = IRCRelay(self, irc)
        status = config.get('status_server', {})
        if status.get('enabled', False):
            from piqueserver.statusserver import StatusServerFactory
            self.status_server = StatusServerFactory(self, status)
        publish = config.get('ban_publish', {})
        if publish.get('enabled', False):
            from piqueserver.banpublish import PublishServer
            self.ban_publish = PublishServer(self, publish)
        ban_subscribe = config.get('ban_subscribe', {})
        if ban_subscribe.get('enabled', True):
            from piqueserver import bansubscribe
            self.ban_manager = bansubscribe.BanManager(self, ban_subscribe)
        # logfile path relative to config dir if not abs path
        logfile = config.get('logfile', '')
        if not os.path.isabs(logfile):
            logfile = os.path.join(cfg.config_dir, logfile)
        if logfile.strip():  # catches empty filename
            if config.get('rotate_daily', False):
                create_filename_path(logfile)
                logging_file = DailyLogFile(logfile, '.')
            else:
                logging_file = open_create(logfile, 'a')
            log.addObserver(log.FileLogObserver(logging_file).emit)
            log.msg('pyspades server started on %s' % time.strftime('%c'))
        log.startLogging(sys.stdout)  # force twisted logging

        self.start_time = reactor.seconds()
        self.end_calls = []
        # TODO: why is this here?
        self.console = create_console(self) # pylint: disable=assignment-from-no-return

        # check for default password usage
        for group, passwords in self.passwords.iteritems():
            if group in DEFAULT_PASSWORDS:
                for password in passwords:
                    if password in DEFAULT_PASSWORDS[group]:
                        print(("WARNING: FOUND DEFAULT PASSWORD '%s'"
                               " IN GROUP '%s'" % (password, group)))

        for password in self.passwords.get('admin', []):
            if not password:
                self.everyone_is_admin = True

        for user_type, func_names in config.get('rights', {}).iteritems():
            for func_name in func_names:
                commands.add_rights(func_name, user_type)

        port = self.port = config.get('port', 32887)
        ServerProtocol.__init__(self, port, interface)
        self.host.intercept = self.receive_callback
        ret = self.set_map_rotation(config['maps'])
        if not ret:
            print('Invalid map in map rotation (%s), exiting.' % ret.map)
            raise SystemExit

        self.update_format()
        self.tip_frequency = config.get('tip_frequency', 0)
        if self.tips is not None and self.tip_frequency > 0:
            reactor.callLater(self.tip_frequency * 60, self.send_tip)

        self.master = config.get('master', True)
        self.set_master()

        get_external_ip(config.get('network_interface', '')).addCallback(
            self.got_external_ip)

    def got_external_ip(self, ip):
        self.ip = ip
        self.identifier = make_server_identifier(ip, self.port)
        print('Server identifier is %s' % self.identifier)

    def set_time_limit(self, time_limit=None, additive=False):
        advance_call = self.advance_call
        add_time = 0.0
        if advance_call is not None:
            add_time = ((advance_call.getTime() - reactor.seconds()) / 60.0)
            advance_call.cancel()
            self.advance_call = None
        time_limit = time_limit or self.default_time_limit
        if time_limit == False:
            for call in self.end_calls[:]:
                call.set(None)
            return

        if additive:
            time_limit = min(time_limit + add_time, self.default_time_limit)

        seconds = time_limit * 60.0
        self.advance_call = reactor.callLater(seconds, self._time_up)

        for call in self.end_calls[:]:
            call.set(seconds)

        if self.time_announce_schedule is not None:
            self.time_announce_schedule.reset()
        self.time_announce_schedule = Scheduler(self)
        for seconds in self.time_announcements:
            self.time_announce_schedule.call_end(seconds,
                                                 self._next_time_announce)

        return time_limit

    def _next_time_announce(self):
        remaining = self.advance_call.getTime() - reactor.seconds()
        if remaining < 60.001:
            if remaining < 10.001:
                self.send_chat('%s...' % int(round(remaining)))
            else:
                self.send_chat('%s seconds remaining.' % int(round(remaining)))
        else:
            self.send_chat('%s minutes remaining.' % int(round(remaining / 60)))

    def _time_up(self):
        self.advance_call = None
        self.advance_rotation('Time up!')

    def advance_rotation(self, message=None):
        self.set_time_limit(False)
        if self.planned_map is None:
            self.planned_map = self.map_rotator.next()
        planned_map = self.planned_map
        self.planned_map = None
        self.on_advance(map)
        if message is None:
            self.set_map_name(planned_map)
        else:
            self.send_chat('%s Next map: %s.' % (message, planned_map.full_name),
                           irc=True)
            reactor.callLater(10, self.set_map_name, planned_map)

    def get_mode_name(self):
        return self.game_mode_name

    def set_map_name(self, rot_info):
        try:
            map_info = self.get_map(rot_info)
        except MapNotFound, e:
            return e
        if self.map_info:
            self.on_map_leave()
        self.map_info = map_info
        self.max_score = self.map_info.cap_limit or self.default_cap_limit
        self.set_map(self.map_info.data)
        self.set_time_limit(self.map_info.time_limit)
        self.update_format()
        return True