├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── etc └── open_eye_dummy.jar ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── src └── main ├── java └── openeye │ ├── CorePlugin.java │ ├── Log.java │ ├── Mod.java │ ├── Proxy.java │ ├── SetupHook.java │ ├── asm │ ├── CallHack.java │ ├── MethodCodeInjector.java │ ├── MethodMatcher.java │ ├── MultiTransformer.java │ ├── SingleClassTransformer.java │ ├── StopTransforming.java │ ├── VisitorHelper.java │ └── injectors │ │ ├── CrashHandlerInjector.java │ │ ├── ExceptionHandlerInjector.java │ │ └── Injectors.java │ ├── config │ ├── ConfigProcessing.java │ ├── ConfigProperty.java │ ├── ConfigPropertyCollector.java │ ├── GsonConfigProcessingEngine.java │ ├── IConfigProcessingEngine.java │ └── IConfigPropertyHolder.java │ ├── logic │ ├── Bootstrap.java │ ├── Config.java │ ├── CrashId.java │ ├── GsonUtils.java │ ├── IContext.java │ ├── INotStoredCrash.java │ ├── ModCollectorFactory.java │ ├── ModMetaCollector.java │ ├── ModState.java │ ├── ReportBuilders.java │ ├── ReportContext.java │ ├── Sanitizer.java │ ├── Sanitizers.java │ ├── SenderWorker.java │ ├── StateHolder.java │ ├── Storages.java │ ├── TagsCollector.java │ └── ThrowableLogger.java │ ├── net │ ├── GenericSender.java │ └── ReportSender.java │ ├── notes │ ├── CommandNotes.java │ ├── ConsoleNoteSink.java │ ├── GuiButtonNotes.java │ ├── GuiNotes.java │ ├── GuiNotesList.java │ ├── JsonNoteSink.java │ ├── NoteCategory.java │ ├── NoteCollector.java │ ├── NoteIcons.java │ ├── NoteLevels.java │ ├── NotesButtonInjector.java │ ├── ScreenNotificationHolder.java │ └── entries │ │ ├── MsgNoteEntry.java │ │ ├── NoteEntry.java │ │ ├── RemoveFileEntry.java │ │ ├── ReportedCrashEntry.java │ │ ├── ResolvedCrashEntry.java │ │ └── SystemNoteEntry.java │ ├── responses │ ├── IExecutableResponse.java │ ├── ResponseErrorAction.java │ ├── ResponseFileContentsAction.java │ ├── ResponseFileInfoAction.java │ ├── ResponseKnownCrashAction.java │ ├── ResponseModMsgAction.java │ ├── ResponsePongAction.java │ ├── ResponseRemoveFileAction.java │ └── ResponseSuspendAction.java │ ├── storage │ ├── GsonArchiveStorage.java │ ├── GsonDirStorage.java │ ├── GsonPredefinedStorage.java │ ├── GsonSessionStorage.java │ ├── GsonSimpleStorage.java │ ├── GsonStorageBase.java │ ├── GsonStreamSource.java │ ├── GsonWorkingStorage.java │ ├── IAppendableStorage.java │ ├── IDataSource.java │ ├── IQueryableStorage.java │ └── IWorkingStorage.java │ ├── struct │ └── TypedCollections.java │ └── utils │ └── NameCollector.java └── resources ├── assets └── openeye │ ├── lang │ ├── de_DE.lang │ ├── en_US.lang │ └── ru_RU.lang │ └── textures │ └── gui │ └── buttons.png ├── identrust_root_x3.pem ├── isrg_root_x1.pem └── mcmod.info /.gitattributes: -------------------------------------------------------------------------------- 1 | *.java text 2 | *.xml text 3 | *.info text 4 | *.lang text 5 | *.mcmeta text 6 | *.md text 7 | *.txt text 8 | 9 | *.png -text 10 | *.ogg -text 11 | *.zip -text 12 | *.jar -text -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OpenMods Java Project .gitignore file 2 | # Last Authored: 20150816 3 | 4 | # Runtime output 5 | *.class 6 | *.log 7 | *.pid 8 | *.log.gz 9 | 10 | # Package Files 11 | *.jar 12 | *.war 13 | *.ear 14 | 15 | # Gradle 16 | .gradle/ 17 | build/ 18 | run/ 19 | bin/ 20 | eclipse/ 21 | 22 | # Eclipse 23 | .classpath 24 | .project 25 | .settings/ 26 | *.launch 27 | 28 | # IntelliJ IDEA 29 | *.iml 30 | *.iws 31 | *.ipr 32 | .idea/ 33 | 34 | # OS Garbage 35 | .DS_Store 36 | Thumbs.db 37 | desktop.ini 38 | 39 | # Misc 40 | /download/ 41 | /config/ 42 | /saves/ 43 | /crash-reports/ 44 | /screenshots/ 45 | /usernamecache.json 46 | /options.txt 47 | /reports/ 48 | usercache.json -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Bartek Bok 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenEye-mod 2 | 3 | Client (mod) part of OpenEye 4 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | mavenCentral() 4 | maven { 5 | name = "forge" 6 | url = "http://files.minecraftforge.net/maven" 7 | } 8 | maven { 9 | name 'OpenMods Third Party' 10 | url 'http://repo.openmods.info/artifactory/simple/thirdparty' 11 | } 12 | } 13 | dependencies { 14 | classpath 'net.minecraftforge.gradle:ForgeGradle:2.3-SNAPSHOT' 15 | classpath 'net.thesilkminer.gradle.translationchecker:TranslationChecker:1.1' 16 | } 17 | } 18 | 19 | repositories { 20 | maven { 21 | name "OpenMods" 22 | url "https://repo.openmods.info/artifactory/openmods" 23 | } 24 | } 25 | 26 | configurations { 27 | mergeJars 28 | 29 | compile { 30 | extendsFrom mergeJars 31 | } 32 | } 33 | 34 | dependencies { 35 | mergeJars "openeye:OpenEye-protocol:1.1.1" 36 | } 37 | 38 | apply plugin: 'net.minecraftforge.gradle.forge' 39 | 40 | archivesBaseName = 'OpenEye' 41 | version = mc_ver + "-" + mod_version 42 | 43 | ext.env = System.getenv() 44 | if (env.BUILD_NUMBER != null) { 45 | version += "-snapshot-" + env.BUILD_NUMBER 46 | } 47 | 48 | ext.in_jenkins = (env.BUILD_TAG != null) // If this works, we'll assume we're in Jenkins atleast. 49 | 50 | def branch = null 51 | def hash = null 52 | def proc1 = "git rev-parse --short HEAD".execute() 53 | proc1.in.eachLine { line -> hash = line } 54 | proc1.err.eachLine { line -> println line } 55 | proc1.waitFor() 56 | 57 | if (!in_jenkins) { 58 | def proc2 = "git rev-parse --abbrev-ref HEAD".execute() 59 | proc2.in.eachLine { line -> branch = line } 60 | proc2.err.eachLine { line -> println line } 61 | proc2.waitFor() 62 | } else { // In Jenkins 63 | branch = env.GIT_BRANCH.minus("origin/") 64 | } 65 | 66 | minecraft { 67 | version = mc_ver + "-" + forge_ver 68 | runDir = "run" 69 | 70 | replaceIn 'openeye/Mod.java' 71 | replace "@VERSION@", mod_version 72 | 73 | mappings = mcp_mappings 74 | } 75 | 76 | processResources { 77 | inputs.property "version", mod_version 78 | 79 | from(sourceSets.main.resources.srcDirs) { 80 | include 'mcmod.info' 81 | expand 'version':mod_version, 'mc_version':mc_ver 82 | } 83 | 84 | from(sourceSets.main.resources.srcDirs) { 85 | exclude 'mcmod.info' 86 | } 87 | } 88 | 89 | jar { 90 | manifest { 91 | attributes 'FMLCorePlugin': 'openeye.CorePlugin', 92 | 'Git-Branch': branch, 93 | 'Git-Hash': hash, 94 | 'Jenkins-Build': in_jenkins 95 | 96 | if (in_jenkins) { 97 | attributes 'Jenkins-Tag': env.BUILD_TAG, 'Jenkins-ID': env.BUILD_ID 98 | } 99 | } 100 | 101 | from configurations.mergeJars.collect { it.isDirectory() ? it : zipTree(it) } 102 | } 103 | 104 | task updateTranslations(type: net.thesilkminer.gradle.plugin.translationchecker.tasks.TranslationCheckTask) { 105 | modId = "openeye" 106 | } 107 | 108 | task checkTranslations(type: net.thesilkminer.gradle.plugin.translationchecker.tasks.TranslationCheckTask) { 109 | modId = "openeye" 110 | dryRun = true 111 | } 112 | 113 | task wrapper (type: Wrapper) { 114 | gradleVersion = "2.11" 115 | } 116 | 117 | -------------------------------------------------------------------------------- /etc/open_eye_dummy.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenMods/OpenData/e657ca3decdba416b9741e375b8c5122887de874/etc/open_eye_dummy.jar -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | mod_version=0.8 2 | mc_ver=1.12.1 3 | forge_ver=14.22.0.2459 4 | mcp_mappings=snapshot_20170624 -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenMods/OpenData/e657ca3decdba416b9741e375b8c5122887de874/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue May 03 17:30:03 CEST 2016 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=http\://services.gradle.org/distributions/gradle-2.11-bin.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /src/main/java/openeye/CorePlugin.java: -------------------------------------------------------------------------------- 1 | package openeye; 2 | 3 | import java.util.Map; 4 | import net.minecraftforge.fml.relauncher.IFMLLoadingPlugin; 5 | import net.minecraftforge.fml.relauncher.IFMLLoadingPlugin.Name; 6 | import net.minecraftforge.fml.relauncher.IFMLLoadingPlugin.TransformerExclusions; 7 | import openeye.logic.Bootstrap; 8 | 9 | @Name("OpenEyePlugin") 10 | @TransformerExclusions("openeye") 11 | public class CorePlugin implements IFMLLoadingPlugin { 12 | 13 | @Override 14 | public String getAccessTransformerClass() { 15 | return null; 16 | } 17 | 18 | @Override 19 | public String[] getASMTransformerClass() { 20 | return new String[] { "openeye.asm.MultiTransformer" }; 21 | } 22 | 23 | @Override 24 | public String getModContainerClass() { 25 | return "openeye.Mod"; 26 | } 27 | 28 | @Override 29 | public String getSetupClass() { 30 | return "openeye.SetupHook"; 31 | } 32 | 33 | @Override 34 | public void injectData(Map data) { 35 | Bootstrap.instance.populateFromInject(data); 36 | } 37 | } -------------------------------------------------------------------------------- /src/main/java/openeye/Log.java: -------------------------------------------------------------------------------- 1 | package openeye; 2 | 3 | import org.apache.logging.log4j.Level; 4 | import org.apache.logging.log4j.LogManager; 5 | import org.apache.logging.log4j.Logger; 6 | 7 | public final class Log { 8 | private Log() {} 9 | 10 | private static final Logger logger; 11 | 12 | static { 13 | logger = LogManager.getLogger("OpenEye"); 14 | } 15 | 16 | private static final Throwable stackInfo = new Throwable(); 17 | 18 | private static String getLogLocation(Throwable t) { 19 | // first element is always log function 20 | 21 | // maybe faster but definitely unsafe implementation: 22 | // JavaLangAccess access = SharedSecrets.getJavaLangAccess(); 23 | // if (access.getStackTraceDepth(t) < 2) return ""; 24 | // final StackTraceElement caller = access.getStackTraceElement(t, 1); 25 | 26 | final StackTraceElement[] stack = t.getStackTrace(); 27 | if (stack.length < 2) return ""; 28 | final StackTraceElement caller = stack[1]; 29 | return caller.getClassName() + "." + caller.getMethodName() + "(" + caller.getFileName() + ":" + caller.getLineNumber() + "): "; 30 | } 31 | 32 | private static void logWithCaller(Throwable callerStack, Level level, String format, Object... data) { 33 | logger.log(level, getLogLocation(callerStack) + String.format(format, data)); 34 | } 35 | 36 | public static void log(Level level, String format, Object... data) { 37 | logWithCaller(stackInfo.fillInStackTrace(), level, format, data); 38 | } 39 | 40 | public static void severe(String format, Object... data) { 41 | logWithCaller(stackInfo.fillInStackTrace(), Level.ERROR, format, data); 42 | } 43 | 44 | public static void warn(String format, Object... data) { 45 | logWithCaller(stackInfo.fillInStackTrace(), Level.WARN, format, data); 46 | } 47 | 48 | public static void info(String format, Object... data) { 49 | logWithCaller(stackInfo.fillInStackTrace(), Level.INFO, format, data); 50 | } 51 | 52 | public static void debug(String format, Object... data) { 53 | logWithCaller(stackInfo.fillInStackTrace(), Level.DEBUG, format, data); 54 | } 55 | 56 | public static void trace(String format, Object... data) { 57 | logWithCaller(stackInfo.fillInStackTrace(), Level.TRACE, format, data); 58 | } 59 | 60 | public static void log(Level level, Throwable ex, String format, Object... data) { 61 | logger.log(level, String.format(format, data), ex); 62 | } 63 | 64 | public static void severe(Throwable ex, String format, Object... data) { 65 | log(Level.ERROR, ex, format, data); 66 | } 67 | 68 | public static void warn(Throwable ex, String format, Object... data) { 69 | log(Level.WARN, ex, format, data); 70 | } 71 | 72 | public static void info(Throwable ex, String format, Object... data) { 73 | log(Level.INFO, ex, format, data); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/openeye/Mod.java: -------------------------------------------------------------------------------- 1 | package openeye; 2 | 3 | import com.google.common.eventbus.EventBus; 4 | import com.google.common.eventbus.Subscribe; 5 | import java.io.File; 6 | import java.net.ProtocolException; 7 | import java.net.URL; 8 | import java.util.Arrays; 9 | import java.util.Collection; 10 | import java.util.concurrent.RunnableFuture; 11 | import net.minecraftforge.fml.client.FMLFileResourcePack; 12 | import net.minecraftforge.fml.client.FMLFolderResourcePack; 13 | import net.minecraftforge.fml.common.DummyModContainer; 14 | import net.minecraftforge.fml.common.LoadController; 15 | import net.minecraftforge.fml.common.ModMetadata; 16 | import net.minecraftforge.fml.common.event.FMLConstructionEvent; 17 | import net.minecraftforge.fml.common.event.FMLInitializationEvent; 18 | import net.minecraftforge.fml.common.event.FMLPostInitializationEvent; 19 | import net.minecraftforge.fml.common.event.FMLServerStartingEvent; 20 | import openeye.logic.Bootstrap; 21 | import openeye.logic.Config; 22 | import openeye.logic.ModCollectorFactory; 23 | import openeye.logic.ModMetaCollector; 24 | import openeye.logic.SenderWorker; 25 | import openeye.logic.StateHolder; 26 | import openeye.logic.ThrowableLogger; 27 | import openeye.notes.CommandNotes; 28 | import openeye.protocol.FileSignature; 29 | import openeye.utils.NameCollector; 30 | 31 | public class Mod extends DummyModContainer { 32 | 33 | private LoadController controller; 34 | 35 | public Mod() { 36 | super(new ModMetadata()); 37 | ModMetadata meta = getMetadata(); 38 | meta.autogenerated = false; 39 | meta.modId = "OpenEye"; 40 | meta.name = "OpenEye"; 41 | meta.version = "@VERSION@"; 42 | meta.authorList = Arrays.asList("boq", "Mikee"); 43 | meta.url = "https://openmods.info/"; 44 | meta.description = "We see you..."; 45 | } 46 | 47 | private SenderWorker worker; 48 | 49 | @Override 50 | public boolean registerBus(EventBus bus, LoadController controller) { 51 | this.controller = controller; 52 | bus.register(this); 53 | return true; 54 | } 55 | 56 | @Subscribe 57 | public void onModConstruct(FMLConstructionEvent evt) { 58 | Proxy.instance().first(); 59 | 60 | ModCollectorFactory factory = new ModCollectorFactory(); 61 | RunnableFuture collector = factory.createCollector( 62 | evt.getASMHarvestedData(), 63 | Bootstrap.instance.getLoader(), 64 | Bootstrap.instance.getTweakers()); 65 | 66 | startMetadataCollection(collector); 67 | 68 | ThrowableLogger.enableResolving(collector); 69 | 70 | final long now = System.currentTimeMillis(); 71 | if (StateHolder.state().suspendUntilTimestamp > now) { 72 | Log.debug("Communication with server suspended, OpenEye will not send or receive any data"); 73 | } else if (!Proxy.instance().isSnooperEnabled()) { 74 | Log.debug("Snooper disabled, OpenEye will not send or receive any data from server"); 75 | } else { 76 | worker = new SenderWorker(collector, StateHolder.state()); 77 | worker.start(); 78 | } 79 | } 80 | 81 | private static void startMetadataCollection(RunnableFuture collector) { 82 | Thread modCollector = new Thread(collector); 83 | modCollector.setName("OpenEye mod meta collector"); 84 | modCollector.start(); 85 | } 86 | 87 | @Subscribe 88 | public void onInit(FMLInitializationEvent evt) { 89 | // give thread enough time to receive IMC 90 | if (worker != null) { 91 | worker.waitForFirstMsg(); 92 | handleUnwantedFiles(); 93 | } 94 | 95 | NameCollector.register(); 96 | Proxy.instance().init(); 97 | } 98 | 99 | private void handleUnwantedFiles() { 100 | Collection dangerousMods = worker.listDangerousFiles(); 101 | 102 | if (!dangerousMods.isEmpty()) { 103 | for (FileSignature signature : dangerousMods) 104 | Log.warn("File suggested for deletion: %s (%s)", signature.filename, signature.signature); 105 | } 106 | } 107 | 108 | public static void crash1() { 109 | try { 110 | File mcDir = Bootstrap.instance.getMcLocation(); 111 | throw new ProtocolException("128.0.0.1 deep one: " + new File(mcDir, "hello.txt")); 112 | } catch (Exception e) { 113 | throw new RuntimeException("u wot m8: 127.0.0.4:5262", e); 114 | } 115 | } 116 | 117 | public static void crash2() { 118 | try { 119 | crash1(); 120 | } catch (RuntimeException e) { 121 | throw new RuntimeException(e); 122 | } 123 | } 124 | 125 | @Subscribe 126 | public void onInit(FMLPostInitializationEvent evt) { 127 | if (Config.crashOnStartup) try { 128 | crash2(); 129 | } catch (RuntimeException e) { 130 | controller.errorOccurred(this, new RuntimeException("Goodbye, cruel world!", e)); 131 | } 132 | } 133 | 134 | @Subscribe 135 | public void onServerStart(FMLServerStartingEvent evt) { 136 | evt.registerServerCommand(new CommandNotes(evt.getServer().getFile("."))); 137 | } 138 | 139 | @Override 140 | public File getSource() { 141 | File injectedSource = Bootstrap.instance.getSelfLocation(); 142 | 143 | if (injectedSource != null) return injectedSource; 144 | 145 | // looks like we are in dev (or broken) env 146 | URL url = getClass().getResource("."); 147 | 148 | try { 149 | File rootFile = new File(url.toURI()); 150 | if (rootFile.getName().equals("openeye")) rootFile = rootFile.getParentFile(); 151 | return rootFile; 152 | } catch (Exception e) { 153 | Log.info(e, "Failed to extract source from URL %s", url); 154 | } 155 | 156 | return null; 157 | } 158 | 159 | @Override 160 | public Class getCustomResourcePackClass() { 161 | File source = getSource(); 162 | if (source == null) { 163 | Log.warn("Failed to get source, resource pack missing"); 164 | return null; 165 | } 166 | return source.isDirectory()? FMLFolderResourcePack.class : FMLFileResourcePack.class; 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /src/main/java/openeye/Proxy.java: -------------------------------------------------------------------------------- 1 | package openeye; 2 | 3 | import com.google.common.base.Strings; 4 | import java.io.File; 5 | import java.io.FileInputStream; 6 | import java.io.InputStream; 7 | import java.util.Properties; 8 | import net.minecraft.client.Minecraft; 9 | import net.minecraftforge.fml.common.FMLCommonHandler; 10 | import openeye.logic.Config; 11 | import openeye.logic.Sanitizers; 12 | import openeye.notes.NotesButtonInjector; 13 | 14 | public abstract class Proxy { 15 | 16 | private static class Client extends Proxy { 17 | @Override 18 | public boolean isSnooperEnabled() { 19 | try { 20 | return Minecraft.getMinecraft().gameSettings.snooperEnabled; 21 | } catch (Exception e) { 22 | Log.warn(e, "Can't read client snooper settings, won't send any data"); 23 | return false; 24 | } 25 | } 26 | 27 | @Override 28 | public String getLanguage() { 29 | return Minecraft.getMinecraft().gameSettings.language; 30 | } 31 | 32 | @Override 33 | public void first() { 34 | try { 35 | String username = Minecraft.getMinecraft().getSession().getUsername(); 36 | if (!Strings.isNullOrEmpty(username)) Sanitizers.addPlayerName(username); 37 | } catch (Throwable t) { 38 | Log.warn(t, "Failed to get player username"); 39 | } 40 | } 41 | 42 | @Override 43 | public void init() { 44 | if (Config.mainScreenNotes) NotesButtonInjector.registerInjector(); 45 | } 46 | } 47 | 48 | private static class Server extends Proxy { 49 | @Override 50 | public boolean isSnooperEnabled() { 51 | try { 52 | File settings = new File("server.properties"); 53 | Properties props = new Properties(); 54 | InputStream input = new FileInputStream(settings); 55 | try { 56 | props.load(input); 57 | } finally { 58 | input.close(); 59 | } 60 | String flag = props.getProperty("snooper-enabled"); 61 | // default value for vanilla is also true 62 | return flag != null? flag.equalsIgnoreCase("true") : true; 63 | } catch (Exception e) { 64 | Log.warn(e, "Can't read server snooper settings, won't send any data"); 65 | return false; 66 | } 67 | } 68 | 69 | @Override 70 | public String getLanguage() { 71 | return "n/a"; 72 | } 73 | 74 | @Override 75 | public void first() {} 76 | 77 | @Override 78 | public void init() {} 79 | } 80 | 81 | public abstract boolean isSnooperEnabled(); 82 | 83 | public abstract String getLanguage(); 84 | 85 | public abstract void first(); 86 | 87 | public abstract void init(); 88 | 89 | private static Proxy instance; 90 | 91 | public static Proxy instance() { 92 | if (instance == null) instance = createProxy(); 93 | return instance; 94 | } 95 | 96 | private static Proxy createProxy() { 97 | switch (FMLCommonHandler.instance().getSide()) { 98 | case CLIENT: 99 | return new Client(); 100 | case SERVER: 101 | return new Server(); 102 | default: 103 | throw new IllegalStateException("Impossibru!"); 104 | } 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /src/main/java/openeye/SetupHook.java: -------------------------------------------------------------------------------- 1 | package openeye; 2 | 3 | import java.util.Map; 4 | import net.minecraftforge.fml.relauncher.IFMLCallHook; 5 | import openeye.logic.Bootstrap; 6 | 7 | public class SetupHook implements IFMLCallHook { 8 | 9 | @Override 10 | public Void call() throws Exception { 11 | Bootstrap.instance.startup(); 12 | return null; 13 | } 14 | 15 | @Override 16 | public void injectData(Map data) { 17 | Bootstrap.instance.populateFromSetupClass(data); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/openeye/asm/CallHack.java: -------------------------------------------------------------------------------- 1 | package openeye.asm; 2 | 3 | import net.minecraft.crash.CrashReport; 4 | import net.minecraft.util.ReportedException; 5 | import openeye.Log; 6 | import openeye.logic.ThrowableLogger; 7 | 8 | public class CallHack { 9 | 10 | /* 11 | * I'm to lazy to do proper signatures in transformer. Obfuscation should make sure this code always uses proper type 12 | */ 13 | public static void callFromCrashHandler(Object o) { 14 | try { 15 | CrashReport report = (CrashReport)o; 16 | ThrowableLogger.processThrowable(report.getCrashCause(), "crash_handler"); 17 | } catch (Throwable t) { 18 | Log.warn(t, "Failed to store crash report %s", o); 19 | } 20 | } 21 | 22 | public static void callForSilentException(Throwable throwable, String location) { 23 | try { 24 | if (throwable instanceof ReportedException) throwable = tryExtractCause((ReportedException)throwable); 25 | ThrowableLogger.processThrowable(throwable, location); 26 | } catch (Throwable t) { 27 | Log.warn(t, "Failed to store exception %s from %s", throwable, location); 28 | } 29 | } 30 | 31 | protected static Throwable tryExtractCause(ReportedException report) { 32 | try { 33 | return report.getCrashReport().getCrashCause(); 34 | } catch (Throwable t) { 35 | Log.warn(t, "Failed to extract report"); 36 | ThrowableLogger.processThrowable(t, "openeye_internal"); 37 | return report; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/openeye/asm/MethodCodeInjector.java: -------------------------------------------------------------------------------- 1 | package openeye.asm; 2 | 3 | import org.objectweb.asm.MethodVisitor; 4 | 5 | public abstract class MethodCodeInjector { 6 | 7 | public final String name; 8 | 9 | public final MethodMatcher matcher; 10 | 11 | public abstract MethodVisitor createVisitor(MethodVisitor parent); 12 | 13 | public MethodCodeInjector(String name, MethodMatcher matcher) { 14 | this.name = name; 15 | this.matcher = matcher; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/openeye/asm/MethodMatcher.java: -------------------------------------------------------------------------------- 1 | package openeye.asm; 2 | 3 | import net.minecraftforge.fml.common.asm.transformers.deobf.FMLDeobfuscatingRemapper; 4 | 5 | public class MethodMatcher { 6 | private final String clsName; 7 | private final String description; 8 | private final String srgName; 9 | private final String mcpName; 10 | 11 | public MethodMatcher(String clsName, String description, String mcpName, String srgName) { 12 | this.clsName = clsName; 13 | this.description = description; 14 | this.srgName = srgName; 15 | this.mcpName = mcpName; 16 | } 17 | 18 | public boolean match(String methodName, String methodDesc) { 19 | if (!methodDesc.equals(description)) return false; 20 | if (methodName.equals(mcpName)) return true; 21 | if (!VisitorHelper.useSrgNames()) return false; 22 | String mapped = FMLDeobfuscatingRemapper.INSTANCE.mapMethodName(clsName, methodName, methodDesc); 23 | return mapped.equals(srgName); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/openeye/asm/MultiTransformer.java: -------------------------------------------------------------------------------- 1 | package openeye.asm; 2 | 3 | import com.google.common.collect.HashMultimap; 4 | import com.google.common.collect.Multimap; 5 | import java.util.Collection; 6 | import java.util.Map; 7 | import net.minecraft.launchwrapper.IClassTransformer; 8 | import openeye.asm.VisitorHelper.TransformProvider; 9 | import openeye.asm.injectors.Injectors; 10 | import org.objectweb.asm.ClassVisitor; 11 | import org.objectweb.asm.ClassWriter; 12 | 13 | public class MultiTransformer implements IClassTransformer { 14 | 15 | private Multimap injectors = HashMultimap.create(); 16 | 17 | public MultiTransformer() { 18 | Injectors.setupInjectors(injectors); 19 | } 20 | 21 | @Override 22 | public byte[] transform(final String name, String transformedName, byte[] bytes) { 23 | if (bytes == null) return bytes; 24 | 25 | for (Map.Entry> clsInjectors : injectors.asMap().entrySet()) { 26 | if (transformedName.equals(clsInjectors.getKey())) { 27 | final Collection methodInjector = clsInjectors.getValue(); 28 | bytes = VisitorHelper.apply(bytes, ClassWriter.COMPUTE_FRAMES, new TransformProvider() { 29 | @Override 30 | public ClassVisitor createVisitor(ClassVisitor cv) { 31 | return new SingleClassTransformer(cv, name, methodInjector); 32 | } 33 | }); 34 | } 35 | } 36 | 37 | return bytes; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/openeye/asm/SingleClassTransformer.java: -------------------------------------------------------------------------------- 1 | package openeye.asm; 2 | 3 | import java.util.Collection; 4 | import openeye.Log; 5 | import org.objectweb.asm.ClassVisitor; 6 | import org.objectweb.asm.MethodVisitor; 7 | import org.objectweb.asm.Opcodes; 8 | 9 | public class SingleClassTransformer extends ClassVisitor { 10 | 11 | private final Collection methodInjectors; 12 | 13 | public SingleClassTransformer(ClassVisitor cv, String obfClassName, Collection injectors) { 14 | super(Opcodes.ASM4, cv); 15 | 16 | this.methodInjectors = injectors; 17 | } 18 | 19 | @Override 20 | public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { 21 | MethodVisitor parent = super.visitMethod(access, name, desc, signature, exceptions); 22 | for (MethodCodeInjector injector : methodInjectors) 23 | if (injector.matcher.match(name, desc)) { 24 | Log.debug("Applying method transformer %s for method %s(%s)", injector.name, name, desc); 25 | return injector.createVisitor(parent); 26 | } 27 | 28 | return parent; 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/openeye/asm/StopTransforming.java: -------------------------------------------------------------------------------- 1 | package openeye.asm; 2 | 3 | public class StopTransforming extends RuntimeException { 4 | private static final long serialVersionUID = 924908274296337742L; 5 | 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/openeye/asm/VisitorHelper.java: -------------------------------------------------------------------------------- 1 | package openeye.asm; 2 | 3 | import com.google.common.base.Preconditions; 4 | import net.minecraft.launchwrapper.Launch; 5 | import org.objectweb.asm.ClassReader; 6 | import org.objectweb.asm.ClassVisitor; 7 | import org.objectweb.asm.ClassWriter; 8 | 9 | public class VisitorHelper { 10 | 11 | public static interface TransformProvider { 12 | public ClassVisitor createVisitor(ClassVisitor cv); 13 | } 14 | 15 | public static byte[] apply(byte[] bytes, int flags, TransformProvider context) { 16 | Preconditions.checkNotNull(bytes); 17 | ClassReader cr = new ClassReader(bytes); 18 | ClassWriter cw = new ClassWriter(cr, flags); 19 | ClassVisitor mod = context.createVisitor(cw); 20 | 21 | try { 22 | cr.accept(mod, 0); 23 | return cw.toByteArray(); 24 | } catch (StopTransforming e) { 25 | return bytes; 26 | } 27 | } 28 | 29 | public static boolean useSrgNames() { 30 | Boolean deobfuscated = (Boolean)Launch.blackboard.get("fml.deobfuscatedEnvironment"); 31 | return deobfuscated == null || !deobfuscated; 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/openeye/asm/injectors/CrashHandlerInjector.java: -------------------------------------------------------------------------------- 1 | package openeye.asm.injectors; 2 | 3 | import java.io.Writer; 4 | import openeye.Log; 5 | import openeye.asm.CallHack; 6 | import org.objectweb.asm.MethodVisitor; 7 | import org.objectweb.asm.Opcodes; 8 | import org.objectweb.asm.Type; 9 | import org.objectweb.asm.commons.Method; 10 | 11 | public class CrashHandlerInjector extends MethodVisitor { 12 | private final Method streamClose; 13 | private final Method callTarget; 14 | private final Type callHackType; 15 | 16 | public CrashHandlerInjector(MethodVisitor mv) { 17 | super(Opcodes.ASM5, mv); 18 | 19 | try { 20 | streamClose = new Method("closeQuietly", Type.VOID_TYPE, new Type[] { Type.getType(Writer.class) }); 21 | callHackType = Type.getType(CallHack.class); 22 | callTarget = Method.getMethod(CallHack.class.getMethod("callFromCrashHandler", Object.class)); 23 | } catch (NoSuchMethodException t) { 24 | throw new RuntimeException(t); 25 | } 26 | } 27 | 28 | @Override 29 | public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean intf) { 30 | super.visitMethodInsn(opcode, owner, name, desc, intf); 31 | if (streamClose.getName().equals(name) && streamClose.getDescriptor().equals(desc)) { 32 | Log.debug("Adding handler for 'crash_handler'"); 33 | visitVarInsn(Opcodes.ALOAD, 0); 34 | visitMethodInsn(Opcodes.INVOKESTATIC, callHackType.getInternalName(), callTarget.getName(), callTarget.getDescriptor(), false); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/openeye/asm/injectors/ExceptionHandlerInjector.java: -------------------------------------------------------------------------------- 1 | package openeye.asm.injectors; 2 | 3 | import com.google.common.base.Preconditions; 4 | import com.google.common.collect.Maps; 5 | import java.util.Map; 6 | import openeye.Log; 7 | import openeye.asm.CallHack; 8 | import org.objectweb.asm.Label; 9 | import org.objectweb.asm.MethodVisitor; 10 | import org.objectweb.asm.Opcodes; 11 | import org.objectweb.asm.Type; 12 | import org.objectweb.asm.commons.Method; 13 | 14 | public class ExceptionHandlerInjector extends MethodVisitor { 15 | 16 | private final Method callTarget; 17 | private final Type callHackType; 18 | 19 | private final String[] excNames; 20 | private final Map excLabels = Maps.newIdentityHashMap(); 21 | private final String excType; 22 | int currentLabel; 23 | private boolean skipHandlers; 24 | 25 | public ExceptionHandlerInjector(MethodVisitor mv, String excType, String... excNames) { 26 | super(Opcodes.ASM5, mv); 27 | 28 | this.excNames = excNames; 29 | this.excType = excType; 30 | 31 | try { 32 | callHackType = Type.getType(CallHack.class); 33 | callTarget = Method.getMethod(CallHack.class.getMethod("callForSilentException", Throwable.class, String.class)); 34 | } catch (NoSuchMethodException t) { 35 | throw new RuntimeException(t); 36 | } 37 | } 38 | 39 | @Override 40 | public void visitTryCatchBlock(Label start, Label end, Label handler, String type) { 41 | super.visitTryCatchBlock(start, end, handler, type); 42 | 43 | if (!skipHandlers && excType.equals(type)) { 44 | try { 45 | String name = excNames[currentLabel++]; 46 | final String prev = excLabels.put(handler, name); 47 | Preconditions.checkState(prev == null || prev.equals(name), "Duplicate handlers for '%s'", name); 48 | } catch (ArrayIndexOutOfBoundsException e) { 49 | Log.warn("Invalid method structure, more than %d exception handlers. Aborting", excNames.length); 50 | skipHandlers = true; 51 | } 52 | } 53 | } 54 | 55 | @Override 56 | public void visitLabel(Label label) { 57 | super.visitLabel(label); 58 | 59 | if (!skipHandlers) { 60 | String name = excLabels.get(label); 61 | if (name != null) addHandler(name); 62 | } 63 | } 64 | 65 | private void addHandler(String location) { 66 | Log.debug("Adding handler for '%s'", location); 67 | super.visitInsn(Opcodes.DUP); 68 | super.visitLdcInsn(location); 69 | super.visitMethodInsn(Opcodes.INVOKESTATIC, callHackType.getInternalName(), callTarget.getName(), callTarget.getDescriptor(), false); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/openeye/asm/injectors/Injectors.java: -------------------------------------------------------------------------------- 1 | package openeye.asm.injectors; 2 | 3 | import com.google.common.collect.Multimap; 4 | import java.io.File; 5 | import net.minecraftforge.fml.common.asm.transformers.deobf.FMLDeobfuscatingRemapper; 6 | import openeye.asm.MethodCodeInjector; 7 | import openeye.asm.MethodMatcher; 8 | import openeye.asm.VisitorHelper; 9 | import org.objectweb.asm.MethodVisitor; 10 | import org.objectweb.asm.Type; 11 | 12 | public class Injectors { 13 | 14 | private static final String CRASH_REPORT_CLS = "net.minecraft.crash.CrashReport"; 15 | private static final String TILE_ENTITY_CLS = "net.minecraft.tileentity.TileEntity"; 16 | private static final String ENTITY_LIST_CLS = "net.minecraft.entity.EntityList"; 17 | private static final String ENTITY_CLS = "net.minecraft.entity.Entity"; 18 | private static final String NBT_TAG_COMPOUND_CLS = "net.minecraft.nbt.NBTTagCompound"; 19 | private static final String WORLD_CLS = "net.minecraft.world.World"; 20 | private static final String CHUNK_CLS = "net.minecraft.world.chunk.Chunk"; 21 | private static final String ANVIL_CHUNK_LOADER = "net.minecraft.world.chunk.storage.AnvilChunkLoader"; 22 | 23 | public static String getClassName(String name) { 24 | name = name.replace('.', '/'); 25 | return VisitorHelper.useSrgNames()? FMLDeobfuscatingRemapper.INSTANCE.unmap(name) : name; 26 | } 27 | 28 | public static void setupInjectors(Multimap injectors) { 29 | String nbtTagCompoundName = getClassName(NBT_TAG_COMPOUND_CLS); 30 | Type nbtTagCompoundType = Type.getObjectType(nbtTagCompoundName); 31 | 32 | String worldName = getClassName(WORLD_CLS); 33 | Type worldType = Type.getObjectType(worldName); 34 | 35 | String entityName = getClassName(ENTITY_CLS); 36 | Type entityType = Type.getObjectType(entityName); 37 | 38 | String tileEntityName = getClassName(TILE_ENTITY_CLS); 39 | Type tileEntityType = Type.getObjectType(tileEntityName); 40 | 41 | String chunkName = getClassName(CHUNK_CLS); 42 | Type chunkType = Type.getObjectType(chunkName); 43 | 44 | { 45 | String crashHandlerName = getClassName(CRASH_REPORT_CLS); 46 | Type fileType = Type.getType(File.class); 47 | 48 | Type methodType = Type.getMethodType(Type.BOOLEAN_TYPE, fileType); 49 | 50 | MethodMatcher matcher = new MethodMatcher(crashHandlerName, methodType.getDescriptor(), "saveToFile", "func_147149_a"); 51 | 52 | injectors.put(CRASH_REPORT_CLS, new MethodCodeInjector("crash_handler", matcher) { 53 | @Override 54 | public MethodVisitor createVisitor(MethodVisitor parent) { 55 | return new CrashHandlerInjector(parent); 56 | } 57 | }); 58 | } 59 | 60 | { 61 | Type methodType = Type.getMethodType(tileEntityType, worldType, nbtTagCompoundType); 62 | 63 | MethodMatcher matcher = new MethodMatcher(tileEntityName, methodType.getDescriptor(), "create", "func_190200_a"); 64 | 65 | injectors.put(TILE_ENTITY_CLS, new MethodCodeInjector("tile_entity_load", matcher) { 66 | @Override 67 | public MethodVisitor createVisitor(MethodVisitor parent) { 68 | return new ExceptionHandlerInjector(parent, "java/lang/Throwable", "tile_entity_construct", "tile_entity_read"); 69 | } 70 | }); 71 | } 72 | 73 | { 74 | String entityListName = getClassName(ENTITY_LIST_CLS); 75 | 76 | { 77 | Type methodType = Type.getMethodType(entityType, nbtTagCompoundType, worldType); 78 | MethodMatcher matcher = new MethodMatcher(entityListName, methodType.getDescriptor(), "createEntityFromNBT", "func_75615_a"); 79 | injectors.put(ENTITY_LIST_CLS, new MethodCodeInjector("entity_read", matcher) { 80 | @Override 81 | public MethodVisitor createVisitor(MethodVisitor parent) { 82 | return new ExceptionHandlerInjector(parent, "java/lang/Exception", "entity_read"); 83 | } 84 | }); 85 | } 86 | 87 | { 88 | Type methodType = Type.getMethodType(entityType, Type.getType(Class.class), worldType); 89 | MethodMatcher matcher = new MethodMatcher(entityListName, methodType.getDescriptor(), "newEntity", "func_191304_a"); 90 | injectors.put(ENTITY_LIST_CLS, new MethodCodeInjector("entity_create", matcher) { 91 | @Override 92 | public MethodVisitor createVisitor(MethodVisitor parent) { 93 | return new ExceptionHandlerInjector(parent, "java/lang/Exception", "entity_create", "entity_create"); // seems to be split in two 94 | } 95 | }); 96 | } 97 | } 98 | 99 | { 100 | String chunkLoaderName = getClassName(ANVIL_CHUNK_LOADER); 101 | 102 | Type methodType = Type.getMethodType(Type.VOID_TYPE, chunkType, worldType, nbtTagCompoundType); 103 | 104 | MethodMatcher matcher = new MethodMatcher(chunkLoaderName, methodType.getDescriptor(), "writeChunkToNBT", "func_75820_a"); 105 | 106 | injectors.put(ANVIL_CHUNK_LOADER, new MethodCodeInjector("chunk_write", matcher) { 107 | @Override 108 | public MethodVisitor createVisitor(MethodVisitor parent) { 109 | return new ExceptionHandlerInjector(parent, "java/lang/Exception", "entity_write", "tile_entity_write"); 110 | } 111 | }); 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/main/java/openeye/config/ConfigProcessing.java: -------------------------------------------------------------------------------- 1 | package openeye.config; 2 | 3 | import com.google.common.base.Preconditions; 4 | import com.google.common.collect.Table; 5 | import com.google.common.collect.TreeBasedTable; 6 | import java.io.File; 7 | import java.util.Collection; 8 | import java.util.List; 9 | import openeye.Log; 10 | 11 | public class ConfigProcessing { 12 | 13 | public static final IConfigProcessingEngine GSON = new GsonConfigProcessingEngine(); 14 | 15 | private static Table categorizeProperties(Collection properties) { 16 | Table result = TreeBasedTable.create(); 17 | 18 | for (IConfigPropertyHolder property : properties) { 19 | IConfigPropertyHolder prev = result.put(property.category(), property.name(), property); 20 | Preconditions.checkState(prev == null, "Duplicated property %s:%s", property.category(), property.name()); 21 | } 22 | 23 | return result; 24 | } 25 | 26 | private static void loadAndDump(File configFile, IConfigProcessingEngine engine, final List holders) { 27 | final Table properties = categorizeProperties(holders); 28 | final boolean modified = engine.loadConfig(configFile, properties); 29 | 30 | if (modified) { 31 | Log.info("Detected missing/malformed fields in file %s, updating", configFile); 32 | engine.dumpConfig(configFile, properties); 33 | } 34 | } 35 | 36 | public static void processConfig(File configFile, Class cls, IConfigProcessingEngine engine) { 37 | final List holders = ConfigPropertyCollector.collectFromClass(cls); 38 | loadAndDump(configFile, engine, holders); 39 | } 40 | 41 | public static void processConfig(File configFile, Object target, boolean excludeStatic, IConfigProcessingEngine engine) { 42 | Preconditions.checkNotNull(target); 43 | final List holders = ConfigPropertyCollector.collectFromInstance(target, excludeStatic); 44 | loadAndDump(configFile, engine, holders); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/openeye/config/ConfigProperty.java: -------------------------------------------------------------------------------- 1 | package openeye.config; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Retention(RetentionPolicy.RUNTIME) 9 | @Target(ElementType.FIELD) 10 | public @interface ConfigProperty { 11 | public String name() default ""; 12 | 13 | public String category(); 14 | 15 | public String comment() default ""; 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/openeye/config/ConfigPropertyCollector.java: -------------------------------------------------------------------------------- 1 | package openeye.config; 2 | 3 | import com.google.common.base.Strings; 4 | import com.google.common.collect.Lists; 5 | import java.lang.reflect.Field; 6 | import java.lang.reflect.Modifier; 7 | import java.util.List; 8 | 9 | public class ConfigPropertyCollector { 10 | 11 | private abstract static class ConfigPropertyHolder implements IConfigPropertyHolder { 12 | 13 | private final String name; 14 | private final String category; 15 | private final String comment; 16 | 17 | public ConfigPropertyHolder(String name, String category, String comment) { 18 | this.name = name; 19 | this.category = category; 20 | this.comment = comment; 21 | } 22 | 23 | @Override 24 | public String name() { 25 | return name; 26 | } 27 | 28 | @Override 29 | public String category() { 30 | return category; 31 | } 32 | 33 | @Override 34 | public String comment() { 35 | return comment; 36 | } 37 | } 38 | 39 | private static class InstancePropertyHolder extends ConfigPropertyHolder { 40 | 41 | private final Field field; 42 | private final Object target; 43 | 44 | public InstancePropertyHolder(Field field, Object target, String name, String category, String comment) { 45 | super(name, category, comment); 46 | this.field = field; 47 | this.target = target; 48 | } 49 | 50 | @Override 51 | public Object getValue() { 52 | try { 53 | return field.get(target); 54 | } catch (IllegalAccessException t) { 55 | throw new RuntimeException(t); 56 | } 57 | } 58 | 59 | @Override 60 | public void setValue(Object value) { 61 | try { 62 | field.set(target, value); 63 | } catch (IllegalAccessException t) { 64 | throw new RuntimeException(t); 65 | } 66 | } 67 | 68 | @Override 69 | public Class getType() { 70 | return field.getType(); 71 | } 72 | } 73 | 74 | private static IConfigPropertyHolder createHolder(ConfigProperty annotation, Field field, Object target) { 75 | String name = annotation.name(); 76 | String category = annotation.category(); 77 | 78 | if (Strings.isNullOrEmpty(name)) name = field.getName(); 79 | if (Strings.isNullOrEmpty(category)) category = null; 80 | 81 | return new InstancePropertyHolder(field, target, name, category, annotation.comment()); 82 | } 83 | 84 | public static List collectFromClass(Class cls) { 85 | List result = Lists.newArrayList(); 86 | 87 | for (Field f : cls.getFields()) { 88 | if (!Modifier.isStatic(f.getModifiers())) continue; 89 | ConfigProperty property = f.getAnnotation(ConfigProperty.class); 90 | if (property != null) result.add(createHolder(property, f, null)); 91 | } 92 | 93 | return result; 94 | } 95 | 96 | public static List collectFromInstance(Object target, boolean excludeStatic) { 97 | List result = Lists.newArrayList(); 98 | 99 | for (Field f : target.getClass().getFields()) { 100 | boolean isStatic = Modifier.isStatic(f.getModifiers()); 101 | if (excludeStatic && isStatic) continue; 102 | ConfigProperty property = f.getAnnotation(ConfigProperty.class); 103 | if (property != null) result.add(createHolder(property, f, isStatic? null : target)); 104 | } 105 | 106 | return result; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/main/java/openeye/config/GsonConfigProcessingEngine.java: -------------------------------------------------------------------------------- 1 | package openeye.config; 2 | 3 | import com.google.common.base.Charsets; 4 | import com.google.common.base.Strings; 5 | import com.google.common.collect.Table; 6 | import com.google.common.io.Closer; 7 | import com.google.gson.Gson; 8 | import com.google.gson.GsonBuilder; 9 | import com.google.gson.JsonElement; 10 | import com.google.gson.JsonObject; 11 | import com.google.gson.internal.Streams; 12 | import com.google.gson.stream.JsonReader; 13 | import com.google.gson.stream.JsonWriter; 14 | import java.io.File; 15 | import java.io.FileInputStream; 16 | import java.io.FileOutputStream; 17 | import java.io.IOException; 18 | import java.io.InputStream; 19 | import java.io.InputStreamReader; 20 | import java.io.OutputStreamWriter; 21 | import java.io.Reader; 22 | import java.io.Writer; 23 | import java.util.Map; 24 | import openeye.Log; 25 | 26 | public class GsonConfigProcessingEngine implements IConfigProcessingEngine { 27 | 28 | private static final String VALUE_TAG = "value"; 29 | private static final String COMMENT_TAG = "comment"; 30 | 31 | private static JsonElement parse(File file) { 32 | try { 33 | InputStream stream = new FileInputStream(file); 34 | try { 35 | Reader fileReader = new InputStreamReader(stream, Charsets.UTF_8); 36 | JsonReader jsonReader = new JsonReader(fileReader); 37 | jsonReader.setLenient(true); 38 | return Streams.parse(jsonReader); 39 | } finally { 40 | stream.close(); 41 | } 42 | } catch (IOException e) { 43 | throw new RuntimeException(e); 44 | } 45 | } 46 | 47 | private static void write(File file, JsonElement element) { 48 | try { 49 | Closer closer = Closer.create(); 50 | try { 51 | FileOutputStream stream = closer.register(new FileOutputStream(file)); 52 | Writer fileWriter = closer.register(new OutputStreamWriter(stream, Charsets.UTF_8)); 53 | JsonWriter jsonWriter = closer.register(new JsonWriter(fileWriter)); 54 | jsonWriter.setIndent(" "); 55 | Streams.write(element, jsonWriter); 56 | } finally { 57 | closer.close(); 58 | } 59 | } catch (IOException e) { 60 | throw new RuntimeException(e); 61 | } 62 | } 63 | 64 | private static JsonElement dumpConfig(Table properties) { 65 | JsonObject result = new JsonObject(); 66 | 67 | Gson gson = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().serializeNulls().create(); 68 | 69 | for (Map.Entry> category : properties.rowMap().entrySet()) { 70 | JsonObject categoryNode = new JsonObject(); 71 | 72 | for (Map.Entry property : category.getValue().entrySet()) { 73 | JsonObject propertyNode = new JsonObject(); 74 | 75 | IConfigPropertyHolder propertyHolder = property.getValue(); 76 | 77 | final String comment = propertyHolder.comment(); 78 | if (!Strings.isNullOrEmpty(comment)) propertyNode.addProperty(COMMENT_TAG, comment); 79 | 80 | try { 81 | Object value = propertyHolder.getValue(); 82 | JsonElement serialized = gson.toJsonTree(value); 83 | propertyNode.add(VALUE_TAG, serialized); 84 | } catch (Exception e) { 85 | Log.warn(e, "Failed to serialize property %s:%s", propertyHolder.category(), propertyHolder.name()); 86 | } 87 | 88 | categoryNode.add(property.getKey(), propertyNode); 89 | } 90 | 91 | result.add(category.getKey(), categoryNode); 92 | } 93 | 94 | return result; 95 | } 96 | 97 | private static boolean loadConfig(JsonElement parsed, Table properties) { 98 | if (!parsed.isJsonObject()) return true; 99 | 100 | Gson gson = new Gson(); 101 | 102 | JsonObject rootNode = parsed.getAsJsonObject(); 103 | 104 | boolean missingFields = false; 105 | 106 | for (Map.Entry> e : properties.rowMap().entrySet()) { 107 | JsonElement categoryTmp = rootNode.get(e.getKey()); 108 | missingFields |= parseCategory(categoryTmp, e.getValue(), gson); 109 | } 110 | 111 | return missingFields; 112 | } 113 | 114 | private static boolean parseCategory(JsonElement categoryElement, Map properties, Gson gson) { 115 | if (!(categoryElement instanceof JsonObject)) return true; 116 | 117 | JsonObject categoryNode = categoryElement.getAsJsonObject(); 118 | 119 | boolean missingFields = false; 120 | for (Map.Entry e : properties.entrySet()) { 121 | JsonElement propertyValue = categoryNode.get(e.getKey()); 122 | missingFields |= parseProperty(propertyValue, e.getValue(), gson); 123 | } 124 | 125 | return missingFields; 126 | } 127 | 128 | private static boolean parseProperty(JsonElement propertyElement, IConfigPropertyHolder property, Gson gson) { 129 | if (!(propertyElement instanceof JsonObject)) return true; 130 | 131 | JsonObject propertyNode = propertyElement.getAsJsonObject(); 132 | 133 | JsonElement value = propertyNode.get(VALUE_TAG); 134 | 135 | if (value == null) return true; 136 | 137 | try { 138 | Object parsedValue = gson.fromJson(value, property.getType()); 139 | property.setValue(parsedValue); 140 | } catch (Exception e) { 141 | Log.warn(e, "Failed to parse value of field %s:%s", property.category(), property.name()); 142 | return true; 143 | } 144 | 145 | JsonElement comment = propertyNode.get(COMMENT_TAG); 146 | 147 | final String expectedComment = property.comment(); 148 | 149 | if (comment == null) { 150 | return !Strings.isNullOrEmpty(expectedComment); 151 | } else if (comment.isJsonPrimitive()) { 152 | String commentValue = comment.getAsString(); 153 | return !expectedComment.equals(commentValue); 154 | } 155 | 156 | return true; 157 | } 158 | 159 | @Override 160 | public boolean loadConfig(File source, Table properties) { 161 | if (source.exists()) { 162 | try { 163 | JsonElement parsed = parse(source); 164 | return loadConfig(parsed, properties); 165 | } catch (Exception e) { 166 | Log.warn(e, "Failed to parse file %s, using defaults", source); 167 | } 168 | } 169 | return true; 170 | } 171 | 172 | @Override 173 | public void dumpConfig(File source, Table properties) { 174 | try { 175 | JsonElement serialized = dumpConfig(properties); 176 | write(source, serialized); 177 | } catch (Exception e) { 178 | Log.warn(e, "Failed to save config to file %s", source); 179 | } 180 | } 181 | 182 | } 183 | -------------------------------------------------------------------------------- /src/main/java/openeye/config/IConfigProcessingEngine.java: -------------------------------------------------------------------------------- 1 | package openeye.config; 2 | 3 | import com.google.common.collect.Table; 4 | import java.io.File; 5 | 6 | public interface IConfigProcessingEngine { 7 | public boolean loadConfig(File source, Table properties); 8 | 9 | public void dumpConfig(File source, Table properties); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/openeye/config/IConfigPropertyHolder.java: -------------------------------------------------------------------------------- 1 | package openeye.config; 2 | 3 | public interface IConfigPropertyHolder { 4 | 5 | public Object getValue(); 6 | 7 | public void setValue(Object value); 8 | 9 | public Class getType(); 10 | 11 | public String name(); 12 | 13 | public String category(); 14 | 15 | public String comment(); 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/openeye/logic/Bootstrap.java: -------------------------------------------------------------------------------- 1 | package openeye.logic; 2 | 3 | import com.google.common.base.Preconditions; 4 | import java.io.File; 5 | import java.util.List; 6 | import java.util.Map; 7 | import net.minecraft.launchwrapper.ITweaker; 8 | import net.minecraft.launchwrapper.LaunchClassLoader; 9 | 10 | public class Bootstrap { 11 | 12 | private Bootstrap() {} 13 | 14 | public static final Bootstrap instance = new Bootstrap(); 15 | 16 | private boolean runtimeDeobfuscationEnabled; 17 | 18 | private List tweakers; 19 | 20 | private LaunchClassLoader loader; 21 | 22 | private File mcLocation; 23 | 24 | private File selfLocation; 25 | 26 | @SuppressWarnings("unchecked") 27 | public void populateFromInject(Map data) { 28 | runtimeDeobfuscationEnabled = (Boolean)data.get("runtimeDeobfuscationEnabled"); 29 | tweakers = (List)data.get("coremodList"); 30 | } 31 | 32 | public void populateFromSetupClass(Map data) { 33 | loader = (LaunchClassLoader)data.get("classLoader"); 34 | mcLocation = (File)data.get("mcLocation"); 35 | selfLocation = (File)data.get("coremodLocation"); 36 | } 37 | 38 | public void startup() { 39 | Preconditions.checkNotNull(mcLocation, "Failed to start OpenEye, no minecraft folder available"); 40 | 41 | Config.load(mcLocation); 42 | 43 | Storages storages = Storages.init(mcLocation); 44 | StateHolder.init(storages); 45 | 46 | Sanitizers.addMinecraftPath(mcLocation); 47 | ThrowableLogger.init(); 48 | } 49 | 50 | public boolean isRuntimeDeobfuscationEnabled() { 51 | return runtimeDeobfuscationEnabled; 52 | } 53 | 54 | public List getTweakers() { 55 | return tweakers; 56 | } 57 | 58 | public LaunchClassLoader getLoader() { 59 | return loader; 60 | } 61 | 62 | public File getMcLocation() { 63 | return mcLocation; 64 | } 65 | 66 | public File getSelfLocation() { 67 | return selfLocation; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/openeye/logic/Config.java: -------------------------------------------------------------------------------- 1 | package openeye.logic; 2 | 3 | import com.google.common.collect.ImmutableSet; 4 | import java.io.File; 5 | import java.util.Set; 6 | import openeye.Log; 7 | import openeye.config.ConfigProcessing; 8 | import openeye.config.ConfigProperty; 9 | 10 | public class Config { 11 | 12 | @ConfigProperty(category = "debug") 13 | public static boolean crashOnStartup = false; 14 | 15 | @ConfigProperty(category = "data") 16 | public static Set tags = ImmutableSet.of(); 17 | 18 | @ConfigProperty(category = "data", comment = "Send only basic information about files instead of full analytics. Only works when 'sendModList' is set to true") 19 | public static boolean scanOnly = false; 20 | 21 | @ConfigProperty(category = "data", comment = "If false, skips sending any mod list - ether basic file information or analytics. OpenEye will respond with information about files unknown to server") 22 | public static boolean sendModList = true; 23 | 24 | @ConfigProperty(category = "data", comment = "If false, skips sending pending crash reports. Please note that pending crashe reports will not be automatically removed.") 25 | public static boolean sendCrashes = true; 26 | 27 | @ConfigProperty(category = "data", comment = "Maximum number of crash reports from single category stored per single run") 28 | public static int storeCrashReportsLimit = 20; 29 | 30 | @ConfigProperty(category = "data", comment = "Maximum number of crash reports to sent (including pending) per single run") 31 | public static int sentCrashReportsLimitTotal = 1000; 32 | 33 | @ConfigProperty(category = "debug") 34 | public static boolean pingOnInitialReport = false; 35 | 36 | @ConfigProperty(category = "debug") 37 | public static boolean dontSend = false; 38 | 39 | @ConfigProperty(category = "debug") 40 | public static boolean debugSanitizer = false; 41 | 42 | @ConfigProperty(category = "features") 43 | public static Set reportsBlacklist = ImmutableSet.of(); 44 | 45 | @ConfigProperty(category = "features") 46 | public static Set responseBlacklist = ImmutableSet.of(); 47 | 48 | @ConfigProperty(category = "gui", comment = "Enables OpenEye additions to main menu screen") 49 | public static boolean mainScreenNotes = true; 50 | 51 | @ConfigProperty(category = "gui", comment = "Enables extra line in main menu screen under buttons (if true, only note button will be displayed)") 52 | public static boolean mainScreenExtraLine = true; 53 | 54 | @ConfigProperty(category = "gui", comment = "X coordinate of notes button") 55 | public static int notesButtonPosX = 104; 56 | 57 | @ConfigProperty(category = "gui", comment = "Y coordinate of notes button") 58 | public static int notesButtonPosY = 96; 59 | 60 | @ConfigProperty(category = "gui", comment = "If false, X and Y coordinates of notes button will be measured from screen width/2 and height/4 (standard MC algorithm)") 61 | public static boolean isNotesButtonPosAbsolute = false; 62 | 63 | @ConfigProperty(category = "gui", comment = "X coordinate of notification line") 64 | public static int extraLinePosX = 0; 65 | 66 | @ConfigProperty(category = "gui", comment = "Y coordinate of notification line") 67 | public static int extraLinePosY = 120; 68 | 69 | @ConfigProperty(category = "gui", comment = "If false, X and Y coordinates of notification line will be measured from screen width/2 and height/4 (standard MC algorithm)") 70 | public static boolean isExtraLinePosAbsolute = false; 71 | 72 | public static void load(File mcLocation) { 73 | try { 74 | File configFolder = new File(mcLocation, "config"); 75 | configFolder.mkdir(); 76 | File configFile = new File(configFolder, "OpenEye.json"); 77 | 78 | ConfigProcessing.processConfig(configFile, Config.class, ConfigProcessing.GSON); 79 | } catch (Exception e) { 80 | Log.warn(e, "Failed to load config"); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/openeye/logic/CrashId.java: -------------------------------------------------------------------------------- 1 | package openeye.logic; 2 | 3 | class CrashId { 4 | public long timestamp; 5 | public int id; 6 | 7 | CrashId(long timestamp, int id) { 8 | this.timestamp = timestamp; 9 | this.id = id; 10 | } 11 | 12 | @Override 13 | public int hashCode() { 14 | final int prime = 31; 15 | int result = 1; 16 | result = prime * result + id; 17 | result = prime * result + (int)(timestamp ^ (timestamp >>> 32)); 18 | return result; 19 | } 20 | 21 | @Override 22 | public boolean equals(Object obj) { 23 | if (this == obj) return true; 24 | 25 | if (obj instanceof CrashId) { 26 | CrashId other = (CrashId)obj; 27 | return id == other.id && timestamp == other.timestamp; 28 | } 29 | return false; 30 | } 31 | 32 | } -------------------------------------------------------------------------------- /src/main/java/openeye/logic/GsonUtils.java: -------------------------------------------------------------------------------- 1 | package openeye.logic; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.GsonBuilder; 5 | import com.google.gson.JsonElement; 6 | import com.google.gson.JsonObject; 7 | import com.google.gson.JsonSerializationContext; 8 | import com.google.gson.JsonSerializer; 9 | import java.lang.reflect.Type; 10 | import net.minecraftforge.fml.common.versioning.ArtifactVersion; 11 | import openeye.struct.TypedCollections; 12 | import openeye.struct.TypedCollections.ReportsList; 13 | import openeye.struct.TypedCollections.ResponseList; 14 | 15 | public class GsonUtils { 16 | 17 | public static final JsonSerializer VERSION_SERIALIZER = new JsonSerializer() { 18 | @Override 19 | public JsonElement serialize(ArtifactVersion src, Type typeOfSrc, JsonSerializationContext context) { 20 | JsonObject obj = new JsonObject(); 21 | obj.addProperty("label", src.getLabel()); 22 | obj.addProperty("version", src.getRangeString()); 23 | return obj; 24 | } 25 | }; 26 | 27 | public static GsonBuilder setupCommonBuilder() { 28 | return new GsonBuilder() 29 | .registerTypeAdapter(ReportsList.class, TypedCollections.REPORT_LIST_CONVERTER) 30 | .registerTypeAdapter(ResponseList.class, TypedCollections.RESPONSE_LIST_CONVERTER) 31 | .registerTypeAdapter(ArtifactVersion.class, VERSION_SERIALIZER); 32 | } 33 | 34 | public static final Gson NET_GSON = setupCommonBuilder().create(); 35 | 36 | public static final Gson PRETTY_GSON = setupCommonBuilder().setPrettyPrinting().create(); 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/openeye/logic/IContext.java: -------------------------------------------------------------------------------- 1 | package openeye.logic; 2 | 3 | import java.io.File; 4 | import java.util.Set; 5 | import openeye.protocol.reports.IReport; 6 | 7 | public interface IContext { 8 | public Set getModsForSignature(String signature); 9 | 10 | public void queueReport(IReport report); 11 | 12 | public void queueFileReport(String signature); 13 | 14 | public void queueFileContents(String signature); 15 | 16 | public void markUnwantedSignature(String signature); 17 | 18 | public File getFileForSignature(String signature); 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/openeye/logic/INotStoredCrash.java: -------------------------------------------------------------------------------- 1 | package openeye.logic; 2 | 3 | public interface INotStoredCrash { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/openeye/logic/ModCollectorFactory.java: -------------------------------------------------------------------------------- 1 | package openeye.logic; 2 | 3 | import java.util.Collection; 4 | import java.util.concurrent.FutureTask; 5 | import java.util.concurrent.RunnableFuture; 6 | import net.minecraft.launchwrapper.ITweaker; 7 | import net.minecraft.launchwrapper.LaunchClassLoader; 8 | import net.minecraftforge.fml.common.discovery.ASMDataTable; 9 | 10 | public class ModCollectorFactory { 11 | 12 | public RunnableFuture createCollector(final ASMDataTable table, final LaunchClassLoader loader, final Collection tweakers) { 13 | return new FutureTask<>(() -> { 14 | return new ModMetaCollector(table, loader, tweakers); 15 | }); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/openeye/logic/ModMetaCollector.java: -------------------------------------------------------------------------------- 1 | package openeye.logic; 2 | 3 | import com.google.common.base.Preconditions; 4 | import com.google.common.base.Strings; 5 | import com.google.common.collect.ImmutableList; 6 | import com.google.common.collect.ImmutableSet; 7 | import com.google.common.collect.Lists; 8 | import com.google.common.collect.Maps; 9 | import com.google.common.collect.Multimap; 10 | import com.google.common.collect.Sets; 11 | import com.google.common.hash.Hashing; 12 | import com.google.common.io.Files; 13 | import java.io.File; 14 | import java.lang.reflect.Field; 15 | import java.net.URL; 16 | import java.security.CodeSource; 17 | import java.util.Collection; 18 | import java.util.List; 19 | import java.util.Map; 20 | import java.util.Set; 21 | import net.minecraft.launchwrapper.IClassTransformer; 22 | import net.minecraft.launchwrapper.ITweaker; 23 | import net.minecraft.launchwrapper.LaunchClassLoader; 24 | import net.minecraftforge.fml.common.Loader; 25 | import net.minecraftforge.fml.common.ModContainer; 26 | import net.minecraftforge.fml.common.ModMetadata; 27 | import net.minecraftforge.fml.common.discovery.ASMDataTable; 28 | import net.minecraftforge.fml.common.discovery.ContainerType; 29 | import net.minecraftforge.fml.common.discovery.ModCandidate; 30 | import net.minecraftforge.fml.common.versioning.ArtifactVersion; 31 | import net.minecraftforge.fml.common.versioning.VersionRange; 32 | import net.minecraftforge.fml.relauncher.IFMLLoadingPlugin; 33 | import net.minecraftforge.fml.relauncher.ReflectionHelper; 34 | import openeye.Log; 35 | import openeye.protocol.Artifact; 36 | import openeye.protocol.FileSignature; 37 | import openeye.protocol.reports.ReportCrash.FileState; 38 | import openeye.protocol.reports.ReportCrash.ModState; 39 | import openeye.protocol.reports.ReportFileContents; 40 | import openeye.protocol.reports.ReportFileInfo; 41 | import openeye.protocol.reports.ReportFileInfo.SerializableMod; 42 | import openeye.protocol.reports.ReportFileInfo.SerializableTweak; 43 | import org.apache.logging.log4j.Level; 44 | 45 | public class ModMetaCollector { 46 | 47 | private static final String SIGNATURE_MINECRAFT_SERVER_JAR = "special:minecraft_server"; 48 | 49 | private static final String SIGNATURE_MINECRAFT_JAR = "special:minecraft"; 50 | 51 | private static final String SIGNATURE_NONE = "special:none"; 52 | 53 | private static class TweakMeta { 54 | private final String pluginName; 55 | private final String className; 56 | 57 | private TweakMeta(String pluginName, String className) { 58 | this.pluginName = Strings.nullToEmpty(pluginName); 59 | this.className = Strings.nullToEmpty(className); 60 | } 61 | 62 | public SerializableTweak toSerializable() { 63 | SerializableTweak result = new SerializableTweak(); 64 | result.plugin = pluginName; 65 | result.cls = className; 66 | return result; 67 | } 68 | } 69 | 70 | private static class ModMeta { 71 | private final String modId; 72 | private final String name; 73 | private final String version; 74 | private final ModMetadata metadata; 75 | private final ModContainer container; 76 | 77 | private ModMeta(ModContainer container) { 78 | this.container = container; 79 | this.modId = Strings.nullToEmpty(container.getModId()); 80 | this.name = Strings.nullToEmpty(container.getName()); 81 | this.version = Strings.nullToEmpty(container.getVersion()); 82 | this.metadata = container.getMetadata(); 83 | } 84 | 85 | private static Collection safeCopy(Collection input) { 86 | if (input == null) return ImmutableList.of(); 87 | return ImmutableList.copyOf(input); 88 | } 89 | 90 | private static Collection copyArtifacts(Collection input) { 91 | if (input == null) return ImmutableList.of(); 92 | 93 | ImmutableList.Builder result = ImmutableList.builder(); 94 | 95 | for (ArtifactVersion version : input) { 96 | final Artifact tmp = new Artifact(); 97 | tmp.label = version.getLabel(); 98 | tmp.version = version.getVersionString(); 99 | result.add(tmp); 100 | } 101 | 102 | return result.build(); 103 | } 104 | 105 | @SuppressWarnings("deprecation") 106 | public SerializableMod toSerializable() { 107 | SerializableMod result = new SerializableMod(); 108 | result.modId = modId; 109 | result.name = name; 110 | result.version = version; 111 | 112 | VersionRange mcVersionRange = container.acceptableMinecraftVersionRange(); 113 | if (mcVersionRange != null) result.mcVersion = Strings.nullToEmpty(mcVersionRange.toString()); 114 | 115 | result.description = Strings.nullToEmpty(metadata.description); 116 | result.url = Strings.nullToEmpty(metadata.url); 117 | result.updateUrl = Strings.nullToEmpty(metadata.updateUrl); 118 | result.credits = Strings.nullToEmpty(metadata.credits); 119 | result.parent = Strings.nullToEmpty(metadata.parent); 120 | result.authors = safeCopy(metadata.authorList); 121 | result.requiredMods = copyArtifacts(metadata.requiredMods); 122 | result.dependants = copyArtifacts(metadata.dependants); 123 | result.dependencies = copyArtifacts(metadata.dependencies); 124 | return result; 125 | } 126 | 127 | public ModState getState() { 128 | ModState state = new ModState(); 129 | state.modId = modId; 130 | state.state = Loader.instance().getModState(container).toString(); 131 | return state; 132 | } 133 | } 134 | 135 | private static class FileMeta { 136 | public final Set classTransformers = Sets.newHashSet(); 137 | public final Map mods = Maps.newHashMap(); 138 | public final List tweakers = Lists.newArrayList(); 139 | public final Set packages = Sets.newHashSet(); 140 | public final File container; 141 | private String signature; 142 | 143 | private FileMeta(File container, String signature) { 144 | this.container = container; 145 | this.signature = signature; 146 | } 147 | 148 | public String signature() { 149 | if (signature == null) { 150 | signature = calculateSignature(container); 151 | } 152 | return signature; 153 | } 154 | 155 | public Long getSize() { 156 | try { 157 | return container.length(); 158 | } catch (Throwable t) { 159 | Log.info(t, "Can't get size info for file %s", container); 160 | } 161 | return null; 162 | } 163 | 164 | public FileState getStates() { 165 | FileState result = new FileState(); 166 | result.signature = signature(); 167 | 168 | List mods = Lists.newArrayList(); 169 | 170 | for (ModMeta meta : this.mods.values()) 171 | mods.add(meta.getState()); 172 | 173 | result.mods = mods; 174 | return result; 175 | } 176 | 177 | public ReportFileInfo generateReport() { 178 | ReportFileInfo report = new ReportFileInfo(); 179 | report.signature = signature(); 180 | report.size = getSize(); 181 | 182 | { 183 | ImmutableList.Builder modsBuilder = ImmutableList.builder(); 184 | for (ModMeta m : mods.values()) 185 | modsBuilder.add(m.toSerializable()); 186 | report.mods = modsBuilder.build(); 187 | } 188 | 189 | { 190 | ImmutableList.Builder tweaksBuilder = ImmutableList.builder(); 191 | for (TweakMeta t : tweakers) 192 | tweaksBuilder.add(t.toSerializable()); 193 | report.tweakers = tweaksBuilder.build(); 194 | } 195 | 196 | report.classTransformers = ImmutableList.copyOf(classTransformers); 197 | report.packages = ImmutableList.copyOf(packages); 198 | 199 | return report; 200 | } 201 | 202 | public ReportFileContents generateFileContentsReport() { 203 | ReportFileContents report = new ReportFileContents(); 204 | report.signature = signature(); 205 | ReportBuilders.fillFileContents(container, report); 206 | return report; 207 | } 208 | 209 | public FileSignature createFileSignature() { 210 | FileSignature result = new FileSignature(); 211 | result.signature = signature(); 212 | result.filename = container.getName(); 213 | return result; 214 | } 215 | } 216 | 217 | private final Map files = Maps.newHashMap(); 218 | 219 | private final Map signatures = Maps.newHashMap(); 220 | 221 | private final long operationDuration; 222 | 223 | private final ASMDataTable table; 224 | 225 | private Map specialSignatures = Maps.newHashMap(); 226 | 227 | ModMetaCollector(ASMDataTable table, LaunchClassLoader loader, Collection tweakers) { 228 | Log.debug("Starting mod metadata collection"); 229 | this.table = table; 230 | long start = System.nanoTime(); 231 | 232 | // Special handling for minecraft jars - some launchers like to repackage. 233 | // Since it creates unique signatures, it may allow identification, if not obfuscated 234 | // (order of calls is significant: client package contains both client and server classes) 235 | assignSignatureToClassSource(SIGNATURE_MINECRAFT_SERVER_JAR, "net.minecraft.server.MinecraftServer"); 236 | assignSignatureToClassSource(SIGNATURE_MINECRAFT_JAR, "net.minecraft.client.main.Main"); 237 | 238 | Collection allCandidates = stealCandidates(table); 239 | collectFilesFromModCandidates(allCandidates); 240 | collectFilesFromClassTransformers(loader, table); 241 | collectFilesFromTweakers(tweakers, table); 242 | collectFilesFromModContainers(table); 243 | 244 | fillSignaturesMap(); 245 | operationDuration = System.nanoTime() - start; 246 | Log.debug("Collection of mod metadata finished. Duration: %.4f ms", operationDuration / 1000000.0d); 247 | } 248 | 249 | private FileMeta createFileMeta(File file) { 250 | final String signature = specialSignatures.get(file); 251 | return new FileMeta(file, signature); 252 | } 253 | 254 | private void assignSignatureToClassSource(String signature, String mainCls) { 255 | try { 256 | Class cls = Class.forName(mainCls); 257 | CodeSource src = cls.getProtectionDomain().getCodeSource(); 258 | URL jarUrl = extractJarUrl(src.getLocation()); 259 | File sourceFile = new File(jarUrl.toURI()); 260 | Preconditions.checkState(sourceFile.isFile(), "Path %s is not file", sourceFile); 261 | specialSignatures.put(sourceFile, signature); 262 | Log.debug("Signature '%s' assigned to file '%s'", signature, sourceFile); 263 | } catch (ClassNotFoundException e) { 264 | Log.debug("Failed to assign signature '%s' to source of class %s - class not found", signature, mainCls); 265 | } catch (Exception e) { 266 | Log.log(Level.DEBUG, e, "Failed to assign signature '%s' to source of class %s", signature, mainCls); 267 | } 268 | } 269 | 270 | private static URL extractJarUrl(URL sourceUrl) throws Exception { 271 | Preconditions.checkState(sourceUrl.getProtocol().equalsIgnoreCase("jar"), "%s is not jar path", sourceUrl); 272 | final String jarFileSource = sourceUrl.getFile(); 273 | final int separator = jarFileSource.indexOf("!/"); 274 | if (separator == -1) throw new IllegalStateException("no !/ found in url spec:" + jarFileSource); 275 | return new URL(jarFileSource.substring(0, separator)); 276 | } 277 | 278 | private FileMeta fromModCandidate(ModCandidate candidate) { 279 | FileMeta fileMeta = createFileMeta(candidate.getModContainer()); 280 | fileMeta.packages.addAll(candidate.getContainedPackages()); 281 | for (ModContainer c : candidate.getContainedMods()) 282 | fileMeta.mods.put(c.getModId(), new ModMeta(c)); 283 | 284 | return fileMeta; 285 | } 286 | 287 | private static String calculateSignature(File file) { 288 | try { 289 | return "sha256:" + Files.hash(file, Hashing.sha256()).toString(); 290 | } catch (Throwable t) { 291 | Log.warn(t, "Can't hash file %s", file); 292 | return SIGNATURE_NONE; 293 | } 294 | } 295 | 296 | private FileMeta getOrCreateData(File file) { 297 | FileMeta data = files.get(file); 298 | if (data == null) { 299 | data = createFileMeta(file); 300 | files.put(file, data); 301 | } 302 | 303 | return data; 304 | } 305 | 306 | private void collectFilesFromModCandidates(Collection candidates) { 307 | for (ModCandidate c : candidates) { 308 | File modContainer = c.getModContainer(); 309 | if (!files.containsKey(modContainer) && c.getSourceType() == ContainerType.JAR) { 310 | FileMeta meta = fromModCandidate(c); 311 | files.put(modContainer, meta); 312 | } 313 | } 314 | } 315 | 316 | private static String extractPackage(String className) { 317 | int pkgIdx = className.lastIndexOf('.'); 318 | if (pkgIdx == -1) return null; 319 | return className.substring(0, pkgIdx); 320 | } 321 | 322 | private static Set getCandidatesForClass(ASMDataTable table, String cls) { 323 | String packageName = extractPackage(cls); 324 | if (Strings.isNullOrEmpty(packageName)) return null; 325 | return table.getCandidatesFor(packageName); 326 | } 327 | 328 | private interface IFileMetaVisitor { 329 | public void visit(FileMeta fileMeta); 330 | } 331 | 332 | private void visitMeta(ASMDataTable table, String cls, IFileMetaVisitor visitor) { 333 | Set candidates = getCandidatesForClass(table, cls); 334 | if (candidates != null) { 335 | for (ModCandidate c : candidates) { 336 | File container = c.getModContainer(); 337 | if (container.isDirectory()) continue; 338 | FileMeta fileMeta = files.get(container); 339 | if (fileMeta == null) { 340 | fileMeta = fromModCandidate(c); 341 | files.put(container, fileMeta); 342 | } 343 | visitor.visit(fileMeta); 344 | } 345 | } 346 | } 347 | 348 | private void registerClassTransformer(ASMDataTable table, final String cls) { 349 | visitMeta(table, cls, new IFileMetaVisitor() { 350 | @Override 351 | public void visit(FileMeta fileMeta) { 352 | fileMeta.classTransformers.add(cls); 353 | } 354 | }); 355 | } 356 | 357 | private static Collection stealCandidates(ASMDataTable table) { 358 | // I'm very sorry for that 359 | try { 360 | Multimap packageMap = ReflectionHelper.getPrivateValue(ASMDataTable.class, table, "packageMap"); 361 | if (packageMap != null) return packageMap.values(); 362 | } catch (Throwable t) { 363 | Log.warn(t, "Can't get ModCandidate map, data will be missing from report"); 364 | 365 | } 366 | return ImmutableList.of(); 367 | } 368 | 369 | private void collectFilesFromModContainers(ASMDataTable table) { 370 | final File dummyEntry = new File("minecraft.jar"); // dummy entry comes from MCP container 371 | for (ModContainer c : Loader.instance().getModList()) { 372 | File f = c.getSource(); 373 | if (f != null && !f.equals(dummyEntry) && !f.isDirectory()) { 374 | FileMeta meta = getOrCreateData(f); 375 | meta.mods.put(c.getModId(), new ModMeta(c)); 376 | } 377 | } 378 | } 379 | 380 | private void collectFilesFromTweakers(Collection tweakers, ASMDataTable table) { 381 | try { 382 | Class coreModWrapper = Class.forName("net.minecraftforge.fml.relauncher.CoreModManager$FMLPluginWrapper"); 383 | Field nameField = coreModWrapper.getField("name"); 384 | nameField.setAccessible(true); 385 | Field pluginField = coreModWrapper.getField("coreModInstance"); 386 | pluginField.setAccessible(true); 387 | Field locationField = coreModWrapper.getField("location"); 388 | locationField.setAccessible(true); 389 | 390 | for (ITweaker tweaker : tweakers) { 391 | try { 392 | File location = (File)locationField.get(tweaker); 393 | if (location == null || location.isDirectory()) continue; 394 | String name = (String)nameField.get(tweaker); 395 | IFMLLoadingPlugin plugin = (IFMLLoadingPlugin)pluginField.get(tweaker); 396 | 397 | FileMeta meta = getOrCreateData(location); 398 | meta.tweakers.add(new TweakMeta(name, plugin.getClass().getName())); 399 | } catch (Throwable t) { 400 | Log.warn(t, "Can't get data for tweaker %s", tweaker); 401 | } 402 | } 403 | } catch (Throwable t) { 404 | Log.warn(t, "Can't get tweaker data"); 405 | } 406 | } 407 | 408 | private void collectFilesFromClassTransformers(LaunchClassLoader loader, ASMDataTable table) { 409 | if (loader != null) { 410 | List transformers = loader.getTransformers(); 411 | for (IClassTransformer transformer : transformers) 412 | registerClassTransformer(table, transformer.getClass().getName()); 413 | } else { 414 | Log.warn("LaunchClassLoader not available"); 415 | } 416 | } 417 | 418 | private void fillSignaturesMap() { 419 | for (FileMeta meta : files.values()) 420 | signatures.put(meta.signature(), meta); 421 | } 422 | 423 | public List listSignatures() { 424 | List result = Lists.newArrayList(); 425 | for (FileMeta meta : files.values()) 426 | result.add(meta.signature); 427 | 428 | return result; 429 | } 430 | 431 | public List collectStates() { 432 | List result = Lists.newArrayList(); 433 | 434 | for (FileMeta meta : files.values()) 435 | result.add(meta.getStates()); 436 | 437 | return result; 438 | } 439 | 440 | public List getAllFiles() { 441 | List result = Lists.newArrayList(); 442 | for (FileMeta meta : files.values()) { 443 | FileSignature tmp = new FileSignature(); 444 | tmp.signature = meta.signature(); 445 | tmp.filename = meta.container.getName(); 446 | result.add(meta.createFileSignature()); 447 | } 448 | 449 | return result; 450 | } 451 | 452 | public Set getAllSignatures() { 453 | Set result = Sets.newHashSet(); 454 | 455 | for (FileMeta meta : files.values()) 456 | result.add(meta.signature()); 457 | 458 | return result; 459 | } 460 | 461 | public long getCollectingDuration() { 462 | return operationDuration; 463 | } 464 | 465 | public ReportFileInfo generateFileReport(String signature) { 466 | FileMeta meta = signatures.get(signature); 467 | return meta != null? meta.generateReport() : null; 468 | } 469 | 470 | public ReportFileContents generateFileContentsReport(String signature) { 471 | FileMeta meta = signatures.get(signature); 472 | return meta != null? meta.generateFileContentsReport() : null; 473 | } 474 | 475 | public Set getModsForSignature(String signature) { 476 | FileMeta meta = signatures.get(signature); 477 | if (meta != null) return ImmutableSet.copyOf(meta.mods.keySet()); 478 | else return ImmutableSet.of(); 479 | } 480 | 481 | public FileSignature getFileForSignature(String signature) { 482 | FileMeta meta = signatures.get(signature); 483 | return meta != null? meta.createFileSignature() : null; 484 | } 485 | 486 | public File getContainerForSignature(String signature) { 487 | FileMeta meta = signatures.get(signature); 488 | return meta != null? meta.container : null; 489 | } 490 | 491 | public static class ClassSource { 492 | public String loadedFrom; 493 | public final Set containingClasses = Sets.newHashSet(); 494 | } 495 | 496 | public ClassSource identifyClassSource(String className) { 497 | String packageName = extractPackage(className); 498 | ClassSource result = new ClassSource(); 499 | 500 | if (packageName != null && (packageName.startsWith("net.minecraft") || packageName.startsWith("net.minecraftforge") || packageName.startsWith("cpw.mods.fml"))) 501 | return null; 502 | 503 | try { 504 | Class cls = Class.forName(className); 505 | CodeSource src = cls.getProtectionDomain().getCodeSource(); 506 | if (src != null) { 507 | URL sourceUrl = extractJarUrl(src.getLocation()); 508 | File sourceFile = new File(sourceUrl.toURI()); 509 | FileMeta meta = files.get(sourceFile); 510 | if (meta != null) result.loadedFrom = meta.signature(); 511 | } 512 | } catch (Throwable t) { 513 | // NO-OP - nothing to save 514 | } 515 | 516 | Set candidates = table.getCandidatesFor(packageName); 517 | for (ModCandidate c : candidates) { 518 | File container = c.getModContainer(); 519 | if (container != null) { 520 | FileMeta meta = files.get(container); 521 | if (meta != null) result.containingClasses.add(meta.signature()); 522 | } 523 | } 524 | 525 | return result; 526 | } 527 | } 528 | -------------------------------------------------------------------------------- /src/main/java/openeye/logic/ModState.java: -------------------------------------------------------------------------------- 1 | package openeye.logic; 2 | 3 | import com.google.common.collect.Sets; 4 | import java.util.Set; 5 | 6 | public class ModState { 7 | public Set installedMods = Sets.newHashSet(); 8 | 9 | public boolean mainMenuInfoDisplayed; 10 | 11 | public boolean infoNotesDisplayed; 12 | 13 | public long suspendUntilTimestamp; 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/openeye/logic/ReportBuilders.java: -------------------------------------------------------------------------------- 1 | package openeye.logic; 2 | 3 | import com.google.common.base.Strings; 4 | import com.google.common.collect.ImmutableMap; 5 | import com.google.common.collect.Lists; 6 | import com.google.common.collect.Sets; 7 | import java.io.File; 8 | import java.io.InputStream; 9 | import java.security.DigestInputStream; 10 | import java.security.MessageDigest; 11 | import java.util.Calendar; 12 | import java.util.Collections; 13 | import java.util.Date; 14 | import java.util.Enumeration; 15 | import java.util.IdentityHashMap; 16 | import java.util.List; 17 | import java.util.Locale; 18 | import java.util.Map; 19 | import java.util.Random; 20 | import java.util.Set; 21 | import java.util.TimeZone; 22 | import java.util.zip.ZipEntry; 23 | import java.util.zip.ZipFile; 24 | import net.minecraftforge.common.ForgeVersion; 25 | import net.minecraftforge.fml.common.FMLCommonHandler; 26 | import net.minecraftforge.fml.common.Loader; 27 | import openeye.Log; 28 | import openeye.Proxy; 29 | import openeye.logic.ModMetaCollector.ClassSource; 30 | import openeye.protocol.reports.ReportAnalytics; 31 | import openeye.protocol.reports.ReportCrash; 32 | import openeye.protocol.reports.ReportCrash.ExceptionInfo; 33 | import openeye.protocol.reports.ReportCrash.StackTrace; 34 | import openeye.protocol.reports.ReportEnvironment; 35 | import openeye.protocol.reports.ReportFileContents; 36 | import openeye.protocol.reports.ReportFileContents.ArchiveDirEntry; 37 | import openeye.protocol.reports.ReportFileContents.ArchiveEntry; 38 | import openeye.protocol.reports.ReportFileContents.ArchiveFileEntry; 39 | import openeye.protocol.reports.ReportKnownFiles; 40 | 41 | public class ReportBuilders { 42 | 43 | private static final TagsCollector TAGS_COLLECTOR = new TagsCollector(); 44 | 45 | private static final Random RANDOM = new Random(); 46 | 47 | private static String getSide() { 48 | return FMLCommonHandler.instance().getSide().toString().toLowerCase(Locale.ENGLISH); 49 | } 50 | 51 | public static ReportKnownFiles buildKnownFilesReport(ModMetaCollector data) { 52 | ReportKnownFiles result = new ReportKnownFiles(); 53 | 54 | result.signatures = data.getAllFiles(); 55 | 56 | return result; 57 | } 58 | 59 | private static String getJavaVersion() { 60 | String vendor = Strings.nullToEmpty(System.getProperty("java.vendor")); 61 | String version = Strings.nullToEmpty(System.getProperty("java.version")); 62 | return vendor + " " + version; 63 | } 64 | 65 | private static Map getEnvVersions() { 66 | ImmutableMap.Builder versions = ImmutableMap.builder(); 67 | versions.put("mcp", Loader.instance().getMCPVersionString()); 68 | versions.put("fml", Loader.instance().getFMLVersionString()); 69 | versions.put("forge", ForgeVersion.getVersion()); 70 | return versions.build(); 71 | } 72 | 73 | private static void fillEnvInfo(ReportEnvironment report) { 74 | report.branding = FMLCommonHandler.instance().getBrandings(true); 75 | 76 | report.runtime = getEnvVersions(); 77 | 78 | report.minecraft = Loader.instance().getMCVersionString(); 79 | 80 | report.javaVersion = getJavaVersion(); 81 | 82 | report.side = getSide(); 83 | 84 | report.obfuscated = Bootstrap.instance.isRuntimeDeobfuscationEnabled(); 85 | 86 | Set tags = TAGS_COLLECTOR.getTags(); 87 | 88 | if (!tags.isEmpty()) report.tags = tags; 89 | } 90 | 91 | public static ReportAnalytics buildAnalyticsReport(ModMetaCollector data, Set prevSignatures) { 92 | ReportAnalytics analytics = new ReportAnalytics(); 93 | 94 | fillEnvInfo(analytics); 95 | 96 | String language = Proxy.instance().getLanguage(); 97 | analytics.language = Strings.isNullOrEmpty(language)? "invalid" : language; 98 | 99 | analytics.locale = Locale.getDefault().toString(); 100 | 101 | TimeZone tz = Calendar.getInstance().getTimeZone(); 102 | analytics.timezone = tz.getID(); 103 | 104 | analytics.workTime = data.getCollectingDuration() / 1000.0f; 105 | 106 | analytics.signatures = data.getAllFiles(); 107 | 108 | Set currentSignatures = data.getAllSignatures(); 109 | 110 | analytics.installedSignatures = Sets.difference(currentSignatures, prevSignatures); 111 | 112 | analytics.uninstalledSignatures = Sets.difference(prevSignatures, currentSignatures); 113 | 114 | return analytics; 115 | } 116 | 117 | private static StackTrace createStackTraceElement(StackTraceElement e, ModMetaCollector collector) { 118 | StackTrace el = new StackTrace(); 119 | final String clsName = e.getClassName(); 120 | el.className = clsName; 121 | el.fileName = e.getFileName(); 122 | el.methodName = e.getMethodName(); 123 | el.lineNumber = e.getLineNumber(); 124 | 125 | if (collector != null) { 126 | ClassSource source = collector.identifyClassSource(clsName); 127 | if (source != null) { 128 | el.signatures = source.containingClasses; 129 | if (source.loadedFrom != null) { 130 | el.source = source.loadedFrom; 131 | el.signatures.add(source.loadedFrom); 132 | } 133 | } 134 | } 135 | return el; 136 | } 137 | 138 | private static ExceptionInfo createStackTrace(Throwable throwable, StackTraceElement[] prevStacktrace, Set alreadySerialized, ModMetaCollector collector) { 139 | if (alreadySerialized.contains(throwable)) return null; // cyclical reference 140 | 141 | ExceptionInfo info = new ExceptionInfo(); 142 | 143 | info.exceptionCls = throwable.getClass().getName(); 144 | info.message = Sanitizers.getSanitizerForThrowable(throwable.getClass()).sanitize(throwable.getMessage()); 145 | 146 | alreadySerialized.add(throwable); 147 | 148 | info.stackTrace = Lists.newArrayList(); 149 | 150 | StackTraceElement[] stackTrace = throwable.getStackTrace(); 151 | 152 | int m = stackTrace.length - 1; 153 | int n = prevStacktrace.length - 1; 154 | while (m >= 0 && n >= 0 && stackTrace[m].equals(prevStacktrace[n])) { 155 | m--; 156 | n--; 157 | } 158 | 159 | for (int i = 0; i <= m; i++) 160 | info.stackTrace.add(createStackTraceElement(stackTrace[i], collector)); 161 | 162 | Throwable cause = throwable.getCause(); 163 | if (cause != null) info.cause = createStackTrace(cause, stackTrace, alreadySerialized, collector); 164 | 165 | return info; 166 | } 167 | 168 | public static ReportCrash buildCrashReport(Throwable throwable, String location, ModMetaCollector collector) { 169 | ReportCrash crash = new ReportCrash(); 170 | 171 | fillEnvInfo(crash); 172 | 173 | crash.timestamp = new Date().getTime(); 174 | 175 | crash.location = location; 176 | 177 | Set blacklist = Collections.newSetFromMap(new IdentityHashMap()); 178 | crash.exception = createStackTrace(throwable, new StackTraceElement[0], blacklist, collector); 179 | 180 | if (collector != null) crash.states = collector.collectStates(); 181 | 182 | crash.random = RANDOM.nextInt(); 183 | 184 | crash.resolved = collector != null; 185 | 186 | return crash; 187 | } 188 | 189 | public static void fillFileContents(File container, ReportFileContents report) { 190 | try { 191 | ZipFile jarFile = new ZipFile(container); 192 | 193 | List dirs = Lists.newArrayList(); 194 | List files = Lists.newArrayList(); 195 | 196 | try { 197 | Enumeration entries = jarFile.entries(); 198 | 199 | while (entries.hasMoreElements()) { 200 | ZipEntry zipEntry = entries.nextElement(); 201 | 202 | if (zipEntry.isDirectory()) { 203 | ArchiveDirEntry resultEntry = new ArchiveDirEntry(); 204 | fillCommonFields(zipEntry, resultEntry); 205 | dirs.add(resultEntry); 206 | } else { 207 | ArchiveFileEntry resultEntry = new ArchiveFileEntry(); 208 | fillCommonFields(zipEntry, resultEntry); 209 | resultEntry.size = zipEntry.getSize(); 210 | resultEntry.crc = Long.toHexString(zipEntry.getCrc()); 211 | resultEntry.signature = createSignature(jarFile.getInputStream(zipEntry)); 212 | files.add(resultEntry); 213 | } 214 | } 215 | } finally { 216 | jarFile.close(); 217 | } 218 | 219 | report.dirs = dirs; 220 | report.files = files; 221 | } catch (Exception e) { 222 | Log.warn(e, "Failed to get contents of file %s", container); 223 | } 224 | } 225 | 226 | private static void fillCommonFields(ZipEntry zipEntry, ArchiveEntry resultEntry) { 227 | resultEntry.filename = zipEntry.getName(); 228 | resultEntry.timestamp = zipEntry.getTime(); 229 | resultEntry.comment = zipEntry.getComment(); 230 | } 231 | 232 | private static String createSignature(InputStream is) throws Exception { 233 | MessageDigest md = MessageDigest.getInstance("SHA-256"); 234 | DigestInputStream dis = new DigestInputStream(is, md); 235 | 236 | byte[] buffer = new byte[1024]; 237 | 238 | while (dis.read(buffer) != -1) {} 239 | 240 | dis.close(); 241 | 242 | byte[] digest = md.digest(); 243 | return "sha256:" + bytesToHex(digest); 244 | } 245 | 246 | private static String bytesToHex(byte[] bytes) { 247 | StringBuilder sb = new StringBuilder(2 * bytes.length); 248 | for (byte b : bytes) 249 | sb.append(HEX[(b >> 4) & 0xf]).append(HEX[b & 0xf]); 250 | return sb.toString(); 251 | } 252 | 253 | private static final char[] HEX = "0123456789abcdef".toCharArray(); 254 | } 255 | -------------------------------------------------------------------------------- /src/main/java/openeye/logic/ReportContext.java: -------------------------------------------------------------------------------- 1 | package openeye.logic; 2 | 3 | import com.google.common.collect.Sets; 4 | import java.io.File; 5 | import java.util.Set; 6 | import openeye.protocol.reports.IReport; 7 | import openeye.struct.TypedCollections.ReportsList; 8 | 9 | class ReportContext implements IContext { 10 | private final ModMetaCollector collector; 11 | private final ReportsList result = new ReportsList(); 12 | private final Set addedFileInfos = Sets.newHashSet(); 13 | private final Set addedFileContents = Sets.newHashSet(); 14 | private final Set unwantedSignatures = Sets.newHashSet(); 15 | 16 | public ReportContext(ModMetaCollector collector) { 17 | this.collector = collector; 18 | } 19 | 20 | @Override 21 | public Set getModsForSignature(String signature) { 22 | return collector.getModsForSignature(signature); 23 | } 24 | 25 | @Override 26 | public File getFileForSignature(String signature) { 27 | return collector.getContainerForSignature(signature); 28 | } 29 | 30 | @Override 31 | public void queueReport(IReport report) { 32 | result.add(report); 33 | } 34 | 35 | @Override 36 | public void queueFileReport(String signature) { 37 | if (!signature.startsWith("special:")) { 38 | if (!addedFileInfos.contains(signature)) { 39 | result.add(collector.generateFileReport(signature)); 40 | addedFileInfos.add(signature); 41 | } 42 | } 43 | } 44 | 45 | @Override 46 | public void queueFileContents(String signature) { 47 | // special files allowed - it may be useful to get minecraft.jar contents 48 | if (!addedFileContents.contains(signature)) { 49 | result.add(collector.generateFileContentsReport(signature)); 50 | addedFileContents.add(signature); 51 | } 52 | } 53 | 54 | @Override 55 | public void markUnwantedSignature(String signature) { 56 | unwantedSignatures.add(signature); 57 | } 58 | 59 | public Set dangerousSignatures() { 60 | return unwantedSignatures; 61 | } 62 | 63 | public ReportsList reports() { 64 | return result; 65 | } 66 | } -------------------------------------------------------------------------------- /src/main/java/openeye/logic/Sanitizer.java: -------------------------------------------------------------------------------- 1 | package openeye.logic; 2 | 3 | import com.google.common.base.Strings; 4 | import com.google.common.collect.Multimap; 5 | import com.google.common.collect.Multimaps; 6 | import java.util.ArrayList; 7 | import java.util.Collection; 8 | import java.util.Comparator; 9 | import java.util.Map; 10 | import java.util.TreeMap; 11 | import openeye.Log; 12 | 13 | public class Sanitizer { 14 | 15 | public interface ITransformer { 16 | public String transform(String input); 17 | } 18 | 19 | public Sanitizer() { 20 | this(null); 21 | } 22 | 23 | public Sanitizer(Sanitizer parent) { 24 | this.parent = parent; 25 | } 26 | 27 | private final Sanitizer parent; 28 | 29 | private static final Comparator REVERSED = new Comparator() { 30 | @Override 31 | public int compare(Integer o1, Integer o2) { 32 | return o2 - o1; 33 | } 34 | }; 35 | 36 | private static Multimap createPriorityList() { 37 | return Multimaps.newMultimap(new TreeMap>(REVERSED), ArrayList::new); 38 | } 39 | 40 | private final Multimap pre = createPriorityList(); 41 | 42 | private final Multimap post = createPriorityList(); 43 | 44 | public void addPre(int priority, ITransformer transformer) { 45 | if (transformer != null) pre.put(priority, transformer); 46 | } 47 | 48 | public void addPost(int priority, ITransformer transformer) { 49 | if (transformer != null) post.put(priority, transformer); 50 | } 51 | 52 | public String sanitize(String input) { 53 | if (Strings.isNullOrEmpty(input)) return ""; 54 | 55 | for (Map.Entry> transformers : pre.asMap().entrySet()) { 56 | for (ITransformer transformer : transformers.getValue()) { 57 | if (Config.debugSanitizer) Log.info("%d %s", transformers.getKey(), transformer); 58 | input = transformer.transform(input); 59 | } 60 | } 61 | 62 | if (parent != null) input = parent.sanitize(input); 63 | 64 | for (Map.Entry> transformers : post.asMap().entrySet()) { 65 | for (ITransformer transformer : transformers.getValue()) { 66 | if (Config.debugSanitizer) Log.info("%d %s", transformers.getKey(), transformer); 67 | input = transformer.transform(input); 68 | } 69 | } 70 | 71 | return input; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/openeye/logic/Sanitizers.java: -------------------------------------------------------------------------------- 1 | package openeye.logic; 2 | 3 | import com.google.common.base.Strings; 4 | import com.google.common.collect.Maps; 5 | import com.google.common.collect.Sets; 6 | import java.io.File; 7 | import java.net.InetAddress; 8 | import java.net.NetworkInterface; 9 | import java.net.ProtocolException; 10 | import java.net.UnknownHostException; 11 | import java.util.Enumeration; 12 | import java.util.Map; 13 | import java.util.Set; 14 | import java.util.regex.Matcher; 15 | import java.util.regex.Pattern; 16 | import net.minecraft.world.World; 17 | import net.minecraft.world.storage.ISaveHandler; 18 | import openeye.Log; 19 | import openeye.logic.Sanitizer.ITransformer; 20 | 21 | public class Sanitizers { 22 | public static final int PRIORITY_SAVE_DIR = 1300; 23 | public static final int PRIORITY_MINECRAFT_DIR = 1200; 24 | public static final int PRIORITY_WORK_DIR = 1100; 25 | public static final int PRIORITY_HOME = 1000; 26 | 27 | public static final int PRIORITY_SAVE_DIR_NAME = 900; 28 | public static final int PRIORITY_WORLD_NAME = 800; 29 | 30 | public static final int PRIORITY_LOCAL_IP = 700; 31 | public static final int PRIORITY_IP_PORT = 600; 32 | 33 | public static final int PRIORITY_LOCAL_HOST = 500; 34 | public static final int PRIORITY_LOCAL_PLAYER = 400; 35 | public static final int PRIORITY_PLAYER_NAME = 300; 36 | public static final int PRIORITY_PLAYER_ID = 200; 37 | public static final int PRIORITY_SYSTEM_USER = 100; 38 | 39 | static class SimpleReplace implements ITransformer { 40 | private final String target; 41 | private final String value; 42 | 43 | public SimpleReplace(String target, String value) { 44 | this.target = target; 45 | this.value = value; 46 | } 47 | 48 | @Override 49 | public String transform(String input) { 50 | return input.replace(target, value); 51 | } 52 | 53 | @Override 54 | public String toString() { 55 | return String.format("'%s'->'%s'", target, value); 56 | } 57 | } 58 | 59 | private static class PropertyReplace extends Sanitizers.SimpleReplace { 60 | public PropertyReplace(String property, String value) { 61 | super(System.getProperty(property), value); 62 | } 63 | } 64 | 65 | private static class PathReplace implements ITransformer { 66 | private final String targetNormal; 67 | private final String targetReversed; 68 | private final String targetDoubled; 69 | private final String value; 70 | 71 | public PathReplace(String target, String value) { 72 | this.targetNormal = target; 73 | if (File.separatorChar == '\\') { 74 | targetReversed = targetNormal.replace('\\', '/'); 75 | targetDoubled = targetNormal.replace("\\", "\\\\"); 76 | } else if (File.separatorChar == '/') { 77 | targetReversed = targetNormal.replace('/', '\\'); 78 | targetDoubled = targetNormal.replace("/", "\\\\"); 79 | } else { 80 | targetReversed = null; 81 | targetDoubled = null; 82 | } 83 | 84 | this.value = value; 85 | } 86 | 87 | @Override 88 | public String transform(String input) { 89 | input = input.replace(targetNormal, value); 90 | if (targetReversed != null) input = input.replace(targetReversed, value); 91 | if (targetDoubled != null) input = input.replace(targetDoubled, value); 92 | return input; 93 | } 94 | 95 | @Override 96 | public String toString() { 97 | return String.format("path '%s'->'%s'", targetNormal, value); 98 | } 99 | } 100 | 101 | private static class SimpleRegexReplace implements ITransformer { 102 | private final Pattern pattern; 103 | private final String replacement; 104 | 105 | public SimpleRegexReplace(String pattern, String replacement) { 106 | this.pattern = Pattern.compile(pattern); 107 | this.replacement = replacement; 108 | } 109 | 110 | @Override 111 | public String transform(String input) { 112 | Matcher match = pattern.matcher(input); 113 | return match.replaceAll(replacement); 114 | } 115 | 116 | @Override 117 | public String toString() { 118 | return "regex: " + replacement; 119 | } 120 | } 121 | 122 | private static final Set ALREADY_REPLACED = Sets.newHashSet(); 123 | 124 | private static final Set DONT_REPLACE = Sets.newHashSet(); 125 | 126 | static { 127 | DONT_REPLACE.add("player"); 128 | DONT_REPLACE.add("MpServer"); 129 | DONT_REPLACE.add("none"); 130 | DONT_REPLACE.add("null"); 131 | } 132 | 133 | private static final Map, Sanitizer> THROWABLE_SANITIZERS = Maps.newHashMap(); 134 | 135 | public static final Sanitizer mainSanitizer = new Sanitizer(); 136 | 137 | public static void addThrowableSanitizer(Class cls, Sanitizer sanitizer) { 138 | THROWABLE_SANITIZERS.put(cls, sanitizer); 139 | } 140 | 141 | public static Sanitizer getSanitizerForThrowable(Class cls) { 142 | Sanitizer result = THROWABLE_SANITIZERS.get(cls); 143 | return result != null? result : mainSanitizer; 144 | } 145 | 146 | public static ITransformer path(String target, String value) { 147 | if (!Strings.isNullOrEmpty(target)) return new PathReplace(target, value); 148 | return null; 149 | } 150 | 151 | public static ITransformer pathNoDuplicate(String target, String value) { 152 | if (!Strings.isNullOrEmpty(target) && !ALREADY_REPLACED.contains(target)) { 153 | ALREADY_REPLACED.add(target); 154 | return new PathReplace(target, value); 155 | } 156 | return null; 157 | } 158 | 159 | public static ITransformer replace(Object target, String value) { 160 | if (target != null) { 161 | String s = target.toString(); 162 | if (s != null && s.length() > 2 && !DONT_REPLACE.contains(s)) return new Sanitizers.SimpleReplace(s, value); 163 | } 164 | return null; 165 | } 166 | 167 | public static ITransformer replaceNoDuplicates(Object target, String value) { 168 | if (target != null) { 169 | String s = target.toString(); 170 | if (s != null && s.length() > 2 && !ALREADY_REPLACED.contains(s) && !DONT_REPLACE.contains(s)) { 171 | ALREADY_REPLACED.add(s); 172 | return new Sanitizers.SimpleReplace(s, value); 173 | } 174 | } 175 | return null; 176 | } 177 | 178 | private static void addLocalAddresses() { 179 | Set ips = Sets.newHashSet(); 180 | Set hosts = Sets.newHashSet(); 181 | try { 182 | Enumeration interfaces = NetworkInterface.getNetworkInterfaces(); 183 | while (interfaces.hasMoreElements()) { 184 | NetworkInterface intf = interfaces.nextElement(); 185 | Enumeration addresses = intf.getInetAddresses(); 186 | while (addresses.hasMoreElements()) { 187 | InetAddress address = addresses.nextElement(); 188 | if (address != null) { 189 | ips.add(address.getHostAddress()); 190 | 191 | String host = address.getHostName(); 192 | if (!ips.contains(host)) hosts.add(host); 193 | } 194 | } 195 | } 196 | } catch (Throwable t) { 197 | Log.warn(t, "Failed to get local IP adresses for sanitization"); 198 | } 199 | 200 | for (String ip : ips) 201 | mainSanitizer.addPre(PRIORITY_LOCAL_IP, replace(ip, "[local ip]")); 202 | 203 | for (String host : hosts) 204 | mainSanitizer.addPre(PRIORITY_LOCAL_HOST, replace(host, "[host]")); 205 | 206 | mainSanitizer.addPre(PRIORITY_IP_PORT, new SimpleRegexReplace("\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}[:~]\\d+", "[ip+port]")); 207 | } 208 | 209 | public static void addWorldNames(World world) { 210 | final ISaveHandler saveHandler = world.getSaveHandler(); 211 | 212 | final File worldDirectory = saveHandler.getWorldDirectory(); 213 | if (worldDirectory != null) { 214 | final String worldDir = worldDirectory.getName(); 215 | mainSanitizer.addPre(PRIORITY_SAVE_DIR_NAME, replaceNoDuplicates(worldDir, "[save dir]")); 216 | } 217 | 218 | try { 219 | File dummy = saveHandler.getMapFileFromName("dummy"); 220 | if (dummy != null) { 221 | String parent = dummy.getParentFile().getParent(); 222 | if (parent != null) mainSanitizer.addPre(PRIORITY_SAVE_DIR, pathNoDuplicate(parent, "[save dir]")); 223 | } 224 | } catch (Throwable t) { 225 | Log.warn(t, "Failed to get sanitizer name for world"); 226 | } 227 | 228 | String worldName = world.getWorldInfo().getWorldName(); 229 | mainSanitizer.addPre(PRIORITY_WORLD_NAME, replaceNoDuplicates(worldName, "[world name]")); 230 | } 231 | 232 | static { 233 | addLocalAddresses(); 234 | 235 | mainSanitizer.addPre(PRIORITY_WORK_DIR, new PathReplace(System.getProperty("user.dir"), "[workdir]")); 236 | mainSanitizer.addPre(PRIORITY_HOME, new PathReplace(System.getProperty("user.home"), "[home]")); 237 | mainSanitizer.addPre(PRIORITY_SYSTEM_USER, new PropertyReplace("user.name", "[user]")); 238 | 239 | Sanitizer ipSanitizer = new Sanitizer(mainSanitizer); 240 | ipSanitizer.addPre(1000, new SimpleRegexReplace("\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}", "[ip]")); 241 | 242 | addThrowableSanitizer(ProtocolException.class, ipSanitizer); 243 | addThrowableSanitizer(UnknownHostException.class, ipSanitizer); 244 | } 245 | 246 | public static void addMinecraftPath(File mcLocation) { 247 | mainSanitizer.addPre(PRIORITY_MINECRAFT_DIR, path(mcLocation.getAbsolutePath(), "[minecraft_dir]")); 248 | } 249 | 250 | public static void addPlayerName(String username) { 251 | mainSanitizer.addPre(PRIORITY_LOCAL_PLAYER, Sanitizers.replace(username, "[local player]")); 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /src/main/java/openeye/logic/SenderWorker.java: -------------------------------------------------------------------------------- 1 | package openeye.logic; 2 | 3 | import com.google.common.base.Preconditions; 4 | import com.google.common.collect.ImmutableList; 5 | import com.google.common.collect.ImmutableSortedMap; 6 | import com.google.common.collect.Lists; 7 | import com.google.common.collect.Maps; 8 | import com.google.common.collect.Sets; 9 | import java.lang.Thread.UncaughtExceptionHandler; 10 | import java.util.Collection; 11 | import java.util.Iterator; 12 | import java.util.List; 13 | import java.util.Map; 14 | import java.util.Set; 15 | import java.util.SortedMap; 16 | import java.util.concurrent.CountDownLatch; 17 | import java.util.concurrent.Future; 18 | import java.util.concurrent.TimeUnit; 19 | import openeye.Log; 20 | import openeye.net.ReportSender; 21 | import openeye.notes.NoteCollector; 22 | import openeye.protocol.FileSignature; 23 | import openeye.protocol.ITypedStruct; 24 | import openeye.protocol.reports.ReportCrash; 25 | import openeye.protocol.reports.ReportPing; 26 | import openeye.responses.IExecutableResponse; 27 | import openeye.storage.IDataSource; 28 | import openeye.storage.IQueryableStorage; 29 | import openeye.struct.TypedCollections.ReportsList; 30 | import openeye.struct.TypedCollections.ResponseList; 31 | 32 | public class SenderWorker implements Runnable { 33 | 34 | private static final String API_HOST = "openeye.openmods.info"; 35 | 36 | private static final String API_PATH = "/api/v1/data"; 37 | // private static final String API_PATH = "/dummy"; 38 | 39 | private final Future collector; 40 | 41 | private final ModState state; 42 | 43 | private final CountDownLatch firstMessageReceived = new CountDownLatch(1); 44 | 45 | private final Set dangerousSignatures = Sets.newHashSet(); 46 | 47 | public SenderWorker(Future collector, ModState state) { 48 | this.collector = collector; 49 | this.state = state; 50 | } 51 | 52 | private static void logException(Throwable throwable, String msg, Object... args) { 53 | ThrowableLogger.processThrowable(throwable, "openeye"); 54 | Log.warn(throwable, msg, args); 55 | } 56 | 57 | private static void store(Object report, String name) { 58 | try { 59 | IDataSource list = Storages.instance().sessionArchive.createNew(name); 60 | list.store(report); 61 | } catch (Exception e) { 62 | Log.warn(e, "Failed to store " + name); 63 | } 64 | } 65 | 66 | private static void filterStructs(Collection structs, Set blacklist) { 67 | Iterator it = structs.iterator(); 68 | 69 | while (it.hasNext()) { 70 | final ITypedStruct struct = it.next(); 71 | final String type = struct.getType(); 72 | if (blacklist.contains(type)) { 73 | Log.debug("Filtered type %s(%s) from list, since it's on blacklist", type, struct); 74 | it.remove(); 75 | } 76 | } 77 | } 78 | 79 | private ReportsList executeResponses(ModMetaCollector collector, ResponseList requests) { 80 | Preconditions.checkState(!requests.isEmpty()); 81 | 82 | final ReportContext context = new ReportContext(collector); 83 | 84 | for (IExecutableResponse request : requests) 85 | request.execute(context); 86 | 87 | dangerousSignatures.addAll(context.dangerousSignatures()); 88 | return context.reports(); 89 | } 90 | 91 | private static SortedMap retrieveAllSources(IQueryableStorage storage) { 92 | final ImmutableSortedMap.Builder result = ImmutableSortedMap.naturalOrder(); 93 | 94 | for (IDataSource source : storage.listAll()) { 95 | try { 96 | result.put(source.getId(), source.retrieve()); 97 | } catch (Throwable t) { 98 | Log.warn(t, "Failed to read entry %s, removing", source.getId()); 99 | source.delete(); 100 | } 101 | } 102 | return result.build(); 103 | } 104 | 105 | private static void removeSources(IQueryableStorage storage, Set ids) { 106 | for (String id : ids) { 107 | final IDataSource entry = storage.getById(id); 108 | if (entry != null) entry.delete(); 109 | } 110 | } 111 | 112 | private static Collection removePendingCrashDuplicates(Map crashes) { 113 | final Map result = Maps.newHashMap(); 114 | 115 | for (Map.Entry e : crashes.entrySet()) { 116 | ReportCrash crash = e.getValue(); 117 | if (crash != null) { 118 | ReportCrash prev = result.put(new CrashId(crash.timestamp, crash.random), crash); 119 | if (prev != null) Log.warn("Found duplicated crash report %s", e.getKey()); 120 | } 121 | } 122 | return ImmutableList.copyOf(crashes.values()); 123 | } 124 | 125 | private static SortedMap selectCrashes(Map pendingCrashes) { 126 | if (!Config.sendCrashes) return ImmutableSortedMap.of(); 127 | 128 | if (Config.sentCrashReportsLimitTotal >= 0 && Config.sentCrashReportsLimitTotal < pendingCrashes.size()) { 129 | final ImmutableSortedMap.Builder result = ImmutableSortedMap.naturalOrder(); 130 | int count = Config.sentCrashReportsLimitTotal; 131 | for (Map.Entry e : pendingCrashes.entrySet()) { 132 | if (--count < 0) break; 133 | result.put(e); 134 | } 135 | return result.build(); 136 | } 137 | 138 | return ImmutableSortedMap.copyOf(pendingCrashes); 139 | } 140 | 141 | protected ReportsList createInitialReport(ModMetaCollector collector, Collection crashes) { 142 | final ReportsList result = new ReportsList(); 143 | 144 | try { 145 | if (Config.sendModList) createAnalyticsReport(collector, result); 146 | if (Config.pingOnInitialReport) result.add(new ReportPing()); 147 | result.addAll(crashes); 148 | } catch (Exception e) { 149 | logException(e, "Failed to create initial report"); 150 | } 151 | 152 | return result; 153 | } 154 | 155 | protected void createAnalyticsReport(ModMetaCollector collector, final ReportsList result) { 156 | try { 157 | if (Config.scanOnly) { 158 | result.add(ReportBuilders.buildKnownFilesReport(collector)); 159 | } else { 160 | result.add(ReportBuilders.buildAnalyticsReport(collector, state.installedMods)); 161 | } 162 | } catch (Exception e) { 163 | logException(e, "Failed to create analytics report"); 164 | } 165 | } 166 | 167 | private void sendReports(ModMetaCollector collector) { 168 | final SortedMap pendingCrashes = retrieveAllSources(Storages.instance().pendingCrashes); 169 | final SortedMap selectedPendingCrashes = selectCrashes(pendingCrashes); 170 | final Collection pendingUniqueCrashes = removePendingCrashDuplicates(selectedPendingCrashes); 171 | 172 | ReportsList currentReports = createInitialReport(collector, pendingUniqueCrashes); 173 | 174 | try { 175 | ReportSender sender = new ReportSender(API_HOST, API_PATH); 176 | 177 | while (!currentReports.isEmpty()) { 178 | filterStructs(currentReports, Config.reportsBlacklist); 179 | store(currentReports, "request"); 180 | 181 | ResponseList response = Config.dontSend? null : sender.sendAndReceive(currentReports); 182 | if (response == null || response.isEmpty()) break; 183 | 184 | filterStructs(response, Config.responseBlacklist); 185 | store(response, "response"); 186 | 187 | currentReports.clear(); 188 | try { 189 | currentReports = executeResponses(collector, response); 190 | } catch (Exception e) { 191 | logException(e, "Failed to execute responses"); 192 | break; 193 | } 194 | 195 | firstMessageReceived.countDown(); // early release - notes send in next packets are ignored 196 | } 197 | 198 | removeSources(Storages.instance().pendingCrashes, selectedPendingCrashes.keySet()); 199 | NoteCollector.INSTANCE.addNote(sender.getEncryptionState()); 200 | } catch (Exception e) { 201 | Log.warn(e, "Failed to send report to " + API_HOST + API_PATH); 202 | } 203 | 204 | } 205 | 206 | @Override 207 | public void run() { 208 | try { 209 | final ModMetaCollector collector = this.collector.get(); 210 | sendReports(collector); 211 | // only update state after mods were successfully sent 212 | StateHolder.state().installedMods = collector.getAllSignatures(); 213 | StateHolder.save(); 214 | } catch (Throwable t) { 215 | logException(t, "Failed to send data to server OpenEye"); 216 | } finally { 217 | firstMessageReceived.countDown(); // can't do much more, releasing lock 218 | } 219 | } 220 | 221 | public void start() { 222 | Thread senderThread = new Thread(this); 223 | senderThread.setName("OpenEye sender thread"); 224 | 225 | senderThread.setUncaughtExceptionHandler(new UncaughtExceptionHandler() { 226 | @Override 227 | public void uncaughtException(Thread t, Throwable e) { 228 | logException(e, "Uncaught exception in data collector thread, report will not be send"); 229 | firstMessageReceived.countDown(); // oh well, better luck next time 230 | } 231 | }); 232 | 233 | senderThread.start(); 234 | } 235 | 236 | public void waitForFirstMsg() { 237 | try { 238 | if (!firstMessageReceived.await(30, TimeUnit.SECONDS)) Log.warn("OpenEye timeouted while waiting for worker thread, data will be incomplete"); 239 | } catch (InterruptedException e) { 240 | Log.warn("Thread interrupted while waiting for msg"); 241 | } 242 | } 243 | 244 | public Collection listDangerousFiles() { 245 | List result = Lists.newArrayList(); 246 | 247 | try { 248 | ModMetaCollector collector = this.collector.get(); 249 | 250 | for (String signature : dangerousSignatures) { 251 | FileSignature file = collector.getFileForSignature(signature); 252 | if (signature != null) result.add(file); 253 | } 254 | } catch (Throwable t) { 255 | Log.warn(t, "Failed to list dangerous files"); 256 | } 257 | 258 | return result; 259 | } 260 | 261 | } 262 | -------------------------------------------------------------------------------- /src/main/java/openeye/logic/StateHolder.java: -------------------------------------------------------------------------------- 1 | package openeye.logic; 2 | 3 | import com.google.common.base.Preconditions; 4 | import openeye.Log; 5 | import openeye.storage.IDataSource; 6 | 7 | public class StateHolder { 8 | private static ModState state = new ModState(); 9 | 10 | private static Runnable saveCallback; 11 | 12 | private static void storeState(ModState state, Storages storages) { 13 | IDataSource stateStorage = storages.state.getById(Storages.STATE_FILE_ID); 14 | stateStorage.store(state); 15 | } 16 | 17 | public static void init(final Storages storages) { 18 | Preconditions.checkState(saveCallback == null, "Double initialization of state storage"); 19 | try { 20 | IDataSource stateStorage = storages.state.getById(Storages.STATE_FILE_ID); 21 | ModState storedState = stateStorage.retrieve(); 22 | if (storedState != null) state = storedState; 23 | } catch (Throwable t) { 24 | Log.warn(t, "Failed to get mod state, reinitializing"); 25 | } 26 | 27 | saveCallback = new Runnable() { 28 | @Override 29 | public void run() { 30 | try { 31 | storeState(state, storages); 32 | } catch (Throwable t) { 33 | System.err.println("[OpenEye] Failed to store state"); 34 | t.printStackTrace(); 35 | } 36 | } 37 | }; 38 | 39 | Runtime.getRuntime().addShutdownHook(new Thread(saveCallback)); 40 | } 41 | 42 | public static void save() { 43 | Preconditions.checkState(saveCallback != null, "State holder not initialized"); 44 | saveCallback.run(); 45 | } 46 | 47 | public static ModState state() { 48 | return state; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/openeye/logic/Storages.java: -------------------------------------------------------------------------------- 1 | package openeye.logic; 2 | 3 | import com.google.common.base.Preconditions; 4 | import java.io.File; 5 | import openeye.protocol.reports.ReportCrash; 6 | import openeye.storage.GsonPredefinedStorage; 7 | import openeye.storage.GsonSessionStorage; 8 | import openeye.storage.GsonWorkingStorage; 9 | import openeye.storage.IAppendableStorage; 10 | import openeye.storage.IQueryableStorage; 11 | import openeye.storage.IWorkingStorage; 12 | 13 | public class Storages { 14 | public static final String STATE_FILE_ID = "state"; 15 | 16 | public final IQueryableStorage state; 17 | public final IWorkingStorage pendingCrashes; 18 | public final IAppendableStorage sessionArchive; 19 | 20 | public final File reportsDir; 21 | 22 | private static Storages instance; 23 | 24 | private Storages(File mcDir) { 25 | reportsDir = new File(mcDir, "reports"); 26 | reportsDir.mkdir(); 27 | 28 | state = new GsonPredefinedStorage<>(reportsDir, ModState.class, GsonUtils.PRETTY_GSON, STATE_FILE_ID); 29 | pendingCrashes = new GsonWorkingStorage<>(reportsDir, "pending-crash", ReportCrash.class, GsonUtils.PRETTY_GSON); 30 | sessionArchive = new GsonSessionStorage<>(reportsDir, "json", Object.class, GsonUtils.PRETTY_GSON); 31 | } 32 | 33 | public static Storages init(File mcDir) { 34 | if (instance == null) instance = new Storages(mcDir); 35 | return instance; 36 | } 37 | 38 | public static Storages instance() { 39 | Preconditions.checkNotNull(instance, "Storage not initialized"); 40 | return instance; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/openeye/logic/TagsCollector.java: -------------------------------------------------------------------------------- 1 | package openeye.logic; 2 | 3 | import com.google.common.base.Splitter; 4 | import com.google.common.collect.Iterables; 5 | import com.google.common.collect.Sets; 6 | import java.util.Map; 7 | import java.util.Set; 8 | import net.minecraft.launchwrapper.Launch; 9 | 10 | public class TagsCollector { 11 | 12 | private static final String ELEMENT_NAME = "openeye.tags"; 13 | private static final Splitter TAG_SPLITTER = Splitter.on(','); 14 | 15 | private Set tags; 16 | 17 | public Set getTags() { 18 | if (tags == null) { 19 | tags = Sets.newHashSet(); 20 | addBlackboardTags(tags); 21 | addEnvTags(tags); 22 | addArgsTags(tags); 23 | addConfigTags(tags); 24 | } 25 | return tags; 26 | } 27 | 28 | private static void addConfigTags(Set result) { 29 | if (Config.tags != null) result.addAll(Config.tags); 30 | } 31 | 32 | private static void addArgsTags(Set result) { 33 | @SuppressWarnings("unchecked") 34 | Map args = (Map)Launch.blackboard.get("launchArgs"); 35 | String tags = args.get(ELEMENT_NAME); 36 | if (tags != null) Iterables.addAll(result, TAG_SPLITTER.split(tags)); 37 | } 38 | 39 | private static void addEnvTags(Set result) { 40 | Map env = System.getenv(); 41 | if (env.containsKey(ELEMENT_NAME)) { 42 | String tags = env.get(ELEMENT_NAME); 43 | Iterables.addAll(result, TAG_SPLITTER.split(tags)); 44 | } 45 | } 46 | 47 | @SuppressWarnings("unchecked") 48 | private static void addBlackboardTags(Set result) { 49 | Object tags = Launch.blackboard.get(ELEMENT_NAME); 50 | if (tags instanceof Set) result.addAll((Set)tags); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/openeye/logic/ThrowableLogger.java: -------------------------------------------------------------------------------- 1 | package openeye.logic; 2 | 3 | import com.google.common.collect.HashMultiset; 4 | import com.google.common.collect.Multiset; 5 | import com.google.common.collect.Queues; 6 | import java.lang.Thread.UncaughtExceptionHandler; 7 | import java.util.Queue; 8 | import java.util.concurrent.Future; 9 | import java.util.concurrent.TimeUnit; 10 | import openeye.Log; 11 | import openeye.protocol.reports.ReportCrash; 12 | import openeye.storage.IDataSource; 13 | 14 | public class ThrowableLogger { 15 | 16 | private static class ThrowableEntry { 17 | public final String location; 18 | public final Throwable throwable; 19 | 20 | public ThrowableEntry(Throwable throwable, String location) { 21 | this.location = location; 22 | this.throwable = throwable; 23 | } 24 | } 25 | 26 | private static Future resolver; 27 | 28 | private static final Queue delayedThrowables = Queues.newConcurrentLinkedQueue(); 29 | 30 | private static final Multiset locationCounters = HashMultiset.create(); 31 | 32 | private static void tryStoreCrash(Throwable throwable, String location) { 33 | ModMetaCollector collector = null; 34 | try { 35 | collector = resolver != null? resolver.get(10, TimeUnit.SECONDS) : null; 36 | } catch (Throwable t) { 37 | Log.warn(t, "Failed to get resolver"); 38 | } 39 | 40 | try { 41 | ReportCrash crashReport = ReportBuilders.buildCrashReport(throwable, location, collector); 42 | 43 | Storages storages = Storages.instance(); 44 | IDataSource crashStorage = storages.pendingCrashes.createNew(); 45 | crashStorage.store(crashReport); 46 | } catch (Throwable t) { 47 | Log.warn(t, "Failed to store crash report"); 48 | } 49 | } 50 | 51 | private static void storeAllPending() { 52 | ThrowableEntry e; 53 | while ((e = delayedThrowables.poll()) != null) 54 | tryStoreCrash(e.throwable, e.location); 55 | } 56 | 57 | public static void init() { 58 | Thread crashDumperThread = new Thread() { 59 | @Override 60 | public void run() { 61 | storeAllPending(); 62 | } 63 | }; 64 | 65 | crashDumperThread.setUncaughtExceptionHandler(new UncaughtExceptionHandler() { 66 | @Override 67 | public void uncaughtException(Thread t, Throwable e) { 68 | System.err.println("[OpenEye] Exception in shutdown handler, report may not be sent"); 69 | e.printStackTrace(); 70 | } 71 | }); 72 | 73 | Runtime.getRuntime().addShutdownHook(crashDumperThread); 74 | } 75 | 76 | public static void processThrowable(Throwable throwable, String location) { 77 | if (throwable instanceof INotStoredCrash) return; 78 | 79 | locationCounters.add(location); 80 | 81 | if (locationCounters.count(location) > Config.storeCrashReportsLimit) { 82 | Log.debug("Limit reached for location %s, skipping %s", location, throwable); 83 | return; 84 | } 85 | 86 | if (resolver != null) tryStoreCrash(throwable, location); 87 | else delayedThrowables.add(new ThrowableEntry(throwable, location)); 88 | } 89 | 90 | public static void enableResolving(Future collector) { 91 | resolver = collector; 92 | storeAllPending(); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/main/java/openeye/net/GenericSender.java: -------------------------------------------------------------------------------- 1 | package openeye.net; 2 | 3 | import com.google.common.base.Preconditions; 4 | import com.google.common.collect.ImmutableList; 5 | import com.google.common.collect.Ordering; 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | import java.io.OutputStream; 9 | import java.net.HttpURLConnection; 10 | import java.net.MalformedURLException; 11 | import java.net.ProtocolException; 12 | import java.net.SocketTimeoutException; 13 | import java.net.URL; 14 | import java.net.URLConnection; 15 | import java.security.GeneralSecurityException; 16 | import java.security.KeyStore; 17 | import java.security.cert.Certificate; 18 | import java.security.cert.CertificateFactory; 19 | import java.util.List; 20 | import java.util.zip.GZIPOutputStream; 21 | import javax.net.ssl.HttpsURLConnection; 22 | import javax.net.ssl.SSLContext; 23 | import javax.net.ssl.SSLHandshakeException; 24 | import javax.net.ssl.SSLSocketFactory; 25 | import javax.net.ssl.TrustManagerFactory; 26 | import openeye.Log; 27 | import org.apache.commons.lang3.tuple.Pair; 28 | 29 | public abstract class GenericSender { 30 | 31 | private final List bundledRoots = ImmutableList.of("isrg_root_x1.pem", "identrust_root_x3.pem"); 32 | 33 | private SSLSocketFactory createSocketFactoryWithRoots(List roots) throws GeneralSecurityException, IOException { 34 | final String defaultKsAlgorithm = KeyStore.getDefaultType(); 35 | KeyStore keyStore = KeyStore.getInstance(defaultKsAlgorithm); 36 | 37 | // for 'full' keystore 38 | // Path ksPath = Paths.get(System.getProperty("java.home"), "lib", "security", "cacerts"); 39 | // keyStore.load(Files.newInputStream(ksPath), "changeit".toCharArray()); 40 | 41 | // for single cert keystore 42 | keyStore.load(null); 43 | 44 | CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); 45 | 46 | for (String root : roots) { 47 | InputStream data = getClass().getClassLoader().getResourceAsStream(root); 48 | Preconditions.checkNotNull(data, "Failed to found resource %s", root); 49 | Certificate cert = certificateFactory.generateCertificate(data); 50 | keyStore.setCertificateEntry(root, cert); 51 | } 52 | 53 | final String defaultTmAlgorithm = TrustManagerFactory.getDefaultAlgorithm(); 54 | TrustManagerFactory tmf = TrustManagerFactory.getInstance(defaultTmAlgorithm); 55 | tmf.init(keyStore); 56 | 57 | SSLContext sslContext = SSLContext.getInstance("TLS"); 58 | sslContext.init(null, tmf.getTrustManagers(), null); 59 | 60 | return sslContext.getSocketFactory(); 61 | } 62 | 63 | @SuppressWarnings("serial") 64 | public static class HttpTransactionException extends RuntimeException { 65 | private HttpTransactionException(String format, Object... args) { 66 | super(String.format(format, args)); 67 | } 68 | 69 | private HttpTransactionException(Throwable cause) { 70 | super(cause); 71 | } 72 | } 73 | 74 | public enum EncryptionState { 75 | NOT_SUPPORTED, 76 | NO_ROOT_CERTIFICATE, 77 | OK, 78 | UNKNOWN; 79 | } 80 | 81 | private enum HttpStatus { 82 | OK, 83 | REDIRECT; 84 | } 85 | 86 | private String host; 87 | 88 | private String path; 89 | 90 | private int maxRetries = 2; 91 | 92 | private int maxRedirects = 5; 93 | 94 | private int timeout = 20000; 95 | 96 | private EncryptionState encryptionState = EncryptionState.UNKNOWN; 97 | 98 | public GenericSender(String host, String path) { 99 | this.host = host; 100 | this.path = path; 101 | } 102 | 103 | public void setMaxRetries(int maxRetries) { 104 | this.maxRetries = maxRetries; 105 | } 106 | 107 | public void setMaxRedirects(int maxRedirects) { 108 | this.maxRedirects = maxRedirects; 109 | } 110 | 111 | public void setTimeout(int timeout) { 112 | this.timeout = timeout; 113 | } 114 | 115 | public EncryptionState getEncryptionState() { 116 | return encryptionState; 117 | } 118 | 119 | public O sendAndReceive(I request) { 120 | int retry = 0; 121 | int redirect = 0; 122 | while (retry < maxRetries) { 123 | Log.debug("Trying to connect to %s%s, retry %s, redirect %s", host, path, retry, redirect); 124 | try { 125 | final HttpURLConnection connection; 126 | try { 127 | final Pair result = createConnection(); 128 | encryptionState = Ordering.natural().min(result.getRight(), encryptionState); 129 | connection = result.getLeft(); 130 | } catch (GeneralSecurityException t) { 131 | // giving up, something broken in encryption 132 | throw new HttpTransactionException(t); 133 | } 134 | 135 | trySendRequest(request, connection); 136 | final HttpStatus statusCode = checkStatusCode(connection); 137 | if (statusCode == HttpStatus.REDIRECT) { 138 | if (redirect++ >= maxRedirects) throw new HttpTransactionException("Too many redirects"); 139 | final String redirectPath = connection.getHeaderField("Location"); 140 | if (redirectPath == null) throw new HttpTransactionException("Invalid redirect"); 141 | try { 142 | final URL url = new URL(redirectPath); 143 | // ignoring protocol and port - there is no valid scenario when this is needed 144 | this.host = url.getHost(); 145 | this.path = url.getPath(); 146 | } catch (MalformedURLException e) { 147 | throw new HttpTransactionException("Invalid redirect: '%s'", redirectPath); 148 | } 149 | connection.disconnect(); 150 | retry = 0; 151 | continue; 152 | } else { 153 | return tryReceiveResponse(connection); 154 | } 155 | } catch (HttpTransactionException e) { 156 | throw e; 157 | } catch (SocketTimeoutException e) { 158 | Log.warn("Connection timed out (retry %d)", retry); 159 | } catch (Throwable t) { 160 | Log.warn(t, "Failed to send/receive report (retry %d)", retry); 161 | } 162 | retry++; 163 | } 164 | 165 | throw new HttpTransactionException("Too much retries"); 166 | } 167 | 168 | private Pair createConnection() throws IOException, GeneralSecurityException { 169 | // non-business versions of Java 6 can't handle our awesome certificates 170 | if (System.getProperty("java.specification.version").equals("1.6")) { 171 | final URL url = new URL("http", host, path); 172 | return createHttpConnection(url); 173 | } else { 174 | final URL url = new URL("https", host, path); 175 | return createHttpsConnection(url); 176 | } 177 | } 178 | 179 | private Pair createHttpConnection(final URL url) throws IOException, ProtocolException { 180 | HttpURLConnection connection = (HttpURLConnection)url.openConnection(); 181 | configureAndConnect(url, connection); 182 | return Pair.of(connection, EncryptionState.NOT_SUPPORTED); 183 | } 184 | 185 | private Pair createHttpsConnection(final URL url) throws IOException, ProtocolException, GeneralSecurityException { 186 | try { 187 | HttpURLConnection connection = (HttpURLConnection)url.openConnection(); 188 | configureAndConnect(url, connection); 189 | return Pair.of(connection, EncryptionState.OK); 190 | } catch (SSLHandshakeException e) { 191 | HttpsURLConnection connection = (HttpsURLConnection)url.openConnection(); 192 | final SSLSocketFactory sslSocketFactory = createSocketFactoryWithRoots(bundledRoots); 193 | connection.setSSLSocketFactory(sslSocketFactory); 194 | configureAndConnect(url, connection); 195 | return Pair.of((HttpURLConnection)connection, EncryptionState.NO_ROOT_CERTIFICATE); 196 | } 197 | } 198 | 199 | private void configureAndConnect(URL url, HttpURLConnection connection) throws ProtocolException, IOException { 200 | connection.setDoInput(true); 201 | connection.setDoOutput(true); 202 | connection.setRequestMethod("POST"); 203 | connection.setConnectTimeout(timeout); 204 | connection.setReadTimeout(timeout); 205 | connection.setRequestProperty("Accept", "application/json"); 206 | connection.setRequestProperty("Content-Encoding", "gzip"); 207 | connection.setRequestProperty("Content-Type", "application/json"); 208 | connection.setRequestProperty("User-Agent", "Die Fledermaus/11"); 209 | connection.setRequestProperty("Host", url.getAuthority()); 210 | // Doing manual redirects 211 | // Partially for logging and partially since parts of behaviour are controlled by global flags 212 | connection.setInstanceFollowRedirects(false); 213 | connection.connect(); 214 | } 215 | 216 | protected void trySendRequest(I request, URLConnection connection) throws IOException { 217 | OutputStream requestStream = connection.getOutputStream(); 218 | requestStream = new GZIPOutputStream(requestStream); 219 | 220 | try { 221 | encodeRequest(requestStream, request); 222 | requestStream.flush(); 223 | } finally { 224 | requestStream.close(); 225 | } 226 | } 227 | 228 | protected HttpStatus checkStatusCode(HttpURLConnection connection) throws IOException { 229 | int statusCode = connection.getResponseCode(); 230 | switch (statusCode) { 231 | case HttpURLConnection.HTTP_OK: 232 | case HttpURLConnection.HTTP_NO_CONTENT: 233 | return HttpStatus.OK; 234 | case 307: // Temporary Redirect 235 | case 308: // Permanent Redirect 236 | // 301 and 302 are invalid, since method cannot change 237 | return HttpStatus.REDIRECT; 238 | case HttpURLConnection.HTTP_NOT_FOUND: 239 | throw new HttpTransactionException("Endpoint not found"); 240 | case HttpURLConnection.HTTP_INTERNAL_ERROR: 241 | throw new HttpTransactionException("Internal server error"); 242 | default: 243 | throw new HttpTransactionException("HttpStatus %d != 200", statusCode); 244 | } 245 | } 246 | 247 | protected O tryReceiveResponse(HttpURLConnection connection) throws IOException { 248 | InputStream stream = connection.getInputStream(); 249 | 250 | try { 251 | return decodeResponse(stream); 252 | } finally { 253 | stream.close(); 254 | } 255 | } 256 | 257 | protected abstract void encodeRequest(OutputStream output, I request) throws IOException; 258 | 259 | protected abstract O decodeResponse(InputStream input) throws IOException; 260 | 261 | } 262 | -------------------------------------------------------------------------------- /src/main/java/openeye/net/ReportSender.java: -------------------------------------------------------------------------------- 1 | package openeye.net; 2 | 3 | import com.google.common.base.Charsets; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.io.InputStreamReader; 7 | import java.io.OutputStream; 8 | import java.io.OutputStreamWriter; 9 | import java.io.Reader; 10 | import java.io.Writer; 11 | import openeye.logic.GsonUtils; 12 | import openeye.struct.TypedCollections.ReportsList; 13 | import openeye.struct.TypedCollections.ResponseList; 14 | 15 | public class ReportSender extends GenericSender { 16 | 17 | public ReportSender(String host, String path) { 18 | super(host, path); 19 | } 20 | 21 | @Override 22 | protected void encodeRequest(OutputStream output, ReportsList request) throws IOException { 23 | Writer writer = new OutputStreamWriter(output, Charsets.UTF_8); 24 | GsonUtils.NET_GSON.toJson(request, writer); 25 | writer.close(); 26 | } 27 | 28 | @Override 29 | protected ResponseList decodeResponse(InputStream input) throws IOException { 30 | Reader reader = new InputStreamReader(input, Charsets.UTF_8); 31 | ResponseList result = GsonUtils.NET_GSON.fromJson(reader, ResponseList.class); 32 | reader.close(); 33 | return result; 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/openeye/notes/CommandNotes.java: -------------------------------------------------------------------------------- 1 | package openeye.notes; 2 | 3 | import com.google.common.base.Joiner; 4 | import com.google.common.collect.Lists; 5 | import com.google.common.collect.Maps; 6 | import java.io.File; 7 | import java.util.Collection; 8 | import java.util.List; 9 | import java.util.Map; 10 | import net.minecraft.command.CommandException; 11 | import net.minecraft.command.ICommand; 12 | import net.minecraft.command.ICommandSender; 13 | import net.minecraft.command.SyntaxErrorException; 14 | import net.minecraft.server.MinecraftServer; 15 | import net.minecraft.util.math.BlockPos; 16 | import openeye.logic.GsonUtils; 17 | import openeye.notes.entries.NoteEntry; 18 | import openeye.storage.GsonSimpleStorage; 19 | import openeye.storage.IAppendableStorage; 20 | 21 | public class CommandNotes implements ICommand { 22 | 23 | private static final String COMMAND_NAME = "eye_notes"; 24 | 25 | interface INoteSink { 26 | public void dump(Collection notes, ICommandSender sender) throws CommandException; 27 | } 28 | 29 | private final Map sinks = Maps.newHashMap(); 30 | 31 | public CommandNotes(File reportDir) { 32 | final IAppendableStorage notesDump = new GsonSimpleStorage<>(reportDir, "notes", "json", Object.class, GsonUtils.PRETTY_GSON); 33 | sinks.put("console", new ConsoleNoteSink()); 34 | sinks.put("json", new JsonNoteSink(notesDump)); 35 | } 36 | 37 | @Override 38 | public int compareTo(ICommand o) { 39 | return COMMAND_NAME.compareTo(o.getName()); 40 | } 41 | 42 | @Override 43 | public String getName() { 44 | return COMMAND_NAME; 45 | } 46 | 47 | @Override 48 | public String getUsage(ICommandSender sender) { 49 | StringBuilder builder = new StringBuilder(COMMAND_NAME).append(" <"); 50 | Joiner.on('|').appendTo(builder, sinks.keySet()); 51 | builder.append(">"); 52 | return builder.toString(); 53 | } 54 | 55 | @Override 56 | public List getAliases() { 57 | return Lists.newArrayList(); 58 | } 59 | 60 | @Override 61 | public void execute(MinecraftServer server, ICommandSender sender, String[] command) throws CommandException { 62 | if (command.length != 1) throw new SyntaxErrorException(); 63 | String sinkType = command[0]; 64 | INoteSink sink = sinks.get(sinkType); 65 | if (sink == null) throw new SyntaxErrorException(); 66 | 67 | sink.dump(NoteCollector.INSTANCE.getNotes(), sender); 68 | } 69 | 70 | @Override 71 | public boolean checkPermission(MinecraftServer server, ICommandSender sender) { 72 | return sender.canUseCommand(4, COMMAND_NAME); 73 | } 74 | 75 | @Override 76 | public List getTabCompletions(MinecraftServer server, ICommandSender sender, String[] command, BlockPos pos) { 77 | List result = Lists.newArrayList(); 78 | 79 | if (command.length == 1) { 80 | String prefix = command[0]; 81 | for (String name : sinks.keySet()) 82 | if (name.startsWith(prefix)) result.add(name); 83 | } 84 | 85 | return result; 86 | } 87 | 88 | @Override 89 | public boolean isUsernameIndex(String[] command, int index) { 90 | return false; 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /src/main/java/openeye/notes/ConsoleNoteSink.java: -------------------------------------------------------------------------------- 1 | package openeye.notes; 2 | 3 | import com.google.common.base.Strings; 4 | import java.util.Collection; 5 | import net.minecraft.command.ICommandSender; 6 | import net.minecraft.util.text.ITextComponent; 7 | import net.minecraft.util.text.TextComponentString; 8 | import net.minecraft.util.text.TextComponentTranslation; 9 | import openeye.notes.CommandNotes.INoteSink; 10 | import openeye.notes.entries.NoteEntry; 11 | 12 | final class ConsoleNoteSink implements INoteSink { 13 | @Override 14 | public void dump(Collection notes, ICommandSender sender) { 15 | int count = 0; 16 | for (NoteEntry note : notes) { 17 | ITextComponent level = new TextComponentTranslation(note.category.translated); 18 | level.getStyle().setColor(note.category.color); 19 | sender.sendMessage(new TextComponentTranslation("openeye.chat.note", count++, level)); 20 | ITextComponent title = note.title(); 21 | title.getStyle().setBold(true); 22 | sender.sendMessage(title); 23 | sender.sendMessage(note.content()); 24 | 25 | String url = note.url(); 26 | if (!Strings.isNullOrEmpty(url)) sender.sendMessage(new TextComponentString(note.url())); 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/main/java/openeye/notes/GuiButtonNotes.java: -------------------------------------------------------------------------------- 1 | package openeye.notes; 2 | 3 | import net.minecraft.client.Minecraft; 4 | import net.minecraft.client.gui.GuiButton; 5 | import net.minecraft.util.ResourceLocation; 6 | import org.lwjgl.opengl.GL11; 7 | 8 | public class GuiButtonNotes extends GuiButton { 9 | 10 | public static final ResourceLocation TEXTURE = new ResourceLocation("openeye", "textures/gui/buttons.png"); 11 | 12 | private NoteIcons icon; 13 | 14 | private boolean blink; 15 | 16 | private int count; 17 | 18 | public GuiButtonNotes(int id, int x, int y) { 19 | super(id, x, y, 20, 20, ""); 20 | } 21 | 22 | public void setBlinking(boolean blinking) { 23 | this.blink = blinking; 24 | } 25 | 26 | public void setIcon(NoteIcons icon) { 27 | this.icon = icon; 28 | } 29 | 30 | @Override 31 | public void drawButton(Minecraft mc, int mouseX, int mouseY, float partialTicks) { 32 | mc.getTextureManager().bindTexture(TEXTURE); 33 | GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F); 34 | final boolean mouseOverButton = isMouseOverButton(mouseX, mouseY); 35 | 36 | int textureU; 37 | 38 | if (blink && ((count++ & 0x10) != 0)) textureU = 40; 39 | else if (mouseOverButton) textureU = 20; 40 | else textureU = 0; 41 | 42 | drawTexturedModalRect(x, y, textureU, 0, width, height); 43 | 44 | drawTexturedModalRect(x + 2, y + 2, icon.textureU + 2, icon.textureV + 2, 16, 16); 45 | } 46 | 47 | protected boolean isMouseOverButton(int mouseX, int mouseY) { 48 | return mouseX >= this.x && mouseY >= this.y && mouseX < this.x + this.width && mouseY < this.y + this.height; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/openeye/notes/GuiNotes.java: -------------------------------------------------------------------------------- 1 | package openeye.notes; 2 | 3 | import com.google.common.base.Strings; 4 | import java.awt.Desktop; 5 | import java.net.URI; 6 | import java.util.List; 7 | import net.minecraft.client.gui.GuiButton; 8 | import net.minecraft.client.gui.GuiConfirmOpenLink; 9 | import net.minecraft.client.gui.GuiScreen; 10 | import net.minecraft.client.resources.I18n; 11 | import openeye.Log; 12 | import openeye.notes.entries.NoteEntry; 13 | 14 | public class GuiNotes extends GuiScreen { 15 | 16 | private static final int ACTION_GOTO_URL = 0; 17 | private static final int BUTTON_FINISHED_ID = 0; 18 | private static final int BUTTON_GOTO_ID = 1; 19 | 20 | private final GuiScreen prevGui; 21 | 22 | private GuiNotesList noteList; 23 | 24 | private int selectedNote = -1; 25 | 26 | private final List notes; 27 | 28 | private GuiButton gotoButton; 29 | 30 | private String gotoUrl; 31 | 32 | public GuiNotes(GuiScreen prevGui, List notes) { 33 | this.prevGui = prevGui; 34 | this.notes = notes; 35 | } 36 | 37 | @Override 38 | public void initGui() { 39 | super.initGui(); 40 | 41 | buttonList.add(new GuiButton(BUTTON_FINISHED_ID, width / 2, height - 30, 150, 20, I18n.format("gui.done"))); 42 | gotoButton = new GuiButton(BUTTON_GOTO_ID, width / 2 - 150, height - 30, 150, 20, I18n.format("openeye.notes.goto_page")); 43 | gotoButton.enabled = false; 44 | buttonList.add(gotoButton); 45 | noteList = new GuiNotesList(this, mc, width, height, 10, height - 40, width, height, notes); 46 | } 47 | 48 | @Override 49 | public void drawScreen(int par1, int par2, float par3) { 50 | drawDefaultBackground(); 51 | noteList.drawScreen(par1, par2, par3); 52 | super.drawScreen(par1, par2, par3); 53 | } 54 | 55 | @Override 56 | protected void actionPerformed(GuiButton button) { 57 | if (button.id == BUTTON_FINISHED_ID) mc.displayGuiScreen(prevGui); 58 | else if (button.id == BUTTON_GOTO_ID) mc.displayGuiScreen(new GuiConfirmOpenLink(this, gotoUrl, ACTION_GOTO_URL, false)); 59 | } 60 | 61 | private static void openURI(String uri) { 62 | try { 63 | URI parsedUri = new URI(uri); 64 | Desktop.getDesktop().browse(parsedUri); 65 | } catch (Throwable t) { 66 | Log.warn(t, "Failed to open URL %s", uri); 67 | } 68 | } 69 | 70 | @Override 71 | public void confirmClicked(boolean result, int action) { 72 | if (action == ACTION_GOTO_URL && result) openURI(gotoUrl); 73 | this.mc.displayGuiScreen(this); 74 | } 75 | 76 | public void selectNote(int slot) { 77 | selectedNote = slot; 78 | gotoUrl = getUrl(slot); 79 | gotoButton.enabled = !Strings.isNullOrEmpty(gotoUrl); 80 | } 81 | 82 | private String getUrl(int slot) { 83 | if (slot >= 0 && slot < notes.size()) { 84 | NoteEntry entry = notes.get(slot); 85 | return entry.url(); 86 | } 87 | 88 | return null; 89 | } 90 | 91 | public boolean isNoteSelected(int slot) { 92 | return selectedNote == slot; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/main/java/openeye/notes/GuiNotesList.java: -------------------------------------------------------------------------------- 1 | package openeye.notes; 2 | 3 | import com.google.common.base.Strings; 4 | import java.util.List; 5 | import net.minecraft.client.Minecraft; 6 | import net.minecraft.client.renderer.Tessellator; 7 | import net.minecraftforge.fml.client.GuiScrollingList; 8 | import openeye.notes.entries.NoteEntry; 9 | import org.lwjgl.opengl.GL11; 10 | 11 | public class GuiNotesList extends GuiScrollingList { 12 | 13 | private static final int ENTRY_HEIGHT = 50; 14 | private final List notes; 15 | private final GuiNotes owner; 16 | private final Minecraft mc; 17 | 18 | public GuiNotesList(GuiNotes owner, Minecraft mc, int width, int height, int top, int bottom, int screenWidth, int screenHeight, List notes) { 19 | super(mc, width, height, top, bottom, 0, ENTRY_HEIGHT, screenWidth, screenHeight); 20 | this.mc = mc; 21 | this.owner = owner; 22 | this.notes = notes; 23 | } 24 | 25 | @Override 26 | protected int getSize() { 27 | return notes.size(); 28 | } 29 | 30 | @Override 31 | protected void elementClicked(int id, boolean var2) { 32 | owner.selectNote(id); 33 | } 34 | 35 | @Override 36 | protected boolean isSelected(int id) { 37 | return owner.isNoteSelected(id); 38 | } 39 | 40 | @Override 41 | protected void drawBackground() {} 42 | 43 | @Override 44 | protected void drawSlot(int slotId, int right, int top, int height, Tessellator tessellator) { 45 | NoteEntry entry = notes.get(slotId); 46 | 47 | GL11.glColor3f(1, 1, 1); 48 | int left = this.left + 10; 49 | mc.renderEngine.bindTexture(GuiButtonNotes.TEXTURE); 50 | NoteIcons icon = entry.category.icon; 51 | owner.drawTexturedModalRect(left, top, icon.textureU + 2, icon.textureV + 2, 16, 16); 52 | 53 | owner.drawString(mc.fontRenderer, entry.title().getFormattedText(), left + 20, top + 4, 0xFFFFFF); 54 | String description = entry.content().getFormattedText(); 55 | 56 | int width = right - left; 57 | if (!Strings.isNullOrEmpty(description)) 58 | mc.fontRenderer.drawSplitString(description, left, top + 20, left + width - 10, 0xCCCCCC); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/openeye/notes/JsonNoteSink.java: -------------------------------------------------------------------------------- 1 | package openeye.notes; 2 | 3 | import com.google.gson.JsonArray; 4 | import com.google.gson.JsonObject; 5 | import java.util.Collection; 6 | import net.minecraft.command.CommandException; 7 | import net.minecraft.command.ICommandSender; 8 | import net.minecraft.util.text.TextComponentTranslation; 9 | import openeye.Log; 10 | import openeye.notes.CommandNotes.INoteSink; 11 | import openeye.notes.entries.NoteEntry; 12 | import openeye.storage.IAppendableStorage; 13 | import openeye.storage.IDataSource; 14 | 15 | final class JsonNoteSink implements INoteSink { 16 | private final IAppendableStorage notesDump; 17 | 18 | JsonNoteSink(IAppendableStorage notesDump) { 19 | this.notesDump = notesDump; 20 | } 21 | 22 | @Override 23 | public void dump(Collection notes, ICommandSender sender) throws CommandException { 24 | JsonArray result = new JsonArray(); 25 | 26 | for (NoteEntry note : notes) { 27 | JsonObject object = note.toJson(); 28 | result.add(object); 29 | } 30 | 31 | try { 32 | IDataSource target = notesDump.createNew(); 33 | target.store(result); 34 | sender.sendMessage(new TextComponentTranslation("openeye.chat.dumped", target.getId())); 35 | } catch (Throwable t) { 36 | Log.warn(t, "Failed to store notes"); 37 | throw new CommandException("openeye.chat.store_failed"); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/openeye/notes/NoteCategory.java: -------------------------------------------------------------------------------- 1 | package openeye.notes; 2 | 3 | import net.minecraft.util.text.TextFormatting; 4 | 5 | public enum NoteCategory { 6 | INFO(NoteIcons.INFO, false, "openeye.category.info", TextFormatting.BLUE), 7 | REPORTED_CRASH(NoteIcons.OK, false, "openeye.level.reported_crash", TextFormatting.GREEN), 8 | WARNING(NoteIcons.WARNING, false, "openeye.category.warning", TextFormatting.YELLOW), 9 | RESOLVED_CRASH(NoteIcons.WARNING, true, "openeye.level.resolved_crash", TextFormatting.GREEN), 10 | ALERT(NoteIcons.ERROR, false, "openeye.level.category", TextFormatting.RED), 11 | CRITICAL(NoteIcons.CRITICAL, true, "openeye.category.critical", TextFormatting.RED), 12 | REMOVE_FILE(NoteIcons.CRITICAL, true, "openeye.category.remove_file", TextFormatting.RED), 13 | SYSTEM_INFO(NoteIcons.EYE, true, "openeye.category.system_info", TextFormatting.AQUA); 14 | 15 | public final NoteIcons icon; 16 | public final boolean important; 17 | public final String translated; 18 | public final TextFormatting color; 19 | 20 | private NoteCategory(NoteIcons icon, boolean important, String translated, TextFormatting color) { 21 | this.icon = icon; 22 | this.important = important; 23 | this.translated = translated; 24 | this.color = color; 25 | } 26 | 27 | public static final NoteCategory[] VALUES = values(); 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/openeye/notes/NoteCollector.java: -------------------------------------------------------------------------------- 1 | package openeye.notes; 2 | 3 | import com.google.common.base.Strings; 4 | import com.google.common.collect.ImmutableList; 5 | import com.google.common.collect.Lists; 6 | import com.google.common.collect.Ordering; 7 | import java.io.File; 8 | import java.text.DateFormat; 9 | import java.text.SimpleDateFormat; 10 | import java.util.Collections; 11 | import java.util.Comparator; 12 | import java.util.Date; 13 | import java.util.List; 14 | import net.minecraft.util.text.ITextComponent; 15 | import net.minecraft.util.text.TextComponentTranslation; 16 | import openeye.logic.ModState; 17 | import openeye.logic.StateHolder; 18 | import openeye.logic.Storages; 19 | import openeye.net.GenericSender.EncryptionState; 20 | import openeye.notes.entries.MsgNoteEntry; 21 | import openeye.notes.entries.NoteEntry; 22 | import openeye.notes.entries.RemoveFileEntry; 23 | import openeye.notes.entries.ReportedCrashEntry; 24 | import openeye.notes.entries.ResolvedCrashEntry; 25 | import openeye.notes.entries.SystemNoteEntry; 26 | import openeye.responses.ResponseKnownCrashAction; 27 | import openeye.responses.ResponseModMsgAction; 28 | import openeye.responses.ResponseRemoveFileAction; 29 | 30 | public class NoteCollector { 31 | 32 | private static final Comparator NOTE_COMPARATOR = new Comparator() { 33 | @Override 34 | public int compare(NoteEntry o1, NoteEntry o2) { 35 | int result = o2.level - o1.level; 36 | if (result != 0) return result; 37 | 38 | return o1.file.compareTo(o2.file); 39 | } 40 | }; 41 | 42 | private final ScreenNotificationHolder menuLine = new ScreenNotificationHolder(); 43 | 44 | private boolean important; 45 | 46 | public static final NoteCollector INSTANCE = new NoteCollector(); 47 | 48 | private final List notes = Lists.newArrayList(); 49 | 50 | private NoteCategory maxCategory = NoteCategory.INFO; 51 | 52 | private NoteCollector() {} 53 | 54 | public synchronized void addNote(NoteEntry entry) { 55 | notes.add(entry); 56 | maxCategory = Ordering.natural().max(maxCategory, entry.category); 57 | important |= entry.category.important; 58 | } 59 | 60 | public void addNote(File file, ResponseModMsgAction note) { 61 | NoteEntry entry = new MsgNoteEntry(file, note); 62 | addNote(entry); 63 | } 64 | 65 | public void addNote(File file, ResponseRemoveFileAction note) { 66 | addNote(new RemoveFileEntry(file, note)); 67 | menuLine.signalDangerousFile(); 68 | } 69 | 70 | public void addNote(ResponseKnownCrashAction note) { 71 | if (Strings.isNullOrEmpty(note.note)) { 72 | addNote(new ReportedCrashEntry(note)); 73 | menuLine.signalCrashReported(); 74 | } else { 75 | addNote(new ResolvedCrashEntry(note)); 76 | menuLine.signalKnownCrash(); 77 | } 78 | } 79 | 80 | public void addNote(EncryptionState encryptionState) { 81 | switch (encryptionState) { 82 | case NO_ROOT_CERTIFICATE: 83 | addNote(new SystemNoteEntry(NoteCategory.WARNING, 11, 84 | new TextComponentTranslation("openeye.note.title.old_java"), 85 | new TextComponentTranslation("openeye.note.content.old_java_recoverable"), 86 | "http://lmgtfy.com/?q=download+java")); 87 | break; 88 | case NOT_SUPPORTED: 89 | addNote(new SystemNoteEntry(NoteCategory.ALERT, 22, 90 | new TextComponentTranslation("openeye.note.title.old_java"), 91 | new TextComponentTranslation("openeye.note.content.old_java_total_failure"), 92 | "http://lmgtfy.com/?q=download+java")); 93 | break; 94 | default: 95 | break; 96 | } 97 | } 98 | 99 | public void addSuspendNote(long suspendUntilTimestamp, String reason) { 100 | final Date suspendEndDate = new Date(suspendUntilTimestamp); 101 | final DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 102 | final String suspendPrintable = dateFormat.format(suspendEndDate); 103 | addNote(new SystemNoteEntry(NoteCategory.INFO, 32, 104 | new TextComponentTranslation("openeye.note.title.suspended"), 105 | Strings.isNullOrEmpty(reason) 106 | ? new TextComponentTranslation("openeye.note.content.suspended_no_reason", suspendPrintable, reason) 107 | : new TextComponentTranslation("openeye.note.content.suspended", suspendPrintable, reason), 108 | "https://openeye.openmods.info")); 109 | } 110 | 111 | public boolean isEmpty() { 112 | return notes.isEmpty(); 113 | } 114 | 115 | public NoteCategory getMaxLevel() { 116 | return maxCategory; 117 | } 118 | 119 | public ITextComponent getScreenMsg() { 120 | return menuLine.getSelectedLine(); 121 | } 122 | 123 | public boolean hasImportantNotes() { 124 | return important; 125 | } 126 | 127 | public List getNotes() { 128 | Collections.sort(notes, NOTE_COMPARATOR); 129 | return ImmutableList.copyOf(notes); 130 | } 131 | 132 | private void addIntroNote(int id, String url) { 133 | String title = "openeye.note.title.intro" + id; 134 | String content = "openeye.note.content.intro" + id; 135 | addNote(new SystemNoteEntry(NoteLevels.SYSTEM_NOTIFICATION_LEVEL + 16 - id, 136 | new TextComponentTranslation(title), 137 | new TextComponentTranslation(content), 138 | url)); 139 | } 140 | 141 | public void finishNoteCollection() { 142 | ModState state = StateHolder.state(); 143 | 144 | if (!state.infoNotesDisplayed) { 145 | addIntroNote(1, "https://openeye.openmods.info"); 146 | addIntroNote(2, "https://openeye.openmods.info"); 147 | addIntroNote(3, "https://openeye.openmods.info/storage-policy"); 148 | 149 | Storages storages = Storages.instance(); 150 | if (storages != null) addIntroNote(4, storages.reportsDir.toURI().toString()); 151 | 152 | addIntroNote(5, "https://github.com/OpenMods/OpenData"); 153 | addIntroNote(6, "https://openeye.openmods.info/configuration"); 154 | state.infoNotesDisplayed = true; 155 | } 156 | 157 | if (!state.mainMenuInfoDisplayed) { 158 | menuLine.signalIntroStuff(); 159 | state.mainMenuInfoDisplayed = true; 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/main/java/openeye/notes/NoteIcons.java: -------------------------------------------------------------------------------- 1 | package openeye.notes; 2 | 3 | public enum NoteIcons { 4 | // Keep thresholds in inverse order 5 | INFO(0, 20), 6 | WARNING(20, 20), 7 | ERROR(40, 20), 8 | CRITICAL(60, 20), 9 | EYE(80, 20), 10 | OK(100, 20); 11 | 12 | public final int textureU; 13 | public final int textureV; 14 | 15 | private NoteIcons(int textureU, int textureV) { 16 | this.textureU = textureU; 17 | this.textureV = textureV; 18 | } 19 | 20 | public static final NoteIcons[] VALUES = values(); 21 | } -------------------------------------------------------------------------------- /src/main/java/openeye/notes/NoteLevels.java: -------------------------------------------------------------------------------- 1 | package openeye.notes; 2 | 3 | /* 4 | * Levels of notifications in list (higher number - higher in list) 5 | */ 6 | public class NoteLevels { 7 | 8 | static final int SYSTEM_NOTIFICATION_LEVEL = 256; 9 | 10 | public static final int REMOVE_FILE_LEVEL = 128; 11 | 12 | public static final int CRITICAL_LEVEL_THRESHOLD = 64; 13 | public static final int ALERT_LEVEL_THRESHOLD = 32; 14 | public static final int WARNING_LEVEL_THRESHOLD = 16; 15 | 16 | public static final int REPORTED_CRASH_LEVEL = 8; 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/openeye/notes/NotesButtonInjector.java: -------------------------------------------------------------------------------- 1 | package openeye.notes; 2 | 3 | import java.util.List; 4 | import net.minecraft.client.Minecraft; 5 | import net.minecraft.client.gui.GuiButton; 6 | import net.minecraft.client.gui.GuiMainMenu; 7 | import net.minecraft.client.gui.GuiScreen; 8 | import net.minecraft.util.text.ITextComponent; 9 | import net.minecraftforge.client.event.GuiScreenEvent.ActionPerformedEvent; 10 | import net.minecraftforge.client.event.GuiScreenEvent.DrawScreenEvent; 11 | import net.minecraftforge.client.event.GuiScreenEvent.InitGuiEvent; 12 | import net.minecraftforge.common.MinecraftForge; 13 | import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; 14 | import openeye.logic.Config; 15 | 16 | public class NotesButtonInjector { 17 | 18 | private static final int BUTTON_NOTES_ID = 666; 19 | private static ITextComponent notification; 20 | 21 | @SuppressWarnings({ "unchecked", "rawtypes" }) 22 | public static void onGuiInit(GuiScreen screen, List buttonList) { 23 | final NoteCollector noteCollector = NoteCollector.INSTANCE; 24 | noteCollector.finishNoteCollection(); 25 | 26 | notification = noteCollector.getScreenMsg(); 27 | 28 | if (!noteCollector.isEmpty()) { 29 | NoteCategory type = noteCollector.getMaxLevel(); 30 | 31 | NoteIcons icon = type.icon; 32 | boolean blinking = noteCollector.hasImportantNotes(); 33 | GuiButtonNotes button = getOrCreateInfoButton(screen, buttonList); 34 | button.setBlinking(blinking); 35 | button.setIcon(icon); 36 | } 37 | } 38 | 39 | private static int getX(GuiScreen screen, boolean isAbsolute, int delta) { 40 | return (isAbsolute? 0 : screen.width / 2) + delta; 41 | } 42 | 43 | private static int getY(GuiScreen screen, boolean isAbsolute, int delta) { 44 | return (isAbsolute? 0 : screen.height / 4) + delta; 45 | } 46 | 47 | private static GuiButtonNotes getOrCreateInfoButton(GuiScreen screen, List buttonList) { 48 | for (GuiButton button : buttonList) 49 | if (button instanceof GuiButtonNotes) return (GuiButtonNotes)button; 50 | 51 | GuiButtonNotes buttonNotes = new GuiButtonNotes(BUTTON_NOTES_ID, 52 | getX(screen, Config.isNotesButtonPosAbsolute, Config.notesButtonPosX), 53 | getY(screen, Config.isNotesButtonPosAbsolute, Config.notesButtonPosY)); 54 | buttonList.add(buttonNotes); 55 | return buttonNotes; 56 | } 57 | 58 | public static void onScreenDraw(GuiScreen screen) { 59 | if (Config.mainScreenExtraLine && notification != null) 60 | screen.drawCenteredString(screen.mc.fontRenderer, 61 | notification.getFormattedText(), 62 | getX(screen, Config.isExtraLinePosAbsolute, Config.extraLinePosX), 63 | getY(screen, Config.isExtraLinePosAbsolute, Config.extraLinePosY), 64 | 0xFFFFFF); 65 | } 66 | 67 | public static void onActionPerformed(Minecraft mc, GuiScreen screen, GuiButton button) { 68 | if (button.id == BUTTON_NOTES_ID) onActionPerformed(mc, screen); 69 | } 70 | 71 | private static void onActionPerformed(Minecraft mc, GuiScreen screen) { 72 | mc.displayGuiScreen(new GuiNotes(screen, NoteCollector.INSTANCE.getNotes())); 73 | } 74 | 75 | @SubscribeEvent 76 | public void onGuiInit(InitGuiEvent evt) { 77 | if (evt.getGui() instanceof GuiMainMenu) onGuiInit(evt.getGui(), evt.getButtonList()); 78 | } 79 | 80 | @SubscribeEvent 81 | public void onGuiInit(DrawScreenEvent.Post evt) { 82 | if (evt.getGui() instanceof GuiMainMenu) onScreenDraw(evt.getGui()); 83 | } 84 | 85 | @SubscribeEvent 86 | public void onActionPerformed(ActionPerformedEvent evt) { 87 | if (evt.getGui() instanceof GuiMainMenu) onActionPerformed(evt.getGui().mc, evt.getGui(), evt.getButton()); 88 | } 89 | 90 | public static void registerInjector() { 91 | MinecraftForge.EVENT_BUS.register(new NotesButtonInjector()); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/main/java/openeye/notes/ScreenNotificationHolder.java: -------------------------------------------------------------------------------- 1 | package openeye.notes; 2 | 3 | import net.minecraft.util.text.ITextComponent; 4 | import net.minecraft.util.text.Style; 5 | import net.minecraft.util.text.TextComponentTranslation; 6 | import net.minecraft.util.text.TextFormatting; 7 | 8 | public class ScreenNotificationHolder { 9 | private static class Entry { 10 | final int level; 11 | public final ITextComponent msg; 12 | 13 | private Entry(int level, ITextComponent msg) { 14 | this.level = level; 15 | this.msg = msg; 16 | } 17 | } 18 | 19 | private final Style REMOVE_FILE_STYLE = new Style().setBold(true).setColor(TextFormatting.RED); 20 | 21 | private final Style KNOWN_CRASH_STYLE = new Style().setColor(TextFormatting.GREEN); 22 | 23 | private final Style INTRO_STYLE = new Style().setColor(TextFormatting.GOLD); 24 | 25 | private Entry selectedLine; 26 | 27 | public void addLine(int level, ITextComponent msg) { 28 | if (selectedLine == null || level > selectedLine.level) selectedLine = new Entry(level, msg); 29 | } 30 | 31 | public void signalDangerousFile() { 32 | addLine(64, new TextComponentTranslation("openeye.main_screen.remove_file").setStyle(REMOVE_FILE_STYLE)); 33 | } 34 | 35 | public void signalCrashReported() { 36 | addLine(8, new TextComponentTranslation("openeye.main_screen.crash_reported")); 37 | } 38 | 39 | public void signalKnownCrash() { 40 | addLine(32, new TextComponentTranslation("openeye.main_screen.known_crash").setStyle(KNOWN_CRASH_STYLE)); 41 | } 42 | 43 | public void signalIntroStuff() { 44 | addLine(256, new TextComponentTranslation("openeye.main_screen.intro").setStyle(INTRO_STYLE)); 45 | } 46 | 47 | public ITextComponent getSelectedLine() { 48 | return selectedLine != null? selectedLine.msg : null; 49 | } 50 | } -------------------------------------------------------------------------------- /src/main/java/openeye/notes/entries/MsgNoteEntry.java: -------------------------------------------------------------------------------- 1 | package openeye.notes.entries; 2 | 3 | import com.google.common.base.Strings; 4 | import com.google.gson.JsonObject; 5 | import java.io.File; 6 | import net.minecraft.util.text.ITextComponent; 7 | import net.minecraft.util.text.TextComponentString; 8 | import net.minecraft.util.text.TextComponentTranslation; 9 | import openeye.notes.NoteCategory; 10 | import openeye.notes.NoteLevels; 11 | import openeye.responses.ResponseModMsgAction; 12 | 13 | public class MsgNoteEntry extends NoteEntry { 14 | private final String description; 15 | private final String signature; 16 | private final String payload; 17 | 18 | public MsgNoteEntry(File file, ResponseModMsgAction msg) { 19 | super(file, calculateFromLevel(msg.level), msg.level); 20 | this.signature = msg.signature; 21 | this.description = msg.description; 22 | this.payload = msg.payload; 23 | } 24 | 25 | private static NoteCategory calculateFromLevel(int level) { 26 | if (level > NoteLevels.CRITICAL_LEVEL_THRESHOLD) return NoteCategory.CRITICAL; 27 | if (level > NoteLevels.ALERT_LEVEL_THRESHOLD) return NoteCategory.ALERT; 28 | else if (level > NoteLevels.WARNING_LEVEL_THRESHOLD) return NoteCategory.WARNING; 29 | else return NoteCategory.INFO; 30 | } 31 | 32 | @Override 33 | public String url() { 34 | return null; // TODO 35 | } 36 | 37 | @Override 38 | public ITextComponent title() { 39 | return new TextComponentTranslation("openeye.notes.title.note", file.getName()); 40 | } 41 | 42 | @Override 43 | public ITextComponent content() { 44 | return new TextComponentString(Strings.nullToEmpty(description)); 45 | } 46 | 47 | @Override 48 | public JsonObject toJson() { 49 | JsonObject result = super.toJson(); 50 | result.addProperty("signature", signature); 51 | result.addProperty("payload", payload); 52 | return result; 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/openeye/notes/entries/NoteEntry.java: -------------------------------------------------------------------------------- 1 | package openeye.notes.entries; 2 | 3 | import com.google.common.base.Strings; 4 | import com.google.gson.JsonObject; 5 | import java.io.File; 6 | import net.minecraft.util.text.ITextComponent; 7 | import openeye.notes.NoteCategory; 8 | 9 | public abstract class NoteEntry { 10 | private static final File DUMMY_FILE = new File("invalid"); 11 | 12 | protected NoteEntry(File file, NoteCategory category, int level) { 13 | this.file = file != null? file : DUMMY_FILE; 14 | this.category = category; 15 | this.level = level; 16 | } 17 | 18 | protected NoteEntry(NoteCategory category, int level) { 19 | this(DUMMY_FILE, category, level); 20 | } 21 | 22 | public final NoteCategory category; 23 | public final int level; 24 | public final File file; 25 | 26 | public abstract String url(); 27 | 28 | public abstract ITextComponent title(); 29 | 30 | public abstract ITextComponent content(); 31 | 32 | public JsonObject toJson() { 33 | JsonObject result = new JsonObject(); 34 | result.addProperty("filename", file.getName()); 35 | result.addProperty("path", file.getPath()); 36 | result.addProperty("category", category.toString()); 37 | result.addProperty("level", level); 38 | 39 | result.addProperty("title", title().getUnformattedText()); 40 | result.addProperty("content", content().getUnformattedText()); 41 | 42 | String url = url(); 43 | if (!Strings.isNullOrEmpty(url)) result.addProperty("url", url); 44 | 45 | return result; 46 | } 47 | } -------------------------------------------------------------------------------- /src/main/java/openeye/notes/entries/RemoveFileEntry.java: -------------------------------------------------------------------------------- 1 | package openeye.notes.entries; 2 | 3 | import com.google.common.base.Strings; 4 | import com.google.gson.JsonObject; 5 | import java.io.File; 6 | import net.minecraft.util.text.ITextComponent; 7 | import net.minecraft.util.text.TextComponentTranslation; 8 | import openeye.notes.NoteCategory; 9 | import openeye.notes.NoteLevels; 10 | import openeye.responses.ResponseRemoveFileAction; 11 | 12 | public class RemoveFileEntry extends NoteEntry { 13 | 14 | private final String signature; 15 | private final String url; 16 | 17 | public RemoveFileEntry(File file, ResponseRemoveFileAction msg) { 18 | super(file, NoteCategory.REMOVE_FILE, NoteLevels.REMOVE_FILE_LEVEL); 19 | this.signature = msg.signature; 20 | this.url = msg.url; 21 | } 22 | 23 | @Override 24 | public ITextComponent title() { 25 | return new TextComponentTranslation("openeye.notes.title.remove_file", file.getName()); 26 | } 27 | 28 | @Override 29 | public ITextComponent content() { 30 | return new TextComponentTranslation("openeye.notes.content.remove_file", file.getName()); 31 | } 32 | 33 | @Override 34 | public String url() { 35 | return Strings.nullToEmpty(url); 36 | } 37 | 38 | @Override 39 | public JsonObject toJson() { 40 | JsonObject result = super.toJson(); 41 | result.addProperty("signature", signature); 42 | return result; 43 | } 44 | } -------------------------------------------------------------------------------- /src/main/java/openeye/notes/entries/ReportedCrashEntry.java: -------------------------------------------------------------------------------- 1 | package openeye.notes.entries; 2 | 3 | import net.minecraft.util.text.ITextComponent; 4 | import net.minecraft.util.text.TextComponentString; 5 | import net.minecraft.util.text.TextComponentTranslation; 6 | import openeye.notes.NoteCategory; 7 | import openeye.notes.NoteLevels; 8 | import openeye.responses.ResponseKnownCrashAction; 9 | 10 | public class ReportedCrashEntry extends NoteEntry { 11 | private final String url; 12 | 13 | public ReportedCrashEntry(ResponseKnownCrashAction msg) { 14 | super(NoteCategory.REPORTED_CRASH, NoteLevels.REPORTED_CRASH_LEVEL); 15 | this.url = msg.url; 16 | } 17 | 18 | @Override 19 | public ITextComponent title() { 20 | return new TextComponentTranslation("openeye.notes.title.reported_crash"); 21 | } 22 | 23 | @Override 24 | public ITextComponent content() { 25 | return new TextComponentString(""); 26 | } 27 | 28 | @Override 29 | public String url() { 30 | return url; 31 | } 32 | } -------------------------------------------------------------------------------- /src/main/java/openeye/notes/entries/ResolvedCrashEntry.java: -------------------------------------------------------------------------------- 1 | package openeye.notes.entries; 2 | 3 | import com.google.common.base.Strings; 4 | import net.minecraft.util.text.ITextComponent; 5 | import net.minecraft.util.text.TextComponentString; 6 | import net.minecraft.util.text.TextComponentTranslation; 7 | import openeye.notes.NoteCategory; 8 | import openeye.responses.ResponseKnownCrashAction; 9 | 10 | public class ResolvedCrashEntry extends NoteEntry { 11 | private final String url; 12 | private final String note; 13 | 14 | public ResolvedCrashEntry(ResponseKnownCrashAction msg) { 15 | super(NoteCategory.RESOLVED_CRASH, 64); 16 | this.url = msg.url; 17 | this.note = msg.note; 18 | } 19 | 20 | @Override 21 | public ITextComponent title() { 22 | return new TextComponentTranslation("openeye.notes.title.resolved_crash"); 23 | } 24 | 25 | @Override 26 | public ITextComponent content() { 27 | return new TextComponentString(Strings.nullToEmpty(note)); 28 | } 29 | 30 | @Override 31 | public String url() { 32 | return url; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/openeye/notes/entries/SystemNoteEntry.java: -------------------------------------------------------------------------------- 1 | package openeye.notes.entries; 2 | 3 | import net.minecraft.util.text.ITextComponent; 4 | import openeye.notes.NoteCategory; 5 | 6 | public class SystemNoteEntry extends NoteEntry { 7 | 8 | private final ITextComponent title; 9 | 10 | private final ITextComponent contents; 11 | 12 | private final String url; 13 | 14 | public SystemNoteEntry(int level, ITextComponent title, ITextComponent contents, String url) { 15 | this(NoteCategory.SYSTEM_INFO, level, title, contents, url); 16 | } 17 | 18 | public SystemNoteEntry(NoteCategory category, int level, ITextComponent title, ITextComponent contents, String url) { 19 | super(category, level); 20 | this.title = title; 21 | this.contents = contents; 22 | this.url = url; 23 | } 24 | 25 | @Override 26 | public ITextComponent title() { 27 | return title; 28 | } 29 | 30 | @Override 31 | public ITextComponent content() { 32 | return contents; 33 | } 34 | 35 | @Override 36 | public String url() { 37 | return url; 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/openeye/responses/IExecutableResponse.java: -------------------------------------------------------------------------------- 1 | package openeye.responses; 2 | 3 | import openeye.logic.IContext; 4 | import openeye.protocol.responses.IResponse; 5 | 6 | public interface IExecutableResponse extends IResponse { 7 | public void execute(IContext context); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/openeye/responses/ResponseErrorAction.java: -------------------------------------------------------------------------------- 1 | package openeye.responses; 2 | 3 | import openeye.Log; 4 | import openeye.logic.IContext; 5 | import openeye.protocol.responses.ResponseError; 6 | 7 | public class ResponseErrorAction extends ResponseError implements IExecutableResponse { 8 | 9 | @Override 10 | public void execute(IContext context) { 11 | Log.warn("Server failed to parse report %d (type: %s)", reportIndex, reportType); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/openeye/responses/ResponseFileContentsAction.java: -------------------------------------------------------------------------------- 1 | package openeye.responses; 2 | 3 | import openeye.logic.IContext; 4 | import openeye.protocol.responses.ResponseFileContents; 5 | 6 | public class ResponseFileContentsAction extends ResponseFileContents implements IExecutableResponse { 7 | 8 | @Override 9 | public void execute(IContext context) { 10 | context.queueFileContents(signature); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/openeye/responses/ResponseFileInfoAction.java: -------------------------------------------------------------------------------- 1 | package openeye.responses; 2 | 3 | import openeye.logic.IContext; 4 | import openeye.protocol.responses.ResponseFileInfo; 5 | 6 | public class ResponseFileInfoAction extends ResponseFileInfo implements IExecutableResponse { 7 | 8 | @Override 9 | public void execute(IContext context) { 10 | context.queueFileReport(signature); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/openeye/responses/ResponseKnownCrashAction.java: -------------------------------------------------------------------------------- 1 | package openeye.responses; 2 | 3 | import openeye.logic.IContext; 4 | import openeye.notes.NoteCollector; 5 | import openeye.protocol.responses.ResponseKnownCrash; 6 | 7 | public class ResponseKnownCrashAction extends ResponseKnownCrash implements IExecutableResponse { 8 | 9 | @Override 10 | public void execute(IContext context) { 11 | NoteCollector.INSTANCE.addNote(this); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/openeye/responses/ResponseModMsgAction.java: -------------------------------------------------------------------------------- 1 | package openeye.responses; 2 | 3 | import java.io.File; 4 | import net.minecraft.nbt.NBTTagCompound; 5 | import net.minecraftforge.fml.common.event.FMLInterModComms; 6 | import openeye.logic.IContext; 7 | import openeye.notes.NoteCollector; 8 | import openeye.protocol.responses.ResponseModMsg; 9 | 10 | public class ResponseModMsgAction extends ResponseModMsg implements IExecutableResponse { 11 | 12 | @Override 13 | public void execute(IContext context) { 14 | for (String modId : context.getModsForSignature(signature)) { 15 | NBTTagCompound msg = new NBTTagCompound(); 16 | msg.setInteger("level", level); 17 | msg.setString("payload", payload); 18 | msg.setString("description", description); 19 | FMLInterModComms.sendMessage(modId, "EyeNotification", msg); 20 | } 21 | 22 | File file = context.getFileForSignature(signature); 23 | NoteCollector.INSTANCE.addNote(file, this); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/openeye/responses/ResponsePongAction.java: -------------------------------------------------------------------------------- 1 | package openeye.responses; 2 | 3 | import openeye.Log; 4 | import openeye.logic.IContext; 5 | import openeye.protocol.responses.ResponsePong; 6 | 7 | public class ResponsePongAction extends ResponsePong implements IExecutableResponse { 8 | 9 | @Override 10 | public void execute(IContext context) { 11 | Log.info("Ping-pong: %s", payload); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/openeye/responses/ResponseRemoveFileAction.java: -------------------------------------------------------------------------------- 1 | package openeye.responses; 2 | 3 | import java.io.File; 4 | import openeye.logic.IContext; 5 | import openeye.notes.NoteCollector; 6 | import openeye.protocol.responses.ResponseRemoveFile; 7 | 8 | public class ResponseRemoveFileAction extends ResponseRemoveFile implements IExecutableResponse { 9 | 10 | @Override 11 | public void execute(IContext context) { 12 | context.markUnwantedSignature(signature); 13 | 14 | File file = context.getFileForSignature(signature); 15 | NoteCollector.INSTANCE.addNote(file, this); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/openeye/responses/ResponseSuspendAction.java: -------------------------------------------------------------------------------- 1 | package openeye.responses; 2 | 3 | import openeye.logic.IContext; 4 | import openeye.logic.StateHolder; 5 | import openeye.notes.NoteCollector; 6 | import openeye.protocol.responses.ResponseSuspend; 7 | 8 | public class ResponseSuspendAction extends ResponseSuspend implements IExecutableResponse { 9 | 10 | @Override 11 | public void execute(IContext context) { 12 | long now = System.currentTimeMillis(); 13 | // duration in seconds 14 | final long suspendUntilTimestamp = now + duration * 1000; 15 | StateHolder.state().suspendUntilTimestamp = suspendUntilTimestamp; 16 | NoteCollector.INSTANCE.addSuspendNote(suspendUntilTimestamp, reason); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/openeye/storage/GsonArchiveStorage.java: -------------------------------------------------------------------------------- 1 | package openeye.storage; 2 | 3 | import com.google.gson.Gson; 4 | import java.io.File; 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | import java.io.OutputStream; 8 | import java.util.zip.ZipEntry; 9 | import java.util.zip.ZipOutputStream; 10 | 11 | public class GsonArchiveStorage extends GsonDirStorage { 12 | 13 | private final String fileName; 14 | 15 | public GsonArchiveStorage(File dir, String prefix, String fileName, Class cls, Gson gson) { 16 | super(dir, prefix, cls, gson, "zip"); 17 | this.fileName = fileName; 18 | } 19 | 20 | @Override 21 | protected void removeEntry(String id) { 22 | // NO-OP 23 | } 24 | 25 | @Override 26 | protected OutputStream createOutputStream(File file) throws IOException { 27 | OutputStream originalOutput = super.createOutputStream(file); 28 | ZipOutputStream zipOutput = new ZipOutputStream(originalOutput); 29 | zipOutput.setLevel(9); 30 | zipOutput.putNextEntry(new ZipEntry(fileName)); 31 | return zipOutput; 32 | } 33 | 34 | @Override 35 | protected InputStream createInputStream(File file) { 36 | throw new UnsupportedOperationException(); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/openeye/storage/GsonDirStorage.java: -------------------------------------------------------------------------------- 1 | package openeye.storage; 2 | 3 | import com.google.common.base.Preconditions; 4 | import com.google.gson.Gson; 5 | import java.io.File; 6 | 7 | public abstract class GsonDirStorage extends GsonStorageBase implements IAppendableStorage { 8 | 9 | protected final File dir; 10 | 11 | protected final String prefix; 12 | 13 | protected GsonDirStorage(File dir, String prefix, Class cls, Gson gson, String extension) { 14 | super(cls, gson, extension); 15 | Preconditions.checkArgument(dir.isDirectory()); 16 | this.dir = dir; 17 | this.prefix = prefix; 18 | } 19 | 20 | @Override 21 | public IDataSource createNew() { 22 | String prefixId = generateId(); 23 | String id; 24 | File file; 25 | int count = 0; 26 | do { 27 | id = prefixId + "-" + count++; 28 | String filename = generateFilename(prefix, id); 29 | file = new File(dir, filename); 30 | } while (file.exists()); 31 | 32 | return createFromFile(id, file); 33 | } 34 | 35 | @Override 36 | public IDataSource createNew(String id) { 37 | String filename = generateFilename(prefix, id); 38 | File file = new File(dir, filename); 39 | return createFromFile(id, file); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/openeye/storage/GsonPredefinedStorage.java: -------------------------------------------------------------------------------- 1 | package openeye.storage; 2 | 3 | import com.google.common.base.Preconditions; 4 | import com.google.common.collect.ImmutableMap; 5 | import com.google.gson.Gson; 6 | import java.io.File; 7 | import java.util.Collection; 8 | import java.util.Map; 9 | 10 | public class GsonPredefinedStorage extends GsonStorageBase implements IQueryableStorage { 11 | 12 | private final Map> sources; 13 | 14 | public GsonPredefinedStorage(File dir, Class cls, Gson gson, String... ids) { 15 | super(cls, gson, "json"); 16 | Preconditions.checkArgument(dir.isDirectory()); 17 | 18 | ImmutableMap.Builder> builder = ImmutableMap.builder(); 19 | 20 | for (String id : ids) { 21 | File f = new File(dir, id + ".json"); 22 | builder.put(id, createFromFile(id, f)); 23 | } 24 | 25 | sources = builder.build(); 26 | } 27 | 28 | @Override 29 | public Collection> listAll() { 30 | return sources.values(); 31 | } 32 | 33 | @Override 34 | public IDataSource getById(String id) { 35 | return sources.get(id); 36 | } 37 | 38 | @Override 39 | protected void removeEntry(String id) { 40 | // NO-OP 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/openeye/storage/GsonSessionStorage.java: -------------------------------------------------------------------------------- 1 | package openeye.storage; 2 | 3 | import com.google.gson.Gson; 4 | import java.io.File; 5 | import java.io.FileOutputStream; 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | import java.io.OutputStream; 9 | import java.io.Writer; 10 | import java.text.DateFormat; 11 | import java.text.SimpleDateFormat; 12 | import java.util.Date; 13 | import java.util.zip.ZipEntry; 14 | import java.util.zip.ZipOutputStream; 15 | 16 | public class GsonSessionStorage implements IAppendableStorage { 17 | 18 | private static final DateFormat FORMATTER = new SimpleDateFormat("yyyy-MM-dd_HH.mm.ss"); 19 | 20 | private final File archiveFile; 21 | 22 | private ZipOutputStream archiveStream; 23 | 24 | protected final Class cls; 25 | 26 | protected final Gson gson; 27 | 28 | private final String ext; 29 | 30 | private int counter; 31 | 32 | public GsonSessionStorage(File dir, String ext, Class cls, Gson gson) { 33 | this.ext = ext; 34 | this.gson = gson; 35 | this.cls = cls; 36 | final String name = generateId() + ".zip"; 37 | archiveFile = new File(dir, name); 38 | } 39 | 40 | private static String generateId() { 41 | synchronized (FORMATTER) { 42 | return FORMATTER.format(new Date()); 43 | } 44 | } 45 | 46 | @Override 47 | public IDataSource createNew() { 48 | return createNew("data"); 49 | } 50 | 51 | private ZipOutputStream archiveStream() throws IOException { 52 | if (archiveStream == null) { 53 | OutputStream stream = new FileOutputStream(archiveFile); 54 | archiveStream = new ZipOutputStream(stream); 55 | 56 | Runtime.getRuntime().addShutdownHook(new Thread() { 57 | @Override 58 | public void run() { 59 | try { 60 | if (archiveStream != null) archiveStream.close(); 61 | } catch (IOException e) { 62 | e.printStackTrace(); 63 | } 64 | } 65 | }); 66 | } 67 | 68 | return archiveStream; 69 | } 70 | 71 | @Override 72 | public IDataSource createNew(String id) { 73 | final String fullId = String.format("%s-%04d.%s", id, counter++, ext); 74 | return new GsonStreamSource(fullId, cls, gson) { 75 | 76 | @Override 77 | public void delete() { 78 | throw new UnsupportedOperationException(); 79 | } 80 | 81 | @Override 82 | protected InputStream createInputStream() { 83 | throw new UnsupportedOperationException(); 84 | } 85 | 86 | @Override 87 | protected void afterWrite(Writer writer) throws IOException { 88 | writer.flush(); 89 | final ZipOutputStream archiveStream = archiveStream(); 90 | archiveStream.closeEntry(); 91 | archiveStream.flush(); 92 | } 93 | 94 | @Override 95 | protected OutputStream createOutputStream() { 96 | try { 97 | ZipOutputStream stream = archiveStream(); 98 | stream.putNextEntry(new ZipEntry(fullId)); 99 | return stream; 100 | } catch (IOException t) { 101 | throw new RuntimeException(t); 102 | } 103 | } 104 | 105 | @Override 106 | protected String description() { 107 | return String.format("%s in archive %s", fullId, archiveFile); 108 | } 109 | 110 | @Override 111 | protected boolean sourceExists() { 112 | return false; 113 | } 114 | }; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/main/java/openeye/storage/GsonSimpleStorage.java: -------------------------------------------------------------------------------- 1 | package openeye.storage; 2 | 3 | import com.google.gson.Gson; 4 | import java.io.File; 5 | import java.io.InputStream; 6 | 7 | public class GsonSimpleStorage extends GsonDirStorage { 8 | 9 | public GsonSimpleStorage(File dir, String prefix, String extension, Class cls, Gson gson) { 10 | super(dir, prefix, cls, gson, extension); 11 | } 12 | 13 | @Override 14 | protected void removeEntry(String id) { 15 | // NO-OP 16 | } 17 | 18 | @Override 19 | protected InputStream createInputStream(File file) { 20 | throw new UnsupportedOperationException(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/openeye/storage/GsonStorageBase.java: -------------------------------------------------------------------------------- 1 | package openeye.storage; 2 | 3 | import com.google.gson.Gson; 4 | import java.io.File; 5 | import java.io.FileInputStream; 6 | import java.io.FileOutputStream; 7 | import java.io.IOException; 8 | import java.io.InputStream; 9 | import java.io.OutputStream; 10 | import java.text.DateFormat; 11 | import java.text.SimpleDateFormat; 12 | import java.util.Date; 13 | import openeye.Log; 14 | 15 | public abstract class GsonStorageBase { 16 | 17 | private static final DateFormat FORMATTER = new SimpleDateFormat("yyyy-MM-dd_HH.mm.ss"); 18 | 19 | protected final Class cls; 20 | 21 | protected final Gson gson; 22 | 23 | protected final String extension; 24 | 25 | protected GsonStorageBase(Class cls, Gson gson, String extension) { 26 | this.cls = cls; 27 | this.gson = gson; 28 | this.extension = extension; 29 | } 30 | 31 | protected static String generateId() { 32 | synchronized (FORMATTER) { 33 | return FORMATTER.format(new Date()); 34 | } 35 | } 36 | 37 | protected String generateFilename(String prefix, String id) { 38 | return String.format("%s-%s.%s", prefix, id, extension); 39 | } 40 | 41 | protected IDataSource createFromFile(String id, final File file) { 42 | return new GsonStreamSource(id, cls, gson) { 43 | @Override 44 | public void delete() { 45 | try { 46 | file.delete(); 47 | removeEntry(id); 48 | } catch (Throwable t) { 49 | Log.warn(t, "Can't delete file %s", file); 50 | } 51 | } 52 | 53 | @Override 54 | protected String description() { 55 | return file.getAbsolutePath(); 56 | } 57 | 58 | @Override 59 | protected OutputStream createOutputStream() { 60 | try { 61 | return GsonStorageBase.this.createOutputStream(file); 62 | } catch (IOException t) { 63 | throw new RuntimeException(t); 64 | } 65 | } 66 | 67 | @Override 68 | protected InputStream createInputStream() { 69 | try { 70 | return GsonStorageBase.this.createInputStream(file); 71 | } catch (IOException t) { 72 | throw new RuntimeException(t); 73 | } 74 | } 75 | 76 | @Override 77 | protected boolean sourceExists() { 78 | return file.exists(); 79 | } 80 | }; 81 | } 82 | 83 | protected abstract void removeEntry(String id); 84 | 85 | protected OutputStream createOutputStream(final File file) throws IOException { 86 | return new FileOutputStream(file); 87 | } 88 | 89 | protected InputStream createInputStream(final File file) throws IOException { 90 | return new FileInputStream(file); 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /src/main/java/openeye/storage/GsonStreamSource.java: -------------------------------------------------------------------------------- 1 | package openeye.storage; 2 | 3 | import com.google.common.base.Charsets; 4 | import com.google.gson.Gson; 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | import java.io.InputStreamReader; 8 | import java.io.OutputStream; 9 | import java.io.OutputStreamWriter; 10 | import java.io.Reader; 11 | import java.io.Writer; 12 | 13 | public abstract class GsonStreamSource implements IDataSource { 14 | 15 | protected final String id; 16 | 17 | protected final Gson gson; 18 | 19 | protected final Class cls; 20 | 21 | protected abstract InputStream createInputStream(); 22 | 23 | protected abstract OutputStream createOutputStream(); 24 | 25 | protected abstract String description(); 26 | 27 | protected abstract boolean sourceExists(); 28 | 29 | protected void afterWrite(Writer writer) throws IOException { 30 | writer.close(); 31 | } 32 | 33 | protected void afterRead(Reader reader) throws IOException { 34 | reader.close(); 35 | } 36 | 37 | public GsonStreamSource(String id, Class cls, Gson gson) { 38 | this.id = id; 39 | this.gson = gson; 40 | this.cls = cls; 41 | } 42 | 43 | @Override 44 | public String getId() { 45 | return id; 46 | } 47 | 48 | @Override 49 | public T retrieve() { 50 | if (!sourceExists()) return null; 51 | 52 | try { 53 | InputStream input = createInputStream(); 54 | Reader reader = new InputStreamReader(input, Charsets.UTF_8); 55 | try { 56 | return gson.fromJson(reader, cls); 57 | } finally { 58 | afterRead(reader); 59 | } 60 | } catch (Throwable t) { 61 | throw new IllegalStateException(String.format("Failed to read JSON data from file %s (id: %s)", description(), id), t); 62 | } 63 | } 64 | 65 | @Override 66 | public void store(T value) { 67 | try { 68 | OutputStream output = createOutputStream(); 69 | Writer writer = new OutputStreamWriter(output, Charsets.UTF_8); 70 | try { 71 | gson.toJson(value, writer); 72 | } finally { 73 | afterWrite(writer); 74 | } 75 | } catch (Throwable t) { 76 | throw new IllegalStateException(String.format("Failed to save JSON data to file %s (id: %s)", description(), id), t); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/openeye/storage/GsonWorkingStorage.java: -------------------------------------------------------------------------------- 1 | package openeye.storage; 2 | 3 | import com.google.common.collect.ImmutableList; 4 | import com.google.common.collect.Maps; 5 | import com.google.gson.Gson; 6 | import java.io.File; 7 | import java.util.Collection; 8 | import java.util.Map; 9 | import java.util.regex.Matcher; 10 | import java.util.regex.Pattern; 11 | 12 | public class GsonWorkingStorage extends GsonDirStorage implements IWorkingStorage { 13 | 14 | private final Map> sources = Maps.newHashMap(); 15 | 16 | public GsonWorkingStorage(File dir, String prefix, Class cls, Gson gson) { 17 | super(dir, prefix, cls, gson, "json"); 18 | 19 | Pattern filePattern = Pattern.compile(prefix + "-(.+)\\.json"); 20 | for (File file : dir.listFiles()) { 21 | String name = file.getName(); 22 | Matcher m = filePattern.matcher(name); 23 | if (m.matches()) { 24 | String id = m.group(1); 25 | sources.put(id, createFromFile(id, file)); 26 | } 27 | } 28 | } 29 | 30 | @Override 31 | protected synchronized void removeEntry(String id) { 32 | sources.remove(id); 33 | } 34 | 35 | @Override 36 | public synchronized Collection> listAll() { 37 | return ImmutableList.copyOf(sources.values()); 38 | } 39 | 40 | @Override 41 | public synchronized IDataSource getById(String id) { 42 | return sources.get(id); 43 | } 44 | 45 | @Override 46 | public synchronized IDataSource createNew() { 47 | String idPrefix = generateId(); 48 | int count = 0; 49 | String id; 50 | do { 51 | id = idPrefix + "-" + count++; 52 | } while (sources.containsKey(id)); 53 | 54 | return createNew(id); 55 | } 56 | 57 | @Override 58 | public synchronized IDataSource createNew(String id) { 59 | IDataSource newSource = super.createNew(id); 60 | sources.put(id, newSource); 61 | return newSource; 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/openeye/storage/IAppendableStorage.java: -------------------------------------------------------------------------------- 1 | package openeye.storage; 2 | 3 | public interface IAppendableStorage { 4 | public IDataSource createNew(); 5 | 6 | public IDataSource createNew(String id); 7 | } -------------------------------------------------------------------------------- /src/main/java/openeye/storage/IDataSource.java: -------------------------------------------------------------------------------- 1 | package openeye.storage; 2 | 3 | public interface IDataSource { 4 | public String getId(); 5 | 6 | public T retrieve(); 7 | 8 | public void store(T value); 9 | 10 | public void delete(); 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/openeye/storage/IQueryableStorage.java: -------------------------------------------------------------------------------- 1 | package openeye.storage; 2 | 3 | import java.util.Collection; 4 | 5 | public interface IQueryableStorage { 6 | 7 | public Collection> listAll(); 8 | 9 | public IDataSource getById(String id); 10 | } -------------------------------------------------------------------------------- /src/main/java/openeye/storage/IWorkingStorage.java: -------------------------------------------------------------------------------- 1 | package openeye.storage; 2 | 3 | public interface IWorkingStorage extends IAppendableStorage, IQueryableStorage { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/openeye/struct/TypedCollections.java: -------------------------------------------------------------------------------- 1 | package openeye.struct; 2 | 3 | import com.google.common.collect.BiMap; 4 | import com.google.common.collect.HashBiMap; 5 | import com.google.gson.JsonArray; 6 | import com.google.gson.JsonDeserializationContext; 7 | import com.google.gson.JsonDeserializer; 8 | import com.google.gson.JsonElement; 9 | import com.google.gson.JsonObject; 10 | import com.google.gson.JsonParseException; 11 | import com.google.gson.JsonSerializationContext; 12 | import com.google.gson.JsonSerializer; 13 | import java.lang.reflect.Type; 14 | import java.util.ArrayList; 15 | import java.util.Collection; 16 | import openeye.Log; 17 | import openeye.protocol.reports.IReport; 18 | import openeye.protocol.reports.ReportAnalytics; 19 | import openeye.protocol.reports.ReportCrash; 20 | import openeye.protocol.reports.ReportFileContents; 21 | import openeye.protocol.reports.ReportFileInfo; 22 | import openeye.protocol.reports.ReportKnownFiles; 23 | import openeye.protocol.reports.ReportPing; 24 | import openeye.protocol.responses.ResponseError; 25 | import openeye.protocol.responses.ResponseFileContents; 26 | import openeye.protocol.responses.ResponseFileInfo; 27 | import openeye.protocol.responses.ResponseKnownCrash; 28 | import openeye.protocol.responses.ResponseModMsg; 29 | import openeye.protocol.responses.ResponsePong; 30 | import openeye.protocol.responses.ResponseRemoveFile; 31 | import openeye.protocol.responses.ResponseSuspend; 32 | import openeye.responses.IExecutableResponse; 33 | import openeye.responses.ResponseErrorAction; 34 | import openeye.responses.ResponseFileContentsAction; 35 | import openeye.responses.ResponseFileInfoAction; 36 | import openeye.responses.ResponseKnownCrashAction; 37 | import openeye.responses.ResponseModMsgAction; 38 | import openeye.responses.ResponsePongAction; 39 | import openeye.responses.ResponseRemoveFileAction; 40 | import openeye.responses.ResponseSuspendAction; 41 | 42 | public class TypedCollections { 43 | 44 | private abstract static class TypedListConverter implements JsonSerializer>, JsonDeserializer> { 45 | private final BiMap> mapping; 46 | 47 | private TypedListConverter(BiMap> mapping) { 48 | this.mapping = mapping; 49 | } 50 | 51 | @Override 52 | public JsonElement serialize(Collection src, Type typeOfSrc, JsonSerializationContext context) { 53 | JsonArray result = new JsonArray(); 54 | 55 | for (T entry : src) { 56 | final Class entryClass = entry.getClass(); 57 | final String type = mapping.inverse().get(entryClass); 58 | if (type != null) { 59 | JsonObject serializedReport = context.serialize(entry).getAsJsonObject(); 60 | serializedReport.addProperty("type", type); 61 | result.add(serializedReport); 62 | } else Log.warn("Trying to serialize class without mapping: %s", entryClass); 63 | } 64 | 65 | return result; 66 | } 67 | 68 | @Override 69 | public Collection deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { 70 | JsonArray requests = json.getAsJsonArray(); 71 | 72 | Collection result = createCollection(); 73 | for (JsonElement e : requests) { 74 | try { 75 | JsonObject obj = e.getAsJsonObject(); 76 | JsonElement type = obj.get("type"); 77 | String typeId = type.getAsString(); 78 | 79 | Class cls = mapping.get(typeId); 80 | if (cls != null) { 81 | T request = context.deserialize(obj, cls); 82 | result.add(request); 83 | } else Log.warn("Invalid request type: %s", typeId); 84 | } catch (Throwable t) { 85 | Log.warn(t, "Failed to deserialize request %s", e); 86 | } 87 | } 88 | 89 | return result; 90 | } 91 | 92 | protected abstract Collection createCollection(); 93 | } 94 | 95 | public static class ReportsList extends ArrayList { 96 | private static final long serialVersionUID = -6580030458427773185L; 97 | } 98 | 99 | public static class ResponseList extends ArrayList { 100 | private static final long serialVersionUID = 4069373518963113118L; 101 | } 102 | 103 | private static final BiMap> REPORTS_TYPES = HashBiMap.create(); 104 | private static final BiMap> RESPONSE_TYPES = HashBiMap.create(); 105 | 106 | public static final Object REPORT_LIST_CONVERTER = new TypedListConverter(REPORTS_TYPES) { 107 | @Override 108 | protected Collection createCollection() { 109 | return new ReportsList(); 110 | } 111 | }; 112 | 113 | public static final Object RESPONSE_LIST_CONVERTER = new TypedListConverter(RESPONSE_TYPES) { 114 | @Override 115 | protected Collection createCollection() { 116 | return new ResponseList(); 117 | } 118 | }; 119 | 120 | static { 121 | REPORTS_TYPES.put(ReportAnalytics.TYPE, ReportAnalytics.class); 122 | REPORTS_TYPES.put(ReportFileInfo.TYPE, ReportFileInfo.class); 123 | REPORTS_TYPES.put(ReportCrash.TYPE, ReportCrash.class); 124 | REPORTS_TYPES.put(ReportPing.TYPE, ReportPing.class); 125 | REPORTS_TYPES.put(ReportKnownFiles.TYPE, ReportKnownFiles.class); 126 | REPORTS_TYPES.put(ReportFileContents.TYPE, ReportFileContents.class); 127 | 128 | RESPONSE_TYPES.put(ResponseFileInfo.TYPE, ResponseFileInfoAction.class); 129 | RESPONSE_TYPES.put(ResponsePong.TYPE, ResponsePongAction.class); 130 | RESPONSE_TYPES.put(ResponseFileContents.TYPE, ResponseFileContentsAction.class); 131 | RESPONSE_TYPES.put(ResponseRemoveFile.TYPE, ResponseRemoveFileAction.class); 132 | RESPONSE_TYPES.put(ResponseModMsg.TYPE, ResponseModMsgAction.class); 133 | RESPONSE_TYPES.put(ResponseError.TYPE, ResponseErrorAction.class); 134 | RESPONSE_TYPES.put(ResponseKnownCrash.TYPE, ResponseKnownCrashAction.class); 135 | RESPONSE_TYPES.put(ResponseSuspend.TYPE, ResponseSuspendAction.class); 136 | } 137 | 138 | } 139 | -------------------------------------------------------------------------------- /src/main/java/openeye/utils/NameCollector.java: -------------------------------------------------------------------------------- 1 | package openeye.utils; 2 | 3 | import com.mojang.authlib.GameProfile; 4 | import net.minecraft.entity.player.EntityPlayer; 5 | import net.minecraftforge.common.MinecraftForge; 6 | import net.minecraftforge.event.entity.EntityJoinWorldEvent; 7 | import net.minecraftforge.event.world.WorldEvent; 8 | import net.minecraftforge.fml.common.eventhandler.EventPriority; 9 | import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; 10 | import net.minecraftforge.fml.common.gameevent.PlayerEvent; 11 | import openeye.logic.Sanitizers; 12 | 13 | public abstract class NameCollector { 14 | 15 | private static void tryAddPlayer(EntityPlayer player) { 16 | GameProfile profile = player.getGameProfile(); 17 | if (profile != null) { 18 | Sanitizers.mainSanitizer.addPre(Sanitizers.PRIORITY_PLAYER_ID, Sanitizers.replaceNoDuplicates(profile.getId(), "[player id]")); 19 | Sanitizers.mainSanitizer.addPre(Sanitizers.PRIORITY_PLAYER_NAME, Sanitizers.replaceNoDuplicates(profile.getName(), "[player name]")); 20 | } 21 | } 22 | 23 | public static class Hooks { 24 | @SubscribeEvent(priority = EventPriority.HIGHEST) 25 | public void onWorldLoad(WorldEvent.Load evt) { 26 | Sanitizers.addWorldNames(evt.getWorld()); 27 | } 28 | 29 | @SubscribeEvent(priority = EventPriority.HIGHEST) 30 | public void onEntityJoin(EntityJoinWorldEvent evt) { 31 | if (evt.getEntity() instanceof EntityPlayer) tryAddPlayer((EntityPlayer)evt.getEntity()); 32 | } 33 | 34 | public void onPlayerLogin(PlayerEvent.PlayerLoggedInEvent evt) { 35 | tryAddPlayer(evt.player); 36 | } 37 | } 38 | 39 | public static void register() { 40 | MinecraftForge.EVENT_BUS.register(new Hooks()); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/resources/assets/openeye/lang/de_DE.lang: -------------------------------------------------------------------------------- 1 | openeye.main_screen.remove_file=OpenEye empfiehlt das Löschen einer Datei 2 | openeye.main_screen.known_crash=OpenEye hat Informationen über einen oder mehrere deiner letzten Programmabstürze 3 | openeye.main_screen.crash_reported=Abstürze auf Server gespeichert 4 | openeye.main_screen.intro=Keine Panik! Klicke auf den blinkenden Knopf, um zu sehen, was los ist. 5 | 6 | openeye.notes.goto_page=Gehe zu URL 7 | 8 | openeye.notes.title.resolved_crash=Lösung für kürzlichen Absturz auf der Seite verfügbar 9 | openeye.notes.title.reported_crash=Absturzbericht zu Server gesendet 10 | openeye.notes.title.note=Anmerkung zu Datei %s 11 | openeye.notes.title.remove_file=Rückruf-Forderung für Datei: %s 12 | 13 | openeye.notes.content.remove_file=OpenEye empfiehlt das Löschen der Datei %s, da sie fehlerhaft sein oder verdächtigen Inhalt haben könnte. Gehe zur Website für Details. 14 | 15 | openeye.note.title.intro1=Willkommen zu OpenEye 16 | openeye.note.content.intro1=OpenEye ist ein Werkzeug, das anonyme Daten über Mod-Nutzung und Absturzberichte sammelt. 17 | 18 | openeye.note.title.intro2=Statistiken 19 | openeye.note.content.intro2=Wähle dieses Feld und klicke "Gehe zu URL", um von diesem Werkzeug gesammelte Statistiken einzusehen. Andere Felder haben auch entsprechende URLs. 20 | 21 | openeye.note.title.intro3=Gesammelte Daten 22 | openeye.note.content.intro3=Die URL dieses Feldes beinhaltet eine Zusammenfassung der Daten, die von OpenEye gesammelt werden. 23 | 24 | openeye.note.title.intro4=Lokale Daten 25 | openeye.note.content.intro4=OpenEye speichert alle gesendeten und empfangenen Informationen in dem Ordner "reports" deiner Minecraft-Installation. Klicke "Gehe zu URL", um diesesn Ordner zu öffnen. 26 | 27 | openeye.note.title.intro5=Quellcode 28 | openeye.note.content.intro5=OpenEye ist eine quelloffene Modifikation. Der Quellcode ist einsehbar auf GitHub. 29 | 30 | openeye.note.title.intro6=Deaktivierung & Konfiguration 31 | openeye.note.content.intro6=OpenEye nutzt die gewöhnlichen Datenerhebungseinstellungen. Sollte dir das nicht reichen, kannst du diese Modifikation auch problemlos entfernen. 32 | 33 | #openeye.note.title.old_java=Old Java version ## NEEDS TRANSLATION ## 34 | 35 | #openeye.note.content.old_java_recoverable=Your Java is too old to handle secure connection to OpenEye server. Fortunately, client was able to provide missing data. Please consider updating to latest version. ## NEEDS TRANSLATION ## 36 | #openeye.note.content.old_java_total_failure=Your Java is too old to handle secure connection to OpenEye server. Please update! ## NEEDS TRANSLATION ## 37 | 38 | #openeye.note.title.suspended=Communication with server suspended ## NEEDS TRANSLATION ## 39 | #openeye.note.content.suspended_no_reason=Server requested to suspend communication until %s ## NEEDS TRANSLATION ## 40 | #openeye.note.content.suspended=Server requested to suspend communication until %s, reason: "%s" ## NEEDS TRANSLATION ## 41 | 42 | openeye.chat.note=Anmerkung %s, Typ: %s 43 | openeye.chat.store_failed=Speichern der Anmerkungen fehlgeschlagen! 44 | openeye.chat.dumped=Anmerkungen in Datei gespeichert: %s 45 | 46 | openeye.category.critical=Kritisch 47 | openeye.category.alert=Alarm 48 | openeye.category.warning=Warnung 49 | openeye.category.info=Info 50 | openeye.category.system_info=System-Info 51 | openeye.category.remove_file=Datei entfernen 52 | openeye.level.reported_crash=Berichteter Programmabsturz 53 | openeye.level.resolved_crash=Gelöster Programmabsturz 54 | -------------------------------------------------------------------------------- /src/main/resources/assets/openeye/lang/en_US.lang: -------------------------------------------------------------------------------- 1 | openeye.main_screen.remove_file=OpenEye suggests removing file 2 | openeye.main_screen.known_crash=OpenEye has information about one or more of your recent crashes 3 | openeye.main_screen.crash_reported=Crashes stored to server 4 | openeye.main_screen.intro=DON'T PANIC! Click flashing button to find out what's going on. 5 | 6 | openeye.notes.goto_page=Go to URL 7 | 8 | openeye.notes.title.resolved_crash=Resolution for recent crash available on site 9 | openeye.notes.title.reported_crash=Crash report sent to server 10 | openeye.notes.title.note=Note for file %s 11 | openeye.notes.title.remove_file=Recall request for file: %s 12 | 13 | openeye.notes.content.remove_file=OpenEye suggests removing file %s, since it may be invalid or contain suspicious contents. Go to website for details. 14 | 15 | openeye.note.title.intro1=Welcome to Open Eye 16 | openeye.note.content.intro1=OpenEye is utility that collects anonymous data about mod usage and crash reports. 17 | 18 | openeye.note.title.intro2=Statistics 19 | openeye.note.content.intro2=Select this item and click "Go to URL" to see statistics collected by this tool. Other items also have assigned URLs. 20 | 21 | openeye.note.title.intro3=Collected data 22 | openeye.note.content.intro3=URL in this item contains summary of data collected by OpenEye. 23 | 24 | openeye.note.title.intro4=Local data 25 | openeye.note.content.intro4=OpenEye stores all sent and received information in reports folder in your MC installation. Click Go to URL to open that folder. 26 | 27 | openeye.note.title.intro5=Source code 28 | openeye.note.content.intro5=OpenEye is open source mod. Source code available on GitHub. 29 | 30 | openeye.note.title.intro6=Disabling & configuration 31 | openeye.note.content.intro6=OpenEye uses vanilla snooper setting. If you are not satisfied with it, you can also safely remove this mod. 32 | 33 | openeye.note.title.old_java=Old Java version 34 | 35 | openeye.note.content.old_java_recoverable=Your Java is too old to handle secure connection to OpenEye server. Fortunately, client was able to provide missing data. Please consider updating to latest version. 36 | openeye.note.content.old_java_total_failure=Your Java is too old to handle secure connection to OpenEye server. Please update! 37 | 38 | openeye.note.title.suspended=Communication with server suspended 39 | openeye.note.content.suspended_no_reason=Server requested to suspend communication until %s 40 | openeye.note.content.suspended=Server requested to suspend communication until %s, reason: "%s" 41 | 42 | openeye.chat.note=Note %s, type: %s 43 | openeye.chat.store_failed=Failed to store notes! 44 | openeye.chat.dumped=Dumped notes to file %s 45 | 46 | openeye.category.critical=Critical 47 | openeye.category.alert=Alert 48 | openeye.category.warning=Warning 49 | openeye.category.info=Info 50 | openeye.category.system_info=System Info 51 | openeye.category.remove_file=Remove file 52 | openeye.level.reported_crash=Reported Crash 53 | openeye.level.resolved_crash=Resolved Crash -------------------------------------------------------------------------------- /src/main/resources/assets/openeye/lang/ru_RU.lang: -------------------------------------------------------------------------------- 1 | openeye.main_screen.remove_file=OpenEye советует удалить файл 2 | openeye.main_screen.known_crash=OpenEye имеет информацию об одном или нескольких из Ваших недавних вылетов 3 | openeye.main_screen.crash_reported=Вылеты хранятся на сервере 4 | openeye.main_screen.intro=НЕ ПАНИКУЙТЕ! Нажмите мигающую кнопку, чтобы выяснить, что происходит. 5 | 6 | openeye.notes.goto_page=Перейти по ссылке 7 | 8 | openeye.notes.title.resolved_crash=Решение для недавнего вылета доступно на сайте 9 | openeye.notes.title.reported_crash=Отчёт о вылете отправлен на сервер 10 | openeye.notes.title.note=Примечание для файла %s 11 | openeye.notes.title.remove_file=Повторное напоминание для файла: %s 12 | 13 | openeye.notes.content.remove_file=OpenEye советует удалить файл %s, так как он может быть недействительным или содержать подозрительный контент. Перейдите на сайт для информации. 14 | 15 | openeye.note.title.intro1=Добро пожаловать в Open Eye 16 | openeye.note.content.intro1=OpenEye - утилита, которая собирает анонимные данные о использовании модификаций и отчёты о вылетах 17 | 18 | openeye.note.title.intro2=Статистика 19 | openeye.note.content.intro2=Выберите этот пункт и нажмите "Перейти по ссылке", чтобы увидеть статистику, собранную этим инструментом. Другие пункты также имеют назначенные ссылки. 20 | 21 | openeye.note.title.intro3=Собранные данные 22 | openeye.note.content.intro3=Ссылка в этом пункте содержит сводку данных, собранных OpenEye 23 | 24 | openeye.note.title.intro4=Локальные данные 25 | openeye.note.content.intro4=OpenEye хранит всю отправленную и принятую информацию в папке отчётов, в Вашей папке MC. Нажмите "Перейти по ссылке", чтобы открыть эту папку. 26 | 27 | openeye.note.title.intro5=Исходный код 28 | openeye.note.content.intro5=OpenEye - модификация с открытым исходным кодом. Исходный код доступен на GitHub. 29 | 30 | openeye.note.title.intro6=Отключение и конфигурация 31 | openeye.note.content.intro6=OpenEye использует настройки сбора информации. Если Вы не удовлетворены ими, Вы также можете безопасно удалить эту модификацию. 32 | 33 | #openeye.note.title.old_java=Old Java version ## NEEDS TRANSLATION ## 34 | 35 | #openeye.note.content.old_java_recoverable=Your Java is too old to handle secure connection to OpenEye server. Fortunately, client was able to provide missing data. Please consider updating to latest version. ## NEEDS TRANSLATION ## 36 | #openeye.note.content.old_java_total_failure=Your Java is too old to handle secure connection to OpenEye server. Please update! ## NEEDS TRANSLATION ## 37 | 38 | #openeye.note.title.suspended=Communication with server suspended ## NEEDS TRANSLATION ## 39 | #openeye.note.content.suspended_no_reason=Server requested to suspend communication until %s ## NEEDS TRANSLATION ## 40 | #openeye.note.content.suspended=Server requested to suspend communication until %s, reason: "%s" ## NEEDS TRANSLATION ## 41 | 42 | openeye.chat.note=Примечание %s, тип: %s 43 | openeye.chat.store_failed=Не удалось сохранить примечания! 44 | openeye.chat.dumped=Примечания сохранены в файл %s 45 | 46 | openeye.category.critical=Критический 47 | openeye.category.alert=Тревога 48 | openeye.category.warning=Предупреждение 49 | openeye.category.info=Информация 50 | openeye.category.system_info=Информация о системе 51 | openeye.category.remove_file=Удалить файл 52 | openeye.level.reported_crash=Сообщённый вылет 53 | openeye.level.resolved_crash=Исправленный вылет 54 | -------------------------------------------------------------------------------- /src/main/resources/assets/openeye/textures/gui/buttons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenMods/OpenData/e657ca3decdba416b9741e375b8c5122887de874/src/main/resources/assets/openeye/textures/gui/buttons.png -------------------------------------------------------------------------------- /src/main/resources/identrust_root_x3.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/ 3 | MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT 4 | DkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow 5 | PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD 6 | Ew5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB 7 | AN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O 8 | rz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq 9 | OLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b 10 | xiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw 11 | 7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD 12 | aeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV 13 | HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG 14 | SIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69 15 | ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr 16 | AvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz 17 | R8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5 18 | JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo 19 | Ob8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ 20 | -----END CERTIFICATE----- -------------------------------------------------------------------------------- /src/main/resources/isrg_root_x1.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw 3 | TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh 4 | cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 5 | WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu 6 | ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY 7 | MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc 8 | h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ 9 | 0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U 10 | A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW 11 | T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH 12 | B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC 13 | B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv 14 | KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn 15 | OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn 16 | jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw 17 | qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI 18 | rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV 19 | HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq 20 | hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL 21 | ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ 22 | 3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK 23 | NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 24 | ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur 25 | TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC 26 | jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc 27 | oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq 28 | 4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA 29 | mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d 30 | emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= 31 | -----END CERTIFICATE----- 32 | -------------------------------------------------------------------------------- /src/main/resources/mcmod.info: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "modid" : "OpenEye", 4 | "name" : "OpenEye", 5 | "version" : "${version}", 6 | "mcversion" : "${mc_version}", 7 | "url" : "https://openeye.openmods.info", 8 | "credits" : "", 9 | "authors": [ 10 | "boq", "Mikee" 11 | ], 12 | "description": "Exception and analytics collector", 13 | "logoFile" : "", 14 | "updateUrl" : "", 15 | "parent" : "", 16 | "screenshots": [ 17 | ] 18 | }] 19 | --------------------------------------------------------------------------------