├── .circleci └── config.yml ├── .gitignore ├── .mvn └── wrapper │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── Dockerfile ├── README.md ├── mvn-settings.xml ├── mvnw ├── mvnw.cmd ├── pom.xml └── src ├── main ├── java │ └── me │ │ └── itzg │ │ └── mcstatus │ │ ├── AppProperties.java │ │ ├── CorsProperties.java │ │ ├── McStatusApplication.java │ │ ├── ServerTimeoutException.java │ │ ├── WebConfigurer.java │ │ ├── WebController.java │ │ ├── model │ │ ├── Motd.java │ │ ├── Player.java │ │ ├── PlayersInfo.java │ │ └── ServerStatus.java │ │ └── services │ │ ├── FormattedMessageConverter.java │ │ ├── OneShotRunner.java │ │ └── ServerStatusService.java └── resources │ ├── application-one-shot.yml │ ├── application.yml │ └── banner.txt └── test ├── java └── me │ └── itzg │ └── mcstatus │ ├── McStatusApplicationTests.java │ └── services │ └── FormattedMessageConverterTest.java └── resources └── cors-test.html /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | jobs: 4 | build: 5 | docker: 6 | - image: circleci/openjdk:8-jdk 7 | 8 | working_directory: ~/repo 9 | 10 | environment: 11 | MAVEN_OPTS: -Xmx1200m 12 | 13 | steps: 14 | - checkout 15 | 16 | - restore_cache: 17 | keys: 18 | - v1-dependencies-{{ checksum "pom.xml" }} 19 | - v1-dependencies- 20 | 21 | - run: mvn dependency:go-offline 22 | 23 | - save_cache: 24 | paths: 25 | - ~/.m2 26 | key: v1-dependencies-{{ checksum "pom.xml" }} 27 | 28 | - run: mvn verify 29 | 30 | - run: 31 | name: "Docker build" 32 | command: > 33 | mvn jib:build 34 | -Djib.to.auth.username=${DOCKER_USER} 35 | -Djib.to.auth.password=${DOCKER_PASSWORD} 36 | 37 | - run: | 38 | mkdir /tmp/artifacts 39 | cp target/*.jar /tmp/artifacts 40 | 41 | - store_artifacts: 42 | path: /tmp/artifacts 43 | 44 | - persist_to_workspace: 45 | root: /tmp/artifacts 46 | paths: 47 | - "*" 48 | 49 | publish-github-release: 50 | docker: 51 | - image: cibuilds/github:0.12 52 | steps: 53 | - attach_workspace: 54 | at: ./artifacts 55 | - run: 56 | name: "Publish to Github" 57 | command: > 58 | ghr 59 | -t ${GITHUB_TOKEN} 60 | -u ${CIRCLE_PROJECT_USERNAME} 61 | -r ${CIRCLE_PROJECT_REPONAME} 62 | -c ${CIRCLE_SHA1} 63 | -delete 64 | ${CIRCLE_TAG} 65 | ./artifacts/ 66 | 67 | workflows: 68 | version: 2 69 | 70 | main: 71 | jobs: 72 | - build: 73 | filters: 74 | tags: 75 | only: /^\d+\.\d+\.\d+$/ 76 | - publish-github-release: 77 | requires: 78 | - build 79 | filters: 80 | branches: 81 | ignore: /.*/ 82 | tags: 83 | only: /^\d+\.\d+\.\d+$/ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | 4 | ### STS ### 5 | .apt_generated 6 | .classpath 7 | .factorypath 8 | .project 9 | .settings 10 | .springBeans 11 | 12 | ### IntelliJ IDEA ### 13 | .idea 14 | *.iws 15 | *.iml 16 | *.ipr 17 | 18 | ### NetBeans ### 19 | nbproject/private/ 20 | build/ 21 | nbbuild/ 22 | dist/ 23 | nbdist/ 24 | .nb-gradle/ -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itzg/mc-status/71b32097323f987b3aebc98ab231465f59d59423/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.0/apache-maven-3.5.0-bin.zip 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:8u171-jre-alpine 2 | 3 | LABEL MAINTAINER=itzg 4 | 5 | ENTRYPOINT ["/usr/bin/java", "-jar", "/usr/share/mc-status.jar"] 6 | 7 | ARG JAR_FILE 8 | ADD target/${JAR_FILE} /usr/share/mc-status.jar -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Deprecated 3 | 4 | This project is deprecated and [mc-monitor](https://github.com/itzg/mc-monitor) should be used instead. 5 | 6 | 7 | [![GitHub release](https://img.shields.io/github/release/itzg/mc-status.svg)](https://github.com/itzg/mc-status/releases/latest) 8 | [![Docker Pulls](https://img.shields.io/docker/pulls/itzg/mc-status.svg)](https://hub.docker.com/r/itzg/mc-status) 9 | 10 | A web application that serves up a little REST API to query and convey the status of Minecraft servers using the native MC protocol 11 | 12 | ## Usage as a REST Server 13 | 14 | To use only the ad hoc query endpoint, `/server`, just start using: 15 | 16 | java -jar mc-status-$VERSION.jar 17 | 18 | The following configuration parameters can be passed after the jar: 19 | 20 | * **--mcstatus.servers**=host[:port] 21 | 22 | A comma separated list of `host[:port]` to pre-configure for use with `/servers` endpoint 23 | 24 | * **--mcstatus.defaultPort**=25565 25 | 26 | The default port to use when absent from the property above or the ad hoc `/server` endpoint 27 | 28 | * **--mcstatus.serverInfoTimeoutSec**=10 29 | 30 | The number of seconds allowed for a server info response to be received 31 | 32 | * **--cors.allowOrigins** 33 | 34 | A comma separated list of specific origins (as URLs) to allow. Implies a setting of `cors.allowAll=false` 35 | 36 | * **--cors.allowAll**=true 37 | 38 | Set to false to disable allowing of all cross-origin requests. 39 | 40 | ## Usage as a one-shot status checker 41 | 42 | You can also run mc-status in a one-shot, terse, script-friendly way by invoking it as: 43 | 44 | java -jar mc-status --one-shot SERVER[:PORT] ... 45 | 46 | If any `SERVER[:PORT]` status checks times out, then the process exits with a non-zero status code. 47 | 48 | Some other options that are useful to include are: 49 | 50 | * **--results**=FILE 51 | 52 | The name of a file where each line is a JSON response structure for the corresponding server. 53 | 54 | * **--mcstatus.exclude-icon**=false 55 | 56 | Can be used to exclude the Base64 icon inclusion. 57 | 58 | * **--mcstatus.exclude-players**=false 59 | 60 | Can be used to exclude the list of player names. The online and max values are still included. 61 | 62 | ## Response Structure 63 | 64 | In response to `GET /server?host=:host&port=:port` 65 | 66 | ```json 67 | { 68 | "host": "localhost", 69 | "port": 25565, 70 | "version": "1.12", 71 | "protocolVersion": 335, 72 | "players": { 73 | "max": 20, 74 | "online": 0, 75 | "players": [] 76 | }, 77 | "description": "A Minecraft Server Powered by Docker", 78 | "icon": "iVBORw0KG...snip...AAASUVORK5CYII=", 79 | "motd": { 80 | "raw": "A Vanilla Minecraft Server powered by Docker", 81 | "html": "A Vanilla Minecraft Server powered by Docker", 82 | "stripped": "A Vanilla Minecraft Server powered by Docker" 83 | } 84 | } 85 | ``` 86 | 87 | **NOTE** the `icon` is the Base64 encoding of a 64x64 PNG image 88 | 89 | In response to `GET /servers` 90 | 91 | ```json 92 | [ 93 | { 94 | "host": "localhost", 95 | "port": 25565, 96 | "version": "1.12", 97 | "protocolVersion": 335, 98 | "players": { 99 | "max": 20, 100 | "online": 0, 101 | "players": [] 102 | }, 103 | "description": "A Minecraft Server Powered by Docker", 104 | "icon": "iVBORw0KG...snip...ORK5CYII=", 105 | "motd": { 106 | "raw": "A Vanilla Minecraft Server powered by Docker", 107 | "html": "A Vanilla Minecraft Server powered by Docker", 108 | "stripped": "A Vanilla Minecraft Server powered by Docker" 109 | } 110 | } 111 | ] 112 | ``` 113 | 114 | Error when timed out connecting to Minecraft server: 115 | 116 | ```json 117 | { 118 | "timestamp": 1497050389168, 119 | "status": 502, 120 | "error": "Bad Gateway", 121 | "exception": "me.itzg.mcstatus.ServerTimeoutException", 122 | "message": "Timed out getting server info from localhost:25567 after 10 seconds", 123 | "path": "/server" 124 | } 125 | ``` 126 | 127 | ## Formatted MOTD 128 | 129 | The server status responses will take care of providing alternate formats for the Message of the Day (MOTD), 130 | where `motd.html` will replace the [formatting codes](https://minecraft.gamepedia.com/Formatting_codes) 131 | with the appropriate `` styling. 132 | 133 | The `motd.stripped` provides a simplified string that removes the formatting codes and normalizes whitespace. 134 | 135 | The `description` and `motd.raw` fields contain the original value as returned by the server. 136 | 137 | For example, this is the response when querying `mc.hypixel.net`: 138 | ```json 139 | { 140 | "host": "mc.hypixel.net", 141 | "port": 25565, 142 | "version": "Requires MC 1.8-1.13", 143 | "protocolVersion": 316, 144 | "players": { 145 | "max": 62000, 146 | "online": 27264, 147 | "players": [] 148 | }, 149 | "description": " §eHypixel Network §c[1.8-1.13]\n §6§lBED WARS CASTLE V2 §7- §2§lMM BUG FIXES", 150 | "icon": "iVBORw0KGgoAAAANSUhEUgAAAE...", 151 | "motd": { 152 | "raw": " §eHypixel Network §c[1.8-1.13]\n §6§lBED WARS CASTLE V2 §7- §2§lMM BUG FIXES", 153 | "html": " Hypixel Network [1.8-1.13]
BED WARS CASTLE V2 - MM BUG FIXES", 154 | "stripped": "Hypixel Network [1.8-1.13] BED WARS CASTLE V2 - MM BUG FIXES" 155 | } 156 | } 157 | ``` 158 | -------------------------------------------------------------------------------- /mvn-settings.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | bintray 9 | ${env.BINTRAY_USERNAME} 10 | ${env.BINTRAY_APIKEY} 11 | 12 | 13 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven2 Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 59 | if [ -z "$JAVA_HOME" ]; then 60 | if [ -x "/usr/libexec/java_home" ]; then 61 | export JAVA_HOME="`/usr/libexec/java_home`" 62 | else 63 | export JAVA_HOME="/Library/Java/Home" 64 | fi 65 | fi 66 | ;; 67 | esac 68 | 69 | if [ -z "$JAVA_HOME" ] ; then 70 | if [ -r /etc/gentoo-release ] ; then 71 | JAVA_HOME=`java-config --jre-home` 72 | fi 73 | fi 74 | 75 | if [ -z "$M2_HOME" ] ; then 76 | ## resolve links - $0 may be a link to maven's home 77 | PRG="$0" 78 | 79 | # need this for relative symlinks 80 | while [ -h "$PRG" ] ; do 81 | ls=`ls -ld "$PRG"` 82 | link=`expr "$ls" : '.*-> \(.*\)$'` 83 | if expr "$link" : '/.*' > /dev/null; then 84 | PRG="$link" 85 | else 86 | PRG="`dirname "$PRG"`/$link" 87 | fi 88 | done 89 | 90 | saveddir=`pwd` 91 | 92 | M2_HOME=`dirname "$PRG"`/.. 93 | 94 | # make it fully qualified 95 | M2_HOME=`cd "$M2_HOME" && pwd` 96 | 97 | cd "$saveddir" 98 | # echo Using m2 at $M2_HOME 99 | fi 100 | 101 | # For Cygwin, ensure paths are in UNIX format before anything is touched 102 | if $cygwin ; then 103 | [ -n "$M2_HOME" ] && 104 | M2_HOME=`cygpath --unix "$M2_HOME"` 105 | [ -n "$JAVA_HOME" ] && 106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 107 | [ -n "$CLASSPATH" ] && 108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 109 | fi 110 | 111 | # For Migwn, ensure paths are in UNIX format before anything is touched 112 | if $mingw ; then 113 | [ -n "$M2_HOME" ] && 114 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 115 | [ -n "$JAVA_HOME" ] && 116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 117 | # TODO classpath? 118 | fi 119 | 120 | if [ -z "$JAVA_HOME" ]; then 121 | javaExecutable="`which javac`" 122 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 123 | # readlink(1) is not available as standard on Solaris 10. 124 | readLink=`which readlink` 125 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 126 | if $darwin ; then 127 | javaHome="`dirname \"$javaExecutable\"`" 128 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 129 | else 130 | javaExecutable="`readlink -f \"$javaExecutable\"`" 131 | fi 132 | javaHome="`dirname \"$javaExecutable\"`" 133 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 134 | JAVA_HOME="$javaHome" 135 | export JAVA_HOME 136 | fi 137 | fi 138 | fi 139 | 140 | if [ -z "$JAVACMD" ] ; then 141 | if [ -n "$JAVA_HOME" ] ; then 142 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 143 | # IBM's JDK on AIX uses strange locations for the executables 144 | JAVACMD="$JAVA_HOME/jre/sh/java" 145 | else 146 | JAVACMD="$JAVA_HOME/bin/java" 147 | fi 148 | else 149 | JAVACMD="`which java`" 150 | fi 151 | fi 152 | 153 | if [ ! -x "$JAVACMD" ] ; then 154 | echo "Error: JAVA_HOME is not defined correctly." >&2 155 | echo " We cannot execute $JAVACMD" >&2 156 | exit 1 157 | fi 158 | 159 | if [ -z "$JAVA_HOME" ] ; then 160 | echo "Warning: JAVA_HOME environment variable is not set." 161 | fi 162 | 163 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 164 | 165 | # traverses directory structure from process work directory to filesystem root 166 | # first directory with .mvn subdirectory is considered project base directory 167 | find_maven_basedir() { 168 | 169 | if [ -z "$1" ] 170 | then 171 | echo "Path not specified to find_maven_basedir" 172 | return 1 173 | fi 174 | 175 | basedir="$1" 176 | wdir="$1" 177 | while [ "$wdir" != '/' ] ; do 178 | if [ -d "$wdir"/.mvn ] ; then 179 | basedir=$wdir 180 | break 181 | fi 182 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 183 | if [ -d "${wdir}" ]; then 184 | wdir=`cd "$wdir/.."; pwd` 185 | fi 186 | # end of workaround 187 | done 188 | echo "${basedir}" 189 | } 190 | 191 | # concatenates all lines of a file 192 | concat_lines() { 193 | if [ -f "$1" ]; then 194 | echo "$(tr -s '\n' ' ' < "$1")" 195 | fi 196 | } 197 | 198 | BASE_DIR=`find_maven_basedir "$(pwd)"` 199 | if [ -z "$BASE_DIR" ]; then 200 | exit 1; 201 | fi 202 | 203 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 204 | echo $MAVEN_PROJECTBASEDIR 205 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 206 | 207 | # For Cygwin, switch paths to Windows format before running java 208 | if $cygwin; then 209 | [ -n "$M2_HOME" ] && 210 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 211 | [ -n "$JAVA_HOME" ] && 212 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 213 | [ -n "$CLASSPATH" ] && 214 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 215 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 216 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 217 | fi 218 | 219 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 220 | 221 | exec "$JAVACMD" \ 222 | $MAVEN_OPTS \ 223 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 224 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 225 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 226 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM http://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven2 Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' 39 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 40 | 41 | @REM set %HOME% to equivalent of $HOME 42 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 43 | 44 | @REM Execute a user defined script before this one 45 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 46 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 47 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 48 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 49 | :skipRcPre 50 | 51 | @setlocal 52 | 53 | set ERROR_CODE=0 54 | 55 | @REM To isolate internal variables from possible post scripts, we use another setlocal 56 | @setlocal 57 | 58 | @REM ==== START VALIDATION ==== 59 | if not "%JAVA_HOME%" == "" goto OkJHome 60 | 61 | echo. 62 | echo Error: JAVA_HOME not found in your environment. >&2 63 | echo Please set the JAVA_HOME variable in your environment to match the >&2 64 | echo location of your Java installation. >&2 65 | echo. 66 | goto error 67 | 68 | :OkJHome 69 | if exist "%JAVA_HOME%\bin\java.exe" goto init 70 | 71 | echo. 72 | echo Error: JAVA_HOME is set to an invalid directory. >&2 73 | echo JAVA_HOME = "%JAVA_HOME%" >&2 74 | echo Please set the JAVA_HOME variable in your environment to match the >&2 75 | echo location of your Java installation. >&2 76 | echo. 77 | goto error 78 | 79 | @REM ==== END VALIDATION ==== 80 | 81 | :init 82 | 83 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 84 | @REM Fallback to current working directory if not found. 85 | 86 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 87 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 88 | 89 | set EXEC_DIR=%CD% 90 | set WDIR=%EXEC_DIR% 91 | :findBaseDir 92 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 93 | cd .. 94 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 95 | set WDIR=%CD% 96 | goto findBaseDir 97 | 98 | :baseDirFound 99 | set MAVEN_PROJECTBASEDIR=%WDIR% 100 | cd "%EXEC_DIR%" 101 | goto endDetectBaseDir 102 | 103 | :baseDirNotFound 104 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 105 | cd "%EXEC_DIR%" 106 | 107 | :endDetectBaseDir 108 | 109 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 110 | 111 | @setlocal EnableExtensions EnableDelayedExpansion 112 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 113 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 114 | 115 | :endReadAdditionalConfig 116 | 117 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 118 | 119 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 120 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 121 | 122 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 123 | if ERRORLEVEL 1 goto error 124 | goto end 125 | 126 | :error 127 | set ERROR_CODE=1 128 | 129 | :end 130 | @endlocal & set ERROR_CODE=%ERROR_CODE% 131 | 132 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 133 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 134 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 135 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 136 | :skipRcPost 137 | 138 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 139 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 140 | 141 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 142 | 143 | exit /B %ERROR_CODE% 144 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | me.itzg 6 | mc-status 7 | 1.5-SNAPSHOT 8 | jar 9 | 10 | mc-status 11 | Simple REST server that conveys Minecraft server status 12 | 13 | 14 | org.springframework.boot 15 | spring-boot-starter-parent 16 | 2.1.5.RELEASE 17 | 18 | 19 | 20 | 21 | UTF-8 22 | UTF-8 23 | 1.8 24 | 25 | 26 | 27 | scm:git:https://github.com/itzg/mc-status 28 | scm:git:https://github.com/itzg/mc-status 29 | HEAD 30 | 31 | 32 | 33 | 34 | itzg 35 | Geoff Bourne 36 | itzgeoff@gmail.com 37 | 38 | 39 | 40 | 41 | 42 | org.springframework.boot 43 | spring-boot-starter-validation 44 | 45 | 46 | org.springframework.boot 47 | spring-boot-starter-web 48 | 49 | 50 | org.springframework.boot 51 | spring-boot-actuator 52 | 53 | 54 | 55 | com.github.Steveice10 56 | MCProtocolLib 57 | 1.12.2-2 58 | 59 | 60 | com.google.guava 61 | guava 62 | 19.0 63 | 64 | 65 | 66 | org.springframework.boot 67 | spring-boot-devtools 68 | runtime 69 | 70 | 71 | org.springframework.boot 72 | spring-boot-configuration-processor 73 | true 74 | 75 | 76 | org.projectlombok 77 | lombok 78 | true 79 | 80 | 81 | org.springframework.boot 82 | spring-boot-starter-test 83 | test 84 | 85 | 86 | 87 | 88 | 89 | 90 | maven-surefire-plugin 91 | 2.22.1 92 | 93 | 95 | false 96 | 97 | 98 | 99 | 100 | org.springframework.boot 101 | spring-boot-maven-plugin 102 | 103 | 104 | 105 | com.google.cloud.tools 106 | jib-maven-plugin 107 | 1.3.0 108 | 109 | 110 | docker.io/itzg/mc-status 111 | 112 | ${project.version} 113 | 114 | 115 | 116 | 117 | 118 | 119 | org.apache.maven.plugins 120 | maven-release-plugin 121 | 2.5.3 122 | 123 | @{project.version} 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | jitpack.io 133 | https://jitpack.io 134 | 135 | 136 | 137 | 138 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/mcstatus/AppProperties.java: -------------------------------------------------------------------------------- 1 | package me.itzg.mcstatus; 2 | 3 | import lombok.Data; 4 | import org.springframework.boot.context.properties.ConfigurationProperties; 5 | import org.springframework.stereotype.Component; 6 | 7 | import java.util.Collections; 8 | import java.util.List; 9 | 10 | /** 11 | * @author Geoff Bourne 12 | * @since Jun 2017 13 | */ 14 | @ConfigurationProperties("mcstatus") 15 | @Component @Data 16 | public class AppProperties { 17 | int defaultPort = 25565; 18 | List servers = Collections.emptyList(); 19 | int serverInfoTimeoutSec = 10; 20 | 21 | boolean excludeIcon = false; 22 | boolean excludePlayers = false; 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/mcstatus/CorsProperties.java: -------------------------------------------------------------------------------- 1 | package me.itzg.mcstatus; 2 | 3 | import lombok.Data; 4 | import org.springframework.boot.context.properties.ConfigurationProperties; 5 | import org.springframework.stereotype.Component; 6 | 7 | import java.util.List; 8 | 9 | /** 10 | * @author Geoff Bourne 11 | * @since Jun 2018 12 | */ 13 | @ConfigurationProperties("cors") @Component 14 | @Data 15 | public class CorsProperties { 16 | boolean allowAll = true; 17 | 18 | String[] allowOrigins; 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/mcstatus/McStatusApplication.java: -------------------------------------------------------------------------------- 1 | package me.itzg.mcstatus; 2 | 3 | import org.springframework.boot.Banner.Mode; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration; 7 | 8 | @SpringBootApplication(exclude = {GsonAutoConfiguration.class}) 9 | public class McStatusApplication { 10 | 11 | public static void main(String[] args) { 12 | final SpringApplication app = new SpringApplication(McStatusApplication.class); 13 | 14 | for (String arg : args) { 15 | if (arg.equals("--one-shot")) { 16 | app.setAdditionalProfiles("one-shot"); 17 | app.setBannerMode(Mode.OFF); 18 | } 19 | } 20 | 21 | app.run(args); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/mcstatus/ServerTimeoutException.java: -------------------------------------------------------------------------------- 1 | package me.itzg.mcstatus; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | import java.util.concurrent.TimeoutException; 7 | 8 | /** 9 | * @author Geoff Bourne 10 | * @since Jun 2017 11 | */ 12 | @ResponseStatus(HttpStatus.BAD_GATEWAY) 13 | public class ServerTimeoutException extends TimeoutException { 14 | public ServerTimeoutException(String message) { 15 | super(message); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/mcstatus/WebConfigurer.java: -------------------------------------------------------------------------------- 1 | package me.itzg.mcstatus; 2 | 3 | import java.util.Arrays; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.web.servlet.config.annotation.CorsRegistry; 8 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 9 | 10 | /** 11 | * @author Geoff Bourne 12 | * @since Jun 2018 13 | */ 14 | @Configuration @Slf4j 15 | public class WebConfigurer implements WebMvcConfigurer { 16 | public static final String CORS_PATH = "/**"; 17 | private final CorsProperties corsProperties; 18 | 19 | @Autowired 20 | public WebConfigurer(CorsProperties corsProperties) { 21 | this.corsProperties = corsProperties; 22 | } 23 | 24 | @Override 25 | public void addCorsMappings(CorsRegistry registry) { 26 | final String[] allowedOrigins = corsProperties.getAllowOrigins(); 27 | if (allowedOrigins != null && allowedOrigins.length > 0) { 28 | log.info("Allowing origins: {}", Arrays.asList(allowedOrigins)); 29 | registry.addMapping(CORS_PATH).allowedOrigins(allowedOrigins); 30 | } 31 | else if (corsProperties.isAllowAll()) { 32 | log.info("Allowing all origins"); 33 | registry.addMapping(CORS_PATH).allowedOrigins("*"); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/mcstatus/WebController.java: -------------------------------------------------------------------------------- 1 | package me.itzg.mcstatus; 2 | 3 | import com.google.common.net.HostAndPort; 4 | import lombok.extern.slf4j.Slf4j; 5 | import me.itzg.mcstatus.model.ServerStatus; 6 | import me.itzg.mcstatus.services.ServerStatusService; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.web.bind.annotation.GetMapping; 9 | import org.springframework.web.bind.annotation.RequestParam; 10 | import org.springframework.web.bind.annotation.RestController; 11 | 12 | import java.util.List; 13 | 14 | /** 15 | * @author Geoff Bourne 16 | * @since Jun 2017 17 | */ 18 | @RestController 19 | @Slf4j 20 | public class WebController { 21 | 22 | private final ServerStatusService serverStatusService; 23 | 24 | @Autowired 25 | public WebController(ServerStatusService serverStatusService) { 26 | this.serverStatusService = serverStatusService; 27 | } 28 | 29 | @GetMapping("/servers") 30 | public List getAllServersStatus() { 31 | 32 | return serverStatusService.getAllServersStatus(); 33 | } 34 | 35 | @GetMapping("/server") 36 | public ServerStatus getServerStatus(@RequestParam String host, @RequestParam(defaultValue = "25565") int port) throws ServerTimeoutException { 37 | 38 | return serverStatusService.getServerStatus(host, port); 39 | } 40 | 41 | private ServerStatus getServerStatus(HostAndPort hostAndPort) throws ServerTimeoutException { 42 | 43 | return serverStatusService.getServerStatus(hostAndPort); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/mcstatus/model/Motd.java: -------------------------------------------------------------------------------- 1 | package me.itzg.mcstatus.model; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * @author Geoff Bourne 7 | * @since Aug 2018 8 | */ 9 | @Data 10 | public class Motd { 11 | String raw; 12 | String html; 13 | String stripped; 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/mcstatus/model/Player.java: -------------------------------------------------------------------------------- 1 | package me.itzg.mcstatus.model; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * @author Geoff Bourne 7 | * @since Jun 2017 8 | */ 9 | @Data 10 | public class Player { 11 | String name; 12 | String id; 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/mcstatus/model/PlayersInfo.java: -------------------------------------------------------------------------------- 1 | package me.itzg.mcstatus.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import lombok.Data; 5 | 6 | import java.util.List; 7 | 8 | /** 9 | * @author Geoff Bourne 10 | * @since Jun 2017 11 | */ 12 | @Data @JsonInclude(value = JsonInclude.Include.NON_NULL) 13 | public class PlayersInfo { 14 | int max; 15 | int online; 16 | List players; 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/mcstatus/model/ServerStatus.java: -------------------------------------------------------------------------------- 1 | package me.itzg.mcstatus.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import lombok.Data; 5 | 6 | /** 7 | * @author Geoff Bourne 8 | * @since Jun 2017 9 | */ 10 | @Data @JsonInclude(value = JsonInclude.Include.NON_NULL) 11 | public class ServerStatus { 12 | String host; 13 | int port; 14 | String version; 15 | int protocolVersion; 16 | PlayersInfo players = new PlayersInfo(); 17 | String description; 18 | byte[] icon; 19 | Motd motd; 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/mcstatus/services/FormattedMessageConverter.java: -------------------------------------------------------------------------------- 1 | package me.itzg.mcstatus.services; 2 | 3 | import com.github.steveice10.mc.protocol.data.message.ChatColor; 4 | import com.github.steveice10.mc.protocol.data.message.ChatFormat; 5 | import com.github.steveice10.mc.protocol.data.message.Message; 6 | import lombok.RequiredArgsConstructor; 7 | import org.springframework.stereotype.Component; 8 | 9 | import java.util.EnumMap; 10 | import java.util.HashMap; 11 | import java.util.List; 12 | import java.util.Map; 13 | import java.util.regex.Matcher; 14 | import java.util.regex.Pattern; 15 | 16 | /** 17 | * @author Geoff Bourne 18 | * @since Aug 2018 19 | */ 20 | @Component 21 | public class FormattedMessageConverter { 22 | enum Type { 23 | COLOR, 24 | FORMAT, 25 | RESET 26 | } 27 | 28 | @RequiredArgsConstructor 29 | static class Entry { 30 | final Type type; 31 | final String style; 32 | } 33 | 34 | private static Map codes = new HashMap<>(); 35 | 36 | public static final String RESET_CODE = "§r"; 37 | 38 | static { 39 | codes.put("§0", new Entry(Type.COLOR, "color:#000000")); 40 | codes.put("§1", new Entry(Type.COLOR, "color:#0000AA")); 41 | codes.put("§2", new Entry(Type.COLOR, "color:#00AA00")); 42 | codes.put("§3", new Entry(Type.COLOR, "color:#00AAAA")); 43 | codes.put("§4", new Entry(Type.COLOR, "color:#AA0000")); 44 | codes.put("§5", new Entry(Type.COLOR, "color:#AA00AA")); 45 | codes.put("§6", new Entry(Type.COLOR, "color:#FFAA00")); 46 | codes.put("§7", new Entry(Type.COLOR, "color:#AAAAAA")); 47 | codes.put("§8", new Entry(Type.COLOR, "color:#555555")); 48 | codes.put("§9", new Entry(Type.COLOR, "color:#5555FF")); 49 | codes.put("§a", new Entry(Type.COLOR, "color:#55FF55")); 50 | codes.put("§b", new Entry(Type.COLOR, "color:#55FFFF")); 51 | codes.put("§c", new Entry(Type.COLOR, "color:#FF5555")); 52 | codes.put("§d", new Entry(Type.COLOR, "color:#FF55FF")); 53 | codes.put("§e", new Entry(Type.COLOR, "color:#FFFF55")); 54 | codes.put("§f", new Entry(Type.COLOR, "color:#FFFFFF")); 55 | 56 | codes.put("§l", new Entry(Type.FORMAT, "font-weight:bold")); 57 | codes.put("§m", new Entry(Type.FORMAT, "text-decoration:line-through")); 58 | codes.put("§n", new Entry(Type.FORMAT, "text-decoration:underline")); 59 | codes.put("§o", new Entry(Type.FORMAT, "font-style:italic")); 60 | 61 | codes.put(RESET_CODE, new Entry(Type.RESET, "")); 62 | } 63 | 64 | private static Map chatColorCodes = new EnumMap<>(ChatColor.class); 65 | static { 66 | chatColorCodes.put(ChatColor.BLACK, "§0"); 67 | chatColorCodes.put(ChatColor.DARK_BLUE, "§1"); 68 | chatColorCodes.put(ChatColor.DARK_GREEN, "§2"); 69 | chatColorCodes.put(ChatColor.DARK_AQUA, "§3"); 70 | chatColorCodes.put(ChatColor.DARK_RED, "§4"); 71 | chatColorCodes.put(ChatColor.DARK_PURPLE, "§5"); 72 | chatColorCodes.put(ChatColor.GOLD, "§6"); 73 | chatColorCodes.put(ChatColor.GRAY, "§7"); 74 | chatColorCodes.put(ChatColor.DARK_GRAY, "§8"); 75 | chatColorCodes.put(ChatColor.BLUE, "§9"); 76 | chatColorCodes.put(ChatColor.GREEN, "§a"); 77 | chatColorCodes.put(ChatColor.AQUA, "§b"); 78 | chatColorCodes.put(ChatColor.RED, "§c"); 79 | chatColorCodes.put(ChatColor.LIGHT_PURPLE, "§d"); 80 | chatColorCodes.put(ChatColor.YELLOW, "§e"); 81 | chatColorCodes.put(ChatColor.WHITE, "§f"); 82 | } 83 | 84 | private static Map chatFormatCodes = new EnumMap<>(ChatFormat.class); 85 | static { 86 | chatFormatCodes.put(ChatFormat.OBFUSCATED, "§k"); 87 | chatFormatCodes.put(ChatFormat.BOLD, "§l"); 88 | chatFormatCodes.put(ChatFormat.STRIKETHROUGH, "§m"); 89 | chatFormatCodes.put(ChatFormat.UNDERLINED, "§n"); 90 | chatFormatCodes.put(ChatFormat.ITALIC, "§o"); 91 | } 92 | 93 | private static final Pattern CODE_PATTERN = Pattern.compile("(§.)"); 94 | 95 | public String convertToHtml(String raw) { 96 | final Matcher m = CODE_PATTERN.matcher(raw); 97 | final StringBuffer sb = new StringBuffer(); 98 | boolean inColor = false; 99 | boolean inFormat = false; 100 | 101 | while (m.find()) { 102 | final String code = m.group(); 103 | final Entry entry = codes.get(code); 104 | final StringBuilder replacement = new StringBuilder(); 105 | if (entry != null) { 106 | switch (entry.type) { 107 | case COLOR: 108 | if (inFormat) { 109 | replacement.append("
"); 110 | inFormat = false; 111 | } 112 | if (inColor) { 113 | replacement.append(""); 114 | } 115 | inColor = true; 116 | break; 117 | case FORMAT: 118 | if (inFormat) { 119 | replacement.append(""); 120 | } 121 | inFormat = true; 122 | break; 123 | case RESET: 124 | if (inFormat) { 125 | replacement.append(""); 126 | } 127 | if (inColor) { 128 | replacement.append(""); 129 | } 130 | inColor = inFormat = false; 131 | break; 132 | } 133 | 134 | replacement.append(entry.type == Type.RESET ? "" : ""); 136 | } 137 | 138 | m.appendReplacement(sb, replacement.toString()); 139 | } 140 | m.appendTail(sb); 141 | if (inColor) { 142 | sb.append(""); 143 | } 144 | if (inFormat) { 145 | sb.append(""); 146 | } 147 | 148 | return sb.toString().replace("\n", "
"); 149 | } 150 | 151 | public String convertPartsToCode(List parts) { 152 | final StringBuilder sb = new StringBuilder(); 153 | 154 | for (Message part : parts) { 155 | sb.append(chatColorCodes.get(part.getStyle().getColor())); 156 | for (ChatFormat format : part.getStyle().getFormats()) { 157 | sb.append(chatFormatCodes.get(format)); 158 | } 159 | sb.append(part.getText()); 160 | } 161 | 162 | return sb.toString(); 163 | } 164 | 165 | public String stripCodes(String raw) { 166 | return CODE_PATTERN.matcher(raw) 167 | .replaceAll("") 168 | .replaceAll("\\s+", " ") 169 | .trim(); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/mcstatus/services/OneShotRunner.java: -------------------------------------------------------------------------------- 1 | package me.itzg.mcstatus.services; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.google.common.net.HostAndPort; 6 | import lombok.extern.slf4j.Slf4j; 7 | import me.itzg.mcstatus.ServerTimeoutException; 8 | import me.itzg.mcstatus.model.ServerStatus; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.boot.ApplicationArguments; 11 | import org.springframework.boot.ApplicationRunner; 12 | import org.springframework.stereotype.Service; 13 | 14 | import java.io.FileOutputStream; 15 | import java.io.PrintStream; 16 | import java.util.ArrayList; 17 | import java.util.List; 18 | import java.util.Optional; 19 | 20 | /** 21 | * @author Geoff Bourne 22 | * @since Jun 2017 23 | */ 24 | @Service 25 | @Slf4j 26 | public class OneShotRunner implements ApplicationRunner { 27 | 28 | private final ServerStatusService serverStatusService; 29 | private final ObjectMapper objectMapper; 30 | 31 | @Autowired 32 | public OneShotRunner(ServerStatusService serverStatusService, ObjectMapper objectMapper) { 33 | this.serverStatusService = serverStatusService; 34 | this.objectMapper = objectMapper; 35 | } 36 | 37 | @Override 38 | public void run(ApplicationArguments applicationArguments) throws Exception { 39 | 40 | final List args = applicationArguments.getNonOptionArgs(); 41 | 42 | if (!args.isEmpty()) { 43 | final List results = applicationArguments.getOptionValues("results"); 44 | 45 | final List outputs = new ArrayList<>(); 46 | outputs.add(System.out); 47 | 48 | final String resultsOut; 49 | if (results != null && !results.isEmpty()) { 50 | resultsOut = results.get(0); 51 | outputs.add(new PrintStream(new FileOutputStream(resultsOut))); 52 | } 53 | else { 54 | resultsOut = null; 55 | } 56 | 57 | final Integer totalFails; 58 | try { 59 | totalFails = args.stream().reduce(0, (fails, s) -> { 60 | try { 61 | final ServerStatus serverStatus = serverStatusService.getServerStatus(HostAndPort.fromString(s)); 62 | 63 | try { 64 | final String str = objectMapper.writeValueAsString(serverStatus); 65 | outputs.forEach(out -> out.println(str)); 66 | } catch (JsonProcessingException e) { 67 | log.error("Failed to serialize JSON for {}", serverStatus, e); 68 | outputs.forEach(out -> out.println("ERROR")); 69 | return 1; 70 | } 71 | 72 | return 0; 73 | } catch (ServerTimeoutException e) { 74 | log.error("Timed out requesting status from {}", s); 75 | outputs.forEach(out -> out.println("ERROR")); 76 | return 1; 77 | } 78 | }, (f1, f2) -> f1 + f2); 79 | } finally { 80 | if (resultsOut != null) { 81 | log.info("Wrote results to {}", resultsOut); 82 | outputs.get(1).close(); 83 | } 84 | } 85 | 86 | System.exit(totalFails); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/mcstatus/services/ServerStatusService.java: -------------------------------------------------------------------------------- 1 | package me.itzg.mcstatus.services; 2 | 3 | import com.github.steveice10.mc.protocol.MinecraftConstants; 4 | import com.github.steveice10.mc.protocol.MinecraftProtocol; 5 | import com.github.steveice10.mc.protocol.data.SubProtocol; 6 | import com.github.steveice10.mc.protocol.data.status.ServerStatusInfo; 7 | import com.github.steveice10.mc.protocol.data.status.handler.ServerInfoHandler; 8 | import com.github.steveice10.packetlib.Client; 9 | import com.github.steveice10.packetlib.Session; 10 | import com.github.steveice10.packetlib.tcp.TcpSessionFactory; 11 | import com.google.common.net.HostAndPort; 12 | import lombok.extern.slf4j.Slf4j; 13 | import me.itzg.mcstatus.AppProperties; 14 | import me.itzg.mcstatus.ServerTimeoutException; 15 | import me.itzg.mcstatus.model.Motd; 16 | import me.itzg.mcstatus.model.Player; 17 | import me.itzg.mcstatus.model.ServerStatus; 18 | import org.springframework.beans.factory.annotation.Autowired; 19 | import org.springframework.stereotype.Service; 20 | import org.springframework.web.bind.annotation.GetMapping; 21 | 22 | import javax.imageio.ImageIO; 23 | import java.io.ByteArrayOutputStream; 24 | import java.io.IOException; 25 | import java.util.List; 26 | import java.util.Objects; 27 | import java.util.concurrent.CompletableFuture; 28 | import java.util.concurrent.ExecutionException; 29 | import java.util.concurrent.TimeUnit; 30 | import java.util.concurrent.TimeoutException; 31 | import java.util.stream.Collectors; 32 | import java.util.stream.Stream; 33 | 34 | @Service 35 | @Slf4j 36 | public class ServerStatusService { 37 | public final AppProperties properties; 38 | private final FormattedMessageConverter formattedMessageConverter; 39 | 40 | @Autowired 41 | public ServerStatusService(AppProperties properties, FormattedMessageConverter formattedMessageConverter) { 42 | this.properties = properties; 43 | this.formattedMessageConverter = formattedMessageConverter; 44 | } 45 | 46 | @GetMapping("/servers") 47 | public List getAllServersStatus() { 48 | 49 | return properties.getServers().stream() 50 | .map(HostAndPort::fromString) 51 | .map(hostAndPort -> { 52 | try { 53 | return getServerStatus(hostAndPort); 54 | } catch (ServerTimeoutException e) { 55 | return null; 56 | } 57 | }) 58 | .filter(Objects::nonNull) 59 | .collect(Collectors.toList()); 60 | } 61 | 62 | public ServerStatus getServerStatus(String host, int port) throws ServerTimeoutException { 63 | 64 | return getServerStatus(HostAndPort.fromParts(host, port)); 65 | } 66 | 67 | public ServerStatus getServerStatus(HostAndPort hostAndPort) throws ServerTimeoutException { 68 | final int port = hostAndPort.getPortOrDefault(properties.getDefaultPort()); 69 | 70 | final CompletableFuture infoFuture = new CompletableFuture(); 71 | 72 | final MinecraftProtocol mcProto = new MinecraftProtocol(SubProtocol.STATUS); 73 | Client client = new Client(hostAndPort.getHostText(), port, mcProto, new TcpSessionFactory()); 74 | client.getSession().setFlag(MinecraftConstants.SERVER_INFO_HANDLER_KEY, new ServerInfoHandler() { 75 | @Override 76 | public void handle(Session session, ServerStatusInfo info) { 77 | final ServerStatus status = new ServerStatus(); 78 | status.setHost(hostAndPort.getHostText()); 79 | status.setPort(port); 80 | 81 | status.setVersion(info.getVersionInfo().getVersionName()); 82 | status.setProtocolVersion(info.getVersionInfo().getProtocolVersion()); 83 | 84 | String rawMotd = info.getDescription().getText(); 85 | status.setDescription(rawMotd); // for backward compatibility 86 | if (rawMotd.trim().isEmpty() && !info.getDescription().getExtra().isEmpty()) { 87 | rawMotd = formattedMessageConverter.convertPartsToCode(info.getDescription().getExtra()); 88 | } 89 | status.setMotd(new Motd()); 90 | status.getMotd().setRaw(rawMotd); 91 | status.getMotd().setHtml(formattedMessageConverter.convertToHtml(rawMotd)); 92 | status.getMotd().setStripped(formattedMessageConverter.stripCodes(rawMotd)); 93 | 94 | status.getPlayers().setOnline(info.getPlayerInfo().getOnlinePlayers()); 95 | status.getPlayers().setMax(info.getPlayerInfo().getMaxPlayers()); 96 | if (!properties.isExcludePlayers()) { 97 | if (info.getPlayerInfo().getPlayers() != null) { 98 | status.getPlayers().setPlayers(Stream.of(info.getPlayerInfo().getPlayers()) 99 | .map(gameProfile -> { 100 | final Player player = new Player(); 101 | player.setName(gameProfile.getName()); 102 | player.setId(gameProfile.getIdAsString()); 103 | return player; 104 | }) 105 | .collect(Collectors.toList())); 106 | } 107 | } 108 | 109 | if (!properties.isExcludeIcon()) { 110 | final ByteArrayOutputStream iconBytesOut = new ByteArrayOutputStream(); 111 | if (info.getIcon() != null) { 112 | try { 113 | ImageIO.write(info.getIcon(), "png", iconBytesOut); 114 | status.setIcon(iconBytesOut.toByteArray()); 115 | } catch (IOException e) { 116 | log.warn("Failed to write image bytes of server icon {} from {}", 117 | e, 118 | info.getIcon(), 119 | hostAndPort); 120 | } 121 | } else { 122 | log.debug("No server icon for {}", status); 123 | } 124 | } 125 | 126 | infoFuture.complete(status); 127 | } 128 | }); 129 | log.info("Getting info from {}", hostAndPort); 130 | client.getSession().connect(); 131 | 132 | try { 133 | try { 134 | return infoFuture.get(properties.getServerInfoTimeoutSec(), TimeUnit.SECONDS); 135 | } catch (TimeoutException e) { 136 | log.warn("Timed out getting server info from {}", hostAndPort); 137 | if (client.getSession().isConnected()) { 138 | log.debug("Disconnecting from {}", hostAndPort); 139 | client.getSession().disconnect("timeout"); 140 | } 141 | throw new ServerTimeoutException(String.format("Timed out getting server info from %s after %d seconds", 142 | hostAndPort, properties.getServerInfoTimeoutSec())); 143 | } 144 | } catch (InterruptedException | ExecutionException e) { 145 | log.warn("Unable to request status from {}", hostAndPort, e); 146 | return null; 147 | } 148 | } 149 | } -------------------------------------------------------------------------------- /src/main/resources/application-one-shot.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | main: 3 | web-application-type: none 4 | logging: 5 | level: 6 | me.itzg.mcstatus: warn -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | logging: 2 | level: 3 | org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainer: info 4 | org.springframework: warn 5 | org.apache.catalina: warn -------------------------------------------------------------------------------- /src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | __ __ ___ ___ ____ __ ____ __ __ ___ 2 | ( \/ )/ __)___ / __)(_ _) /__\ (_ _)( )( )/ __) 3 | ) (( (__(___)\__ \ )( /(__)\ )( )(__)( \__ \ 4 | (_/\/\_)\___) (___/ (__)(__)(__)(__) (______)(___/ -------------------------------------------------------------------------------- /src/test/java/me/itzg/mcstatus/McStatusApplicationTests.java: -------------------------------------------------------------------------------- 1 | package me.itzg.mcstatus; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.test.context.junit4.SpringRunner; 7 | 8 | @RunWith(SpringRunner.class) 9 | @SpringBootTest 10 | public class McStatusApplicationTests { 11 | 12 | @Test 13 | public void contextLoads() { 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/test/java/me/itzg/mcstatus/services/FormattedMessageConverterTest.java: -------------------------------------------------------------------------------- 1 | package me.itzg.mcstatus.services; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | 6 | import static org.junit.Assert.assertEquals; 7 | 8 | /** 9 | * @author Geoff Bourne 10 | * @since Aug 2018 11 | */ 12 | public class FormattedMessageConverterTest { 13 | private FormattedMessageConverter converter; 14 | 15 | @Before 16 | public void setUp() { 17 | converter = new FormattedMessageConverter(); 18 | } 19 | 20 | @Test 21 | public void testNoCodes() { 22 | final String result = converter.convertToHtml("Just some text"); 23 | assertEquals("Just some text", result); 24 | } 25 | 26 | @Test 27 | public void testSingleColor() { 28 | final String result = converter.convertToHtml("§bMinecraft Server"); 29 | 30 | assertEquals("Minecraft Server", result); 31 | } 32 | 33 | @Test 34 | public void testHypixel() { 35 | final String result = converter.convertToHtml(" §eHypixel Network §c[1.8-1.13]\n" + 36 | " §6§lBED WARS CASTLE V2 §7- §2§lMM BUG FIXES"); 37 | assertEquals(" Hypixel Network " + 38 | "[1.8-1.13]
" + 39 | "BED WARS CASTLE V2 " + 40 | "- " + 41 | "MM BUG FIXES", 42 | result); 43 | } 44 | 45 | @Test 46 | public void testStripNoCodes() { 47 | final String result = converter.stripCodes("Just some text"); 48 | 49 | assertEquals("Just some text", result); 50 | } 51 | 52 | @Test 53 | public void testStripSingleColor() { 54 | final String result = converter.stripCodes("§bMinecraft Server"); 55 | 56 | assertEquals("Minecraft Server", result); 57 | } 58 | 59 | @Test 60 | public void testStripHypixel() { 61 | final String result = converter.stripCodes(" §eHypixel Network §c[1.8-1.13]\n" + 62 | " §6§lBED WARS CASTLE V2 §7- §2§lMM BUG FIXES"); 63 | 64 | assertEquals("Hypixel Network [1.8-1.13] BED WARS CASTLE V2 - MM BUG FIXES", result); 65 | } 66 | } -------------------------------------------------------------------------------- /src/test/resources/cors-test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | MC Status Test 6 | 7 | 8 | 9 |
Version:
10 |
11 | 12 | 32 | 33 | 34 | --------------------------------------------------------------------------------