├── .gitignore ├── Minecraft_Server.jar ├── README.md ├── banned-ips.json ├── banned-players.json ├── bukkit.yml ├── commands.yml ├── eula.txt ├── help.yml ├── logging.json ├── ops.json ├── permissions.yml ├── plugins ├── RaspberryJuice │ └── config.yml └── raspberryjuice-1.8tc.jar ├── server.properties ├── superops.txt ├── usercache.json ├── whitelist.json ├── wrapper-data └── json │ ├── permissions.json │ ├── usercache.json │ ├── web.pkl │ └── wrapper.pkl ├── wrapper.properties.json └── wrapper ├── __init__.py ├── __main__.py ├── api ├── __init__.py ├── backups.py ├── base.py ├── entity.py ├── helpers.py ├── minecraft.py ├── player.py ├── world.py └── wrapperconfig.py ├── core ├── __init__.py ├── backups.py ├── buildinfo.py ├── commands.py ├── config.py ├── consoleuser.py ├── entities.py ├── events.py ├── exceptions.py ├── irc.py ├── mcserver.py ├── mcuuid.py ├── nbt.py ├── permissions.py ├── plugins.py ├── scripts.py ├── storage.py └── wrapper.py ├── management ├── __init__.py ├── dashboard.py ├── html │ ├── admin.html │ ├── css │ │ ├── admin.css │ │ ├── bootstrap-theme.css │ │ ├── bootstrap-theme.css.map │ │ ├── bootstrap-theme.min.css │ │ ├── bootstrap.css │ │ ├── bootstrap.css.map │ │ ├── bootstrap.min.css │ │ ├── index.html │ │ └── signin.css │ ├── favicon.ico │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.svg │ │ ├── glyphicons-halflings-regular.ttf │ │ └── glyphicons-halflings-regular.woff │ ├── index.html │ ├── js │ │ ├── bootstrap.js │ │ ├── bootstrap.min.js │ │ ├── docs.min.js │ │ ├── excanvas.min.js │ │ ├── flot-data.js │ │ ├── ie10-viewport-bug-workaround.js │ │ ├── jquery.flot.js │ │ ├── jquery.flot.pie.js │ │ ├── jquery.flot.resize.js │ │ ├── jquery.js │ │ └── offcanvas.js │ ├── login.html │ └── requests.js └── web.py ├── proxy ├── __init__.py ├── base.py ├── clientconnection.py ├── constants.py ├── mcpackets_cb.py ├── mcpackets_sb.py ├── packet.py ├── parse_cb.py ├── parse_sb.py └── serverconnection.py ├── test.py └── utils ├── __init__.py ├── encryption.py ├── entities.py ├── items.py ├── log.py └── readkey.py /.gitignore: -------------------------------------------------------------------------------- 1 | logs/* 2 | *.pyc 3 | world/* 4 | world_nether/* 5 | world_the_end/* -------------------------------------------------------------------------------- /Minecraft_Server.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeachCraft/TeachCraft-Server/446f5b54f53ca5b4d3a938846252a687c7f0c2ee/Minecraft_Server.jar -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Out of the box features 2 | 3 | - Execute Python against the server, giving your character superpowers! [API] [Examples] 4 | - Weather is turned off 5 | - No hostile mobs 6 | - Permanent nightvision potion effect 7 | - Permanent sword + bow in your inventory (used for interacting with in python) [Example] 8 | - No authentication required 9 | 10 | ### To Run 11 | 12 | Launch multiplayer server by running this command in your terminal (where 1G is the amount of RAM dedicated to the server): 13 | ``` 14 | java -Xms1G -Xmx1G -jar Minecraft_Server.jar 15 | ``` 16 | 17 | Now when you can connect to this server using Multiplayer -> Direct Connect -> 127.0.0.1 18 | 19 | You can also launch the server using our embedded minecraft-wrapper via python, which gets you an auto-reboot on server crash: 20 | ``` 21 | python ./wrapper 22 | ``` 23 | If you do that, you can change the parameters for how the server launches in wrapper.properties.json 24 | 25 | ### To connect to server 26 | 27 | - Launch Minecraft v1.8 on Mojang's official launcher [Video] 28 | - Or launch with our custom python launcher, already hardcoded to v1.8 29 | 30 | ### Open Source Libraries Used 31 | 32 | - Raspberry Juice (for python api to MC server) 33 | - minecraft-wrapper (for auto-reboot on crash) 34 | -------------------------------------------------------------------------------- /banned-ips.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /banned-players.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /bukkit.yml: -------------------------------------------------------------------------------- 1 | # This is the main configuration file for Bukkit. 2 | # As you can see, there's actually not that much to configure without any plugins. 3 | # For a reference for any variable inside this file, check out the Bukkit Wiki at 4 | # http://wiki.bukkit.org/Bukkit.yml 5 | # 6 | # If you need help on this file, feel free to join us on irc or leave a message 7 | # on the forums asking for advice. 8 | # 9 | # IRC: #spigot @ irc.spi.gt 10 | # (If this means nothing to you, just go to http://www.spigotmc.org/pages/irc/ ) 11 | # Forums: http://www.spigotmc.org/ 12 | # Bug tracker: http://www.spigotmc.org/go/bugs 13 | 14 | 15 | settings: 16 | allow-end: true 17 | warn-on-overload: true 18 | permissions-file: permissions.yml 19 | update-folder: update 20 | plugin-profiling: false 21 | connection-throttle: 4000 22 | query-plugins: true 23 | deprecated-verbose: default 24 | shutdown-message: Server closed 25 | spawn-limits: 26 | monsters: 70 27 | animals: 15 28 | water-animals: 5 29 | ambient: 15 30 | chunk-gc: 31 | period-in-ticks: 600 32 | load-threshold: 0 33 | ticks-per: 34 | animal-spawns: 400 35 | monster-spawns: 1 36 | autosave: 6000 37 | aliases: now-in-commands.yml 38 | database: 39 | username: bukkit 40 | isolation: SERIALIZABLE 41 | driver: org.sqlite.JDBC 42 | password: walrus 43 | url: jdbc:sqlite:{DIR}{NAME}.db 44 | -------------------------------------------------------------------------------- /commands.yml: -------------------------------------------------------------------------------- 1 | # This is the commands configuration file for Bukkit. 2 | # For documentation on how to make use of this file, check out the Bukkit Wiki at 3 | # http://wiki.bukkit.org/Commands.yml 4 | # 5 | # If you need help on this file, feel free to join us on irc or leave a message 6 | # on the forums asking for advice. 7 | # 8 | # IRC: #spigot @ irc.spi.gt 9 | # (If this means nothing to you, just go to http://www.spigotmc.org/pages/irc/ ) 10 | # Forums: http://www.spigotmc.org/ 11 | # Bug tracker: http://www.spigotmc.org/go/bugs 12 | 13 | command-block-overrides: [] 14 | aliases: 15 | icanhasbukkit: 16 | - version $1- 17 | -------------------------------------------------------------------------------- /eula.txt: -------------------------------------------------------------------------------- 1 | #By changing the setting below to TRUE you are indicating your agreement to our EULA (https://account.mojang.com/documents/minecraft_eula). 2 | #Sat Nov 21 10:32:20 CST 2015 3 | eula=true 4 | -------------------------------------------------------------------------------- /help.yml: -------------------------------------------------------------------------------- 1 | # This is the help configuration file for Bukkit. 2 | # 3 | # By default you do not need to modify this file. Help topics for all plugin commands are automatically provided by 4 | # or extracted from your installed plugins. You only need to modify this file if you wish to add new help pages to 5 | # your server or override the help pages of existing plugin commands. 6 | # 7 | # This file is divided up into the following parts: 8 | # -- general-topics: lists admin defined help topics 9 | # -- index-topics: lists admin defined index topics 10 | # -- amend-topics: lists topic amendments to apply to existing help topics 11 | # -- ignore-plugins: lists any plugins that should be excluded from help 12 | # 13 | # Examples are given below. When amending command topic, the string will be replaced with the existing value 14 | # in the help topic. Color codes can be used in topic text. The color code character is & followed by 0-F. 15 | # ================================================================ 16 | # 17 | # Set this to true to list the individual command help topics in the master help. 18 | # command-topics-in-master-index: true 19 | # 20 | # Each general topic will show up as a separate topic in the help index along with all the plugin command topics. 21 | # general-topics: 22 | # Rules: 23 | # shortText: Rules of the server 24 | # fullText: | 25 | # &61. Be kind to your fellow players. 26 | # &B2. No griefing. 27 | # &D3. No swearing. 28 | # permission: topics.rules 29 | # 30 | # Each index topic will show up as a separate sub-index in the help index along with all the plugin command topics. 31 | # To override the default help index (displayed when the user executes /help), name the index topic "Default". 32 | # index-topics: 33 | # Ban Commands: 34 | # shortText: Player banning commands 35 | # preamble: Moderator - do not abuse these commands 36 | # permission: op 37 | # commands: 38 | # - /ban 39 | # - /ban-ip 40 | # - /banlist 41 | # 42 | # Topic amendments are used to change the content of automatically generated plugin command topics. 43 | # amended-topics: 44 | # /stop: 45 | # shortText: Stops the server cold....in its tracks! 46 | # fullText: - This kills the server. 47 | # permission: you.dont.have 48 | # 49 | # Any plugin in the ignored plugins list will be excluded from help. The name must match the name displayed by 50 | # the /plugins command. Ignore "Bukkit" to remove the standard bukkit commands from the index. Ignore "All" 51 | # to completely disable automatic help topic generation. 52 | # ignore-plugins: 53 | # - PluginNameOne 54 | # - PluginNameTwo 55 | # - PluginNameThree 56 | 57 | -------------------------------------------------------------------------------- /logging.json: -------------------------------------------------------------------------------- 1 | { 2 | "disable_existing_loggers": false, 3 | "handlers": { 4 | "error_file_handler": { 5 | "encoding": "utf8", 6 | "backupCount": 20, 7 | "filters": [], 8 | "level": "ERROR", 9 | "formatter": "file", 10 | "class": "utils.log.WrapperHandler", 11 | "maxBytes": 10485760, 12 | "filename": "logs/wrapper/wrapper.errors.log" 13 | }, 14 | "console": { 15 | "stream": "ext://sys.stdout", 16 | "formatter": "standard", 17 | "class": "logging.StreamHandler", 18 | "filters": [], 19 | "level": "INFO" 20 | }, 21 | "wrapper_file_handler": { 22 | "encoding": "utf8", 23 | "backupCount": 20, 24 | "filters": [], 25 | "level": "INFO", 26 | "formatter": "file", 27 | "class": "utils.log.WrapperHandler", 28 | "maxBytes": 10485760, 29 | "filename": "logs/wrapper/wrapper.log" 30 | } 31 | }, 32 | "wrapperversion": 1.2, 33 | "formatters": { 34 | "file": { 35 | "datefmt": "%Y-%m-%d %H:%M:%S", 36 | "format": "[%(asctime)s] [%(name)s/%(levelname)s]: %(message)s" 37 | }, 38 | "standard": { 39 | "datefmt": "%H:%M:%S", 40 | "()": "utils.log.ColorFormatter", 41 | "format": "[%(asctime)s] [%(name)s/%(levelname)s]: %(message)s" 42 | } 43 | }, 44 | "version": 1, 45 | "root": { 46 | "handlers": [ 47 | "console", 48 | "wrapper_file_handler", 49 | "error_file_handler" 50 | ], 51 | "level": "NOTSET" 52 | } 53 | } -------------------------------------------------------------------------------- /ops.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "uuid": "3cc665fc-59d6-3ad7-9975-9be6c6ad15d8", 4 | "name": "seanybob", 5 | "level": 4, 6 | "bypassesPlayerLimit": false 7 | } 8 | ] -------------------------------------------------------------------------------- /permissions.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeachCraft/TeachCraft-Server/446f5b54f53ca5b4d3a938846252a687c7f0c2ee/permissions.yml -------------------------------------------------------------------------------- /plugins/RaspberryJuice/config.yml: -------------------------------------------------------------------------------- 1 | # default config.yml 2 | port: 4711 3 | -------------------------------------------------------------------------------- /plugins/raspberryjuice-1.8tc.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeachCraft/TeachCraft-Server/446f5b54f53ca5b4d3a938846252a687c7f0c2ee/plugins/raspberryjuice-1.8tc.jar -------------------------------------------------------------------------------- /server.properties: -------------------------------------------------------------------------------- 1 | #Minecraft server properties 2 | #Fri Jul 07 20:58:38 CDT 2017 3 | spawn-protection=1 4 | max-tick-time=60000 5 | generator-settings= 6 | force-gamemode=true 7 | allow-nether=true 8 | gamemode=0 9 | broadcast-console-to-ops=false 10 | enable-query=false 11 | player-idle-timeout=0 12 | difficulty=0 13 | spawn-monsters=true 14 | broadcast-rcon-to-ops=false 15 | op-permission-level=4 16 | resource-pack-hash= 17 | announce-player-achievements=false 18 | pvp=true 19 | snooper-enabled=false 20 | level-type=DEFAULT 21 | hardcore=false 22 | enable-command-block=true 23 | max-players=30 24 | network-compression-threshold=-1 25 | max-world-size=29999984 26 | rcon.port=25575 27 | server-port=25565 28 | debug=false 29 | server-ip= 30 | spawn-npcs=true 31 | allow-flight=true 32 | level-name=world 33 | view-distance=10 34 | resource-pack= 35 | spawn-animals=true 36 | white-list=false 37 | rcon.password=pycraft 38 | generate-structures=true 39 | online-mode=false 40 | max-build-height=256 41 | level-seed= 42 | enable-rcon=true 43 | motd=TeachCraft 44 | -------------------------------------------------------------------------------- /superops.txt: -------------------------------------------------------------------------------- 1 | =10 2 | =9 -------------------------------------------------------------------------------- /usercache.json: -------------------------------------------------------------------------------- 1 | [{"name":"bob","uuid":"8e289159-2034-3a16-96b9-9fa637848b3b","expiresOn":"2017-08-07 20:59:12 -0500"},{"name":"seanybob","uuid":"3cc665fc-59d6-3ad7-9975-9be6c6ad15d8","expiresOn":"2017-07-17 08:22:49 -0500"},{"name":"seanybob","uuid":"22070326-2b0e-4d7a-bfa5-b6b2643a64c6","expiresOn":"2017-07-17 08:27:32 -0500"}] -------------------------------------------------------------------------------- /whitelist.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /wrapper-data/json/permissions.json: -------------------------------------------------------------------------------- 1 | { 2 | "converted": "Yes", 3 | "groups": {}, 4 | "users": {} 5 | } -------------------------------------------------------------------------------- /wrapper-data/json/usercache.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /wrapper-data/json/web.pkl: -------------------------------------------------------------------------------- 1 | }qUkeys]s. -------------------------------------------------------------------------------- /wrapper-data/json/wrapper.pkl: -------------------------------------------------------------------------------- 1 | }q(Udisabled_plugins]U ServerStartedI01 2 | u. -------------------------------------------------------------------------------- /wrapper.properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "Backups": { 3 | "backup-compression": false, 4 | "backup-folders": [ 5 | "server.properties", 6 | "world" 7 | ], 8 | "backup-interval": 3600, 9 | "backup-location": "backup-directory", 10 | "backup-notification": true, 11 | "backups-keep": 10, 12 | "enabled": false 13 | }, 14 | "Entities": { 15 | "enable-entity-controls": false, 16 | "entity-update-frequency": 4, 17 | "thin-Chicken": 30, 18 | "thin-Cow": 40, 19 | "thin-Sheep": 40, 20 | "thin-cow": 40, 21 | "thin-zombie_pigman": 200, 22 | "thinning-activation-threshhold": 100, 23 | "thinning-frequency": 30 24 | }, 25 | "Gameplay": { 26 | "use-timer-tick-event": false 27 | }, 28 | "General": { 29 | "auto-restart": true, 30 | "command": "java -jar -Xmx2G -Xms1G Minecraft_Server.jar nogui", 31 | "encoding": "UTF-8", 32 | "server-directory": ".", 33 | "shell-scripts": false, 34 | "timed-reboot": false, 35 | "timed-reboot-minutes": 1440, 36 | "timed-reboot-warning-minutes": 5 37 | }, 38 | "IRC": { 39 | "autorun-irc-commands": [ 40 | "COMMAND 1", 41 | "COMMAND 2" 42 | ], 43 | "channels": [ 44 | "#wrapper" 45 | ], 46 | "command-character": ".", 47 | "control-from-irc": false, 48 | "control-irc-pass": "password", 49 | "irc-enabled": false, 50 | "nick": "MinecraftWrap", 51 | "obstruct-nicknames": false, 52 | "password": null, 53 | "port": 6667, 54 | "server": "teachcraft.net", 55 | "show-channel-server": true, 56 | "show-irc-join-part": true 57 | }, 58 | "Misc": { 59 | "command-prefix": "/", 60 | "default-restart-message": "Server restarting...", 61 | "reboot-message": "Server is conducting a scheduled reboot. The server will be back momentarily!", 62 | "stop-message": "Stopping The Minecraft Server", 63 | "use-readline": true 64 | }, 65 | "Proxy": { 66 | "convert-player-files": false, 67 | "hidden-ops": [], 68 | "max-players": 1024, 69 | "online-mode": true, 70 | "proxy-bind": "0.0.0.0", 71 | "proxy-enabled": false, 72 | "proxy-port": 25565, 73 | "proxy-sub-world": false, 74 | "silent-ipban": true, 75 | "spigot-mode": false 76 | }, 77 | "Updates": { 78 | "auto-update-branch": null, 79 | "auto-update-wrapper": false, 80 | "dev-branch": "https://raw.githubusercontent.com/benbaptist/minecraft-wrapper/development/build/version.json", 81 | "stable-branch": "https://raw.githubusercontent.com/benbaptist/minecraft-wrapper/master/build/version.json" 82 | }, 83 | "Web": { 84 | "public-stats": true, 85 | "server-name": "Minecraft Server", 86 | "web-allow-file-management": true, 87 | "web-bind": "127.0.0.1", 88 | "web-enabled": false, 89 | "web-password": "password", 90 | "web-port": 8070 91 | } 92 | } -------------------------------------------------------------------------------- /wrapper/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (C) 2016, 2017 - BenBaptist and Wrapper.py developer(s). 4 | # https://github.com/benbaptist/minecraft-wrapper 5 | # This program is distributed under the terms of the GNU 6 | # General Public License, version 3 or later. 7 | -------------------------------------------------------------------------------- /wrapper/__main__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (C) 2016, 2017 - BenBaptist and Wrapper.py developer(s). 4 | # https://github.com/benbaptist/minecraft-wrapper 5 | # This program is distributed under the terms of the GNU 6 | # General Public License, version 3 or later. 7 | 8 | import os 9 | import sys 10 | from core.wrapper import Wrapper 11 | from api.helpers import getjsonfile 12 | from utils.log import configure_logger 13 | import argparse 14 | 15 | parser = argparse.ArgumentParser( 16 | description='Wrapper.py startup arguments', 17 | epilog='Created by SurestTexas00') 18 | 19 | parser.add_argument('--encoding', "-e", default='utf-8', 20 | action='store_true', help=' Specify an encoding' 21 | ' (other than utf-8') 22 | parser.add_argument('--betterconsole', "-b", default=False, 23 | action='store_true', help='Use "better ' 24 | ' console" feature to anchor your imput at' 25 | ' the bottom of the console (anti- scroll-away' 26 | ' feature)') 27 | 28 | args = parser.parse_args() 29 | 30 | version = sys.version_info 31 | VERSION = version[0] 32 | SUBVER = version[1] 33 | 34 | PY3 = VERSION > 2 35 | MINSUB = 7 36 | if PY3: 37 | MINSUB = 4 38 | 39 | 40 | def main(wrapper_start_args): 41 | # same as 'use-readline = True' 42 | better_console = wrapper_start_args.betterconsole 43 | encoding = wrapper_start_args.encoding 44 | 45 | config = getjsonfile("wrapper.properties", ".", encodedas=encoding) 46 | 47 | if config and "Misc" in config: 48 | if "use-readline" in config["Misc"]: 49 | # use readline = not using better_console 50 | better_console = not(config["Misc"]["use-readline"]) 51 | 52 | configure_logger(betterconsole=better_console) 53 | 54 | # __init__ wrapper and set up logging 55 | wrapper = Wrapper() 56 | log = wrapper.log 57 | 58 | # start first wrapper log entry 59 | log.info("Wrapper.py started - Version %s", wrapper.getbuildstring()) 60 | log.debug("Wrapper is using Python %s.%s.", sys.version_info[0], SUBVER) 61 | 62 | # flag python version problems 63 | if SUBVER < MINSUB: 64 | log.warning( 65 | "You are using Python %s.%s. There are Wrapper dependencies" 66 | " and methods that may require a minimum version of %s.%s." 67 | " Please press and to acknowledge and continue" 68 | " (anything else to exit)..." % 69 | (VERSION, SUBVER, VERSION, MINSUB)) 70 | userstdin = sys.stdin.readline().strip() 71 | if userstdin.lower() != "y": 72 | print("bye..") 73 | sys.exit(1) 74 | 75 | # start wrapper 76 | # noinspection PyBroadException 77 | try: 78 | wrapper.start() 79 | except SystemExit: 80 | if not wrapper.configManager.exit: 81 | os.system("reset") 82 | wrapper.plugins.disableplugins() 83 | 84 | # save-all is required to have a flush argument 85 | wrapper.javaserver.console("save-all flush") 86 | wrapper.javaserver.stop("Wrapper.py received shutdown signal - bye") 87 | wrapper.halt = True 88 | except Exception as ex: 89 | log.critical("Wrapper.py crashed - stopping server to be safe (%s)", 90 | ex, exc_info=True) 91 | wrapper.halt = True 92 | wrapper.plugins.disableplugins() 93 | try: 94 | wrapper.javaserver.stop("Wrapper.py crashed - please contact" 95 | " the server host as soon as possible") 96 | except AttributeError as exc: 97 | log.critical("Wrapper has no server instance. Server is likely " 98 | "killed but could still be running, or it " 99 | "might be corrupted! (%s)", exc, exc_info=True) 100 | 101 | if __name__ == "__main__": 102 | main(args) 103 | -------------------------------------------------------------------------------- /wrapper/api/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (C) 2016, 2017 - BenBaptist and Wrapper.py developer(s). 4 | # https://github.com/benbaptist/minecraft-wrapper 5 | # This program is distributed under the terms of the GNU 6 | # General Public License, version 3 or later.. 7 | -------------------------------------------------------------------------------- /wrapper/api/backups.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (C) 2016, 2017 - BenBaptist and Wrapper.py developer(s). 4 | # https://github.com/benbaptist/minecraft-wrapper 5 | # This program is distributed under the terms of the GNU 6 | # General Public License, version 3 or later. 7 | 8 | 9 | # noinspection PyPep8Naming 10 | class Backups(object): 11 | """ 12 | .. code:: python 13 | 14 | def __init__(self, wrapper) 15 | 16 | .. 17 | 18 | These methods are accessed using 'self.api.backups' 19 | 20 | .. code:: python 21 | 22 | = self.api.backups 23 | . 24 | 25 | .. 26 | 27 | This class wraps the wrapper.backups functions. Wrapper starts 28 | core.backups.py class Backups (as .backups). This API 29 | class manipulates the backups instance within core.wrapper 30 | 31 | """ 32 | 33 | def __init__(self, wrapper): 34 | self.wrapper = wrapper 35 | self.log = wrapper.log 36 | 37 | def verifyTarInstalled(self): 38 | """ 39 | checks for tar on users system. 40 | 41 | :returns: True if installed, False if not (along with error logs 42 | and console messages). 43 | 44 | """ 45 | return self.wrapper.backups.dotarchecks() 46 | 47 | def performBackup(self): 48 | """ 49 | Perform an immediate backup 50 | 51 | :returns: check console for messages (or wrapper backup Events) 52 | 53 | """ 54 | self.wrapper.backups.dobackup() 55 | 56 | def pruneBackups(self): 57 | """ 58 | prune backups according to wrapper properties settings. 59 | 60 | :returns: Output to console and logs 61 | 62 | """ 63 | self.wrapper.backups.pruneoldbackups() 64 | 65 | def disableBackups(self): 66 | """ 67 | Allow plugin to temporarily shut off backups (only during 68 | this wrapper session). 69 | 70 | :returns: None 71 | 72 | """ 73 | self.wrapper.backups.enabled = False 74 | 75 | def enableBackups(self): 76 | """ 77 | Allow plugin to re-enable disabled backups or enable backups 78 | during this wrapper session. 79 | 80 | :returns: 81 | :True: tar is installed 82 | :False: tar is not installed 83 | 84 | """ 85 | self.wrapper.backups.enabled = True 86 | if not self.wrapper.backups.timerstarted: 87 | if not self.wrapper.backups.dotarchecks(): 88 | return False 89 | self.wrapper.backups.timerstarted = True 90 | self.wrapper.backups.api.registerEvent( 91 | "timer.second", self.wrapper.backups.eachsecond) 92 | 93 | def adjustBackupInterval(self, desired_interval): 94 | """ 95 | Adjust the backup interval for automatic backups. 96 | 97 | :arg desired_interval: interval in seconds for regular backups 98 | 99 | :returns: 100 | 101 | """ 102 | interval = int(desired_interval) 103 | self.wrapper.backups.config["Backups"]["backup-interval"] = interval 104 | self.wrapper.configManager.save() 105 | self.wrapper.backups.backup_interval = interval 106 | 107 | def adjustBackupsKept(self, desired_number): 108 | """ 109 | Adjust the number of backups kept. 110 | 111 | :arg desired_number: number of desired backups 112 | 113 | :returns: 114 | 115 | """ 116 | num_kept = int(desired_number) 117 | self.wrapper.backups.config["Backups"]["backups-keep"] = num_kept 118 | self.wrapper.configManager.save() 119 | -------------------------------------------------------------------------------- /wrapper/api/entity.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (C) 2016, 2017 - BenBaptist and Wrapper.py developer(s). 4 | # https://github.com/benbaptist/minecraft-wrapper 5 | # This program is distributed under the terms of the GNU 6 | # General Public License, version 3 or later. 7 | 8 | from time import sleep 9 | import threading 10 | from core.entities import Entities as Entitytypes 11 | 12 | # move to different future objects module? 13 | from core.entities import Objects as Objecttypes 14 | 15 | 16 | # noinspection PyPep8Naming 17 | class EntityControl(object): 18 | """ 19 | .. code:: python 20 | 21 | def __init__(self, mcserver) 22 | 23 | .. 24 | 25 | This class is accessed using 26 | .. code:: python 27 | 28 | = self.api.minecraft.getEntityControl() 29 | . 30 | .. 31 | 32 | Valid only with a functioning server. 33 | 34 | Entity controls are established by console when wrapper 35 | reads "preparing ...." 36 | 37 | """ 38 | 39 | def __init__(self, mcserver): 40 | self.chunks = {} 41 | 42 | self._javaserver = mcserver 43 | self._log = mcserver.log 44 | 45 | # Entities - living beings (includes XP orbs!) 46 | pre1_11 = self._javaserver.version_compute < 11100 47 | entitylistobject = Entitytypes(pre1_11) 48 | self.entitytypes = entitylistobject.entitylist 49 | 50 | # objects.. non living entities, minecarts, falling sand, 51 | # primed TNT. armorstands, projectiles.. 52 | # not directly used here.. but is referenced by parse_cb for 53 | # 'parse_play_spawn_object' 54 | # move to different future objects module? 55 | objectlistobject = Objecttypes() 56 | self.objecttypes = objectlistobject.objectlist 57 | 58 | # load config settings 59 | self.entityControl = self._javaserver.config["Entities"][ 60 | "enable-entity-controls"] 61 | self.entityProcessorFrequency = self._javaserver.config["Entities"][ 62 | "entity-update-frequency"] 63 | self.thiningFrequency = self._javaserver.config["Entities"][ 64 | "thinning-frequency"] 65 | self.startThinningThreshshold = self._javaserver.config["Entities"][ 66 | "thinning-activation-threshhold"] 67 | # self.kill_aura_radius = self.javaserver.config["Entities"][ 68 | # "player-thinning-radius"] 69 | 70 | self.entities = {} 71 | self._abortep = False 72 | 73 | # entity processor thread 74 | t = threading.Thread(target=self._entity_processor, 75 | name="entProc", args=()) 76 | t.daemon = True 77 | t.start() 78 | 79 | # entity killer thread 80 | if self.entityControl: 81 | ekt = threading.Thread(target=self._entity_thinner, 82 | name="entKill", args=()) 83 | ekt.daemon = True 84 | ekt.start() 85 | 86 | def __del__(self): 87 | self._abortep = True 88 | 89 | # noinspection PyBroadException 90 | def getEntityByEID(self, eid): 91 | """ 92 | Returns the entity context or False if the specified entity 93 | ID doesn't exist. 94 | 95 | CAUTION understand that entities are very DYNAMIC. The 96 | entity object you get could be modified or even deleted 97 | at any time! 98 | 99 | """ 100 | try: 101 | return self.entities[eid] 102 | except Exception: # as e: 103 | # self.log.debug("getEntityByEID returned False: %s", e) 104 | return False 105 | 106 | def countActiveEntities(self): 107 | """ 108 | return an integer count of all entities. 109 | 110 | """ 111 | return len(self.entities) 112 | 113 | def countEntitiesInPlayer(self, playername): 114 | """ 115 | returns a list of entity info dictionaries 116 | 117 | see getEntityInfo(self, eid) 118 | 119 | :sample: 120 | .. code:: python 121 | 122 | [ 123 | {}, 124 | {}, 125 | {}, 126 | {} 127 | ] 128 | 129 | .. 130 | 131 | (Pycharm return definition) 132 | @:type Dict 133 | 134 | """ 135 | ents = [] 136 | entities = self.entities 137 | for v in iter(entities.values()): 138 | if v.clientname == playername: 139 | about = v.about_entity 140 | if about: 141 | ents.append(about()) 142 | return ents 143 | 144 | def getEntityInfo(self, eid): 145 | """ 146 | Get a dictionary of info on the specified EID. Returns 147 | None if fails 148 | 149 | :Sample item: 150 | .. code:: python 151 | 152 | { 153 | # the player in whose world the entity exists 154 | "player": "SapperLeader2", 155 | "rodeBy": False, 156 | # eid of entity - if two or more players share 157 | # chunks, this could be the same creeper in 158 | # both player's world/client. It would be in the 159 | # other player's client under another eid, of 160 | # course... 161 | "eid": 126, 162 | "name": "Creeper", 163 | "Riding": False, 164 | "position": [ 165 | 3333, 166 | 29, 167 | 2847 168 | ], 169 | # the type code for Creeper 170 | "type": 50, 171 | "isObject": False, 172 | # uuids are only on 1.9+ , but should be unique to object 173 | "uuid": "fae14015-dde6-4e07-b5e5-f27536937a79" 174 | } 175 | .. 176 | 177 | """ 178 | try: 179 | return self.getEntityByEID(eid).aboutEntity() 180 | except AttributeError: 181 | return None 182 | 183 | def existsEntityByEID(self, eid): 184 | """ 185 | Test whether the specified eid is valid 186 | 187 | """ 188 | if eid in self.entities: 189 | return True 190 | else: 191 | return False 192 | 193 | def killEntityByEID(self, eid, dropitems=False, count=1): 194 | """ 195 | Takes the entity by eid and kills the first entity of 196 | that type centered at the coordinates where that entity is. 197 | 198 | :Args: 199 | :eid: Entity EID on server 200 | :dropitems: whether or not the entity death will drop 201 | loot. Only works if gamerule doMobDrops is true. 202 | :count: used to specify more than one entity; again, 203 | centers on the specified eid location. 204 | 205 | """ 206 | entityinfo = self.getEntityInfo(eid) 207 | if not entityinfo: 208 | return 209 | 210 | pos = entityinfo["position"] 211 | entitydesc = entityinfo["name"] 212 | if dropitems: 213 | # kill them (get loots if server has doMobDrops set to true) 214 | self._javaserver.console( 215 | "kill @e[type=%s,x=%d,y=%d,z=%d,c=%s]" % ( 216 | entitydesc, pos[0], pos[1], pos[2], count)) 217 | else: 218 | # send them into void (no loots) 219 | self._javaserver.console( 220 | "tp @e[type=%s,x=%d,y=%d,z=%d,c=%s] ~ -500 ~" % ( 221 | entitydesc, pos[0], pos[1], pos[2], count)) 222 | 223 | def _entity_processor(self): 224 | self._log.debug("_entityprocessor thread started.") 225 | timer = float(0) 226 | # server is running 227 | while self._javaserver.state in (1, 2, 4) and not self._abortep: 228 | timer += .1 229 | sleep(.1) 230 | # timer for removing stale entities we want a FAST response 231 | # to server shutdowns (activate while loop frequently) 232 | if timer < float(self.entityProcessorFrequency): 233 | continue 234 | timer = float(0) 235 | 236 | # start looking for stale client entities 237 | players = self._javaserver.players 238 | playerlist = [] 239 | for player in players: 240 | playerlist.append(player) 241 | entity_eids = list(self.entities.keys()) 242 | for eid in entity_eids: 243 | if self.getEntityByEID(eid).clientname not in playerlist: 244 | # noinspection PyBroadException 245 | try: 246 | self.entities.pop(eid, None) 247 | except: 248 | pass 249 | self._log.debug("_entityprocessor thread closed.") 250 | 251 | # each entity IS a dictionary, so... 252 | # noinspection PyTypeChecker 253 | def _entity_thinner(self): 254 | self._log.debug("_entity_thinner thread started.") 255 | timer = float(0) 256 | 257 | # while server is running 258 | while self._javaserver.state in (1, 2, 4) and not self._abortep: 259 | 260 | timer += .1 261 | sleep(.1) 262 | # timer for removing stale entities we want a FAST response 263 | # to server shutdowns (activate while loop frequently) 264 | if timer < float(self.thiningFrequency): 265 | continue 266 | timer = float(0) 267 | 268 | if self.countActiveEntities() < self.startThinningThreshshold: 269 | # don't bother, server load is light. 270 | continue 271 | 272 | # gather player list 273 | players = self._javaserver.players 274 | playerlist = [] 275 | for player in players: 276 | playerlist.append(player) 277 | 278 | # loop through playerlist 279 | for playerclient in playerlist: 280 | players_position = self._javaserver.getplayer( 281 | playerclient).getPosition() 282 | his_entities = self.countEntitiesInPlayer(playerclient) 283 | if len(his_entities) < self.startThinningThreshshold: 284 | # don't worry with this player, his load is light. 285 | continue 286 | 287 | # now we need to count each entity type 288 | counts = {} 289 | for entity in his_entities: 290 | if entity["name"] in counts: 291 | counts[entity["name"]] += 1 292 | else: 293 | counts[entity["name"]] = 1 # like {"Cow": 1} 294 | 295 | for mob_type in counts: 296 | if "thin-%s" % mob_type in self._javaserver.config[ 297 | "Entities"]: 298 | maxofthiskind = self._javaserver.config[ 299 | "Entities"]["thin-%s" % mob_type] 300 | if counts[mob_type] >= maxofthiskind: 301 | 302 | # turn off console_spam 303 | server_msg = "Teleported %s to" % mob_type 304 | if server_msg not in self._javaserver.spammy_stuff: 305 | self._javaserver.spammy_stuff.append( 306 | "Teleported %s to" % mob_type) 307 | 308 | # can't be too agressive with killing because 309 | # entitycount might be off/lagging 310 | # kill half of any mob above this number 311 | killcount = (counts[mob_type] - maxofthiskind) // 2 312 | if killcount > 1: 313 | self._kill_around_player( 314 | players_position, "%s" % mob_type, 315 | killcount) 316 | 317 | self._log.debug("_entity_thinner thread closed.") 318 | 319 | def _kill_around_player(self, position, entity_name, count): 320 | pos = position 321 | # send those creatures away 322 | self._log.debug("killing %d %s" % (count, entity_name)) 323 | self._javaserver.console( 324 | "tp @e[type=%s,x=%d,y=%d,z=%d,c=%s] ~ -500 ~" % 325 | (entity_name, pos[0], pos[1], pos[2], count)) 326 | -------------------------------------------------------------------------------- /wrapper/api/world.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (C) 2016, 2017 - BenBaptist and Wrapper.py developer(s). 4 | # https://github.com/benbaptist/minecraft-wrapper 5 | # This program is distributed under the terms of the GNU 6 | # General Public License, version 3 or later. 7 | 8 | import struct 9 | import json 10 | 11 | 12 | # noinspection PyPep8Naming 13 | class World(object): 14 | """ 15 | .. code:: python 16 | 17 | def __init__(self, name, mcserver) 18 | 19 | .. 20 | 21 | This class is accessed via getWorld(). Requires a running server. 22 | 23 | .. code:: python 24 | 25 | = self.api.minecraft.getWorld() 26 | . 27 | .. 28 | 29 | World is established by console when wrapper reads "preparing ...." 30 | 31 | """ 32 | def __init__(self, name, mcserver): 33 | self.name = name 34 | self.javaserver = mcserver 35 | self.log = mcserver.log 36 | 37 | self.chunks = {} # not implemented 38 | 39 | def __str__(self): 40 | return self.name 41 | 42 | def setBlock(self, x, y, z, tilename, damage=0, mode="replace", data=None): 43 | if not data: 44 | data = {} 45 | self.javaserver.console("setblock %d %d %d %s %d %s %s" % ( 46 | x, y, z, tilename, damage, mode, json.dumps(data))) 47 | 48 | def fill(self, position1, position2, tilename, damage=0, mode="destroy", data=None): 49 | """ 50 | Fill a 3D cube with a certain block. 51 | 52 | :Args: 53 | :position1: tuple x, y, z 54 | :position2: tuple x, y, z 55 | :damage: see minecraft Wiki 56 | :mode: destroy, hollow, keep, outline 57 | :data: see minecraft Wiki 58 | 59 | """ 60 | if not data: 61 | data = {} 62 | if mode not in ("destroy", "hollow", "keep", "outline"): 63 | raise Exception("Invalid mode: %s" % mode) 64 | x1, y1, z1 = position1 65 | x2, y2, z2 = position2 66 | if self.javaserver.protocolVersion < 6: 67 | raise Exception("Must be running Minecraft 1.8 or above" 68 | " to use the world.fill() method.") 69 | else: 70 | self.javaserver.console( 71 | "fill %d %d %d %d %d %d %s %d %s %s" % ( 72 | x1, y1, z1, x2, y2, z2, 73 | tilename, damage, mode, json.dumps(data))) 74 | 75 | def replace(self, position1, position2, tilename1, damage1, tilename2, damage2=0): 76 | """ 77 | Replace specified blocks within a 3D cube with another specified block. 78 | 79 | :Args: see minecraft Wiki 80 | 81 | """ 82 | x1, y1, z1 = position1 83 | x2, y2, z2 = position2 84 | if self.javaserver.protocolVersion < 6: 85 | raise Exception( 86 | "Must be running Minecraft 1.8 or above" 87 | " to use the world.replace() method.") 88 | else: 89 | self.javaserver.console( 90 | "fill %d %d %d %d %d %d %s %d replace %s %d" % ( 91 | x1, y1, z1, x2, y2, z2, 92 | tilename2, damage2, tilename1, damage1)) 93 | return 94 | 95 | def getBlock(self, pos): 96 | """ 97 | not implemented 98 | 99 | """ 100 | x, y, z = pos 101 | chunkx, chunkz = int(x / 16), int(z / 16) 102 | localx, localz = (x / 16.0 - x / 16) * 16, (z / 16.0 - z / 16) * 16 103 | # print chunkx, chunkz, localx, y, localz 104 | return self.chunks[chunkx][chunkz].getBlock(localx, y, localz) 105 | 106 | def setChunk(self, x, z, chunk): 107 | """ not implemented """ 108 | if x not in self.chunks: 109 | self.chunks[x] = {} 110 | self.chunks[x][z] = chunk 111 | 112 | 113 | # noinspection PyPep8Naming 114 | class Chunk(object): 115 | """ 116 | not implemented 117 | 118 | """ 119 | def __init__(self, bytesarray, x, z): 120 | self.ids = struct.unpack("<" + ("H" * (len(bytesarray) / 2)), bytesarray) 121 | self.x = x 122 | self.z = z 123 | # for i,v in enumerate(bytesarray): 124 | # y = math.ceil(i/256) 125 | # if y not in self.blocks: self.blocks[y] = {} 126 | # z = int((i/256.0 - int(i/256.0)) * 16) 127 | # if z not in self.blocks[y]: self.blocks[y][z] = {} 128 | # x = (((i/256.0 - int(i/256.0)) * 16) - z) * 16 129 | # if x not in self.blocks[y][z]: self.blocks[y][z][x] = i 130 | 131 | def getBlock(self, x, y, z): 132 | # print x, y, z 133 | i = int((y * 256) + (z * 16) + x) 134 | bid = self.ids[i] 135 | return bid 136 | -------------------------------------------------------------------------------- /wrapper/api/wrapperconfig.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (C) 2016, 2017 - BenBaptist and Wrapper.py developer(s). 4 | # https://github.com/benbaptist/minecraft-wrapper 5 | # This program is distributed under the terms of the GNU 6 | # General Public License, version 3 or later. 7 | 8 | # noinspection PySingleQuotedDocstring 9 | ''' 10 | This file format is a bit non-pythonic, but is intended to produce a nicer 11 | ReST file for the documentation. Three `'` quotes are disregarded by 12 | our document production methods. Lines are also greater than 79 characters... 13 | In fact, they are whatever length should get grouped together as a single 14 | sentence or paragraph. Double CR's are treated as a single CR by ReST 15 | parsers. 16 | ''' 17 | 18 | # def Config file items and layout: 19 | # """ 20 | ''' 21 | 22 | *wrapperconfig.py is the default config file. Changes made 23 | here are inserted or deleted from the the wrapper config 24 | each time wrapper starts.* 25 | 26 | *Items marked as "deprecated" get removed from the wrapper 27 | config when wrapper starts. These are are not valid items. 28 | they only exist so that they will get removed from older 29 | wrapper versions. This is intended to keep the actual 30 | wrapper.config.json file from getting cluttered with old 31 | unused items.* 32 | 33 | *The wrapper.config.json file contents will look like this, 34 | but without all the comment lines.* 35 | 36 | ''' 37 | 38 | CONFIG = { 39 | 40 | # Automatic backups with pruning. Intervals are specified in seconds. 41 | 42 | "Backups": 43 | 44 | { 45 | 46 | "backup-compression": False, 47 | 48 | # Specify files and folders you want backed up. Items must be in your server folder (see 'General' section) 49 | 50 | "backup-folders": 51 | 52 | [ 53 | "server.properties", 54 | 55 | "world" 56 | 57 | ], 58 | 59 | "backup-interval": 3600, 60 | 61 | # backup location is inside wrapper's directory 62 | 63 | "backup-location": "backup-directory", 64 | 65 | "backup-notification": True, 66 | 67 | "backups-keep": 10, 68 | 69 | "enabled": False 70 | 71 | }, 72 | 73 | "Gameplay": 74 | 75 | { 76 | 77 | # Use of timer-tick is not recommended. 1/20th of a second timer option for plugin use. May impact wrapper performance negatively. 78 | 79 | "use-timer-tick-event": False, 80 | 81 | }, 82 | 83 | "Entities": 84 | 85 | { 86 | 87 | # whether to use the wrapper entity controls. 88 | 89 | "enable-entity-controls": False, 90 | 91 | # how often the entity processor updates world entity counts 92 | 93 | "entity-update-frequency": 4, 94 | 95 | # how often thinning of mobs runs, in seconds. a large difference between this and the entity update frequency will ensure no 'overkill" occurs. 96 | 97 | "thinning-frequency": 30, 98 | 99 | # when mobs < this threshhold, thinning is inactive (server or player) 100 | 101 | "thinning-activation-threshhold": 100, 102 | 103 | # The following items thin specific mobs over the stated count. This only happens after the total mob count threshold above is met first. For example, 'thin-Cow: 40` starts thinning cows > 40. Entity names must match minecraft naming exactly as they would appear in the game. 104 | 105 | "thin-Cow": 40, 106 | 107 | # 1.11 naming! Check /wrapper-date/json/entities.json 108 | 109 | # there are some surprising changes, like "PigZombie" is now zombie_pigman and EntityHorse is horse, etc 110 | 111 | "thin-cow": 40, 112 | 113 | "thin-zombie_pigman": 200, 114 | 115 | "thin-Sheep": 40, 116 | 117 | "thin-Chicken": 30 118 | 119 | }, 120 | 121 | "Updates": 122 | 123 | { 124 | 125 | # Use one of the names listed herein (i.e. 'stable-branch') 126 | 127 | "auto-update-branch": None, 128 | 129 | # If True, an "auto-update-branch" must be specified. 130 | 131 | "auto-update-wrapper": False, 132 | 133 | # You can point these to another branch, if desired. 134 | 135 | "stable-branch": 136 | 137 | "https://raw.githubusercontent.com/benbaptist/minecraft-wrapper/master/build/version.json", 138 | 139 | "dev-branch": 140 | 141 | "https://raw.githubusercontent.com/benbaptist/minecraft-wrapper/development/build/version.json", 142 | 143 | }, 144 | 145 | # look 'n' feel type customizations 146 | 147 | "Misc": 148 | 149 | { 150 | 151 | # if you change command-prefix, no minecraft command will work. Bug or feature? -TODO not sure. 152 | 153 | "command-prefix": "/", 154 | 155 | "reboot-message": "Server is conducting a scheduled reboot. The server will be back momentarily!", 156 | 157 | "default-restart-message": "Server restarting...", 158 | 159 | "stop-message": "Stopping The Minecraft Server", 160 | 161 | # readline is likely to be more-cross platform, but does not use wrapper's ability to keep console keystroke entries visually intact while server produces output. 162 | 163 | "use-readline": True 164 | 165 | }, 166 | 167 | "General": 168 | 169 | { 170 | 171 | "auto-restart": True, 172 | 173 | # You will need to update this to your particular server start command line. 174 | 175 | "command": "java -jar -Xmx2G -Xms1G server.jar nogui", 176 | 177 | "encoding": "UTF-8", 178 | 179 | # wrapper detects server version and adjusts accordingly now 180 | 181 | "pre-1.7-mode": "deprecated", 182 | 183 | # Using the default '.' roots the server in the same folder with wrapper. Change this to another folder to keep the wrapper and server folders separate. Do not use a trailing slash... e.g. - '/full/pathto/the/server' 184 | 185 | "server-directory": ".", 186 | 187 | # server-name was moved to Web (it is used only by web module in code) 188 | 189 | "server-name": "deprecated", 190 | 191 | "shell-scripts": False, 192 | 193 | "timed-reboot": False, 194 | 195 | # Deprecated for consistency with timed reboot warning 'minutes' 196 | 197 | "timed-reboot-seconds": "deprecated", 198 | 199 | "timed-reboot-minutes": 1440, 200 | 201 | "timed-reboot-warning-minutes": 5, 202 | 203 | # The remaining items and functionality were moved to group "Updates" and deprecated from this section. 204 | 205 | "auto-update-branch": "deprecated", 206 | 207 | "auto-update-dev-build": "deprecated", 208 | 209 | "auto-update-wrapper": "deprecated", 210 | 211 | "stable-branch": "deprecated", 212 | 213 | "dev-branch": "deprecated", 214 | 215 | }, 216 | 217 | # This allows your users to communicate to and from the server via IRC and vice versa. 218 | 219 | "IRC": 220 | 221 | { 222 | 223 | "autorun-irc-commands": 224 | 225 | [ 226 | "COMMAND 1", 227 | 228 | "COMMAND 2" 229 | 230 | ], 231 | 232 | "channels": 233 | 234 | [ 235 | "#wrapper" 236 | 237 | ], 238 | 239 | "command-character": ".", 240 | 241 | "control-from-irc": False, 242 | 243 | "control-irc-pass": "password", 244 | 245 | "irc-enabled": False, 246 | 247 | "nick": "MinecraftWrap", 248 | 249 | "obstruct-nicknames": False, 250 | 251 | "password": None, 252 | 253 | "port": 6667, 254 | 255 | "server": "benbaptist.com", 256 | 257 | "show-channel-server": True, 258 | 259 | "show-irc-join-part": True 260 | 261 | }, 262 | 263 | 264 | 265 | "Proxy": 266 | 267 | # This is a man-in-the-middle proxy similar to BungeeCord, which is used for extra plugin functionality. online-mode must be set to False in server.properties. Make sure that the server is not accessible directly from the outside world. 268 | 269 | # Note: the online-mode option here refers to the proxy only, not to the server's offline mode. Each server's online mode will depend on its setting in server.properties. It is recommended that you turn network-compression-threshold to -1 (off) in server.properties for fewer issues. 270 | 271 | { 272 | 273 | "convert-player-files": False, 274 | 275 | # This actually does nothing in the code. TODO - re-implement this somewhere? perhaps in the server JSON response? 276 | 277 | "max-players": 1024, 278 | 279 | # the wrapper's online mode, NOT the server. 280 | 281 | "online-mode": True, 282 | 283 | "proxy-bind": "0.0.0.0", 284 | 285 | "proxy-enabled": False, 286 | 287 | # if wrapper is a sub world (wrapper needs to do extra work to spawn the player). 288 | 289 | "proxy-sub-world": False, 290 | 291 | # the wrapper's proxy port that accepts client connections from the internet. This port is exposed to the internet via your port forwards. 292 | 293 | "proxy-port": 25565, 294 | 295 | # Deprecated - This port is autoconfigured from server console output now. 296 | 297 | "server-port": "deprecated", 298 | 299 | "spigot-mode": False, 300 | 301 | # silent bans cause your server to ignore sockets from that IP (for IP bans). This will cause your server to appear offline and avoid possible confrontations. 302 | 303 | "silent-ipban": True, 304 | 305 | "hidden-ops": 306 | 307 | # these players do not appear in the sample server player list pings. 308 | 309 | [ 310 | 311 | "SurestTexas00", 312 | 313 | "BenBaptist" 314 | 315 | ] 316 | 317 | }, 318 | 319 | "Web": 320 | 321 | { 322 | 323 | "public-stats": True, 324 | 325 | "web-allow-file-management": True, 326 | 327 | "web-bind": "0.0.0.0", 328 | 329 | "web-enabled": False, 330 | 331 | "web-password": "password", 332 | 333 | "web-port": 8070, 334 | 335 | "server-name": "Minecraft Server", 336 | 337 | } 338 | 339 | } 340 | 341 | # """ 342 | -------------------------------------------------------------------------------- /wrapper/core/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (C) 2016, 2017 - BenBaptist and Wrapper.py developer(s). 4 | # https://github.com/benbaptist/minecraft-wrapper 5 | # This program is distributed under the terms of the GNU 6 | # General Public License, version 3 or later. 7 | -------------------------------------------------------------------------------- /wrapper/core/backups.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (C) 2016, 2017 - BenBaptist and Wrapper.py developer(s). 4 | # https://github.com/benbaptist/minecraft-wrapper 5 | # This program is distributed under the terms of the GNU 6 | # General Public License, version 3 or later. 7 | 8 | import datetime 9 | import time 10 | 11 | import subprocess 12 | import os 13 | import platform 14 | 15 | from api.base import API 16 | from api.helpers import putjsonfile, getjsonfile, mkdir_p 17 | 18 | # I should probably not use irc=True when broadcasting, and instead should just rely on events and having 19 | # MCserver.py and irc.py print messages themselves for the sake of consistency. 20 | 21 | 22 | class Backups(object): 23 | 24 | def __init__(self, wrapper): 25 | self.wrapper = wrapper 26 | self.config = wrapper.config 27 | self.encoding = self.config["General"]["encoding"] 28 | self.log = wrapper.log 29 | self.api = API(wrapper, "Backups", internal=True) 30 | 31 | self.interval = 0 32 | self.backup_interval = self.config["Backups"]["backup-interval"] 33 | self.time = time.time() 34 | self.backups = [] 35 | self.enabled = self.config["Backups"]["enabled"] # allow plugins to shutdown backups via api 36 | self.timerstarted = False 37 | if self.enabled and self.dotarchecks(): # only register event if used and tar installed! 38 | self.api.registerEvent("timer.second", self.eachsecond) 39 | self.timerstarted = True 40 | self.log.debug("Backups Enabled..") 41 | 42 | # noinspection PyUnusedLocal 43 | def eachsecond(self, payload): 44 | self.interval += 1 45 | if time.time() - self.time > self.backup_interval and self.enabled: 46 | self.dobackup() 47 | 48 | def pruneoldbackups(self, filename="IndependentPurge"): 49 | if len(self.backups) > self.config["Backups"]["backups-keep"]: 50 | self.log.info("Deleting old backups...") 51 | while len(self.backups) > self.config["Backups"]["backups-keep"]: 52 | backup = self.backups[0] 53 | if not self.wrapper.events.callevent("wrapper.backupDelete", {"file": filename}): 54 | break 55 | try: 56 | os.remove('%s/%s' % (self.config["Backups"]["backup-location"], backup[1])) 57 | except Exception as e: 58 | self.log.error("Failed to delete backup (%s)", e) 59 | self.log.info("Deleting old backup: %s", 60 | datetime.datetime.fromtimestamp(int(backup[0])).strftime('%Y-%m-%d_%H:%M:%S')) 61 | # hink = self.backups[0][1][:] # not used... 62 | del self.backups[0] 63 | putjsonfile(self.backups, "backups", self.config["Backups"]["backup-location"]) 64 | 65 | def dotarchecks(self): 66 | # Check if tar is installed 67 | which = "where" if platform.system() == "Windows" else "which" 68 | if not subprocess.call([which, "tar"]) == 0: 69 | self.wrapper.events.callevent("wrapper.backupFailure", 70 | {"reasonCode": 1, "reasonText": "Tar is not installed. Please install " 71 | "tar before trying to make backups."}) 72 | self.log.error("Backups will not work, because tar does not appear to be installed!") 73 | self.log.error("If you are on a Linux-based system, please install it through your preferred package " 74 | "manager.") 75 | self.log.error("If you are on Windows, you can find GNU/Tar from this link: http://goo.gl/SpJSVM") 76 | return False 77 | else: 78 | return True 79 | 80 | def dobackup(self): 81 | self.log.debug("Backup starting.") 82 | self._settime() 83 | self._checkforbackupfolder() 84 | self._getbackups() # populate self.backups 85 | self._performbackup() 86 | self.log.debug("Backup cycle complete.") 87 | 88 | def _checkforbackupfolder(self): 89 | if not os.path.exists(self.config["Backups"]["backup-location"]): 90 | self.log.warning("Backup location %s does not exist -- creating target location...", 91 | self.config["Backups"]["backup-location"]) 92 | mkdir_p(self.config["Backups"]["backup-location"]) 93 | 94 | def _doserversaving(self, desiredstate=True): 95 | """ 96 | :param desiredstate: True = turn serversaving on 97 | False = turn serversaving off 98 | :return: 99 | 100 | Future expansion to allow config of server saving state glabally in config. Plan to include a glabal 101 | config option for periodic or continuous server disk saving of the minecraft server. 102 | """ 103 | if desiredstate: 104 | self.api.minecraft.console("save-all flush") # flush argument is required 105 | self.api.minecraft.console("save-on") 106 | else: 107 | self.api.minecraft.console("save-all flush") # flush argument is required 108 | self.api.minecraft.console("save-off") 109 | time.sleep(0.5) 110 | 111 | def _performbackup(self): 112 | timestamp = int(time.time()) 113 | 114 | # Turn off server saves... 115 | self._doserversaving(False) 116 | 117 | # Create tar arguments 118 | filename = "backup-%s.tar" % datetime.datetime.fromtimestamp(int(timestamp)).strftime("%Y-%m-%d_%H.%M.%S") 119 | if self.config["Backups"]["backup-compression"]: 120 | filename += ".gz" 121 | arguments = ["tar", "czf", "%s/%s" % (self.config["Backups"]["backup-location"].replace(" ", "\\ "), 122 | filename)] 123 | else: 124 | arguments = ["tar", "cfpv", "%s/%s" % (self.config["Backups"]["backup-location"], filename)] 125 | 126 | # Process begin Events 127 | if not self.wrapper.events.callevent("wrapper.backupBegin", {"file": filename}): 128 | self.log.warning("A backup was scheduled, but was cancelled by a plugin!") 129 | return 130 | if self.config["Backups"]["backup-notification"]: 131 | self.api.minecraft.broadcast("&cBacking up... lag may occur!", irc=False) 132 | 133 | # Do backups 134 | serverpath = self.config["General"]["server-directory"] 135 | for backupfile in self.config["Backups"]["backup-folders"]: 136 | backup_file_and_path = "%s/%s" % (serverpath, backupfile) 137 | if os.path.exists(backup_file_and_path): 138 | arguments.append(backup_file_and_path) 139 | else: 140 | self.log.warning("Backup file '%s' does not exist - canceling backup", backup_file_and_path) 141 | self.wrapper.events.callevent("wrapper.backupFailure", {"reasonCode": 3, 142 | "reasonText": "Backup file '%s' does not exist." 143 | % backup_file_and_path}) 144 | return 145 | statuscode = os.system(" ".join(arguments)) 146 | 147 | # TODO add a wrapper properties config item to set save mode of server 148 | # restart saves, call finish Events 149 | self._doserversaving() 150 | if self.config["Backups"]["backup-notification"]: 151 | self.api.minecraft.broadcast("&aBackup complete!", irc=False) 152 | self.wrapper.events.callevent("wrapper.backupEnd", {"file": filename, "status": statuscode}) 153 | self.backups.append((timestamp, filename)) 154 | 155 | # Prune backups 156 | self.pruneoldbackups(filename) 157 | 158 | # Check for success 159 | if not os.path.exists(self.config["Backups"]["backup-location"] + "/" + filename): 160 | self.wrapper.events.callevent("wrapper.backupFailure", 161 | {"reasonCode": 2, "reasonText": "Backup file didn't exist after the tar " 162 | "command executed - assuming failure."}) 163 | 164 | def _getbackups(self): 165 | if len(self.backups) == 0 and os.path.exists(self.config["Backups"]["backup-location"] + "/backups.json"): 166 | loadcode = getjsonfile("backups", self.config["Backups"]["backup-location"], 167 | encodedas=self.encoding) 168 | if not loadcode: 169 | self.log.error("NOTE - backups.json was unreadable. It might be corrupted. Backups will no " 170 | "longer be automatically pruned.") 171 | self.wrapper.events.callevent("wrapper.backupFailure", { 172 | "reasonCode": 4, 173 | "reasonText": "backups.json is corrupted. Please contact an administer instantly, as this " 174 | "may be critical." 175 | }) 176 | self.backups = [] 177 | else: 178 | self.backups = loadcode 179 | else: 180 | if len(os.listdir(self.config["Backups"]["backup-location"])) > 0: 181 | # import old backups from previous versions of Wrapper.py 182 | backuptimestamps = [] 183 | for backupNames in os.listdir(self.config["Backups"]["backup-location"]): 184 | # noinspection PyBroadException,PyUnusedLocal 185 | try: 186 | backuptimestamps.append(int(backupNames[backupNames.find('-') + 1:backupNames.find('.')])) 187 | except Exception as e: 188 | pass 189 | backuptimestamps.sort() 190 | for backupI in backuptimestamps: 191 | self.backups.append((int(backupI), "backup-%s.tar" % str(backupI))) 192 | 193 | def _settime(self): 194 | self.time = time.time() 195 | -------------------------------------------------------------------------------- /wrapper/core/buildinfo.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Do not edit this file to bump versions; it is built automatically 3 | 4 | __version__ = [0, 11, 5] 5 | __build__ = 208 6 | __branch__ = 'dev' 7 | -------------------------------------------------------------------------------- /wrapper/core/config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (C) 2016, 2017 - BenBaptist and Wrapper.py developer(s). 4 | # https://github.com/benbaptist/minecraft-wrapper 5 | # This program is distributed under the terms of the GNU 6 | # General Public License, version 3 or later. 7 | 8 | import os 9 | import sys 10 | import logging 11 | from api.helpers import getjsonfile, putjsonfile 12 | from api.wrapperconfig import * 13 | 14 | 15 | class Config(object): 16 | def __init__(self): 17 | self.log = logging.getLogger('Config') 18 | self.config = {} 19 | self.exit = False 20 | 21 | def loadconfig(self): 22 | # load older versions of wrapper.properties to preserve prior settings. 23 | if os.path.exists("wrapper.properties"): 24 | with open("wrapper.properties", "r") as f: 25 | oldconfig = f.read() 26 | oldconfig = "Deprecated File! Use the 'wrapper.properties.json' instead!\n\n%s" % oldconfig 27 | with open("_wrapper.properties", "w") as f: 28 | f.write(oldconfig) 29 | os.remove("wrapper.properties") 30 | 31 | # Create new config if none exists 32 | if not os.path.exists("wrapper.properties.json"): 33 | putjsonfile(CONFIG, "wrapper.properties", sort=True) 34 | self.exit = True 35 | 36 | # Read existing configuration 37 | self.config = getjsonfile("wrapper.properties") # the only data file that must be UTF-8 38 | if self.config is None: 39 | self.log.error("I think you messed up the Json formatting of your " 40 | "wrapper.properties.json file. " 41 | "Take your file and have it checked at: \n" 42 | "http://jsonlint.com/") 43 | self.exit = True 44 | 45 | # detection and addition must be separated to prevent changing dictionary while iterating over it. 46 | # detect changes 47 | changesmade = False 48 | deprecated_entries = [] 49 | new_sections = [] 50 | new_entries = [] 51 | for section in CONFIG: 52 | if section not in self.config: 53 | self.log.debug("Adding section [%s] to configuration", section) 54 | new_sections.append(section) 55 | changesmade = True 56 | 57 | for configitem in CONFIG[section]: 58 | if section in self.config: 59 | # mark deprecated items for deletion 60 | if configitem in self.config[section]: 61 | if CONFIG[section][configitem] == "deprecated": 62 | self.log.debug("Deprecated item '%s' in section '%s'. - removing it from" 63 | " wrapper properties", configitem, section) 64 | deprecated_entries.append([section, configitem]) 65 | changesmade = True 66 | # mark new items for addition 67 | else: 68 | # handle new items in an existing section 69 | if CONFIG[section][configitem] != "deprecated": # avoid re-adding deprecated items 70 | self.log.debug("Item '%s' in section '%s' not in wrapper properties - adding it!", 71 | configitem, section) 72 | new_entries.append([section, configitem]) 73 | changesmade = True 74 | else: 75 | # handle new items in a (new) section 76 | self.log.debug("Item '%s' in new section '%s' not in wrapper properties - adding it!", 77 | configitem, section) 78 | new_entries.append([section, configitem]) 79 | changesmade = True 80 | 81 | # Apply changes and save. 82 | if changesmade: 83 | # add new section 84 | if len(new_sections) > 0: 85 | for added_section in new_sections: 86 | self.config[added_section] = {} 87 | 88 | # Removed deprecated entries 89 | if len(deprecated_entries) > 0: 90 | for removed in deprecated_entries: 91 | del self.config[removed[0]][removed[1]] 92 | 93 | # Add new entries 94 | if len(new_entries) > 0: 95 | for added in new_entries: 96 | self.config[added[0]][added[1]] = CONFIG[added[0]][added[1]] 97 | 98 | self.save() 99 | self.exit = True 100 | 101 | if self.exit: 102 | self.log.warning( 103 | "Updated wrapper.properties.json file - check and edit configuration if needed and start again.") 104 | sys.exit() 105 | 106 | def change_item(self, section, item, desired_value): 107 | if section in self.config: 108 | if item in self.config[section]: 109 | self.config[section][item] = desired_value 110 | return True 111 | else: 112 | self.log.error("Item '%s' not found in section '%s' of the wrapper.properties.json" % (item, section)) 113 | return False 114 | else: 115 | self.log.error("Section '%s' does not exist in the wrapper.properties.json" % section) 116 | return False 117 | 118 | def save(self): 119 | putjsonfile(self.config, "wrapper.properties", sort=True) 120 | -------------------------------------------------------------------------------- /wrapper/core/consoleuser.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (C) 2016, 2017 - BenBaptist and Wrapper.py developer(s). 4 | # https://github.com/benbaptist/minecraft-wrapper 5 | # This program is distributed under the terms of the GNU 6 | # General Public License, version 3 or later. 7 | 8 | """ 9 | Collection of Virtual player classes used elsewhere. 10 | """ 11 | 12 | import time 13 | from api.helpers import readout 14 | 15 | 16 | # - due to being refrerenced by the external wrapper API that is camelCase 17 | # noinspection PyUnresolvedReferences,PyPep8Naming,PyBroadException 18 | class ConsolePlayer(object): 19 | """ 20 | This class minimally represents the console as a player so 21 | that the console can use wrapper/plugin commands. 22 | 23 | used by core.wrapper.py 24 | 25 | """ 26 | 27 | def __init__(self, wrapper): 28 | self.username = "*Console*" 29 | self.loggedIn = time.time() 30 | self.wrapper = wrapper 31 | self.log = wrapper.log 32 | self.abort = wrapper.halt 33 | 34 | # these map minecraft color codes to "approximate" ANSI 35 | # terminal color used by our color formatter. 36 | self.message_number_coders = {'0': 'black', 37 | '1': 'blue', 38 | '2': 'green', 39 | '3': 'cyan', 40 | '4': 'red', 41 | '5': 'magenta', 42 | '6': 'yellow', 43 | '7': 'white', 44 | '8': 'black', 45 | '9': 'blue', 46 | 'a': 'green', 47 | 'b': 'cyan', 48 | 'c': 'red', 49 | 'd': 'magenta', 50 | 'e': 'yellow', 51 | 'f': 'white' 52 | } 53 | 54 | # these do the same for color names (things like 'red', 55 | # 'white', 'yellow, etc, not needing conversion...) 56 | self.messsage_color_coders = {'dark_blue': 'blue', 57 | 'dark_green': 'green', 58 | 'dark_aqua': 'cyan', 59 | 'dark_red': 'red', 60 | 'dark_purple': 'magenta', 61 | 'gold': 'yellow', 62 | 'gray': 'white', 63 | 'dark_gray': 'black', 64 | 'aqua': 'cyan', 65 | 'light_purple': 'magenta' 66 | } 67 | 68 | @staticmethod 69 | def isOp(): 70 | return 10 71 | 72 | def __str__(self): 73 | """ 74 | Permit the console to have a nice display instead of 75 | returning the object instance notation. 76 | """ 77 | return "CONSOLE OPERATOR" 78 | 79 | def message(self, message): 80 | """ 81 | This is a substitute for the player.message() that plugins and 82 | the command interface expect for player objects. It translates 83 | chat type messages intended for a minecraft client into 84 | printed colorized console lines. 85 | """ 86 | displaycode, displaycolor = "5", "magenta" 87 | display = str(message) 88 | if type(message) is dict: 89 | jsondisplay = message 90 | else: 91 | jsondisplay = False 92 | # format "&c" type color (roughly) to console formatters color 93 | if display[0:1] == "&": 94 | displaycode = display[1:1] 95 | display = display[2:] 96 | if displaycode in self.message_number_coders: 97 | displaycolor = self.message_number_coders[displaycode] 98 | if jsondisplay: # or use json formatting, if available 99 | if "text" in jsondisplay: 100 | display = jsondisplay["text"] 101 | if "color" in jsondisplay: 102 | displaycolor = jsondisplay["color"] 103 | if displaycolor in self.messsage_color_coders: 104 | displaycolor = self.messsage_color_coders[displaycolor] 105 | readout(display, "", "", pad=15, command_text_fg=displaycolor, 106 | usereadline=self.wrapper.use_readline) 107 | 108 | @staticmethod 109 | def hasPermission(*args): 110 | """return console as always having the requested permission""" 111 | if args: 112 | return True 113 | -------------------------------------------------------------------------------- /wrapper/core/entities.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (C) 2016, 2017 - BenBaptist and Wrapper.py developer(s). 4 | # https://github.com/benbaptist/minecraft-wrapper 5 | # This program is distributed under the terms of the GNU 6 | # General Public License, version 3 or later. 7 | 8 | 9 | from time import time as currtime 10 | from api.helpers import putjsonfile 11 | from utils.entities import ENTITIES, PRE1_11_RENAMES 12 | from utils.items import BLOCKS 13 | 14 | try: 15 | import requests 16 | except ImportError: 17 | requests = False 18 | 19 | # Sample 20 | # ENTITIES = { 21 | # "1": { 22 | # "name": "item" 23 | # }, 24 | # "2": { 25 | # "name": "xp_orb" 26 | # }, 27 | # "3": { 28 | # "name": "area_effect_cloud" ... 29 | 30 | 31 | # Sample 32 | # BLOCKS = { 33 | # 0: { 34 | # "meta": { 35 | # 0: "Air" 36 | # }, 37 | # "tilename": "air" 38 | # }, 39 | # 1: { 40 | # "meta": { 41 | # 0: "Stone", 42 | # 1: "Granite", 43 | # 2: "Polished Granite", 44 | # 3: "Diorite", 45 | # 4: "Polished Diorite", 46 | # 5: "Andesite", 47 | # 6: "Polished Andesite" 48 | # }, 49 | # "tilename": "stone" 50 | # }, ... 51 | 52 | OBJECTS = { 53 | 1: "Boat", 54 | 2: "Item Stack", 55 | 3: "Area Effect Cloud", 56 | 10: "Minecart", 57 | 11: "Minecart (storage), 0.98, 0.7", 58 | 12: "(unused since 1.6.x), Minecart (powered), 0.98,0.7", 59 | 50: "Activated TNT", 60 | 51: "EnderCrystal", 61 | 60: "Arrow (projectile)", 62 | 61: "Snowball (projectile)", 63 | 62: "Egg (projectile)", 64 | 63: "FireBall (ghast projectile)", 65 | 64: "FireCharge (blaze projectile)", 66 | 65: "Thrown Enderpearl", 67 | 66: "Wither Skull (projectile)", 68 | 67: "Shulker Bullet", 69 | 70: "Falling Objects", 70 | 71: "Item frames", 71 | 72: "Eye of Ender", 72 | 73: "Thrown Potion", 73 | 74: "Falling Dragon Egg", 74 | 75: "Thrown Exp Bottle", 75 | 76: "Firework Rocket", 76 | 77: "Leash Knot", 77 | 78: "ArmorStand", 78 | 90: "Fishing Float", 79 | 91: "Spectral Arrow", 80 | 92: "Tipped Arrow", 81 | 93: "Dragon Fireball" 82 | } 83 | 84 | 85 | class Entities(object): 86 | def __init__(self, apply_pre1_11=False): 87 | self.entitylist = ENTITIES 88 | if apply_pre1_11: 89 | self.apply_pre1_11() 90 | # provide a readout file for the user's reference. 91 | putjsonfile(self.entitylist, "entities", "./wrapper-data/json/") 92 | 93 | def apply_pre1_11(self): 94 | for entities in self.entitylist: 95 | if self.entitylist[entities]["name"] in PRE1_11_RENAMES: 96 | self.entitylist[entities]["name"] = PRE1_11_RENAMES[self.entitylist[entities]["name"]] 97 | else: 98 | component = self.entitylist[entities]["name"].split("_") 99 | component = [item.capitalize() for item in component] 100 | concatenated = "".join(component) 101 | self.entitylist[entities]["name"] = concatenated 102 | 103 | 104 | class Items(object): 105 | def __init__(self): 106 | self.itemslist = BLOCKS 107 | 108 | 109 | class Objects(object): 110 | """Objects are actually probably a part of entities""" 111 | def __init__(self): 112 | self.objectlist = OBJECTS 113 | 114 | 115 | class Entity(object): 116 | def __init__(self, eid, uuid, entitytype, entityname, position, look, isobject, playerclientname): 117 | self.eid = eid # Entity ID 118 | self.uuid = uuid # Entity UUID 119 | self.entitytype = entitytype # Type of Entity 120 | self.position = position # (x, y, z) 121 | self.look = look # Head Position 122 | self.rodeBy = False 123 | self.riding = False 124 | self.isObject = isobject # Boat/Minecart/other non-living Entities are objects 125 | self.entityname = entityname 126 | self.active = currtime() 127 | self.clientname = playerclientname 128 | 129 | def __str__(self): 130 | return self.entitytype 131 | 132 | def move_relative(self, position): 133 | """ Move the entity relative to their position, unless it is illegal. 134 | 135 | This only "tracks" its' position (does not set the position) 136 | 137 | Args: 138 | position: 139 | """ 140 | x, y, z = position 141 | oldposition = [self.position[0], self.position[1], self.position[2]] 142 | oldposition[0] += x / (128 * 32.0) 143 | oldposition[1] += y / (128 * 32.0) 144 | oldposition[2] += z / (128 * 32.0) 145 | self.position = (oldposition[0], oldposition[1], oldposition[2]) 146 | if self.rodeBy: 147 | self.rodeBy.position = self.position 148 | 149 | def teleport(self, position): 150 | """ Track entity teleports to a specific location. """ 151 | self.position = (position[0] / 32, position[1] / 32, position[2] / 32) # Fixed point numbers... 152 | if self.rodeBy: 153 | self.rodeBy.position = self.position 154 | 155 | def about_entity(self): 156 | info = { 157 | "eid": self.eid, 158 | "uuid": str(self.uuid), 159 | "type": self.entitytype, 160 | "position": [int(self.position[0]), int(self.position[1]), int(self.position[2])], 161 | "rodeBy": self.rodeBy, 162 | "Riding": self.riding, 163 | "isObject": self.isObject, 164 | "name": self.entityname, 165 | "player": self.clientname 166 | } 167 | return info 168 | 169 | 170 | def _test(): 171 | pass 172 | # from api.helpers import getjsonfile, putjsonfile 173 | 174 | # "http://minecraft-ids.grahamedgecombe.com/items.json" 175 | # "http://minecraft-ids.grahamedgecombe.com/entities.json" 176 | 177 | # (save items/entities on disk) 178 | 179 | # x = getjsonfile("ents", "/home/surest/github/Wrapper/wrapper/utils") 180 | # putjsonfile(x, "ents", "/home/surest/github/Wrapper/wrapper/utils", 181 | # indent_spaces=4) 182 | 183 | # blocks = {} 184 | # for item in x: 185 | # if item["type"] in blocks: 186 | # blocks[item["type"]]["meta"][item["meta"]] = item["name"] 187 | # else: 188 | # blocks[item["type"]] = {"tilename": item["text_type"], 189 | # "meta": {item["meta"]: item["name"]}} 190 | 191 | # entities = {} 192 | # for item in x: 193 | # entities[item["type"]] = {"name": item["text_type"], "alt_name": 194 | # item["name"]} 195 | 196 | # putjsonfile(entities, "processed", 197 | # "/home/surest/github/Wrapper/wrapper/utils", indent_spaces=4) 198 | 199 | # x = Entities() 200 | # x.apply_pre1_11() 201 | # putjsonfile(x.entitylist, "processed", 202 | # "/home/surest/github/Wrapper/wrapper/utils", indent_spaces=4) 203 | 204 | if __name__ == "__main__": 205 | _test() 206 | -------------------------------------------------------------------------------- /wrapper/core/events.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (C) 2016, 2017 - BenBaptist and Wrapper.py developer(s). 4 | # https://github.com/benbaptist/minecraft-wrapper 5 | # This program is distributed under the terms of the GNU 6 | # General Public License, version 3 or later. 7 | 8 | 9 | class Events(object): 10 | 11 | def __init__(self, wrapper): 12 | self.wrapper = wrapper 13 | self.log = wrapper.log 14 | self.listeners = [] 15 | self.events = {} 16 | 17 | def __getitem__(self, index): 18 | if not type(index) == str: 19 | raise Exception("A string must be passed - got %s" % type(index)) 20 | return self.events[index] 21 | 22 | def __setitem__(self, index, value): 23 | if not type(index) == str: 24 | raise Exception("A string must be passed - got %s" % type(index)) 25 | self.events[index] = value 26 | return self.events[index] 27 | 28 | def __delitem__(self, index): 29 | if not type(index) == str: 30 | raise Exception("A string must be passed - got %s" % type(index)) 31 | del self.events[index] 32 | 33 | def __iter__(self): 34 | for i in self.events: 35 | yield i 36 | 37 | def callevent(self, event, payload): 38 | if event == "player.runCommand": 39 | if not self.wrapper.commands.playercommand(payload): 40 | return False 41 | 42 | # listeners is normally empty. Supposed to be part of the blockForEvent code. 43 | for sock in self.listeners: 44 | sock.append({"event": event, "payload": payload}) 45 | 46 | payload_status = True 47 | # old_payload = payload # retaining the original payload might be helpful for the future features. 48 | 49 | # in all plugins with this event listed.. 50 | for plugin_id in self.events: 51 | 52 | # for each plugin... 53 | if event in self.events[plugin_id]: 54 | 55 | # run the plugin code and get the plugin's return value 56 | result = None 57 | try: 58 | # 'self.events[plugin_id][event]' is the 59 | # pass 'payload' as the argument for the plugin-defined event code function 60 | result = self.events[plugin_id][event](payload) 61 | except Exception as e: 62 | self.log.exception("Plugin '%s' \nexperienced an exception calling '%s': \n%s", 63 | plugin_id, event, e) 64 | 65 | # Evaluate this plugin's result 66 | # every plugin will be given equal time to run it's event code. however, if one plugin 67 | # returns a False, no payload changes will be possible. 68 | # 69 | if result is None: # Don't change the payload status 70 | pass 71 | elif result is False: # mark this event permanently as False 72 | payload_status = False 73 | elif result is True: # Again, don't change the payload status 74 | pass 75 | else: 76 | # A payload is being returned 77 | # if any plugin rejects the event, no payload changes will be authorized. 78 | if payload_status is not False: 79 | # the next plugin looking at this event sees the new payload. 80 | if type(result) == dict: 81 | payload, payload_status = result 82 | else: 83 | # non dictionary payloads are deprecated and will be overridden by dict payloads 84 | # dict payloads are those that return the payload in the same format as it was passed. 85 | payload_status = result 86 | return payload_status 87 | -------------------------------------------------------------------------------- /wrapper/core/exceptions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (C) 2016, 2017 - BenBaptist and Wrapper.py developer(s). 4 | # https://github.com/benbaptist/minecraft-wrapper 5 | # This program is distributed under the terms of the GNU 6 | # General Public License, version 3 or later. 7 | 8 | """ 9 | Custom Wrapper Exception Classes 10 | """ 11 | 12 | 13 | class WrapperException(Exception): 14 | """ Base Wrapper Exception Class """ 15 | pass 16 | 17 | 18 | class UnsupportedOSException(WrapperException): 19 | """ Exception raised when a command is not supported by the OS """ 20 | pass 21 | 22 | 23 | class NonExistentPlugin(WrapperException): 24 | """ Exception raised when a plugin does not exist """ 25 | pass 26 | 27 | 28 | class MalformedFileError(WrapperException): 29 | """ Exception raised on parse error """ 30 | pass 31 | 32 | 33 | class InvalidServerStartedError(WrapperException): 34 | """ Exception raised when the MCServer is not in the correct state """ 35 | pass 36 | 37 | 38 | class UnsupportedMinecraftProtocol(WrapperException): 39 | """ Exception raised when a non-supported protocol is passed to mcpacket.py """ 40 | pass 41 | -------------------------------------------------------------------------------- /wrapper/core/mcuuid.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (C) 2016, 2017 - BenBaptist and Wrapper.py developer(s). 4 | # https://github.com/benbaptist/minecraft-wrapper 5 | # This program is distributed under the terms of the GNU 6 | # General Public License, version 3 or later. 7 | 8 | # system imports 9 | import uuid 10 | import hashlib 11 | import time 12 | import requests 13 | 14 | 15 | class MCUUID(uuid.UUID): 16 | """ 17 | This class is currently not being used, but may be beneficial in regards to 18 | conforming UUIDs 19 | """ 20 | 21 | # noinspection PyShadowingBuiltins 22 | def __init__(self, hex=None, bytes=None, bytes_le=None, fields=None, int=None, version=None): 23 | super(MCUUID, self).__init__(hex, bytes, bytes_le, fields, int, version) 24 | 25 | @property 26 | def string(self): 27 | return str(self) 28 | 29 | 30 | class UUIDS(object): 31 | def __init__(self, wrapper): 32 | self.wrapper = wrapper 33 | self.log = wrapper.log 34 | self.usercache = self.wrapper.usercache 35 | 36 | @staticmethod 37 | def formatuuid(playeruuid): 38 | """ 39 | Takes player's hex string uuid with no dashes and returns it as astring with the dashes 40 | 41 | :param playeruuid: string of player uuid with no dashes (such as you might get back from Mojang) 42 | :return: string hex format "8-4-4-4-12" 43 | """ 44 | return MCUUID(hex=playeruuid).string 45 | 46 | @staticmethod 47 | def getuuidfromname(name): 48 | """ 49 | Get the offline vanilla server UUID 50 | 51 | :param name: The playername (gets hashed as "OfflinePlayer:") 52 | :return: a MCUUID object based on the name 53 | """ 54 | playername = "OfflinePlayer:%s" % name 55 | m = hashlib.md5() 56 | m.update(playername.encode("utf-8")) 57 | d = bytearray(m.digest()) 58 | d[6] &= 0x0f 59 | d[6] |= 0x30 60 | d[8] &= 0x3f 61 | d[8] |= 0x80 62 | return MCUUID(bytes=bytes(d)) 63 | 64 | def getuuidbyusername(self, username, forcepoll=False): 65 | """ 66 | Lookup user's UUID using the username. Primarily searches the wrapper usercache. If record is 67 | older than 30 days (or cannot be found in the cache), it will poll Mojang and also attempt a full 68 | update of the cache using getusernamebyuuid as well. 69 | 70 | :param username: username as string 71 | :param forcepoll: force polling even if record has been cached in past 30 days 72 | :returns: returns the online/Mojang MCUUID object from the given name. Updates the wrapper usercache.json 73 | Yields False if failed. 74 | """ 75 | user_name = "%s" % username # create a new name variable that is unrelated the the passed variable. 76 | frequency = 2592000 # 30 days. 77 | if forcepoll: 78 | frequency = 3600 # do not allow more than hourly 79 | user_uuid_matched = None 80 | for useruuid in self.usercache: # try wrapper cache first 81 | if user_name == self.usercache[useruuid]["localname"]: 82 | # This search need only be done by 'localname', which is always populated and is always 83 | # the same as the 'name', unless a localname has been assigned on the server (such as 84 | # when "falling back' on an old name).''' 85 | if (time.time() - self.usercache[useruuid]["time"]) < frequency: 86 | return MCUUID(useruuid) 87 | # if over the time frequency, it needs to be updated by using actual last polled name. 88 | user_name = self.usercache[useruuid]["name"] 89 | user_uuid_matched = useruuid # cache for later in case multiple name changes require a uuid lookup. 90 | 91 | # try mojang (a new player or player changed names.) 92 | # requests seems to =have a builtin json() method 93 | r = requests.get("https://api.mojang.com/users/profiles/minecraft/%s" % user_name) 94 | if r.status_code == 200: 95 | useruuid = self.formatuuid(r.json()["id"]) # returns a string uuid with dashes 96 | correctcapname = r.json()["name"] 97 | if user_name != correctcapname: # this code may not be needed if problems with /perms are corrected. 98 | self.log.warning("%s's name is not correctly capitalized (offline name warning!)", correctcapname) 99 | # This should only run subject to the above frequency (hence use of forcepoll=True) 100 | nameisnow = self.getusernamebyuuid(useruuid, forcepoll=True) 101 | if nameisnow: 102 | return MCUUID(useruuid) 103 | return False 104 | elif r.status_code == 204: # try last matching UUID instead. This will populate current name back in 'name' 105 | if user_uuid_matched: 106 | nameisnow = self.getusernamebyuuid(user_uuid_matched, forcepoll=True) 107 | if nameisnow: 108 | return MCUUID(user_uuid_matched) 109 | return False 110 | else: 111 | return False # No other options but to fail request 112 | 113 | def getusernamebyuuid(self, useruuid, forcepoll=False): 114 | """ 115 | Returns the username from the specified UUID. 116 | If the player has never logged in before and isn't in the user cache, it will poll Mojang's API. 117 | Polling is restricted to once per day. 118 | Updates will be made to the wrapper usercache.json when this function is executed. 119 | 120 | :param useruuid: string UUID 121 | :param forcepoll: force polling even if record has been cached in past 30 days. 122 | 123 | :returns: returns the username from the specified uuid, else returns False if failed. 124 | """ 125 | frequency = 2592000 # if called directly, can update cache daily (refresh names list, etc) 126 | if forcepoll: 127 | frequency = 600 # 10 minute limit 128 | 129 | theirname = None 130 | if useruuid in self.usercache: # if user is in the cache... 131 | theirname = self.usercache[useruuid]["localname"] 132 | if int((time.time() - self.usercache[useruuid]["time"])) < frequency: 133 | return theirname # dont re-poll if same time frame (daily = 86400). 134 | # continue on and poll... because user is not in cache or is old record that needs re-polled 135 | # else: # user is not in cache 136 | names = self._pollmojanguuid(useruuid) 137 | numbofnames = 0 138 | if names is not False: # service returned data 139 | numbofnames = len(names) 140 | 141 | if numbofnames == 0: 142 | if theirname is not None: 143 | self.usercache[useruuid]["time"] = time.time() - frequency + 7200 # may try again in 2 hours 144 | return theirname 145 | return False # total FAIL 146 | 147 | pastnames = [] 148 | if useruuid not in self.usercache: 149 | self.usercache[useruuid] = { 150 | "time": time.time(), 151 | "original": None, 152 | "name": None, 153 | "online": True, 154 | "localname": None, 155 | "IP": None, 156 | "names": [] 157 | } 158 | 159 | for nameitem in names: 160 | if "changedToAt" not in nameitem: # find the original name 161 | self.usercache[useruuid]["original"] = nameitem["name"] 162 | self.usercache[useruuid]["online"] = True 163 | self.usercache[useruuid]["time"] = time.time() 164 | if numbofnames == 1: # The user has never changed their name 165 | self.usercache[useruuid]["name"] = nameitem["name"] 166 | if self.usercache[useruuid]["localname"] is None: 167 | self.usercache[useruuid]["localname"] = nameitem["name"] 168 | break 169 | else: 170 | # Convert java milleseconds to time.time seconds 171 | changetime = nameitem["changedToAt"] / 1000 172 | oldname = nameitem["name"] 173 | if len(pastnames) == 0: 174 | pastnames.append({"name": oldname, "date": changetime}) 175 | continue 176 | if changetime > pastnames[0]["date"]: 177 | pastnames.insert(0, {"name": oldname, "date": changetime}) 178 | else: 179 | pastnames.append({"name": oldname, "date": changetime}) 180 | self.usercache[useruuid]["names"] = pastnames 181 | if numbofnames > 1: 182 | self.usercache[useruuid]["name"] = pastnames[0]["name"] 183 | if self.usercache[useruuid]["localname"] is None: 184 | self.usercache[useruuid]["localname"] = pastnames[0]["name"] 185 | return self.usercache[useruuid]["localname"] 186 | 187 | def _pollmojanguuid(self, user_uuid): 188 | """ 189 | attempts to poll Mojang with the UUID 190 | :param user_uuid: string uuid with dashes 191 | :returns: 192 | False - could not resolve the uuid 193 | - otherwise, a list of names... 194 | """ 195 | 196 | r = requests.get( 197 | "https://api.mojang.com/user/profiles/%s/names" % 198 | str(user_uuid).replace("-", "")) 199 | if r.status_code == 200: 200 | return r.json() 201 | if r.status_code == 204: 202 | return False 203 | else: 204 | rx = requests.get("https://status.mojang.com/check") 205 | if rx.status_code == 200: 206 | rx = rx.json() 207 | for entry in rx: 208 | if "account.mojang.com" in entry: 209 | if entry["account.mojang.com"] == "green": 210 | self.log.warning("Mojang accounts is green, but request failed - have you " 211 | "over-polled (large busy server) or supplied an incorrect UUID??") 212 | self.log.warning("uuid: %s", user_uuid) 213 | self.log.warning("response: \n%s", str(rx)) 214 | return False 215 | elif entry["account.mojang.com"] in ("yellow", "red"): 216 | self.log.warning("Mojang accounts is experiencing issues (%s).", 217 | entry["account.mojang.com"]) 218 | return False 219 | self.log.warning("Mojang Status found, but corrupted or in an unexpected format (status " 220 | "code %s)", r.status_code) 221 | return False 222 | else: 223 | self.log.warning("Mojang Status not found - no internet connection, perhaps? " 224 | "(status code may not exist)") 225 | try: 226 | return self.usercache[user_uuid]["name"] 227 | except TypeError: 228 | return False 229 | -------------------------------------------------------------------------------- /wrapper/core/permissions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (C) 2016, 2017 - BenBaptist and Wrapper.py developer(s). 4 | # https://github.com/benbaptist/minecraft-wrapper 5 | # This program is distributed under the terms of the GNU 6 | # General Public License, version 3 or later. 7 | 8 | 9 | import ast 10 | import fnmatch 11 | import copy 12 | import json 13 | 14 | 15 | class Permissions(object): 16 | """All permissions logic for wrapper. with 1.0.0 release (and 17 | all earlier dev versions), we will start enforcing the use of 18 | all lowercase groups and permissions. The return items on 19 | these functions are generally intended for use as a printout 20 | for the calling function; either a console message or 21 | player.message(). 22 | 23 | Pass all UUIDs as string! 24 | 25 | players are only indentified by UUID. 26 | 27 | """ 28 | 29 | def __init__(self, wrapper): 30 | self.wrapper = wrapper 31 | self.log = self.wrapper.log 32 | 33 | # permissions storage Data object 34 | self.permissions = wrapper.permissions 35 | 36 | # populate dictionary items to prevent errors due to missing items 37 | if "groups" not in self.permissions: 38 | self.permissions["groups"] = {} 39 | if "users" not in self.permissions: 40 | self.permissions["users"] = {} 41 | 42 | # Remove deprecated item 43 | if "Default" in self.permissions["groups"]: 44 | if len(self.permissions["groups"]["Default"]["permissions"]) > 0: 45 | self.log.error( 46 | "Your permissions structure contains a 'Default' group" 47 | " item with some permissions in it. This is now" 48 | " deprecated by Wrapper's permission logic. Use the" 49 | " 'registerPermission' API item to register default" 50 | " permissions. Manually delete the 'Default' group to" 51 | " make this error go away...") 52 | else: 53 | result = self.group_delete("Default") 54 | self.log.debug(result) 55 | 56 | # enforcing lowercase perms now, clean data up 57 | self.empty_user = {"groups": [], "permissions": {}} 58 | self.clean_perms_data() 59 | 60 | def fill_user(self, uuid): 61 | self.permissions["users"][uuid] = copy.deepcopy(self.empty_user) 62 | 63 | def clean_perms_data(self): 64 | 65 | deletes = [] 66 | for user in self.permissions["users"]: 67 | if self.permissions[ 68 | "users"][user] == self.empty_user: 69 | deletes.append(user) 70 | for stale_user in deletes: 71 | del self.permissions["users"][stale_user] 72 | 73 | if "converted" in self.permissions: 74 | return 75 | self.permissions["converted"] = "Yes" 76 | permstring = json.dumps(self.permissions) 77 | newstring = permstring.lower() 78 | self.permissions = json.loads(newstring) 79 | 80 | def group_create(self, groupname): 81 | """Will create a new (lowercase) groupname.""" 82 | groupname = groupname.lower() 83 | if groupname in self.permissions["groups"]: 84 | return "Group '%s' already exists!" % groupname 85 | 86 | self.permissions["groups"][groupname] = {"permissions": {}} 87 | return "Created a new permissions group '%s'." % groupname 88 | 89 | def group_delete(self, groupname): 90 | """Will attempt to delete groupname, regardless of case.""" 91 | deletename = groupname.lower() 92 | if deletename in self.permissions["groups"]: 93 | self.permissions["groups"].pop(deletename) 94 | return "Deleted permissions group '%s'." % deletename 95 | if groupname in self.permissions["groups"]: 96 | self.permissions["groups"].pop(groupname) 97 | return "Deleted permissions group '%s'." % groupname 98 | return "Group '%s' does not exist!" % deletename 99 | 100 | def group_set_permission(self, groupname, node="", value=True): 101 | """Sets a permission node for a group.""" 102 | setname = groupname.lower() 103 | 104 | # Group not found 105 | if not self.group_exists(setname): 106 | return "Group '%s' does not exist!" % groupname 107 | 108 | # ensure valid node 109 | if not node or node == "": 110 | return "Invalid permission node name: '%s'" % node 111 | 112 | # allow string true/false or boolean True/False as argument 113 | try: 114 | # noinspection PyUnresolvedReferences 115 | if value.lower() in ("true", "false"): 116 | value = ast.literal_eval(value) 117 | # else items default to True below - "value = bool(value) or False" 118 | except AttributeError: 119 | pass 120 | value = bool(value) or False 121 | 122 | # ensure lower case 123 | setnode = node.lower() 124 | 125 | # set the node 126 | self.permissions["groups"][setname]["permissions"][setnode] = value 127 | return "Added node/group '%s' to Group '%s'!" % (setnode, setname) 128 | 129 | def group_delete_permission(self, group, node): 130 | 131 | setgroup = group.lower() 132 | setnode = node.lower() 133 | if not self.group_exists(setgroup): 134 | return "Group '%s' does not exist!" % group 135 | 136 | if setnode in self.permissions["groups"][setgroup]["permissions"]: 137 | del self.permissions["groups"][setgroup]["permissions"][setnode] 138 | return "Removed permission node '%s' from group '%s'." % ( 139 | setnode, setgroup) 140 | 141 | def group_exists(self, groupname): 142 | if groupname.lower() in self.permissions["groups"]: 143 | return True 144 | return False 145 | 146 | def _group_match(self, node, group, all_groups): 147 | """return true if conditions met: 148 | 1) (node in self.permissions["groups"]) -> the group exists 149 | 2) self.permissions["groups"][group]["permissions"][node] -> 150 | the node is set to True 151 | 3) node not already in all_groups 152 | """ 153 | if (node in self.permissions["groups"]) and self.permissions[ 154 | "groups"][group]["permissions"][node] and ( 155 | node not in all_groups): 156 | return True 157 | return False 158 | 159 | def _group_find_children(self, groups): 160 | # Start with all groups user is in: 161 | allgroups = groups 162 | itemstoprocess = groups[:] 163 | 164 | while len(itemstoprocess) > 0: 165 | # pop out each group to process one-by-one 166 | groupname = itemstoprocess.pop(0) 167 | 168 | # this must be checked because a race condition can 169 | # render the groupname non-existent. 170 | if groupname in self.permissions["groups"]: 171 | for group_node in self.permissions[ 172 | "groups"][groupname]["permissions"]: 173 | if self._group_match(group_node, groupname, allgroups): 174 | allgroups.append(group_node.lower()) 175 | itemstoprocess.append(group_node) 176 | return allgroups 177 | 178 | def has_permission( 179 | self, uuid, node=None, group_match=True, find_child_groups=True): 180 | """If the uuid has the specified permission node (either 181 | directly, or inherited from a group that it is in), 182 | it will return the value (usually True) of the node. 183 | Otherwise, it returns False. 184 | """ 185 | if uuid not in self.permissions["users"]: 186 | self.fill_user(uuid) 187 | # we dont just return false because it could be a first- 188 | # time check for a default or None permission. 189 | 190 | if node is None: 191 | return True 192 | 193 | # ensure lower case 194 | node = node.lower() 195 | 196 | # user has permission directly 197 | for perm in self.permissions["users"][uuid]["permissions"]: 198 | if node in fnmatch.filter([node], perm): 199 | return self.permissions[ 200 | "users"][uuid]["permissions"][perm] 201 | 202 | # return a registered permission; 203 | for pid in self.wrapper.registered_permissions: 204 | if node in self.wrapper.registered_permissions[pid]: 205 | return self.wrapper.registered_permissions[pid][node] 206 | 207 | # an optional way out because group processing can be expensive 208 | if not group_match: 209 | return False 210 | 211 | # summary of groups, which will include child groups 212 | allgroups = [] 213 | 214 | # get the user's groups 215 | for group in self.permissions["users"][uuid]["groups"]: 216 | allgroups.append(group) 217 | 218 | if find_child_groups: 219 | allgroups = self._group_find_children(allgroups) 220 | 221 | # return if group matches 222 | for group in allgroups: 223 | # this must be checked because a race condition can 224 | # render the groupname non-existent. 225 | if group in self.permissions["groups"]: 226 | for perm in self.permissions["groups"][group]["permissions"]: 227 | if node in fnmatch.filter([node], perm): 228 | return self.permissions["groups"][group][ 229 | "permissions"][perm] 230 | 231 | # no permission; 232 | return False 233 | 234 | def set_permission(self, uuid, node, value=True): 235 | """Adds the specified permission node and optionally a 236 | (boolean) value for that permission. For instance, 237 | set to False will cause denial of permission. 238 | """ 239 | # allow string true/false or boolean True/False as argument 240 | try: 241 | # noinspection PyUnresolvedReferences 242 | if value.lower() in ("true", "false"): 243 | value = ast.literal_eval(value) 244 | # else items default to True below - "value = bool(value) or False" 245 | except AttributeError: 246 | pass 247 | value = bool(value) or False 248 | 249 | if uuid not in self.permissions["users"]: 250 | self.fill_user(uuid) 251 | 252 | self.permissions["users"][uuid]["permissions"][node.lower()] = value 253 | 254 | def remove_permission(self, uuid, node): 255 | """Completely removes a permission node from the player. They 256 | will still inherit this permission from their groups or from 257 | plugin defaults. Returns True/False success.""" 258 | 259 | if uuid not in self.permissions["users"]: 260 | self.fill_user(uuid) 261 | # Since the user did not even have a permission, return. 262 | return True 263 | node = node.lower() 264 | 265 | if node in self.permissions["users"][uuid]["permissions"]: 266 | del self.permissions["users"][uuid]["permissions"][node] 267 | return True 268 | 269 | self.log.debug("Uuid:%s does not have permission node '%s'" % ( 270 | uuid, node)) 271 | return False 272 | 273 | def has_group(self, uuid, group): 274 | """Returns a boolean of whether or not the player is in 275 | the specified permission group. 276 | """ 277 | group_lower = group.lower() 278 | 279 | # group does not exist 280 | if not self.group_exists(group_lower): 281 | return False 282 | 283 | # user had no permission data 284 | if uuid not in self.permissions["users"]: 285 | self.fill_user(uuid) 286 | return False 287 | 288 | # user has this group ... 289 | if group_lower in self.permissions["users"][uuid]["groups"]: 290 | return True 291 | 292 | # user does not have the group 293 | return False 294 | 295 | def get_groups(self, uuid): 296 | """Returns a list of permission groups that the player is in. 297 | :returns: a list of groups the user is in. 298 | """ 299 | 300 | if uuid not in self.permissions["users"]: 301 | self.fill_user(uuid) 302 | # Had not permissions set at all.. 303 | return [] 304 | return self.permissions["users"][uuid]["groups"] 305 | 306 | def set_group(self, uuid, group, creategroup=False): 307 | """ 308 | Adds the player to a specified group. Returns False if 309 | group does not exist (set debiug to see error). 310 | 311 | 312 | :returns: Boolean; True if operation succeeds, False 313 | if it fails (set debug mode to see/log error). 314 | 315 | """ 316 | # (deprecate uppercase checks once wrapper hits 2.x.x) 317 | group = group.lower() 318 | if group not in self.permissions["groups"]: 319 | if creategroup: 320 | self.log.warning("No group with the name '%s' exists-" 321 | " creating a new group!", group) 322 | self.group_create(group) 323 | else: 324 | self.log.debug("No group with the name '%s' exists", group) 325 | return False 326 | 327 | if uuid not in self.permissions["users"]: 328 | self.fill_user(uuid) 329 | self.permissions["users"][uuid]["groups"].append(group) 330 | 331 | # return the resulting change (as verification) 332 | return self.has_group(uuid, group) 333 | 334 | def remove_group(self, uuid, group): 335 | """Removes the player to a specified group.""" 336 | 337 | group = group.lower() 338 | if uuid not in self.permissions["users"]: 339 | self.fill_user(uuid) 340 | return True 341 | 342 | if group in self.permissions["users"][uuid]["groups"]: 343 | self.permissions["users"][uuid]["groups"].remove(group) 344 | return True 345 | 346 | self.log.debug("UUID:%s was not part of the group '%s'" % ( 347 | uuid, group)) 348 | return False 349 | 350 | 351 | def _test(): 352 | pass 353 | 354 | 355 | if __name__ == "__main__": 356 | _test() 357 | -------------------------------------------------------------------------------- /wrapper/core/plugins.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (C) 2016, 2017 - BenBaptist and Wrapper.py developer(s). 4 | # https://github.com/benbaptist/minecraft-wrapper 5 | # This program is distributed under the terms of the GNU 6 | # General Public License, version 3 or later. 7 | 8 | import os 9 | import logging 10 | import importlib 11 | import sys 12 | 13 | from api.base import API 14 | from api.helpers import mkdir_p 15 | 16 | 17 | class Plugins(object): 18 | 19 | def __init__(self, wrapper): 20 | self.wrapper = wrapper 21 | self.log = wrapper.log 22 | self.config = wrapper.config 23 | self.plugins = {} 24 | self.plugins_loaded = [] 25 | if "disabled_plugins" not in self.wrapper.storage: 26 | self.wrapper.storage["disabled_plugins"] = [] 27 | if not os.path.exists("wrapper-plugins"): 28 | mkdir_p("wrapper-plugins") 29 | sys.path.append("wrapper-plugins") 30 | 31 | def __getitem__(self, index): 32 | if not type(index) == str: 33 | raise Exception("A string must be passed - got %s" % type(index)) 34 | return self.plugins[index] 35 | 36 | def __setitem__(self, index, value): 37 | if not type(index) == str: 38 | raise Exception("A string must be passed - got %s" % type(index)) 39 | self.plugins[index] = value 40 | return self.plugins[index] 41 | 42 | def __delitem__(self, index): 43 | if not type(index) == str: 44 | raise Exception("A string must be passed - got %s" % type(index)) 45 | del self.plugins[index] 46 | 47 | def __iter__(self): 48 | for i in self.plugins: 49 | yield i 50 | 51 | def loadplugin(self, name, available_files): 52 | if name in self.plugins_loaded: 53 | # Don't try to load a previously errored ( attempted to load..) plugin 54 | return False 55 | self.log.debug("Reading plugin file %s.py ...", name) 56 | # hack to remove previously loaded modules during reloads 57 | if name in sys.modules: 58 | del sys.modules[name] 59 | 60 | plugin = importlib.import_module(name) 61 | pid = getattr(plugin, 'ID', name) # from the plugin head.. e.g., 'ID = "com.benbaptist.plugins.essentials"' 62 | disabled = getattr(plugin, 'DISABLED', False) 63 | dependencies = getattr(plugin, 'DEPENDENCIES', []) 64 | 65 | if pid in self.wrapper.storage["disabled_plugins"] or disabled: 66 | self.log.debug("Plugin '%s' disabled - not loading", name) 67 | return True 68 | 69 | if pid in self.plugins: # Once successfully loaded, further attempts to load the plugin are ignored 70 | self.log.debug("Plugin '%s' is already loaded (probably as a dependency) - not reloading", name) 71 | return True 72 | 73 | # check for unloaded dependencies and develop a list of required dependencies. 74 | good_deps = True 75 | dep_loads = [] 76 | if dependencies: 77 | for dep in dependencies: 78 | # allow a user to specify name or full filename for dependencies 79 | dep_name = dep 80 | if dep[-3:] == '.py': 81 | dep_name = dep[:-3] 82 | if dep_name in available_files: 83 | # if the plugin was already loaded, the dependency is satisfied... 84 | if dep_name not in self.plugins_loaded: 85 | dep_loads.append(dep_name) 86 | else: 87 | good_deps = False 88 | self.log.warn("Plugin '%s'.py is missing a dependency: '%s.py'", name, dep_name) 89 | if not good_deps: 90 | self.log.warn("Plugin '%s'.py failed to load because of missing dependencies.", name) 91 | return False 92 | 93 | # load the required dependencies first. 94 | for dependency in dep_loads: 95 | if self.loadplugin(dependency, available_files): 96 | self.log.debug("Dependency '%s' loaded.", dependency) 97 | self.plugins_loaded.append(name) 98 | else: 99 | self.log.warn("Dependency '%s' could not be loaded.", dependency) 100 | self.log.warn("Plugin '%s'.py failed to load because of missing dependency '%s'.", name, dependency) 101 | self.plugins_loaded.append(name) 102 | return False 103 | 104 | # Finally, initialize this plugin 105 | self.log.debug("Loading plugin %s...", name) 106 | if not getattr(plugin, 'Main', False): 107 | self.log.warn("Plugin '%s' is malformed and missing a class 'Main'", name) 108 | self.plugins_loaded.append(name) 109 | return False 110 | has_onenable = getattr(getattr(plugin, 'Main', False), 'onEnable', False) 111 | if not has_onenable: 112 | self.log.warn("Plugin '%s' is missing an 'onEnable' method.", name) 113 | self.plugins_loaded.append(name) 114 | return False 115 | 116 | main = plugin.Main(API(self.wrapper, name, pid), logging.getLogger(name)) 117 | self.plugins[pid] = {"main": main, "good": True, "module": plugin} # "events": {}, "commands": {}} 118 | self.plugins[pid]["name"] = getattr(plugin, "NAME", name) 119 | self.plugins[pid]["version"] = getattr(plugin, 'VERSION', (0, 1)) 120 | self.plugins[pid]["summary"] = getattr(plugin, 'SUMMARY', None) 121 | self.plugins[pid]["description"] = getattr(plugin, 'DESCRIPTION', None) 122 | self.plugins[pid]["author"] = getattr(plugin, 'AUTHOR', None) 123 | self.plugins[pid]["website"] = getattr(plugin, 'WEBSITE', None) 124 | self.plugins[pid]["filename"] = "%s.py" % name 125 | self.wrapper.commands[pid] = {} 126 | self.wrapper.events[pid] = {} 127 | self.wrapper.registered_permissions[pid] = {} 128 | self.wrapper.help[pid] = {} 129 | main.onEnable() 130 | self.log.info("Plugin %s loaded...", name) 131 | self.plugins_loaded.append(name) 132 | return True 133 | 134 | def unloadplugin(self, plugin): 135 | try: 136 | self.plugins[plugin]["main"].onDisable() 137 | self.log.debug("Plugin %s disabled with no errors.", plugin) 138 | except AttributeError: 139 | self.log.debug("Plugin %s disabled (has no onDisable() event).", plugin) 140 | except Exception as e: 141 | self.log.exception("Error while disabling plugin '%s': \n%s", plugin, e) 142 | finally: 143 | del self.wrapper.commands[plugin] 144 | del self.wrapper.events[plugin] 145 | del self.wrapper.help[plugin] 146 | self.plugins_loaded = [] 147 | 148 | def loadplugins(self): 149 | self.log.info("Loading plugins...") 150 | files = os.listdir("wrapper-plugins") 151 | py_files = [] 152 | for i in files: 153 | name = i[:-3] 154 | ext = i[-3:] 155 | if ext == ".py": 156 | if name not in py_files: 157 | py_files.append(name) 158 | 159 | for names in py_files: 160 | self.loadplugin(names, py_files) 161 | 162 | def disableplugins(self): 163 | self.log.info("Disabling plugins...") 164 | for i in self.plugins: 165 | self.unloadplugin(i) 166 | self.plugins = {} 167 | self.log.info("Disabling plugins...Done!") 168 | 169 | def reloadplugins(self): 170 | self.plugins_loaded = [] 171 | self.disableplugins() 172 | self.loadplugins() 173 | self.log.info("Plugins reloaded") 174 | -------------------------------------------------------------------------------- /wrapper/core/scripts.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (C) 2016, 2017 - BenBaptist and Wrapper.py developer(s). 4 | # https://github.com/benbaptist/minecraft-wrapper 5 | # This program is distributed under the terms of the GNU 6 | # General Public License, version 3 or later. 7 | 8 | # This looks interesting, but not quite sure the intention 9 | 10 | import os 11 | import stat 12 | 13 | from api.base import API 14 | from api.helpers import mkdir_p 15 | 16 | scripts = { 17 | "server-start.sh": """ # This script is called just before the server starts. 18 | # It's safe to make changes to the world file, server.properties, etc. since the server 19 | # has not started yet. 20 | # Arguments passed to this script: None """, 21 | "server-stop.sh": """ # This script is called right after the server has stopped. 22 | # It's safe to make changes to the world file, server.properties, etc. since the server 23 | # is completely shutdown. 24 | # Arguments passed to this script: None """, 25 | "backup-begin.sh": """ # This script is called when a backup starts. 26 | # Note that the backup hasn't started yet at the time of calling this script, and thus 27 | # the file is non-existent. 28 | # Arguments passed to this script: None """, 29 | "backup-finish.sh": """ # This script is called when a backup has finished. 30 | # Arguments passed to this script: backup-filename """ 31 | } 32 | 33 | 34 | # noinspection PyMethodMayBeStatic,PyUnusedLocal 35 | class Scripts(object): 36 | 37 | def __init__(self, wrapper): 38 | self.api = API(wrapper, "Scripts", internal=True) 39 | self.wrapper = wrapper 40 | self.config = wrapper.config 41 | 42 | # Register the events 43 | self.api.registerEvent("server.start", self._startserver) 44 | self.api.registerEvent("server.stopped", self._stopserver) 45 | self.api.registerEvent("wrapper.backupBegin", self._backupbegin) 46 | self.api.registerEvent("wrapper.backupEnd", self._backupend) 47 | 48 | self.createdefaultscripts() 49 | 50 | def createdefaultscripts(self): 51 | if not os.path.exists("wrapper-data"): 52 | mkdir_p("wrapper-data") 53 | if not os.path.exists("wrapper-data/scripts"): 54 | mkdir_p("wrapper-data/scripts") 55 | for script in scripts: 56 | path = "wrapper-data/scripts/%s" % script 57 | if not os.path.exists(path): 58 | with open(path, "w") as f: 59 | f.write(scripts[script]) 60 | os.chmod(path, os.stat(path).st_mode | stat.S_IEXEC) 61 | # Events 62 | 63 | def _startserver(self, payload): 64 | os.system("wrapper-data/scripts/server-start.sh") 65 | 66 | def _stopserver(self, payload): 67 | os.system("wrapper-data/scripts/server-stop.sh") 68 | 69 | def _backupbegin(self, payload): 70 | os.system("wrapper-data/scripts/backup-begin.sh %s" % payload["file"]) 71 | 72 | def _backupend(self, payload): 73 | os.system("wrapper-data/scripts/backup-finish.sh %s" % payload["file"]) 74 | -------------------------------------------------------------------------------- /wrapper/core/storage.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (C) 2016, 2017 - BenBaptist and Wrapper.py developer(s). 4 | # https://github.com/benbaptist/minecraft-wrapper 5 | # This program is distributed under the terms of the GNU 6 | # General Public License, version 3 or later. 7 | 8 | # from __future__ import unicode_literals 9 | 10 | import os 11 | import sys 12 | import time 13 | import logging 14 | from api.helpers import mkdir_p, putjsonfile, getjsonfile 15 | from core.config import Config 16 | import threading 17 | 18 | version = sys.version_info 19 | PY3 = version[0] > 2 20 | 21 | if PY3: 22 | str2 = str 23 | # noinspection PyPep8Naming 24 | import pickle as Pickle 25 | else: 26 | # noinspection PyUnresolvedReferences 27 | str2 = unicode 28 | # noinspection PyUnresolvedReferences 29 | import cPickle as Pickle 30 | 31 | 32 | class Storage(object): 33 | 34 | def __init__(self, name, root="wrapper-data/json", 35 | encoding="default", pickle=True): 36 | self.Data = {} 37 | self.name = name 38 | self.root = root 39 | self.pickle = pickle 40 | self.configManager = Config() 41 | self.configManager.loadconfig() 42 | self.log = logging.getLogger('Storage.py') 43 | 44 | if encoding == "default": 45 | self.encoding = self.configManager.config["General"]["encoding"] 46 | else: 47 | self.encoding = encoding 48 | 49 | if self.pickle: 50 | self.file_ext = "pkl" 51 | else: 52 | self.file_ext = "json" 53 | 54 | self.load() 55 | self.timer = time.time() 56 | self.abort = False 57 | 58 | t = threading.Thread(target=self.periodicsave, args=()) 59 | t.daemon = True 60 | t.start() 61 | 62 | def periodicsave(self): 63 | # doing it this way (versus just sleeping for 60 seconds), 64 | # allows faster shutdown response 65 | while not self.abort: 66 | if time.time() - self.timer > 60: 67 | self.save() 68 | self.timer = time.time() 69 | time.sleep(1) 70 | 71 | def load(self): 72 | mkdir_p(self.root) 73 | if not os.path.exists( 74 | "%s/%s.%s" % (self.root, self.name, self.file_ext)): 75 | # load old json storages if there is no pickled 76 | # file (and if storage is using pickle) 77 | if self.pickle: 78 | self.Data = self.json_load() 79 | # save to the selected file mode (json or pkl) 80 | self.save() 81 | if self.pickle: 82 | self.Data = self.pickle_load() 83 | else: 84 | self.Data = self.json_load() 85 | 86 | def save(self): 87 | if not os.path.exists(self.root): 88 | mkdir_p(self.root) 89 | if self.pickle: 90 | self.pickle_save() 91 | else: 92 | self.json_save() 93 | 94 | def pickle_save(self): 95 | if "human" in self.encoding.lower(): 96 | _protocol = 0 97 | else: 98 | # using something less than HIGHEST allows both Pythons 2/3 99 | # to use the files interchangeably. It should also allow 100 | # moving files between machines with different configurations 101 | # with fewer issues. 102 | # 103 | # Python 2 cPickle does not have a DEFAULT_PROTOCOL 104 | # constant like Python 3 pickle (else I would just 105 | # use the Default (currently 3, I believe). 106 | # 107 | # This will probably use either 1 or 2 depending on 108 | # which python you use. 109 | # 110 | # We imported either pickle (Py3) or cPickle (Py2) depending 111 | # on what wrapper detected. Both are implemented in C for 112 | # speed. 113 | # 114 | # The MAIN POINT: 115 | # I wanted the code to use something better/faster than 116 | # Human-readable (unless that is what you specify), while 117 | # still permitting some portability of the final files 118 | _protocol = Pickle.HIGHEST_PROTOCOL // 2 119 | 120 | with open("%s/%s.%s" % ( 121 | self.root, self.name, self.file_ext), "wb") as f: 122 | Pickle.dump(self.Data, f, protocol=_protocol) 123 | 124 | def json_save(self): 125 | putcode = putjsonfile(self.Data, self.name, self.root) 126 | if not putcode: 127 | self.log.exception( 128 | "Error encoutered while saving json data:\n'%s/%s.%s'" 129 | "\nData Dump:\n%s" % ( 130 | self.root, self.name, self.file_ext, self.Data)) 131 | 132 | def pickle_load(self): 133 | with open("%s/%s.%s" % ( 134 | self.root, self.name, self.file_ext), "rb") as f: 135 | return Pickle.load(f) 136 | 137 | def json_load(self): 138 | try_load = getjsonfile(self.name, self.root, encodedas=self.encoding) 139 | if try_load in (None, False): 140 | self.log.exception("bad/non-existent file or data '%s/%s.%s'" % 141 | (self.root, self.name, self.file_ext)) 142 | return {} 143 | else: 144 | return try_load 145 | 146 | def close(self): 147 | self.abort = True 148 | self.save() 149 | -------------------------------------------------------------------------------- /wrapper/management/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (C) 2016, 2017 - BenBaptist and Wrapper.py developer(s). 4 | # https://github.com/benbaptist/minecraft-wrapper 5 | # This program is distributed under the terms of the GNU 6 | # General Public License, version 3 or later. 7 | -------------------------------------------------------------------------------- /wrapper/management/dashboard.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (C) 2016, 2017 - BenBaptist and Wrapper.py developer(s). 4 | # https://github.com/benbaptist/minecraft-wrapper 5 | # This program is distributed under the terms of the GNU 6 | # General Public License, version 3 or later. 7 | 8 | import time 9 | import threading 10 | import random 11 | import datetime 12 | import logging 13 | 14 | from core.storage import Storage 15 | 16 | try: 17 | from flask import Flask 18 | from flask_socketio import SocketIO 19 | except ImportError: 20 | Flask = False 21 | flask_socketio = False 22 | 23 | if Flask: 24 | from flask import g, redirect, url_for, render_template, request, make_response, Response, Markup 25 | from flask_socketio import send, emit, join_room, leave_room 26 | 27 | 28 | class Web(object): 29 | 30 | def __init__(self, wrapper): 31 | self.wrapper = wrapper 32 | self.config = wrapper.config # Remember if you need to save use 'wrapper.configManager.save()' not config.save 33 | self.log = logging.getLogger('Web') 34 | 35 | if not Flask: 36 | self.config["Web"]["web-enabled"] = False 37 | self.wrapper.configManager.save() 38 | self.log.critical("You don't have the 'flask/flask_socketio' dashboard dependencies installed " 39 | "on your system. You can now restart, but Web mode is disabled.") 40 | self.wrapper.halt = True 41 | 42 | self.app = Flask(__name__) 43 | self.app.config['SECRET_KEY'] = "".join([chr(random.randrange(48, 90)) for i in range(32)]) # LOL 44 | self.socketio = SocketIO(self.app) 45 | 46 | # Flask filters 47 | def strftime(f): 48 | return datetime.datetime.fromtimestamp(int(f)).strftime('%Y-%m-%d @ %I:%M%p') 49 | 50 | self.app.jinja_env.filters["strftime"] = strftime 51 | 52 | # Register handlers 53 | self.add_decorators() 54 | 55 | self.data = Storage("dash") 56 | if "keys" not in self.data.Data: 57 | self.data.Data["keys"] = [] 58 | 59 | self.loginAttempts = 0 60 | self.lastAttempt = 0 61 | self.disableLogins = 0 62 | 63 | # Start thread for running server 64 | t = threading.Thread(target=self.run, args=()) 65 | t.daemon = True 66 | t.start() 67 | 68 | def __del__(self): 69 | self.data.close() 70 | 71 | # Authorization methods 72 | def checkLogin(self, password): 73 | if time.time() - self.disableLogins < 60: 74 | return False # Threshold for logins 75 | if password == self.config["Web"]["web-password"]: 76 | return True 77 | self.loginAttempts += 1 78 | if self.loginAttempts > 10 and time.time() - self.lastAttempt < 60: 79 | self.disableLogins = time.time() 80 | self.log.warning("Disabled login attempts for one minute") 81 | self.lastAttempt = time.time() 82 | 83 | def makeKey(self, rememberme): 84 | chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@-_" 85 | a = "".join([random.choice(chars) for i in range(64)]) 86 | 87 | self.data.Data["keys"].append([a, time.time(), rememberme]) 88 | return a 89 | 90 | def validateKey(self): 91 | if "__wrapper_cookie" not in request.cookie: 92 | return False 93 | 94 | key = request.cookie["__wrapper_cookie"] 95 | for i in self.data.Data["keys"]: 96 | expiretime = 7884000 # Three weeks old 97 | if len(i) > 2: 98 | if not i[2]: 99 | expiretime = 21600 100 | # Validate key and ensure it's under the expiretime 101 | if i[0] == key and time.time() - i[1] < expiretime: 102 | self.loginAttempts = 0 103 | return True 104 | return False 105 | 106 | def removeKey(self, key): 107 | for i, v in enumerate(self.data.Data["keys"]): 108 | if v[0] == key: 109 | del self.data.Data["keys"][i] 110 | 111 | # Dectorators and misc. 112 | def add_decorators(self): 113 | @self.app.before_request 114 | def handle(): 115 | print("I'm a freakin' sandwich dude!") 116 | 117 | @self.app.route("/") 118 | def index(): 119 | return render_template("dashboard.html") 120 | 121 | @self.app.route("/login", methods=["GET", "POST"]) 122 | def login(): 123 | badpass = False 124 | if request.method == "POST": 125 | password = request.form["password"] 126 | rememberme = "remember" in request.form 127 | 128 | if self.checkLogin(password): 129 | key = self.makeKey(rememberme) 130 | return redirect("/") 131 | # self.log.warning("%s logged in to web mode (remember me: %s)", request.addr, rememberme) 132 | else: 133 | badpass = True 134 | 135 | return render_template("login.html", badPass=badpass) 136 | 137 | @self.socketio.on('connect') 138 | def handle_connect(): 139 | pass 140 | 141 | @self.socketio.on('disconnect') 142 | def handle_disconnect(): 143 | pass 144 | 145 | def run(self): 146 | # Need a method to end this Thread! 147 | # the ending code needs a self.data.close() to close the storage object 148 | self.socketio.run(self.app, host=self.config["Web"]["web-bind"], 149 | port=self.config["Web"]["web-port"]) 150 | -------------------------------------------------------------------------------- /wrapper/management/html/css/admin.css: -------------------------------------------------------------------------------- 1 | 2 | html, 3 | body { 4 | overflow-x: hidden; 5 | } 6 | body { 7 | padding-top: 40px; 8 | } 9 | footer { 10 | padding: 30px 0; 11 | } 12 | .subtext{ 13 | font-size: 13px; 14 | color: grey; 15 | display: inline; 16 | } 17 | .player{ 18 | width: 100%; 19 | height: 30px; 20 | background-color: #428bca; 21 | color: white; 22 | vertical-align: baseline; 23 | font-weight: 700; 24 | font-size: 13px; 25 | padding: 7px; 26 | border-bottom: 1 solid #dddddd; 27 | } 28 | @media screen and (max-width: 767px) { 29 | .row-offcanvas { 30 | position: relative; 31 | -webkit-transition: all .25s ease-out; 32 | -o-transition: all .25s ease-out; 33 | transition: all .25s ease-out; 34 | } 35 | 36 | .row-offcanvas-right { 37 | right: 0; 38 | } 39 | 40 | .row-offcanvas-left { 41 | left: 0; 42 | } 43 | 44 | .row-offcanvas-right 45 | .sidebar-offcanvas { 46 | right: -50%; /* 6 columns */ 47 | } 48 | 49 | .row-offcanvas-left 50 | .sidebar-offcanvas { 51 | left: -50%; /* 6 columns */ 52 | } 53 | 54 | .row-offcanvas-right.active { 55 | right: 50%; /* 6 columns */ 56 | } 57 | 58 | .row-offcanvas-left.active { 59 | left: 50%; /* 6 columns */ 60 | } 61 | 62 | .sidebar-offcanvas { 63 | position: absolute; 64 | top: 0; 65 | width: 50%; /* 6 columns */ 66 | } 67 | } 68 | .nav-sidebar { 69 | margin-right: -21px; /* 20px padding + 1px border */ 70 | margin-bottom: 20px; 71 | margin-left: -20px; 72 | } 73 | .nav-sidebar > li > a { 74 | padding-right: 20px; 75 | padding-left: 20px; 76 | } 77 | .nav-sidebar > .active > a, 78 | .nav-sidebar > .active > a:hover, 79 | .nav-sidebar > .active > a:focus { 80 | color: #fff; 81 | background-color: #428bca; 82 | } 83 | .main { 84 | padding: 20px; 85 | } 86 | @media (min-width: 768px) { 87 | .main { 88 | padding-right: 40px; 89 | padding-left: 40px; 90 | } 91 | } 92 | .main .page-header { 93 | margin-top: 0; 94 | } 95 | .sidebar { 96 | display: none; 97 | } 98 | @media (min-width: 768px) { 99 | .sidebar { 100 | position: fixed; 101 | top: 51px; 102 | bottom: 0; 103 | left: 0; 104 | z-index: 1000; 105 | display: block; 106 | padding-left: 20px; 107 | padding-right: 20px; 108 | overflow-x: hidden; 109 | overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */ 110 | background-color: #f5f5f5; 111 | border-right: 1px solid #eee; 112 | } 113 | } 114 | 115 | -------------------------------------------------------------------------------- /wrapper/management/html/css/bootstrap-theme.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.2.0 (http://getbootstrap.com) 3 | * Copyright 2011-2014 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */.btn-default,.btn-primary,.btn-success,.btn-info,.btn-warning,.btn-danger{text-shadow:0 -1px 0 rgba(0,0,0,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075)}.btn-default:active,.btn-primary:active,.btn-success:active,.btn-info:active,.btn-warning:active,.btn-danger:active,.btn-default.active,.btn-primary.active,.btn-success.active,.btn-info.active,.btn-warning.active,.btn-danger.active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn:active,.btn.active{background-image:none}.btn-default{text-shadow:0 1px 0 #fff;background-image:-webkit-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-o-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#e0e0e0));background-image:linear-gradient(to bottom,#fff 0,#e0e0e0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#dbdbdb;border-color:#ccc}.btn-default:hover,.btn-default:focus{background-color:#e0e0e0;background-position:0 -15px}.btn-default:active,.btn-default.active{background-color:#e0e0e0;border-color:#dbdbdb}.btn-default:disabled,.btn-default[disabled]{background-color:#e0e0e0;background-image:none}.btn-primary{background-image:-webkit-linear-gradient(top,#428bca 0,#2d6ca2 100%);background-image:-o-linear-gradient(top,#428bca 0,#2d6ca2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#428bca),to(#2d6ca2));background-image:linear-gradient(to bottom,#428bca 0,#2d6ca2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff2d6ca2', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#2b669a}.btn-primary:hover,.btn-primary:focus{background-color:#2d6ca2;background-position:0 -15px}.btn-primary:active,.btn-primary.active{background-color:#2d6ca2;border-color:#2b669a}.btn-primary:disabled,.btn-primary[disabled]{background-color:#2d6ca2;background-image:none}.btn-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#419641));background-image:linear-gradient(to bottom,#5cb85c 0,#419641 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#3e8f3e}.btn-success:hover,.btn-success:focus{background-color:#419641;background-position:0 -15px}.btn-success:active,.btn-success.active{background-color:#419641;border-color:#3e8f3e}.btn-success:disabled,.btn-success[disabled]{background-color:#419641;background-image:none}.btn-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#2aabd2));background-image:linear-gradient(to bottom,#5bc0de 0,#2aabd2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#28a4c9}.btn-info:hover,.btn-info:focus{background-color:#2aabd2;background-position:0 -15px}.btn-info:active,.btn-info.active{background-color:#2aabd2;border-color:#28a4c9}.btn-info:disabled,.btn-info[disabled]{background-color:#2aabd2;background-image:none}.btn-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#eb9316));background-image:linear-gradient(to bottom,#f0ad4e 0,#eb9316 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#e38d13}.btn-warning:hover,.btn-warning:focus{background-color:#eb9316;background-position:0 -15px}.btn-warning:active,.btn-warning.active{background-color:#eb9316;border-color:#e38d13}.btn-warning:disabled,.btn-warning[disabled]{background-color:#eb9316;background-image:none}.btn-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c12e2a));background-image:linear-gradient(to bottom,#d9534f 0,#c12e2a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#b92c28}.btn-danger:hover,.btn-danger:focus{background-color:#c12e2a;background-position:0 -15px}.btn-danger:active,.btn-danger.active{background-color:#c12e2a;border-color:#b92c28}.btn-danger:disabled,.btn-danger[disabled]{background-color:#c12e2a;background-image:none}.thumbnail,.img-thumbnail{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{background-color:#e8e8e8;background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{background-color:#357ebd;background-image:-webkit-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:-o-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#428bca),to(#357ebd));background-image:linear-gradient(to bottom,#428bca 0,#357ebd 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0);background-repeat:repeat-x}.navbar-default{background-image:-webkit-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-o-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#f8f8f8));background-image:linear-gradient(to bottom,#fff 0,#f8f8f8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075)}.navbar-default .navbar-nav>.active>a{background-image:-webkit-linear-gradient(top,#ebebeb 0,#f3f3f3 100%);background-image:-o-linear-gradient(top,#ebebeb 0,#f3f3f3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#ebebeb),to(#f3f3f3));background-image:linear-gradient(to bottom,#ebebeb 0,#f3f3f3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff3f3f3', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.075);box-shadow:inset 0 3px 9px rgba(0,0,0,.075)}.navbar-brand,.navbar-nav>li>a{text-shadow:0 1px 0 rgba(255,255,255,.25)}.navbar-inverse{background-image:-webkit-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-o-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#3c3c3c),to(#222));background-image:linear-gradient(to bottom,#3c3c3c 0,#222 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x}.navbar-inverse .navbar-nav>.active>a{background-image:-webkit-linear-gradient(top,#222 0,#282828 100%);background-image:-o-linear-gradient(top,#222 0,#282828 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#222),to(#282828));background-image:linear-gradient(to bottom,#222 0,#282828 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222', endColorstr='#ff282828', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.25);box-shadow:inset 0 3px 9px rgba(0,0,0,.25)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-nav>li>a{text-shadow:0 -1px 0 rgba(0,0,0,.25)}.navbar-static-top,.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}.alert{text-shadow:0 1px 0 rgba(255,255,255,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05)}.alert-success{background-image:-webkit-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#c8e5bc));background-image:linear-gradient(to bottom,#dff0d8 0,#c8e5bc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);background-repeat:repeat-x;border-color:#b2dba1}.alert-info{background-image:-webkit-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#b9def0));background-image:linear-gradient(to bottom,#d9edf7 0,#b9def0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);background-repeat:repeat-x;border-color:#9acfea}.alert-warning{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#f8efc0));background-image:linear-gradient(to bottom,#fcf8e3 0,#f8efc0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);background-repeat:repeat-x;border-color:#f5e79e}.alert-danger{background-image:-webkit-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-o-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#e7c3c3));background-image:linear-gradient(to bottom,#f2dede 0,#e7c3c3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);background-repeat:repeat-x;border-color:#dca7a7}.progress{background-image:-webkit-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#ebebeb),to(#f5f5f5));background-image:linear-gradient(to bottom,#ebebeb 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x}.progress-bar{background-image:-webkit-linear-gradient(top,#428bca 0,#3071a9 100%);background-image:-o-linear-gradient(top,#428bca 0,#3071a9 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#428bca),to(#3071a9));background-image:linear-gradient(to bottom,#428bca 0,#3071a9 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3071a9', GradientType=0);background-repeat:repeat-x}.progress-bar-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#449d44));background-image:linear-gradient(to bottom,#5cb85c 0,#449d44 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);background-repeat:repeat-x}.progress-bar-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#31b0d5));background-image:linear-gradient(to bottom,#5bc0de 0,#31b0d5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);background-repeat:repeat-x}.progress-bar-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#ec971f));background-image:linear-gradient(to bottom,#f0ad4e 0,#ec971f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);background-repeat:repeat-x}.progress-bar-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c9302c));background-image:linear-gradient(to bottom,#d9534f 0,#c9302c 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);background-repeat:repeat-x}.progress-bar-striped{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.list-group{border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{text-shadow:0 -1px 0 #3071a9;background-image:-webkit-linear-gradient(top,#428bca 0,#3278b3 100%);background-image:-o-linear-gradient(top,#428bca 0,#3278b3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#428bca),to(#3278b3));background-image:linear-gradient(to bottom,#428bca 0,#3278b3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3278b3', GradientType=0);background-repeat:repeat-x;border-color:#3278b3}.panel{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.05);box-shadow:0 1px 2px rgba(0,0,0,.05)}.panel-default>.panel-heading{background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.panel-primary>.panel-heading{background-image:-webkit-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:-o-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#428bca),to(#357ebd));background-image:linear-gradient(to bottom,#428bca 0,#357ebd 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0);background-repeat:repeat-x}.panel-success>.panel-heading{background-image:-webkit-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#d0e9c6));background-image:linear-gradient(to bottom,#dff0d8 0,#d0e9c6 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);background-repeat:repeat-x}.panel-info>.panel-heading{background-image:-webkit-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#c4e3f3));background-image:linear-gradient(to bottom,#d9edf7 0,#c4e3f3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);background-repeat:repeat-x}.panel-warning>.panel-heading{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#faf2cc));background-image:linear-gradient(to bottom,#fcf8e3 0,#faf2cc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);background-repeat:repeat-x}.panel-danger>.panel-heading{background-image:-webkit-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-o-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#ebcccc));background-image:linear-gradient(to bottom,#f2dede 0,#ebcccc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);background-repeat:repeat-x}.well{background-image:-webkit-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#e8e8e8),to(#f5f5f5));background-image:linear-gradient(to bottom,#e8e8e8 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x;border-color:#dcdcdc;-webkit-box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1)} -------------------------------------------------------------------------------- /wrapper/management/html/css/index.html: -------------------------------------------------------------------------------- 1 | Test! 2 | -------------------------------------------------------------------------------- /wrapper/management/html/css/signin.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 40px; 3 | padding-bottom: 40px; 4 | background-color: #eee; 5 | } 6 | 7 | .form-signin { 8 | max-width: 330px; 9 | padding: 15px; 10 | margin: 0 auto; 11 | } 12 | .form-signin .form-signin-heading, 13 | .form-signin .checkbox { 14 | margin-bottom: 10px; 15 | } 16 | .form-signin .checkbox { 17 | font-weight: normal; 18 | } 19 | .form-signin .form-control { 20 | position: relative; 21 | height: auto; 22 | -webkit-box-sizing: border-box; 23 | -moz-box-sizing: border-box; 24 | box-sizing: border-box; 25 | padding: 10px; 26 | font-size: 16px; 27 | } 28 | .form-signin .form-control:focus { 29 | z-index: 2; 30 | } 31 | .form-signin input[type="email"] { 32 | margin-bottom: -1px; 33 | border-bottom-right-radius: 0; 34 | border-bottom-left-radius: 0; 35 | } 36 | .form-signin input[type="password"] { 37 | margin-bottom: 10px; 38 | border-top-left-radius: 0; 39 | border-top-right-radius: 0; 40 | } 41 | -------------------------------------------------------------------------------- /wrapper/management/html/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeachCraft/TeachCraft-Server/446f5b54f53ca5b4d3a938846252a687c7f0c2ee/wrapper/management/html/favicon.ico -------------------------------------------------------------------------------- /wrapper/management/html/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeachCraft/TeachCraft-Server/446f5b54f53ca5b4d3a938846252a687c7f0c2ee/wrapper/management/html/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /wrapper/management/html/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeachCraft/TeachCraft-Server/446f5b54f53ca5b4d3a938846252a687c7f0c2ee/wrapper/management/html/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /wrapper/management/html/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeachCraft/TeachCraft-Server/446f5b54f53ca5b4d3a938846252a687c7f0c2ee/wrapper/management/html/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /wrapper/management/html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Public Stats 4 | 7 | 8 | 9 | 10 | 11 | 12 |
Admin Login 13 | 14 | -------------------------------------------------------------------------------- /wrapper/management/html/js/ie10-viewport-bug-workaround.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * IE10 viewport hack for Surface/desktop Windows 8 bug 3 | * Copyright 2014 Twitter, Inc. 4 | * Licensed under the Creative Commons Attribution 3.0 Unported License. For 5 | * details, see http://creativecommons.org/licenses/by/3.0/. 6 | */ 7 | 8 | // See the Getting Started docs for more information: 9 | // http://getbootstrap.com/getting-started/#support-ie10-width 10 | 11 | (function () { 12 | 'use strict'; 13 | if (navigator.userAgent.match(/IEMobile\/10\.0/)) { 14 | var msViewportStyle = document.createElement('style') 15 | msViewportStyle.appendChild( 16 | document.createTextNode( 17 | '@-ms-viewport{width:auto!important}' 18 | ) 19 | ) 20 | document.querySelector('head').appendChild(msViewportStyle) 21 | } 22 | })(); 23 | -------------------------------------------------------------------------------- /wrapper/management/html/js/jquery.flot.resize.js: -------------------------------------------------------------------------------- 1 | /* Flot plugin for automatically redrawing plots as the placeholder resizes. 2 | 3 | Copyright (c) 2007-2013 IOLA and Ole Laursen. 4 | Licensed under the MIT license. 5 | 6 | It works by listening for changes on the placeholder div (through the jQuery 7 | resize event plugin) - if the size changes, it will redraw the plot. 8 | 9 | There are no options. If you need to disable the plugin for some plots, you 10 | can just fix the size of their placeholders. 11 | 12 | */ 13 | 14 | /* Inline dependency: 15 | * jQuery resize event - v1.1 - 3/14/2010 16 | * http://benalman.com/projects/jquery-resize-plugin/ 17 | * 18 | * Copyright (c) 2010 "Cowboy" Ben Alman 19 | * Dual licensed under the MIT and GPL licenses. 20 | * http://benalman.com/about/license/ 21 | */ 22 | 23 | (function($,h,c){var a=$([]),e=$.resize=$.extend($.resize,{}),i,k="setTimeout",j="resize",d=j+"-special-event",b="delay",f="throttleWindow";e[b]=250;e[f]=true;$.event.special[j]={setup:function(){if(!e[f]&&this[k]){return false}var l=$(this);a=a.add(l);$.data(this,d,{w:l.width(),h:l.height()});if(a.length===1){g()}},teardown:function(){if(!e[f]&&this[k]){return false}var l=$(this);a=a.not(l);l.removeData(d);if(!a.length){clearTimeout(i)}},add:function(l){if(!e[f]&&this[k]){return false}var n;function m(s,o,p){var q=$(this),r=$.data(this,d);r.w=o!==c?o:q.width();r.h=p!==c?p:q.height();n.apply(this,arguments)}if($.isFunction(l)){n=l;return m}else{n=l.handler;l.handler=m}}};function g(){i=h[k](function(){a.each(function(){var n=$(this),m=n.width(),l=n.height(),o=$.data(this,d);if(m!==o.w||l!==o.h){n.trigger(j,[o.w=m,o.h=l])}});g()},e[b])}})(jQuery,this); 24 | 25 | (function ($) { 26 | var options = { }; // no options 27 | 28 | function init(plot) { 29 | function onResize() { 30 | var placeholder = plot.getPlaceholder(); 31 | 32 | // somebody might have hidden us and we can't plot 33 | // when we don't have the dimensions 34 | if (placeholder.width() == 0 || placeholder.height() == 0) 35 | return; 36 | 37 | plot.resize(); 38 | plot.setupGrid(); 39 | plot.draw(); 40 | } 41 | 42 | function bindEvents(plot, eventHolder) { 43 | plot.getPlaceholder().resize(onResize); 44 | } 45 | 46 | function shutdown(plot, eventHolder) { 47 | plot.getPlaceholder().unbind("resize", onResize); 48 | } 49 | 50 | plot.hooks.bindEvents.push(bindEvents); 51 | plot.hooks.shutdown.push(shutdown); 52 | } 53 | 54 | $.plot.plugins.push({ 55 | init: init, 56 | options: options, 57 | name: 'resize', 58 | version: '1.0' 59 | }); 60 | })(jQuery); -------------------------------------------------------------------------------- /wrapper/management/html/js/offcanvas.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function () { 2 | $('[data-toggle="offcanvas"]').click(function () { 3 | $('.row-offcanvas').toggleClass('active') 4 | }); 5 | }); -------------------------------------------------------------------------------- /wrapper/management/html/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Admin Login - Wrapper.py 5 | 6 | 7 | 8 | 9 | 41 | 51 | 52 | 53 |
54 | 57 | 69 |
70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /wrapper/management/html/requests.js: -------------------------------------------------------------------------------- 1 | // Hooray, I just hit 111 commits on Wrapper.py's development branch. 2 | // Why am I saying this here? Because easter eggs. 3 | requests = {} 4 | requests.action = function(action, arguments){ 5 | args = "" 6 | for(i in arguments){ 7 | console.log(i) 8 | args += "&" + i + "=" + encodeURIComponent(arguments[i]) 9 | if(i == undefined) continue 10 | } 11 | // console.log("GET Regular Request: /action/"+action+"?"+args) 12 | var xml = new XMLHttpRequest() 13 | xml.open("GET", "/action/"+action+"?"+args, false) 14 | xml.send() 15 | return JSON.parse(xml.responseText) 16 | } 17 | requests.admin = function(action, arguments){ 18 | args = "key=" + localStorage.sessionKey 19 | for(i in arguments){ 20 | if(i == undefined) continue 21 | args += "&" + i + "=" + encodeURIComponent(arguments[i]) 22 | } 23 | // console.log("GET Admin Request: /action/"+action+"?"+args) 24 | var xml = new XMLHttpRequest() 25 | xml.open("GET", "/action/"+action+"?"+args, false) 26 | try{xml.send()}catch(err){return false} 27 | var response = JSON.parse(xml.responseText) 28 | if (response["status"] == "error") 29 | return false 30 | return response["payload"] 31 | } 32 | requests.adminThreaded = function(action, arguments, callBack){ 33 | args = "key=" + localStorage.sessionKey 34 | for(i in arguments){ 35 | if(i == undefined) continue 36 | args += "&" + i + "=" + encodeURIComponent(arguments[i]) 37 | } 38 | try{ 39 | var xml = new XMLHttpRequest() 40 | xml.open("GET", "/action/"+action+"?"+args, true) 41 | }catch(err){return false} 42 | try{xml.send()}catch(err){return false} 43 | xml.onreadystatechange = function(){ 44 | if(xml.readyState == 4){ 45 | try{ 46 | var response = JSON.parse(xml.responseText) 47 | }catch(err){ 48 | callBack(false) 49 | return 50 | } 51 | callBack(response["payload"]) 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /wrapper/proxy/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (C) 2016, 2017 - BenBaptist and Wrapper.py developer(s). 4 | # https://github.com/benbaptist/minecraft-wrapper 5 | # This program is distributed under the terms of the GNU 6 | # General Public License, version 3 or later. 7 | -------------------------------------------------------------------------------- /wrapper/proxy/constants.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (C) 2016, 2017 - BenBaptist and Wrapper.py developer(s). 4 | # https://github.com/benbaptist/minecraft-wrapper 5 | # This program is distributed under the terms of the GNU 6 | # General Public License, version 3 or later. 7 | 8 | # Mincraft version constants 9 | # use these constants decide how a packet should be parsed. 10 | 11 | # Still in development at versions 201-210(6/14/16) 12 | 13 | 14 | PROTOCOL_MAX = 4000 15 | 16 | PROTOCOL_1_11 = 314 17 | 18 | PROTOCOL_1_10 = 205 19 | 20 | # post- 1.9.3 "pre" releases (1.9.3 pre-2 -) 21 | PROTOCOL_1_9_4 = 110 22 | 23 | # PAGE: http://wiki.vg/index.php?title=Protocol&oldid=7817 24 | # post- 1.9 "pre" releases (1.9.2 - 1.9.3 pre-1) 25 | PROTOCOL_1_9_3PRE3 = 109 26 | 27 | # PAGE: http://wiki.vg/index.php?title=Protocol&oldid=7617 28 | # post- 1.9 "pre" releases (1.9.1 pre-3 through 1.9.1) 29 | PROTOCOL_1_9_1PRE = 108 30 | # first stable 1.9 release 31 | PROTOCOL_1_9REL1 = 107 32 | 33 | # Between 49-106, the protocol is incredibly unstable. 34 | # Packet numbers changed almost weekly. using a version in this range 35 | # will raise as UnsupportedMinecraftProtocol Exception 36 | 37 | # start of 1.9 snapshots 38 | PROTOCOL_1_9START = 48 39 | 40 | # PAGE: http://wiki.vg/index.php?title=Protocol&oldid=7368 41 | # 1.8.9 42 | PROTOCOL_1_8END = 47 43 | # 1.8 snapshots start- # 44 | PROTOCOL_1_8START = 6 45 | 46 | # PAGE: http://wiki.vg/index.php?title=Protocol&oldid=6003 47 | # 1.7.6 - 1.7.10 48 | PROTOCOL_1_7_9 = 5 49 | 50 | # PAGE: http://wiki.vg/index.php?title=Protocol&oldid=5486 51 | # 1.7.1-pre to 1.7.5 52 | PROTOCOL_1_7 = 4 53 | 54 | """Minecraft version 1.6.4 and older used a protocol versioning 55 | scheme separate from the current one. Accordingly, an old protocol 56 | version number may ambiguously refer to an one of those old versions 57 | and from the list above. Do not run a 1.6.4 server with proxy mode.""" 58 | 59 | # parser constants 60 | PKT = 0 61 | PARSER = 1 62 | 63 | # Data constants 64 | # ------------------------------------------------ 65 | 66 | STRING = 0 67 | JSON = 1 68 | UBYTE = 2 69 | BYTE = 3 70 | INT = 4 71 | SHORT = 5 72 | USHORT = 6 73 | LONG = 7 74 | DOUBLE = 8 75 | FLOAT = 9 76 | BOOL = 10 77 | VARINT = 11 78 | BYTEARRAY = 12 79 | BYTEARRAY_SHORT = 13 80 | POSITION = 14 81 | 82 | # gets full slot info, including NBT data. 83 | SLOT = 15 84 | 85 | # This fellow is a bit of a hack that allows getting the 86 | # basic slot data where the NBT part may be buggy or 87 | # you are not sure you are correctly parsing the NBT 88 | # data (like in older pre-1.8 minecrafts). 89 | SLOT_NO_NBT = 18 90 | 91 | UUID = 16 92 | 93 | # this is the old pre-1.9 metadata parsing. 94 | METADATA = 17 95 | # It is radically different in 1.9+ now (through 11.2 atm) 96 | METADATA_1_9 = 19 97 | 98 | 99 | # Both of these just read or send the rest of the packet in its raw bytes form. 100 | REST = 90 101 | RAW = 90 102 | 103 | # allows the insertion of padding into argument lists. 104 | # Any field with this designation is just silently skipped. 105 | NULL = 100 106 | -------------------------------------------------------------------------------- /wrapper/proxy/mcpackets_cb.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (C) 2016, 2017 - BenBaptist and Wrapper.py developer(s). 4 | # https://github.com/benbaptist/minecraft-wrapper 5 | # This program is distributed under the terms of the GNU 6 | # General Public License, version 3 or later. 7 | 8 | from core.exceptions import UnsupportedMinecraftProtocol 9 | 10 | from proxy.constants import * 11 | 12 | """ 13 | Ways to reference packets by names and not hard-coded numbers. 14 | 15 | This attempts to follow the wiki as much as possible. 16 | 17 | the ServerBound and ClientBound classes take an integer protocol argument 18 | to determine the packet values. 19 | 20 | Protocol constants are named as follows: 21 | first two digits are major version, third digit in minor version. 22 | example: PROTOCOL_1_8_9 means - version 1.8.9. 23 | Explanatory text (pre, start, etc) may be last. 24 | 25 | set something False/unimplemented using 0xEE 26 | 27 | """ 28 | 29 | 30 | class Packets(object): 31 | def __init__(self, protocol): 32 | 33 | if PROTOCOL_1_8END < protocol < PROTOCOL_1_9REL1: 34 | raise UnsupportedMinecraftProtocol 35 | 36 | # Login, Status, and Ping packets 37 | # ------------------------------- 38 | self.LOGIN_DISCONNECT = 0x00 39 | self.LOGIN_ENCR_REQUEST = 0x01 40 | self.LOGIN_SUCCESS = 0x02 41 | self.LOGIN_SET_COMPRESSION = 0X03 42 | 43 | # the json data represented as a string 44 | self.PING_JSON_RESPONSE = 0x00 45 | # PONG sent in response to Client PING 46 | self.PING_PONG = 0x01 47 | 48 | # play mode packets 49 | # ------------------------------- 50 | # Base set 1.7 - 1.8.9 - The packet numbers were the same, 51 | # although parsing differed amongst versions 52 | self.KEEP_ALIVE = [0x00, [INT]] 53 | self.JOIN_GAME = [0x01, [INT, UBYTE, BYTE, UBYTE, UBYTE, STRING]] 54 | self.CHAT_MESSAGE = [0x02, [STRING, NULL]] 55 | self.TIME_UPDATE = 0x03 56 | self.ENTITY_EQUIPMENT = 0x04 # TODO - never parsed by wrapper 57 | self.SPAWN_POSITION = 0x05 58 | self.UPDATE_HEALTH = 0x06 # TODO - never parsed by wrapper 59 | self.RESPAWN = 0x07 60 | self.PLAYER_POSLOOK = 0x08 61 | self.HELD_ITEM_CHANGE = 0x09 # TODO - never parsed by wrapper 62 | self.USE_BED = 0x0a 63 | self.ANIMATION = 0x0b 64 | self.SPAWN_PLAYER = 0x0c 65 | self.COLLECT_ITEM = 0x0d # TODO - never parsed by wrapper 66 | self.SPAWN_OBJECT = 0x0e 67 | self.SPAWN_MOB = 0x0f 68 | self.SPAWN_PAINTING = 0x10 # TODO - never parsed by wrapper 69 | self.SPAWN_EXPERIENCE_ORB = 0x11 # TODO - never parsed by wrapper 70 | self.ENTITY_VELOCITY = 0x12 # TODO - never parsed by wrapper before 71 | self.DESTROY_ENTITIES = 0x13 72 | self.ENTITY = 0x14 73 | self.ENTITY_RELATIVE_MOVE = 0x15 74 | self.ENTITY_LOOK = 0x16 # TODO - never parsed by wrapper before 75 | self.ENTITY_LOOK_AND_RELATIVE_MOVE = 0x17 # TODO - never parsed by wrapper 76 | self.ENTITY_TELEPORT = 0x18 77 | self.ENTITY_HEAD_LOOK = 0x19 78 | self.ENTITY_STATUS = 0x1a 79 | self.ATTACH_ENTITY = 0x1b 80 | # [VARINT, METADATA] This one and NBT things are broke in 1.7 81 | self.ENTITY_METADATA = [0x1c, [VARINT, RAW]] 82 | self.ENTITY_EFFECT = 0x1d 83 | self.REMOVE_ENTITY_EFFECT = 0x1e 84 | self.SET_EXPERIENCE = 0x1f 85 | self.ENTITY_PROPERTIES = 0x20 86 | self.CHUNK_DATA = 0x21 87 | self.MULTI_BLOCK_CHANGE = 0x22 # TODO - never parsed by wrapper before 88 | self.BLOCK_CHANGE = 0x23 89 | self.BLOCK_ACTION = 0x24 # TODO - never parsed by wrapper before 90 | self.BLOCK_BREAK_ANIMATION = 0x25 # TODO - never parsed by wrapper before 91 | self.MAP_CHUNK_BULK = 0x26 92 | self.EXPLOSION = 0x27 # TODO - never parsed by wrapper before 93 | self.EFFECT = 0x28 # TODO - never parsed by wrapper before 94 | self.SOUND_EFFECT = 0x29 95 | self.PARTICLE = 0x2a 96 | self.CHANGE_GAME_STATE = 0x2b 97 | self.SPAWN_GLOBAL_ENTITY = 0x2c # TODO - never parsed by wrapper before 98 | self.OPEN_WINDOW = 0x2d 99 | self.CLOSE_WINDOW = 0x2e # TODO - never parsed by wrapper before 100 | self.SET_SLOT = 0x2f 101 | self.WINDOW_ITEMS = 0x30 102 | self.WINDOW_PROPERTY = 0x31 # TODO - never parsed by wrapper before 103 | self.CONFIRM_TRANSACTION = 0x32 # TODO - never parsed by wrapper before 104 | self.UPDATE_SIGN = 0x33 # TODO - never parsed by wrapper before 105 | self.MAP = 0x34 # TODO - never parsed by wrapper before 106 | self.UPDATE_BLOCK_ENTITY = 0x35 # TODO - never parsed by wrapper before 107 | self.OPEN_SIGN_EDITOR = 0x36 # TODO - never parsed by wrapper before 108 | self.STATISTICS = 0x37 # TODO - never parsed by wrapper before 109 | self.PLAYER_LIST_ITEM = 0x38 110 | self.PLAYER_ABILITIES = 0x39 111 | self.TAB_COMPLETE = 0x3a # TODO - never parsed by wrapper before 112 | self.SCOREBOARD_OBJECTIVE = 0x3b # TODO - never parsed by wrapper before 113 | self.UPDATE_SCORE = 0x3c # TODO - never parsed by wrapper before 114 | self.DISPLAY_SCOREBOARD = 0x3d # TODO - never parsed by wrapper before 115 | self.TEAMS = 0x3e # TODO - never parsed by wrapper before 116 | self.PLUGIN_MESSAGE = 0x3F 117 | self.DISCONNECT = 0x40 118 | self.SERVER_DIFFICULTY = 0x41 # TODO - never parsed by wrapper before 119 | self.COMBAT_EVENT = 0x42 # TODO - never parsed by wrapper before 120 | self.CAMERA = 0x43 # TODO - never parsed by wrapper before 121 | self.WORLD_BORDER = 0x44 # TODO - never parsed by wrapper before 122 | self.TITLE = 0x45 # TODO - never parsed by wrapper before 123 | self.BROKEN_SET_COMPRESSION_REMOVED1_9 = 0x46 124 | self.PLAYER_LIST_HEADER_AND_FOOTER = 0x47 # TODO - never parsed by wrapper before 125 | self.RESOURCE_PACK_SEND = 0x48 126 | self.UPDATE_ENTITY_NBT = 0x49 # TODO - never parsed by wrapper before 127 | 128 | # NEW to 1.9 129 | self.PACKET_THAT_EXISTS_IN_OTHER_PROTOCOLS_BUT_NOT_THIS_ONE = 0xee 130 | # ALL VERSIONS handle chunk unloading DIFFERENTLY - CAVEAT EMPTOR! 131 | self.UNLOAD_CHUNK = 0xee 132 | self.NAMED_SOUND_EFFECT = 0xee 133 | self.BOSS_BAR = 0xee 134 | self.SET_COOLDOWN = 0xee 135 | self.VEHICLE_MOVE = 0xee 136 | self.SET_PASSENGERS = 0xee 137 | 138 | # 1.8 changes 139 | if protocol >= PROTOCOL_1_8START: 140 | # Parsing changes 141 | self.KEEP_ALIVE[PARSER] = [VARINT] 142 | self.CHAT_MESSAGE[PARSER] = [JSON, BYTE] 143 | self.ENTITY_METADATA[PARSER] = [VARINT, METADATA] 144 | 145 | # 1.9 changes 146 | if protocol >= PROTOCOL_1_9REL1: 147 | self.SPAWN_OBJECT = 0x00 148 | self.SPAWN_EXPERIENCE_ORB = 0x01 149 | self.SPAWN_GLOBAL_ENTITY = 0x02 150 | self.SPAWN_MOB = 0x03 151 | self.SPAWN_PAINTING = 0x04 152 | self.SPAWN_PLAYER = 0x05 153 | self.ANIMATION = 0x06 154 | self.STATISTICS = 0x07 155 | self.BLOCK_BREAK_ANIMATION = 0x08 156 | self.UPDATE_BLOCK_ENTITY = 0x09 157 | self.BLOCK_ACTION = 0x0a 158 | self.BLOCK_CHANGE = 0x0b 159 | self.BOSS_BAR = 0x0c # TODO NEW 160 | self.SERVER_DIFFICULTY = 0x0d 161 | self.TAB_COMPLETE = 0x0e 162 | self.CHAT_MESSAGE[PKT] = 0x0f 163 | self.MULTI_BLOCK_CHANGE = 0x10 164 | self.CONFIRM_TRANSACTION = 0x11 165 | self.CLOSE_WINDOW = 0x12 166 | self.OPEN_WINDOW = 0x13 167 | self.WINDOW_ITEMS = 0x14 168 | self.WINDOW_PROPERTY = 0x15 169 | self.SET_SLOT = 0x16 170 | self.SET_COOLDOWN = 0x17 # TODO NEW 171 | self.PLUGIN_MESSAGE = 0x18 172 | self.NAMED_SOUND_EFFECT = 0x19 # TODO NEW 173 | self.DISCONNECT = 0x1a 174 | self.ENTITY_STATUS = 0x1b 175 | self.EXPLOSION = 0x1c 176 | # ALL VERSIONS handle chunk unloading DIFFERENTLY - CAVEAT EMPTOR! 177 | self.UNLOAD_CHUNK = 0x1d # TODO NEW 178 | self.CHANGE_GAME_STATE = 0x1e 179 | self.KEEP_ALIVE[PKT] = 0x1f 180 | self.CHUNK_DATA = 0x20 181 | self.EFFECT = 0x21 182 | self.PARTICLE = 0x22 183 | self.JOIN_GAME[PKT] = 0x23 184 | self.MAP = 0x24 185 | self.ENTITY_RELATIVE_MOVE = 0x25 186 | self.ENTITY_LOOK_AND_RELATIVE_MOVE = 0x26 187 | self.ENTITY_LOOK = 0x27 188 | self.ENTITY = 0x28 189 | self.VEHICLE_MOVE = 0x29 # TODO NEW 190 | self.OPEN_SIGN_EDITOR = 0x2a 191 | self.PLAYER_ABILITIES = 0x2b 192 | self.COMBAT_EVENT = 0x2c 193 | self.PLAYER_LIST_ITEM = 0x2d 194 | self.PLAYER_POSLOOK = 0x2e 195 | self.USE_BED = 0x2f 196 | self.DESTROY_ENTITIES = 0x30 197 | self.REMOVE_ENTITY_EFFECT = 0x31 198 | self.RESOURCE_PACK_SEND = 0x32 199 | self.RESPAWN = 0x33 200 | self.ENTITY_HEAD_LOOK = 0x34 201 | self.WORLD_BORDER = 0x35 202 | self.CAMERA = 0x36 203 | self.HELD_ITEM_CHANGE = 0x37 204 | self.DISPLAY_SCOREBOARD = 0x38 205 | self.ENTITY_METADATA = [0x39, [VARINT, METADATA_1_9]] 206 | self.ATTACH_ENTITY = 0x3a 207 | self.ENTITY_VELOCITY = 0x3b 208 | self.ENTITY_EQUIPMENT = 0x3c 209 | self.SET_EXPERIENCE = 0x3d 210 | self.UPDATE_HEALTH = 0x3e 211 | self.SCOREBOARD_OBJECTIVE = 0x3f 212 | self.SET_PASSENGERS = 0x40 # TODO NEW 213 | self.TEAMS = 0x41 214 | self.UPDATE_SCORE = 0x42 215 | self.SPAWN_POSITION = 0x43 216 | self.TIME_UPDATE = 0x44 217 | self.TITLE = 0x45 # did not change 218 | self.UPDATE_SIGN = 0x46 219 | self.SOUND_EFFECT = 0x47 220 | self.PLAYER_LIST_HEADER_AND_FOOTER = 0x48 221 | self.COLLECT_ITEM = 0x49 222 | self.ENTITY_TELEPORT = 0x4a 223 | self.ENTITY_PROPERTIES = 0x4b 224 | self.ENTITY_EFFECT = 0x4c 225 | 226 | # removed 227 | self.UPDATE_ENTITY_NBT = 0xee 228 | self.MAP_CHUNK_BULK = 0xee 229 | self.BROKEN_SET_COMPRESSION_REMOVED1_9 = 0xee 230 | 231 | # parsing changes 232 | self.JOIN_GAME[PARSER] = [INT, UBYTE, INT, UBYTE, UBYTE, STRING] 233 | 234 | # 1.9.4 - 1.11 changes 235 | # http://wiki.vg/index.php?title=Protocol&oldid=7819#Entity_Properties 236 | # still good packet numbers through protocol 315 237 | if protocol > PROTOCOL_1_9_4: 238 | self.UPDATE_SIGN = 0xee 239 | self.SOUND_EFFECT = 0x46 240 | self.PLAYER_LIST_HEADER_AND_FOOTER = 0x47 241 | self.COLLECT_ITEM = 0x48 242 | self.ENTITY_TELEPORT = 0x49 243 | self.ENTITY_PROPERTIES = 0x4a 244 | self.ENTITY_EFFECT = 0x4b 245 | -------------------------------------------------------------------------------- /wrapper/proxy/mcpackets_sb.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (C) 2016, 2017 - BenBaptist and Wrapper.py developer(s). 4 | # https://github.com/benbaptist/minecraft-wrapper 5 | # This program is distributed under the terms of the GNU 6 | # General Public License, version 3 or later. 7 | 8 | from core.exceptions import UnsupportedMinecraftProtocol 9 | 10 | from proxy.constants import * 11 | 12 | """ 13 | Ways to reference packets by names and not hard-coded numbers. 14 | 15 | This attempts to follow the wiki as much as possible. 16 | 17 | the ServerBound and ClientBound classes take an integer protocol argument 18 | to determine the packet values. 19 | 20 | Protocol constants are named as follows: 21 | first two digits are major version, third digit in minor version. 22 | example: PROTOCOL_1_8_9 means - version 1.8.9. 23 | Explanatory text (pre, start, etc) may be last. 24 | 25 | set something False/unimplemented using 0xEE 26 | 27 | """ 28 | 29 | 30 | class Packets(object): 31 | def __init__(self, protocol): 32 | 33 | if PROTOCOL_1_8END < protocol < PROTOCOL_1_9REL1: 34 | raise UnsupportedMinecraftProtocol 35 | 36 | # Login, Status, and Ping packets 37 | # ------------------------------- 38 | # set server to STATUS(1) or LOGIN(2) mode. 39 | self.HANDSHAKE = 0x00 40 | # Server sends server json list data in response packet 41 | self.REQUEST = 0x00 42 | # server responds with a PONG 43 | self.STATUS_PING = 0x01 44 | # contains the "name" of user. Sent after handshake for LOGIN 45 | self.LOGIN_START = 0x00 46 | # client response to ENCR_REQUEST 47 | self.LOGIN_ENCR_RESPONSE = 0x01 48 | 49 | # Play packets 50 | # ------------------------------- 51 | # 1.7 - 1.7.10 PLAY packets 52 | self.KEEP_ALIVE = [0x00, [INT]] 53 | self.CHAT_MESSAGE = 0x01 54 | self.USE_ENTITY = 0x02 55 | self.PLAYER = 0x03 56 | self.PLAYER_POSITION = 0x04 57 | self.PLAYER_LOOK = 0x05 58 | self.PLAYER_POSLOOK = 0x06 59 | self.PLAYER_DIGGING = 0x07 60 | self.PLAYER_BLOCK_PLACEMENT = 0x08 61 | self.HELD_ITEM_CHANGE = 0x09 62 | self.ANIMATION = 0x0a # TODO NEW 63 | self.ENTITY_ACTION = 0x0b # TODO NEW 64 | self.STEER_VEHICLE = 0x0c # TODO NEW 65 | self.CLOSE_WINDOW = 0x0b # TODO NEW 66 | self.CLICK_WINDOW = 0x0e 67 | self.CONFIRM_TRANSACTION = 0x0f # TODO NEW 68 | self.CREATIVE_INVENTORY_ACTION = 0x10 # TODO NEW 69 | self.ENCHANT_ITEM = 0x11 # TODO NEW 70 | self.PLAYER_UPDATE_SIGN = 0x12 71 | self.PLAYER_ABILITIES = 0x13 72 | self.TAB_COMPLETE = 0x14 # TODO NEW 73 | self.CLIENT_SETTINGS = 0x15 74 | self.CLIENT_STATUS = 0x16 75 | self.PLUGIN_MESSAGE = 0x17 76 | 77 | # new packets unimplemented in 1.7 78 | self.SPECTATE = 0xee 79 | self.RESOURCE_PACK_STATUS = 0xee 80 | self.TELEPORT_CONFIRM = 0xee 81 | self.USE_ITEM = 0xee 82 | self.VEHICLE_MOVE = 0xee 83 | self.STEER_BOAT = 0xee 84 | 85 | # Parsing changes 86 | if protocol >= PROTOCOL_1_8START: 87 | self.KEEP_ALIVE[PARSER] = [VARINT] 88 | 89 | if PROTOCOL_1_9START > protocol >= PROTOCOL_1_8START: 90 | self.SPECTATE = 0x18 91 | self.RESOURCE_PACK_STATUS = 0x19 92 | 93 | # 1.9 94 | if protocol >= PROTOCOL_1_9REL1: 95 | self.TELEPORT_CONFIRM = 0x00 96 | self.TAB_COMPLETE = 0x01 # TODO NEW 97 | self.CHAT_MESSAGE = 0x02 98 | self.CLIENT_STATUS = 0x03 99 | self.CLIENT_SETTINGS = 0x04 100 | self.CONFIRM_TRANSACTION = 0x05 # TODO NEW 101 | self.ENCHANT_ITEM = 0x06 # TODO NEW 102 | self.CLICK_WINDOW = 0x07 103 | self.CLOSE_WINDOW = 0x08 # TODO NEW 104 | self.PLUGIN_MESSAGE = 0x09 105 | self.USE_ENTITY = 0x0a 106 | self.KEEP_ALIVE[PKT] = 0x0b 107 | self.PLAYER_POSITION = 0x0c 108 | self.PLAYER_POSLOOK = 0x0d 109 | self.PLAYER_LOOK = 0x0e 110 | self.PLAYER = 0x0f 111 | self.VEHICLE_MOVE = 0x10 # TODO NEW 112 | self.STEER_BOAT = 0x11 # TODO NEW 113 | self.PLAYER_ABILITIES = 0x12 114 | self.PLAYER_DIGGING = 0x13 115 | self.ENTITY_ACTION = 0x14 # TODO NEW 116 | self.STEER_VEHICLE = 0x15 # TODO NEW 117 | self.RESOURCE_PACK_STATUS = 0x16 # TODO NEW 118 | self.HELD_ITEM_CHANGE = 0x17 119 | self.CREATIVE_INVENTORY_ACTION = 0x18 # TODO NEW 120 | self.PLAYER_UPDATE_SIGN = 0x19 121 | self.ANIMATION = 0x1a # TODO NEW 122 | self.SPECTATE = 0x1b 123 | self.PLAYER_BLOCK_PLACEMENT = 0x1c 124 | self.USE_ITEM = 0x1d 125 | -------------------------------------------------------------------------------- /wrapper/proxy/serverconnection.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (C) 2016, 2017 - BenBaptist and Wrapper.py developer(s). 4 | # https://github.com/benbaptist/minecraft-wrapper 5 | # This program is distributed under the terms of the GNU 6 | # General Public License, version 3 or later. 7 | 8 | # standard 9 | import socket 10 | import threading 11 | import time 12 | import traceback 13 | 14 | # local 15 | from proxy.packet import Packet 16 | from proxy.parse_cb import ParseCB 17 | from proxy import mcpackets_sb 18 | from proxy import mcpackets_cb 19 | 20 | from proxy.constants import * 21 | 22 | 23 | # noinspection PyMethodMayBeStatic 24 | class ServerConnection(object): 25 | def __init__(self, client, ip=None, port=None): 26 | """ 27 | This class ServerConnection is a "fake" client connecting 28 | to the server. It receives "CLIENT BOUND" packets from 29 | server, parses them, and forards them on to the client. 30 | 31 | ServerConnection receives the parent client as it's argument. 32 | It's wrapper and proxy instances are passed from the Client. 33 | Therefore, a server instance does not really validly exist 34 | unless it has a valid parent client. 35 | 36 | Client, by contrast, can exist and run in the absence 37 | of a server. 38 | """ 39 | 40 | # TODO server needs to be a true child of clientconnection process. 41 | # It should not close its own instance, etc 42 | 43 | # basic __init__ items from passed arguments 44 | self.client = client 45 | self.wrapper = client.wrapper 46 | self.proxy = client.proxy 47 | self.log = client.wrapper.log 48 | self.ip = ip 49 | self.port = port 50 | 51 | # server setup and operating paramenters 52 | self.abort = False 53 | self.state = self.proxy.HANDSHAKE 54 | self.packet = None 55 | self.parse_cb = None 56 | self.buildmode = False 57 | 58 | # dictionary of parser packet constants and associated parsing methods 59 | self.parsers = {} 60 | 61 | self.infos_debug = "(player=%s, IP=%s, Port=%s)" % ( 62 | self.client.username, self.ip, self.port) 63 | self.version = -1 64 | 65 | # self parsers get updated here 66 | self._refresh_server_version() 67 | 68 | # temporary assignment. The actual socket is assigned later. 69 | self.server_socket = socket.socket() 70 | 71 | self.infos_debug = "(player=%s, IP=%s, Port=%s)" % ( 72 | self.client.username, self.ip, self.port) 73 | 74 | def _refresh_server_version(self): 75 | """Get serverversion for mcpackets use""" 76 | 77 | self.version = self.wrapper.javaserver.protocolVersion 78 | self.pktSB = mcpackets_sb.Packets(self.version) 79 | self.pktCB = mcpackets_cb.Packets(self.version) 80 | self.parse_cb = ParseCB(self, self.packet) 81 | self._define_parsers() 82 | 83 | if self.version > PROTOCOL_1_7: 84 | # used by ban code to enable wrapper group help for ban items. 85 | self.wrapper.api.registerPermission("mc1.7.6", value=True) 86 | 87 | def send(self, packetid, xpr, payload): 88 | """ Not supported. A wrapper of packet.send(), which is 89 | further a wrapper for packet.sendpkt(); both wrappers 90 | exist for older code compatability purposes only for 91 | 0.7.x version plugins that might use it.""" 92 | 93 | self.log.debug("deprecated server.send() called. Use " 94 | "server.packet.sendpkt() for best performance.") 95 | self.packet.send(packetid, xpr, payload) 96 | pass 97 | 98 | def connect(self): 99 | """ This simply establishes the tcp socket connection and 100 | starts the flush loop, NOTHING MORE. """ 101 | self.state = self.proxy.LOGIN 102 | # Connect to this wrapper's javaserver (core/mcserver.py) 103 | if self.ip is None: 104 | self.server_socket.connect(("localhost", 105 | self.wrapper.javaserver.server_port)) 106 | 107 | # Connect to some other server (or an offline wrapper) 108 | else: 109 | self.server_socket.connect((self.ip, self.port)) 110 | 111 | # start packet handler 112 | self.packet = Packet(self.server_socket, self) 113 | self.packet.version = self.client.clientversion 114 | 115 | # define parsers 116 | self.parse_cb = ParseCB(self, self.packet) 117 | self._define_parsers() 118 | 119 | t = threading.Thread(target=self.flush_loop, args=()) 120 | t.daemon = True 121 | t.start() 122 | 123 | def close_server(self, reason="Disconnected", lobby_return=False): 124 | """ 125 | :lobby_return: determines whether the client should be 126 | aborted too. 127 | :return: 128 | """ 129 | 130 | # todo remove this and fix reason code 131 | # print(reason) 132 | 133 | if lobby_return: 134 | # stop parsing PLAY packets to prevent further "disconnects" 135 | self.state = self.proxy.LOBBY 136 | self.log.debug("Disconnecting proxy server socket connection." 137 | " %s", self.infos_debug) 138 | 139 | # end 'handle' cleanly 140 | self.abort = True 141 | time.sleep(0.1) 142 | # noinspection PyBroadException 143 | try: 144 | self.server_socket.shutdown(2) 145 | self.log.debug("Sucessfully closed server socket for" 146 | " %s", self.infos_debug) 147 | 148 | # todo - we need to discover our expected exception 149 | except: 150 | self.log.debug("Server socket for %s already " 151 | "closed", self.infos_debug) 152 | pass 153 | 154 | if not lobby_return: 155 | self.client.abort = True 156 | 157 | # allow packet to be GC'ed 158 | self.packet = None 159 | 160 | def flush_loop(self): 161 | while not self.abort: 162 | try: 163 | self.packet.flush() 164 | except socket.error: 165 | self.log.debug("Socket_error- server socket was closed" 166 | " %s", self.infos_debug) 167 | break 168 | time.sleep(0.01) 169 | self.log.debug("server connection flush_loop thread ended." 170 | " %s", self.infos_debug) 171 | 172 | def handle(self): 173 | while not self.abort: 174 | # get packet 175 | try: 176 | pkid, original = self.packet.grabpacket() 177 | except EOFError as eof: 178 | # This error is often erroneous, see 179 | # https://github.com/suresttexas00/minecraft-wrapper/issues/30 180 | self.log.debug("%s server Packet EOF" 181 | " (%s)", self.infos_debug, eof) 182 | return self._break_handle() 183 | 184 | # Bad file descriptor occurs anytime a socket is closed. 185 | except socket.error: 186 | self.log.debug("%s Failed to grab packet [SERVER]" 187 | " socket error", self.infos_debug) 188 | return self._break_handle() 189 | except Exception as e: 190 | # anything that gets here is a bona-fide error we 191 | # need to become aware of 192 | self.log.debug("%s Failed to grab packet [SERVER]" 193 | " (%s):", self.infos_debug, e) 194 | return self._break_handle() 195 | 196 | # parse it 197 | if self.parse(pkid) and self.client.state in ( 198 | self.proxy.PLAY, self.proxy.LOBBY): 199 | try: 200 | self.client.packet.send_raw(original) 201 | if self.proxy.trace: 202 | self._do_trace(pkid, self.state) 203 | 204 | except Exception as e: 205 | self.log.debug("[SERVER %s] Could not send packet" 206 | " (%s): (%s): \n%s", 207 | self.infos_debug, pkid, e, traceback) 208 | return self._break_handle() 209 | 210 | def _do_trace(self, pkid, state): 211 | name = str(self.parsers[state][pkid]).split(" ")[0] 212 | if pkid not in self.proxy.ignoredCB: 213 | self.log.warn("<=CB %s (%s)", hex(pkid), name) 214 | 215 | def _break_handle(self): 216 | if self.state == self.proxy.LOBBY: 217 | self.log.info("%s is without a server now.", self.client.username) 218 | # self.close_server("%s server connection closing..." % 219 | # self.client.username, lobby_return=True) 220 | else: 221 | self.close_server("%s server connection" 222 | " closing..." % self.client.username) 223 | return 224 | 225 | def _parse_keep_alive(self): 226 | data = self.packet.readpkt( 227 | self.pktSB.KEEP_ALIVE[PARSER]) 228 | self.packet.sendpkt( 229 | self.pktSB.KEEP_ALIVE[PKT], 230 | self.pktSB.KEEP_ALIVE[PARSER], 231 | data) 232 | return False 233 | 234 | def _transmit_upstream(self): 235 | """ transmit wrapper channel status info to the server's 236 | direction to help sync hub/lobby wrappers """ 237 | 238 | channel = "WRAPPER|SYNC" 239 | 240 | # received SYNC from the client (this is a child wrapper) 241 | received = self.proxy.shared["received"] 242 | 243 | # if true, this is a multiworld (child wrapper instance) 244 | sent = self.proxy.shared["sent"] 245 | state = self.state 246 | 247 | if self.version < PROTOCOL_1_8START: 248 | self.packet.sendpkt( 249 | self.pktCB.PLUGIN_MESSAGE, 250 | [STRING, SHORT, BOOL, BOOL, BYTE], 251 | [channel, 3, received, sent, state]) 252 | else: 253 | self.packet.sendpkt( 254 | self.pktCB.PLUGIN_MESSAGE, 255 | [STRING, BOOL, BOOL, BYTE], 256 | [channel, received, sent, state]) 257 | 258 | # PARSERS SECTION 259 | # ----------------------------- 260 | 261 | # Login parsers 262 | # ----------------------- 263 | def _parse_login_disconnect(self): 264 | message = self.packet.readpkt([STRING]) 265 | self.log.info("Disconnected from server: %s", message) 266 | self.close_server(message) 267 | return False 268 | 269 | def _parse_login_encr_request(self): 270 | self.close_server("Server is in online mode. Please turn it off " 271 | "in server.properties and allow wrapper to " 272 | "handle the authetication.") 273 | return False 274 | 275 | # Login Success - UUID & Username are sent in this packet as strings 276 | def _parse_login_success(self): 277 | self.state = self.proxy.PLAY 278 | # todo - we may not need to assign this to a variable. 279 | # (we supplied uuid/name anyway!) 280 | # noinspection PyUnusedLocal 281 | data = self.packet.readpkt([STRING, STRING]) 282 | return False 283 | 284 | def _parse_login_set_compression(self): 285 | data = self.packet.readpkt([VARINT]) 286 | # ("varint:threshold") 287 | if data[0] != -1: 288 | self.packet.compression = True 289 | self.packet.compressThreshold = data[0] 290 | else: 291 | self.packet.compression = False 292 | self.packet.compressThreshold = -1 293 | time.sleep(10) 294 | return # False 295 | 296 | # Lobby parsers 297 | # ----------------------- 298 | def _parse_lobby_disconnect(self): 299 | message = self.packet.readpkt([JSON]) 300 | self.log.info("%s went back to Hub", self.client.username) 301 | self.close_server(message, lobby_return=True) 302 | 303 | def parse(self, pkid): 304 | try: 305 | return self.parsers[self.state][pkid]() 306 | except KeyError: 307 | self.parsers[self.state][pkid] = self._parse_built 308 | if self.buildmode: 309 | # some code here to document un-parsed packets? 310 | pass 311 | return True 312 | 313 | # Do nothing parser 314 | def _parse_built(self): 315 | return True 316 | 317 | def _define_parsers(self): 318 | # the packets we parse and the methods that parse them. 319 | self.parsers = { 320 | self.proxy.HANDSHAKE: {}, # maps identically to OFFLINE ( '0' ) 321 | self.proxy.LOGIN: { 322 | self.pktCB.LOGIN_DISCONNECT: 323 | self._parse_login_disconnect, 324 | self.pktCB.LOGIN_ENCR_REQUEST: 325 | self._parse_login_encr_request, 326 | self.pktCB.LOGIN_SUCCESS: 327 | self._parse_login_success, 328 | self.pktCB.LOGIN_SET_COMPRESSION: 329 | self._parse_login_set_compression 330 | }, 331 | self.proxy.PLAY: { 332 | self.pktCB.COMBAT_EVENT: 333 | self.parse_cb.parse_play_combat_event, 334 | self.pktCB.KEEP_ALIVE[PKT]: 335 | self._parse_keep_alive, 336 | self.pktCB.CHAT_MESSAGE[PKT]: 337 | self.parse_cb.parse_play_chat_message, 338 | self.pktCB.JOIN_GAME[PKT]: 339 | self.parse_cb.parse_play_join_game, 340 | self.pktCB.TIME_UPDATE: 341 | self.parse_cb.parse_play_time_update, 342 | self.pktCB.SPAWN_POSITION: 343 | self.parse_cb.parse_play_spawn_position, 344 | self.pktCB.RESPAWN: 345 | self.parse_cb.parse_play_respawn, 346 | self.pktCB.PLAYER_POSLOOK: 347 | self.parse_cb.parse_play_player_poslook, 348 | self.pktCB.USE_BED: 349 | self.parse_cb.parse_play_use_bed, 350 | self.pktCB.SPAWN_PLAYER: 351 | self.parse_cb.parse_play_spawn_player, 352 | self.pktCB.SPAWN_OBJECT: 353 | self.parse_cb.parse_play_spawn_object, 354 | self.pktCB.SPAWN_MOB: 355 | self.parse_cb.parse_play_spawn_mob, 356 | self.pktCB.ENTITY_RELATIVE_MOVE: 357 | self.parse_cb.parse_play_entity_relative_move, 358 | self.pktCB.ENTITY_TELEPORT: 359 | self.parse_cb.parse_play_entity_teleport, 360 | self.pktCB.ATTACH_ENTITY: 361 | self.parse_cb.parse_play_attach_entity, 362 | self.pktCB.DESTROY_ENTITIES: 363 | self.parse_cb.parse_play_destroy_entities, 364 | self.pktCB.MAP_CHUNK_BULK: 365 | self.parse_cb.parse_play_map_chunk_bulk, 366 | self.pktCB.CHANGE_GAME_STATE: 367 | self.parse_cb.parse_play_change_game_state, 368 | self.pktCB.OPEN_WINDOW: 369 | self.parse_cb.parse_play_open_window, 370 | self.pktCB.SET_SLOT: 371 | self.parse_cb.parse_play_set_slot, 372 | self.pktCB.WINDOW_ITEMS: 373 | self.parse_cb.parse_play_window_items, 374 | self.pktCB.ENTITY_PROPERTIES: 375 | self.parse_cb.parse_play_entity_properties, 376 | self.pktCB.PLAYER_LIST_ITEM: 377 | self.parse_cb.parse_play_player_list_item, 378 | self.pktCB.DISCONNECT: 379 | self.parse_cb.parse_play_disconnect, 380 | self.pktCB.ENTITY_METADATA[PKT]: 381 | self.parse_cb.parse_entity_metadata, 382 | }, 383 | self.proxy.LOBBY: { 384 | self.pktCB.DISCONNECT: 385 | self._parse_lobby_disconnect, 386 | self.pktCB.KEEP_ALIVE[PKT]: 387 | self._parse_keep_alive 388 | } 389 | } 390 | -------------------------------------------------------------------------------- /wrapper/test.py: -------------------------------------------------------------------------------- 1 | import api 2 | 3 | from api.base import API 4 | 5 | #from api.base import API 6 | api = API(wrapper, "Web", internal=True) 7 | world = api.minecraft.getWorld() -------------------------------------------------------------------------------- /wrapper/utils/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (C) 2016, 2017 - BenBaptist and Wrapper.py developer(s). 4 | # https://github.com/benbaptist/minecraft-wrapper 5 | # This program is distributed under the terms of the GNU 6 | # General Public License, version 3 or later. 7 | -------------------------------------------------------------------------------- /wrapper/utils/encryption.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (C) 2016, 2017 - BenBaptist and Wrapper.py developer(s). 4 | # https://github.com/benbaptist/minecraft-wrapper 5 | # This program is distributed under the terms of the GNU 6 | # General Public License, version 3 or later. 7 | 8 | 9 | from Crypto.PublicKey import RSA 10 | from Crypto import Random 11 | from Crypto.Cipher import AES 12 | 13 | # Py3-2 14 | import sys 15 | PY3 = sys.version_info > (3,) 16 | 17 | 18 | def decode_public_key(thebytes): 19 | """Decodes a public RSA key in ASN.1 format as defined by x.509""" 20 | return RSA.importKey(thebytes) 21 | 22 | 23 | def encode_public_key(key): 24 | """Encodes a public RSA key in ASN.1 format as defined by x.509""" 25 | return key.publickey().exportKey(format="DER") 26 | 27 | 28 | def generate_key_pair(): 29 | """Generates a 1024 bit RSA key pair""" 30 | return RSA.generate(1024) 31 | 32 | 33 | def generate_random_bytes(length): 34 | return Random.get_random_bytes(length) 35 | 36 | 37 | def generate_server_id(): 38 | """Generates 20 random hex characters""" 39 | if PY3: 40 | return "".join("%02x" % c for c in generate_random_bytes(10)) 41 | else: 42 | return "".join("%02x" % ord(c) for c in generate_random_bytes(10)) 43 | 44 | 45 | def generate_challenge_token(): 46 | """Generates 4 random bytes""" 47 | return generate_random_bytes(4) 48 | 49 | 50 | def decrypt_shared_secret(encrypted_key, private_key): 51 | """Decrypts the PKCS#1 padded shared secret using the private RSA key""" 52 | return _pkcs1_unpad(private_key.decrypt(encrypted_key)) 53 | 54 | 55 | # noinspection PyPep8Naming 56 | def AES128CFB8(shared_secret): 57 | """Creates a AES128 stream cipher using cfb8 mode""" 58 | return AES.new(shared_secret, AES.MODE_CFB, shared_secret) 59 | 60 | 61 | def _pkcs1_unpad(thebytes): 62 | null_byte = '\x00' 63 | if PY3: 64 | null_byte = 0x00 65 | pos = thebytes.find(null_byte) 66 | if pos > 0: 67 | return thebytes[pos + 1:] 68 | -------------------------------------------------------------------------------- /wrapper/utils/entities.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (C) 2016, 2017 - BenBaptist and Wrapper.py developer(s). 4 | # https://github.com/benbaptist/minecraft-wrapper 5 | # This program is distributed under the terms of the GNU 6 | # General Public License, version 3 or later. 7 | PRE1_11_RENAMES = { 8 | # Mob Entities 9 | "chest_minecart": "MinecartChest", 10 | "commandblock_minecart": "MinecartCommandBlock", 11 | "egg": "ThrownEgg", 12 | "ender_pearl": "ThrownEnderpearl", 13 | "falling_block": "FallingSand", 14 | "fireworks_rocket": "FireworksRocketEntity", 15 | "furnace_minecart": "MinecartFurnace", 16 | "hopper_minecart": "MinecartHopper", 17 | "horse": "EntityHorse", 18 | "magma_cube": "LavaSlime", 19 | "minecart": "MinecartRideable", 20 | "mooshroom": "MushroomCow", 21 | "ocelot": "Ozelot", 22 | "potion": "ThrownPotion", 23 | "spawner_minecart": "MinecartSpawner", 24 | "tnt": "PrimedTnt", 25 | "tnt_minecart": "MinecartTNT", 26 | "wither": "WitherBoss", 27 | "xp_bottle": "ThrownExpBottle", 28 | "xp_orb": "XPOrb", 29 | "zombie_pigman": "PigZombie", 30 | # Block Entities 31 | "brewing_stand": "Cauldron", 32 | "command_block": "Control", 33 | "daylight_detector": "DLDetector", 34 | "dispenser": "Trap", 35 | "enchanting_table": "EnchantTable", 36 | "end_portal": "AirPortal", 37 | "jukebox": "RecordPlayer", 38 | "noteblock": "Music", 39 | "structure_block": "Structure", 40 | } 41 | 42 | 43 | ENTITIES = { 44 | 1: { 45 | "name": "item" 46 | }, 47 | 2: { 48 | "name": "xp_orb" 49 | }, 50 | 3: { 51 | "name": "area_effect_cloud" 52 | }, 53 | 4: { 54 | "name": "elder_guardian" 55 | }, 56 | 5: { 57 | "name": "wither_skeleton" 58 | }, 59 | 6: { 60 | "name": "stray" 61 | }, 62 | 7: { 63 | "name": "egg" 64 | }, 65 | 8: { 66 | "name": "leash_knot" 67 | }, 68 | 9: { 69 | "name": "painting" 70 | }, 71 | 10: { 72 | "name": "arrow" 73 | }, 74 | 11: { 75 | "name": "snowball" 76 | }, 77 | 12: { 78 | "name": "fireball" 79 | }, 80 | 13: { 81 | "name": "small_fireball" 82 | }, 83 | 14: { 84 | "name": "ender_pearl" 85 | }, 86 | 15: { 87 | "name": "eye_of_ender_signal" 88 | }, 89 | 16: { 90 | "name": "potion" 91 | }, 92 | 17: { 93 | "name": "xp_bottle" 94 | }, 95 | 18: { 96 | "name": "item_frame" 97 | }, 98 | 19: { 99 | "name": "wither_skull" 100 | }, 101 | 20: { 102 | "name": "tnt" 103 | }, 104 | 21: { 105 | "name": "falling_block" 106 | }, 107 | 22: { 108 | "name": "fireworks_rocket" 109 | }, 110 | 23: { 111 | "name": "husk" 112 | }, 113 | 24: { 114 | "name": "spectral_arrow" 115 | }, 116 | 25: { 117 | "name": "shulker_bullet" 118 | }, 119 | 26: { 120 | "name": "dragon_fireball" 121 | }, 122 | 27: { 123 | "name": "zombie_villager" 124 | }, 125 | 28: { 126 | "name": "skeleton_horse" 127 | }, 128 | 29: { 129 | "name": "zombie_horse" 130 | }, 131 | 30: { 132 | "name": "armor_stand" 133 | }, 134 | 31: { 135 | "name": "donkey" 136 | }, 137 | 32: { 138 | "name": "mule" 139 | }, 140 | 33: { 141 | "name": "evocation_fangs" 142 | }, 143 | 34: { 144 | "name": "evocation_illager" 145 | }, 146 | 35: { 147 | "name": "vex" 148 | }, 149 | 36: { 150 | "name": "vindication_illager" 151 | }, 152 | 40: { 153 | "name": "commandblock_minecart" 154 | }, 155 | 41: { 156 | "name": "boat" 157 | }, 158 | 42: { 159 | "name": "minecart" 160 | }, 161 | 43: { 162 | "name": "chest_minecart" 163 | }, 164 | 44: { 165 | "name": "furnace_minecart" 166 | }, 167 | 45: { 168 | "name": "tnt_minecart" 169 | }, 170 | 46: { 171 | "name": "hopper_minecart" 172 | }, 173 | 47: { 174 | "name": "spawner_minecart" 175 | }, 176 | 50: { 177 | "name": "creeper" 178 | }, 179 | 51: { 180 | "name": "skeleton" 181 | }, 182 | 52: { 183 | "name": "spider" 184 | }, 185 | 53: { 186 | "name": "giant" 187 | }, 188 | 54: { 189 | "name": "zombie" 190 | }, 191 | 55: { 192 | "name": "slime" 193 | }, 194 | 56: { 195 | "name": "ghast" 196 | }, 197 | 57: { 198 | "name": "zombie_pigman" 199 | }, 200 | 58: { 201 | "name": "enderman" 202 | }, 203 | 59: { 204 | "name": "cave_spider" 205 | }, 206 | 60: { 207 | "name": "silverfish" 208 | }, 209 | 61: { 210 | "name": "blaze" 211 | }, 212 | 62: { 213 | "name": "magma_cube" 214 | }, 215 | 63: { 216 | "name": "ender_dragon" 217 | }, 218 | 64: { 219 | "name": "wither" 220 | }, 221 | 65: { 222 | "name": "bat" 223 | }, 224 | 66: { 225 | "name": "witch" 226 | }, 227 | 67: { 228 | "name": "endermite" 229 | }, 230 | 68: { 231 | "name": "guardian" 232 | }, 233 | 69: { 234 | "name": "shulker" 235 | }, 236 | 90: { 237 | "name": "pig" 238 | }, 239 | 91: { 240 | "name": "sheep" 241 | }, 242 | 92: { 243 | "name": "cow" 244 | }, 245 | 93: { 246 | "name": "chicken" 247 | }, 248 | 94: { 249 | "name": "squid" 250 | }, 251 | 95: { 252 | "name": "wolf" 253 | }, 254 | 96: { 255 | "name": "mooshroom" 256 | }, 257 | 97: { 258 | "name": "snowman" 259 | }, 260 | 98: { 261 | "name": "ocelot" 262 | }, 263 | 99: { 264 | "name": "villager_golem" 265 | }, 266 | 100: { 267 | "name": "horse" 268 | }, 269 | 101: { 270 | "name": "rabbit" 271 | }, 272 | 102: { 273 | "name": "polar_bear" 274 | }, 275 | 103: { 276 | "name": "llama" 277 | }, 278 | 104: { 279 | "name": "llama_spit" 280 | }, 281 | 120: { 282 | "name": "villager" 283 | }, 284 | 200: { 285 | "name": "ender_crystal" 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /wrapper/utils/log.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (C) 2016, 2017 - BenBaptist and Wrapper.py developer(s). 4 | # https://github.com/benbaptist/minecraft-wrapper 5 | # This program is distributed under the terms of the GNU 6 | # General Public License, version 3 or later. 7 | 8 | import json 9 | import os 10 | import logging 11 | from logging.config import dictConfig 12 | 13 | # noinspection PyProtectedMember 14 | from api.helpers import mkdir_p, _use_style 15 | 16 | DEFAULT = { 17 | "wrapperversion": 1.2, 18 | "version": 1, 19 | "disable_existing_loggers": False, 20 | "formatters": { 21 | "standard": { 22 | "()": "utils.log.ColorFormatter", 23 | "format": "[%(asctime)s] [%(name)s/%(levelname)s]: %(message)s", 24 | "datefmt": "%H:%M:%S" 25 | }, 26 | "file": { 27 | "format": "[%(asctime)s] [%(name)s/%(levelname)s]: %(message)s", 28 | "datefmt": "%Y-%m-%d %H:%M:%S" 29 | } 30 | }, 31 | "handlers": { 32 | "console": { 33 | "class": "logging.StreamHandler", 34 | "level": "INFO", 35 | "formatter": "standard", 36 | "filters": [], 37 | "stream": "ext://sys.stdout" 38 | }, 39 | "wrapper_file_handler": { 40 | "class": "utils.log.WrapperHandler", 41 | "level": "INFO", 42 | "formatter": "file", 43 | "filters": [], 44 | "filename": "logs/wrapper/wrapper.log", 45 | "maxBytes": 10485760, 46 | "backupCount": 20, 47 | "encoding": "utf8" 48 | }, 49 | "error_file_handler": { 50 | "class": "utils.log.WrapperHandler", 51 | "level": "ERROR", 52 | "formatter": "file", 53 | "filters": [], 54 | "filename": "logs/wrapper/wrapper.errors.log", 55 | "maxBytes": 10485760, 56 | "backupCount": 20, 57 | "encoding": "utf8" 58 | } 59 | }, 60 | "root": { 61 | "level": "NOTSET", 62 | "handlers": ["console", "wrapper_file_handler", "error_file_handler"] 63 | } 64 | } 65 | 66 | 67 | def configure_logger(betterconsole=False): 68 | loadconfig(betterconsole=betterconsole) 69 | logging.getLogger() 70 | 71 | 72 | def loadconfig(betterconsole=False, configfile="logging.json"): 73 | dictConfig(DEFAULT) # Load default config 74 | try: 75 | if os.path.isfile(configfile): 76 | with open(configfile, "r") as f: 77 | conf = json.load(f) 78 | 79 | # Use newer logging configuration, if the one on disk is too old 80 | if "wrapperversion" not in conf or \ 81 | (conf["wrapperversion"] < DEFAULT["wrapperversion"]): 82 | with open(configfile, "w") as f: 83 | f.write(json.dumps(DEFAULT, indent=4, 84 | separators=(',', ': '))) 85 | logging.warning("Logging configuration updated (%s) -- creat" 86 | "ing new logging configuration", configfile) 87 | else: 88 | if betterconsole: 89 | readcurrent = conf["formatters"]["standard"]["format"] 90 | conf["formatters"]["standard"]["format"] = ( 91 | # go up one line to print - '^[1A' (in hex ASCII) 92 | "\x1b\x5b\x31\x41%s\r\n" % readcurrent) 93 | dictConfig(conf) 94 | logging.info("Logging configuration file (%s) located and " 95 | "loaded, logging configuration set!", configfile) 96 | else: 97 | with open(configfile, "w") as f: 98 | f.write(json.dumps(DEFAULT, indent=4, separators=(',', ': '))) 99 | logging.warning("Unable to locate %s -- Creating default logging " 100 | "configuration", configfile) 101 | except Exception as e: 102 | logging.exception("Unable to load or create %s! (%s)", configfile, e) 103 | 104 | 105 | class ColorFormatter(logging.Formatter): 106 | """This custom formatter will format console color/option 107 | (bold, italic, etc) and output based on logging level.""" 108 | def __init__(self, *args, **kwargs): 109 | super(ColorFormatter, self).__init__(*args, **kwargs) 110 | 111 | # noinspection PyUnusedLocal 112 | def format(self, record): 113 | args = record.args 114 | msg = record.msg 115 | 116 | # Only style on *nix since windows doesn't support ANSI 117 | if os.name in ("posix", "mac"): 118 | if record.levelno == logging.INFO: 119 | info_style = _use_style(foreground="green") 120 | msg = info_style(msg) 121 | elif record.levelno == logging.DEBUG: 122 | debug_style = _use_style(foreground="cyan") 123 | msg = debug_style(msg) 124 | elif record.levelno == logging.WARNING: 125 | warn_style = _use_style(foreground="yellow", options=("bold",)) 126 | msg = warn_style(msg) 127 | elif record.levelno == logging.ERROR: 128 | error_style = _use_style(foreground="red", options=("bold",)) 129 | msg = error_style(msg) 130 | elif record.levelno == logging.CRITICAL: 131 | crit_style = _use_style(foreground="black", background="red", 132 | options=("bold",)) 133 | msg = crit_style(msg) 134 | 135 | record.msg = msg 136 | 137 | return super(ColorFormatter, self).format(record) 138 | 139 | 140 | # noinspection PyPep8Naming,PyUnresolvedReferences 141 | class WrapperHandler(logging.handlers.RotatingFileHandler): 142 | def __init__(self, filename, mode='a', maxBytes=0, 143 | backupCount=0, encoding=None, delay=0): 144 | mkdir_p(os.path.dirname(filename)) 145 | super(WrapperHandler, self).__init__(filename, mode, maxBytes, 146 | backupCount, encoding, delay) 147 | -------------------------------------------------------------------------------- /wrapper/utils/readkey.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (C) 2017 - SurestTexas00. 4 | # This program is distributed under the terms of the GNU 5 | # General Public License, version 3 or later. 6 | # 7 | # However, this file is based heavily on this gist: 8 | # http://code.activestate.com/recipes/134892/ 9 | # and is therefore also attributed to DannyYoo and company. 10 | # 11 | # Some elements (like the ESCAPE_SEQUENCES construct) are 12 | # attibuted to Miguel Ángel García (@magmax9) and his 13 | # readchar package. 14 | 15 | 16 | import sys 17 | 18 | WINDOWS = True 19 | try: 20 | import msvcrt 21 | except ImportError: 22 | WINDOWS = False 23 | import tty 24 | import termios 25 | 26 | # Linux keyboard constants 27 | ESC = '\x1b' 28 | TAB = '\x09' 29 | LF = '\x0d' 30 | CR = '\x0a' 31 | ENTER = '\x0d' 32 | BACKSPACE = '\x7f' 33 | CTRL_A = '\x01' 34 | CTRL_B = '\x02' 35 | CTRL_C = '\x03' 36 | CTRL_D = '\x04' 37 | CTRL_E = '\x05' 38 | CTRL_F = '\x06' 39 | CTRL_Z = '\x1a' 40 | ALT_TAB = '\x1b\x09' 41 | ALT_A = '\x1b\x61' 42 | CTRL_ALT_A = '\x1b\x01' 43 | UP = '\x1b\x5b\x41' 44 | DOWN = '\x1b\x5b\x42' 45 | LEFT = '\x1b\x5b\x44' 46 | RIGHT = '\x1b\x5b\x43' 47 | CTRL_ALT_DEL = '\x1b\x5b\x33\x5e' 48 | F1 = '\x1b\x4f\x50' 49 | F2 = '\x1b\x4f\x51' 50 | F3 = '\x1b\x4f\x52' 51 | F4 = '\x1b\x4f\x53' 52 | F5 = '\x1b\x5b\x31\x35\x7e' 53 | F1_1 = '\x1b\x5b\x5b\x41' 54 | F2_1 = '\x1b\x5b\x5b\x42' 55 | F3_1 = '\x1b\x5b\x5b\x43' 56 | F4_1 = '\x1b\x5b\x5b\x44' 57 | F5_1 = '\x1b\x5b\x5b\x45' 58 | F6 = '\x1b\x5b\x31\x37\x7e' 59 | F7 = '\x1b\x5b\x31\x38\x7e' 60 | F8 = '\x1b\x5b\x31\x39\x7e' 61 | F9 = '\x1b\x5b\x32\x30\x7e' 62 | F10 = '\x1b\x5b\x32\x31\x7e' 63 | F11_1 = '\x1b\x5b\x32\x33\x7e' 64 | F11 = '\x1b\x5b\x32\x33\x7e\x1b' 65 | F12_1 = '\x1b\x5b\x32\x34\x7e' 66 | F12 = '\x1b\x5b\x32\x34\x7e\x08' 67 | PAGE_UP = '\x1b\x5b\x35\x7e' 68 | PAGE_DOWN = '\x1b\x5b\x36\x7e' 69 | HOME_1 = '\x1b\x5b\x31\x7e' 70 | END_1 = '\x1b\x5b\x34\x7e' 71 | INSERT = '\x1b\x5b\x32\x7e' 72 | DELETE = '\x1b\x5b\x33\x7e' 73 | HOME = '\x1b\x5b\x48' 74 | END = '\x1b\x5b\x46' 75 | # Windows 76 | BACKSPACE_WIN = '\x08' 77 | CTRL_X_WIN = '\x18' 78 | CTRL_ALT_A_WIN = '\x00\x1e' 79 | UP_WIN = '\xe0\x48' 80 | DOWN_WIN = '\xe0\x50' 81 | LEFT_WIN = '\xe0\x4b' 82 | RIGHT_WIN = '\xe0\x4d' 83 | F1_WIN = '\x00\x3b' 84 | F2_WIN = '\x00\x3c' 85 | F3_WIN = '\x00\x3d' 86 | F4_WIN = '\x00\x3e' 87 | F5_WIN = '\x00\x3f' 88 | F6_WIN = '\x00\x40' 89 | F7_WIN = '\x00\x41' 90 | F8_WIN = '\x00\x42' 91 | F9_WIN = '\x00\x43' 92 | F10_WIN = '\x00\x44' 93 | F11_WIN = '\xe0\x85' 94 | F12_WIN = '\xe0\x86' 95 | PAGE_UP_WIN = '\xe0\x49' 96 | PAGE_DOWN_WIN = '\xe0\x51' 97 | INSERT_WIN = '\xe0\x52' 98 | DELETE_WIN = '\xe0\x53' 99 | HOME_WIN = '\xe0\x47' 100 | END_WIN = '\xe0\x4f' 101 | PAGE_UP_WIN_NUMLOCK = '\x00\x49' 102 | PAGE_DOWN_WIN_NUMLOCK = '\x00\x51' 103 | HOME_WIN_NUMLOCK = '\x00\x47' 104 | END_WIN_NUMLOCK = '\x00\x4f' 105 | UP_WIN_NUMLOCK = '\x00\x48' 106 | DOWN_WIN_NUMLOCK = '\x00\x50' 107 | LEFT_WIN_NUMLOCK = '\x00\x4b' 108 | RIGHT_WIN_NUMLOCK = '\x00\x4d' 109 | INSERT_WIN_NUMLOCK = '\x00\x52' 110 | DELETE_WIN_NUMLOCK = '\x00\x53' 111 | 112 | NAMES = { 113 | ESC: 'esc', 114 | TAB: 'tab', 115 | LF: 'lf', 116 | CR: 'cr', 117 | ENTER: 'enter', 118 | BACKSPACE: 'backspace', 119 | CTRL_A: 'ctrl-a', 120 | CTRL_B: 'ctrl-b', 121 | CTRL_C: 'ctrl-c', 122 | CTRL_D: 'ctrl-d', 123 | CTRL_E: 'ctrl-e', 124 | CTRL_F: 'ctrl-f', 125 | CTRL_Z: 'ctrl-z', 126 | ALT_TAB: 'alt-tab', 127 | ALT_A: 'alt-a', 128 | CTRL_ALT_A: 'ctrl-alt-a', 129 | UP: 'up', 130 | DOWN: 'down', 131 | LEFT: 'left', 132 | RIGHT: 'right', 133 | CTRL_ALT_DEL: 'ctrl-alt-del', 134 | F1: 'f1', 135 | F2: 'f2', 136 | F3: 'f3', 137 | F4: 'f4', 138 | F5: 'f5', 139 | F1_1: 'f1', 140 | F2_1: 'f2', 141 | F3_1: 'f3', 142 | F4_1: 'f4', 143 | F5_1: 'f5', 144 | F6: 'f6', 145 | F7: 'f7', 146 | F8: 'f8', 147 | F9: 'f9', 148 | F10: 'f10', 149 | F11: 'f11', 150 | F12: 'f12', 151 | F11_1: 'f11', 152 | F12_1: 'f12', 153 | PAGE_UP: 'page-up', 154 | PAGE_DOWN: 'page-down', 155 | HOME_1: 'home', 156 | END_1: 'end', 157 | INSERT: 'insert', 158 | DELETE: 'delete', 159 | HOME: 'home', 160 | END: 'end', 161 | BACKSPACE_WIN: 'backspace', 162 | CTRL_X_WIN: 'ctrl-x', 163 | CTRL_ALT_A_WIN: 'ctrl-alt-a', 164 | UP_WIN: 'up', 165 | DOWN_WIN: 'down', 166 | LEFT_WIN: 'left', 167 | RIGHT_WIN: 'right', 168 | F1_WIN: 'f1', 169 | F2_WIN: 'f2', 170 | F3_WIN: 'f3', 171 | F4_WIN: 'f4', 172 | F5_WIN: 'f5', 173 | F6_WIN: 'f6', 174 | F7_WIN: 'f7', 175 | F8_WIN: 'f8', 176 | F9_WIN: 'f9', 177 | F10_WIN: 'f10', 178 | F11_WIN: 'f11', 179 | F12_WIN: 'f12', 180 | PAGE_UP_WIN: 'page-up', 181 | PAGE_DOWN_WIN: 'page-down', 182 | INSERT_WIN: 'insert', 183 | DELETE_WIN: 'delete', 184 | HOME_WIN: 'home', 185 | END_WIN: 'end', 186 | PAGE_UP_WIN_NUMLOCK: 'page-up', 187 | PAGE_DOWN_WIN_NUMLOCK: 'page-down', 188 | HOME_WIN_NUMLOCK: 'home', 189 | END_WIN_NUMLOCK: 'end', 190 | UP_WIN_NUMLOCK: 'up', 191 | DOWN_WIN_NUMLOCK: 'down', 192 | LEFT_WIN_NUMLOCK: 'left', 193 | RIGHT_WIN_NUMLOCK: 'right', 194 | INSERT_WIN_NUMLOCK: 'insert', 195 | DELETE_WIN_NUMLOCK: 'delete', 196 | } 197 | 198 | ESCAPE_SEQUENCES = ( 199 | ESC, 200 | ESC + '\x5b', 201 | ESC + '\x5b' + '\x5b', 202 | ESC + '\x5b' + '\x31', 203 | ESC + '\x5b' + '\x32', 204 | ESC + '\x5b' + '\x33', 205 | ESC + '\x5b' + '\x34', 206 | ESC + '\x5b' + '\x35', 207 | ESC + '\x5b' + '\x36', 208 | 209 | ESC + '\x5b' + '\x31' + '\x33', 210 | ESC + '\x5b' + '\x31' + '\x34', 211 | ESC + '\x5b' + '\x31' + '\x35', 212 | ESC + '\x5b' + '\x31' + '\x36', 213 | ESC + '\x5b' + '\x31' + '\x37', 214 | ESC + '\x5b' + '\x31' + '\x38', 215 | ESC + '\x5b' + '\x31' + '\x39', 216 | 217 | ESC + '\x5b' + '\x32' + '\x30', 218 | ESC + '\x5b' + '\x32' + '\x31', 219 | ESC + '\x5b' + '\x32' + '\x32', 220 | ESC + '\x5b' + '\x32' + '\x33', 221 | ESC + '\x5b' + '\x32' + '\x34', 222 | ESC + '\x5b' + '\x32' + '\x33' + '\x7e', 223 | ESC + '\x5b' + '\x32' + '\x34' + '\x7e', 224 | ESC + '\x4f', 225 | 226 | ESC + ESC, 227 | ESC + ESC + '\x5b', 228 | ESC + ESC + '\x5b' + '\x32', 229 | ESC + ESC + '\x5b' + '\x33', 230 | 231 | # Windows sequences 232 | '\x00', 233 | '\xe0', 234 | ) 235 | 236 | 237 | class _Getch(object): 238 | """Gets a single character from standard input. Does not echo to the 239 | screen.""" 240 | def __init__(self): 241 | try: 242 | self.getch = _GetchWindows() 243 | except ImportError: 244 | self.getch = _GetchUnix() 245 | 246 | def __call__(self): return self.getch() 247 | 248 | 249 | class _GetchUnix(object): 250 | def __init__(self): 251 | pass 252 | 253 | def __call__(self): 254 | fd = sys.stdin.fileno() 255 | old_settings = termios.tcgetattr(fd) 256 | try: 257 | tty.setraw(sys.stdin.fileno()) 258 | ch = sys.stdin.read(1) 259 | finally: 260 | termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) 261 | return ch 262 | 263 | 264 | class _GetchWindows(object): 265 | def __init__(self): 266 | if not WINDOWS: 267 | # purposely cause import error for try-except 268 | # noinspection PyUnresolvedReferences 269 | import msvcrt 270 | 271 | def __call__(self): 272 | # noinspection PyUnresolvedReferences 273 | return msvcrt.getch().decode('latin-1') 274 | 275 | 276 | def getcharacter(): 277 | g = _Getch() 278 | charbuffer = "" 279 | while True: 280 | # noinspection PyArgumentEqualDefault 281 | char1 = g.__call__() 282 | if (charbuffer + char1) not in ESCAPE_SEQUENCES: 283 | charbuffer += char1 284 | break 285 | 286 | if (charbuffer + char1) == charbuffer: 287 | break 288 | 289 | charbuffer += char1 290 | return charbuffer 291 | 292 | 293 | def convertchar(charbuffer): 294 | if charbuffer in NAMES: 295 | return NAMES[charbuffer] 296 | return None 297 | 298 | 299 | def _test(): 300 | running = True 301 | while running: 302 | charbuffer = getcharacter() 303 | name = convertchar(charbuffer) 304 | if name == "up": 305 | print("UP key...") 306 | it = "\\x".join("{:02x}".format(ord(c)) for c in charbuffer) 307 | if name: 308 | print(name) 309 | else: 310 | print("\\x%s" % it) 311 | if name == "ctrl-c": 312 | break 313 | 314 | 315 | if __name__ == "__main__": 316 | _test() 317 | --------------------------------------------------------------------------------