├── .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 | [](https://github.com/itzg/mc-status/releases/latest)
8 | [](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 |
--------------------------------------------------------------------------------