├── .gitignore ├── README.md ├── build.gradle ├── gradle └── wrapper │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src └── main ├── java └── xproxy │ ├── Xproxy.java │ ├── conf │ └── XproxyConfig.java │ ├── core │ ├── AttributeKeys.java │ ├── Connection.java │ ├── Independent.java │ └── RequestContext.java │ ├── downstream │ ├── DownStreamHandler.java │ └── XproxyDownStreamChannelInitializer.java │ ├── upstream │ ├── UpStreamHandler.java │ ├── XproxyUpStreamChannelInitializer.java │ └── lb │ │ ├── RoundRobin.java │ │ └── RoundRobinFactory.java │ └── util │ ├── AntPathMatcher.java │ └── PlatformUtil.java └── resources ├── logback.xml └── xproxy.yml /.gitignore: -------------------------------------------------------------------------------- 1 | # maven ignore 2 | target/ 3 | *.jar 4 | *.war 5 | *.zip 6 | *.tar 7 | *.tar.gz 8 | 9 | #vertx 10 | .vertx/ 11 | 12 | # gradle 13 | .gradletasknamecache 14 | .gradle/ 15 | bin/ 16 | build/ 17 | 18 | # eclipse ignore 19 | .settings/ 20 | .project 21 | .classpath 22 | 23 | # idea ignore 24 | .idea/ 25 | *.ipr 26 | *.iml 27 | *.iws 28 | 29 | # temp ignore 30 | *.log 31 | *.cache 32 | *.diff 33 | *.patch 34 | *.tmp 35 | 36 | # system ignore 37 | .DS_Store 38 | Thumbs.db 39 | 40 | classes/ 41 | .factorypath 42 | .apt_generated/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Xproxy 2 | > Mini Nginx, High Performance ^-^ ! 3 | 4 | ## config 5 | 6 | ~~~yml 7 | # accept connections on the specified port 8 | listen: 8000 9 | 10 | # keepalive timeout for all downstream connetions, second 11 | keepalive_timeout: 65 12 | 13 | # netty worker threads(auto = cpu cores) 14 | worker_threads: auto 15 | 16 | # max connections per worker 17 | worker_connections: 102400 18 | 19 | # all virtual hosts configurations 20 | servers: 21 | localhost1: 22 | - 23 | path: /* 24 | proxy_pass: http://localhost1_pool 25 | localhost2: 26 | - 27 | path: /* 28 | proxy_pass: http://localhost2_pool 29 | 30 | # all upstream configurations 31 | upstreams: 32 | localhost1_pool: 33 | keepalive: 16 # for all backends in current pool 34 | servers: 35 | - 127.0.0.1:8080 36 | - 127.0.0.2:8080 37 | localhost2_pool: 38 | keepalive: 32 # for all backends in current pool 39 | servers: 40 | - 127.0.0.1:8088 41 | - 127.0.0.2:8088 42 | - 127.0.0.3:8088 43 | - 127.0.0.4:8088 44 | ~~~ -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java' 2 | apply plugin: 'application' 3 | 4 | sourceCompatibility = 1.8 5 | targetCompatibility = 1.8 6 | 7 | mainClassName = "xproxy.Xproxy" 8 | 9 | applicationDefaultJvmArgs = ["-Xmn1280m", 10 | "-Xms3g", 11 | "-Xmx3g", 12 | "-XX:MetaspaceSize=256m", 13 | "-XX:MaxMetaspaceSize=256m", 14 | "-XX:+UseConcMarkSweepGC", 15 | "-XX:+UseCMSInitiatingOccupancyOnly", 16 | "-XX:CMSInitiatingOccupancyFraction=80", 17 | "-Xloggc:Xproxy.log", 18 | "-XX:+PrintGCDetails", 19 | "-XX:+PrintGCDateStamps", 20 | "-XX:+PrintTenuringDistribution", 21 | "-XX:+HeapDumpOnOutOfMemoryError", 22 | "-XX:HeapDumpPath=Xproxy.hprof", 23 | "-XX:+DisableExplicitGC", 24 | "-XX:+UseBiasedLocking"] 25 | 26 | applicationDistribution.from("src/main/resources") { 27 | into "conf" 28 | } 29 | 30 | repositories{ 31 | mavenLocal(); 32 | mavenCentral() 33 | } 34 | 35 | dependencies { 36 | compile 'org.slf4j:slf4j-api:1.7.6' 37 | compile 'ch.qos.logback:logback-core:1.1.2' 38 | compile 'ch.qos.logback:logback-classic:1.1.2' 39 | compile 'io.netty:netty-all:4.1.11.Final' 40 | compile 'org.apache.commons:commons-lang3:3.5' 41 | compile 'commons-collections:commons-collections:3.2.2' 42 | compile 'com.fasterxml.jackson.core:jackson-databind:2.8.8' 43 | compile 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.8.8' 44 | testCompile 'junit:junit:4.11' 45 | } 46 | 47 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri May 26 20:52:43 CST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-bin.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn ( ) { 37 | echo "$*" 38 | } 39 | 40 | die ( ) { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 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 | # Escape application args 158 | save ( ) { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /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 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 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 Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'Xproxy' 2 | -------------------------------------------------------------------------------- /src/main/java/xproxy/Xproxy.java: -------------------------------------------------------------------------------- 1 | package xproxy; 2 | 3 | import java.util.Arrays; 4 | 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | import io.netty.bootstrap.ServerBootstrap; 9 | import io.netty.buffer.PooledByteBufAllocator; 10 | import io.netty.channel.Channel; 11 | import io.netty.channel.ChannelOption; 12 | import io.netty.channel.EventLoopGroup; 13 | import io.netty.channel.WriteBufferWaterMark; 14 | import io.netty.util.concurrent.DefaultThreadFactory; 15 | import xproxy.conf.XproxyConfig; 16 | import xproxy.conf.XproxyConfig.ConfigException; 17 | import xproxy.core.Independent; 18 | import xproxy.downstream.DownStreamHandler; 19 | import xproxy.downstream.XproxyDownStreamChannelInitializer; 20 | import xproxy.upstream.lb.RoundRobinFactory; 21 | 22 | public class Xproxy { 23 | 24 | private static final Logger logger = LoggerFactory.getLogger(Xproxy.class); 25 | 26 | private final RoundRobinFactory robinFactory = new RoundRobinFactory(); 27 | 28 | private DownStreamHandler downStreamHandler; 29 | 30 | public static void main(String[] args) { 31 | Xproxy xproxy = new Xproxy(); 32 | try { 33 | xproxy.initializeAndRun(args); 34 | } catch (ConfigException e) { 35 | logger.error("Invalid config, exiting abnormally", e); 36 | System.err.println("Invalid config, exiting abnormally"); 37 | System.exit(2); 38 | } 39 | } 40 | 41 | protected void initializeAndRun(String[] args) throws ConfigException { 42 | XproxyConfig config = new XproxyConfig(); 43 | if (args.length == 1) { 44 | config.parse(args[0]); 45 | } else { 46 | throw new IllegalArgumentException("Invalid args:" + Arrays.toString(args)); 47 | } 48 | robinFactory.init(config); 49 | downStreamHandler = new DownStreamHandler(config, robinFactory); 50 | runFromConfig(config); 51 | } 52 | 53 | public void runFromConfig(XproxyConfig config) { 54 | 55 | EventLoopGroup bossGroup = Independent.newEventLoopGroup(1, new DefaultThreadFactory("Xproxy-Boss-Thread")); 56 | EventLoopGroup workerGroup = Independent.newEventLoopGroup(config.workerThreads(), 57 | new DefaultThreadFactory("Xproxy-Downstream-Worker-Thread")); 58 | 59 | try { 60 | ServerBootstrap b = new ServerBootstrap(); 61 | b.group(bossGroup, workerGroup); 62 | b.channel(Independent.serverChannelClass()); 63 | 64 | // connections wait for accept(influenced by maxconn) 65 | b.option(ChannelOption.SO_BACKLOG, 1024); 66 | b.option(ChannelOption.SO_REUSEADDR, true); 67 | b.childOption(ChannelOption.SO_KEEPALIVE, true); 68 | b.childOption(ChannelOption.TCP_NODELAY, true); 69 | b.childOption(ChannelOption.SO_SNDBUF, 32 * 1024); 70 | b.childOption(ChannelOption.SO_RCVBUF, 32 * 1024); 71 | // temporary settings, need more tests 72 | b.childOption(ChannelOption.WRITE_BUFFER_WATER_MARK, new WriteBufferWaterMark(8 * 1024, 32 * 1024)); 73 | b.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT); 74 | // default is true, reduce thread context switching 75 | b.childOption(ChannelOption.SINGLE_EVENTEXECUTOR_PER_GROUP, true); 76 | 77 | b.childHandler(new XproxyDownStreamChannelInitializer(config, downStreamHandler)); 78 | 79 | Channel ch = b.bind(config.listen()).syncUninterruptibly().channel(); 80 | 81 | logger.info(String.format("bind to %d success.", config.listen())); 82 | 83 | ch.closeFuture().syncUninterruptibly(); 84 | } finally { 85 | bossGroup.shutdownGracefully(); 86 | workerGroup.shutdownGracefully(); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/xproxy/conf/XproxyConfig.java: -------------------------------------------------------------------------------- 1 | package xproxy.conf; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.util.ArrayList; 6 | import java.util.HashMap; 7 | import java.util.List; 8 | import java.util.Map; 9 | import java.util.Map.Entry; 10 | 11 | import org.apache.commons.collections.CollectionUtils; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | 15 | import com.fasterxml.jackson.annotation.JsonProperty; 16 | import com.fasterxml.jackson.databind.DeserializationFeature; 17 | import com.fasterxml.jackson.databind.ObjectMapper; 18 | import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; 19 | 20 | import xproxy.util.AntPathMatcher; 21 | 22 | public class XproxyConfig { 23 | 24 | private static final Logger logger = LoggerFactory.getLogger(XproxyConfig.class); 25 | 26 | private static final String UPSTREAM_POOL_PREFIX = "http://"; 27 | 28 | private static final String AUTO = "auto"; 29 | 30 | private static final int DEFAULT_HTTP_PORT = 80; 31 | 32 | private final AntPathMatcher pathMatcher = new AntPathMatcher(); 33 | 34 | @JsonProperty("listen") 35 | private int listen; 36 | 37 | @JsonProperty("keepalive_timeout") 38 | private int keepaliveTimeout; 39 | 40 | @JsonProperty("worker_threads") 41 | private String workerThreads; 42 | private int workers; 43 | 44 | @JsonProperty("worker_connections") 45 | private int workerConnections; 46 | 47 | @JsonProperty("servers") 48 | private Map> servers; 49 | 50 | @JsonProperty("upstreams") 51 | private Map upstreams; 52 | 53 | private Map> us = new HashMap<>(); 54 | 55 | public void parse(String path) throws ConfigException { 56 | File configFile = new File(path); 57 | 58 | logger.info("Reading configuration from: " + configFile); 59 | 60 | ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); 61 | try { 62 | if (!configFile.exists()) { 63 | throw new IllegalArgumentException(configFile.toString() + " file is missing"); 64 | } 65 | 66 | mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); 67 | parseConfig(mapper.readValue(new File(path), XproxyConfig.class)); 68 | } catch (IOException e) { 69 | throw new ConfigException("Error processing " + path, e); 70 | } catch (IllegalArgumentException e) { 71 | throw new ConfigException("Error processing " + path, e); 72 | } 73 | } 74 | 75 | public void parseConfig(XproxyConfig xproxyConfig) throws ConfigException { 76 | listen = xproxyConfig.listen; 77 | keepaliveTimeout = xproxyConfig.keepaliveTimeout; 78 | workerConnections = xproxyConfig.workerConnections; 79 | workerThreads = xproxyConfig.workerThreads; 80 | 81 | if (AUTO.equalsIgnoreCase(workerThreads)) { 82 | workers = Runtime.getRuntime().availableProcessors(); 83 | } else { 84 | try { 85 | workers = Integer.parseInt(workerThreads); 86 | } catch (NumberFormatException e) { 87 | throw new ConfigException("worker_threads invalid", e); 88 | } 89 | } 90 | 91 | upstreams = new HashMap<>(); 92 | for (Entry entry : xproxyConfig.upstreams.entrySet()) { 93 | upstreams.put(UPSTREAM_POOL_PREFIX + entry.getKey(), entry.getValue()); 94 | } 95 | 96 | servers = new HashMap<>(); 97 | if (DEFAULT_HTTP_PORT != listen) { 98 | for (Entry> entry : xproxyConfig.servers.entrySet()) { 99 | servers.put(entry.getKey() + ":" + listen, entry.getValue()); 100 | } 101 | } else { 102 | servers = xproxyConfig.servers; 103 | } 104 | 105 | List hosts; 106 | List servers; 107 | for (Entry upstreamEntry : upstreams.entrySet()) { 108 | hosts = upstreamEntry.getValue().servers(); 109 | if (CollectionUtils.isEmpty(hosts)) { 110 | continue; 111 | } 112 | servers = new ArrayList<>(1 << 2); 113 | for (String host : hosts) { 114 | servers.add(new Server(host, upstreamEntry.getValue().keepAlive())); 115 | } 116 | us.put(upstreamEntry.getKey(), servers); 117 | } 118 | } 119 | 120 | public int listen() { 121 | return listen; 122 | } 123 | 124 | public int keepaliveTimeout() { 125 | return keepaliveTimeout; 126 | } 127 | 128 | public int workerThreads() { 129 | return workers; 130 | } 131 | 132 | public int workerConnections() { 133 | return workerConnections; 134 | } 135 | 136 | public Map> upstreams() { 137 | return us; 138 | } 139 | 140 | public String proxyPass(String serverName, String uri) { 141 | List locations = servers.get(serverName); 142 | if (CollectionUtils.isEmpty(locations)) { 143 | return null; 144 | } 145 | for (Location location : locations) { 146 | if (pathMatcher.match(location.path(), uri)) { 147 | return location.proxypass(); 148 | } 149 | } 150 | return null; 151 | } 152 | 153 | @SuppressWarnings("serial") 154 | public static class ConfigException extends Exception { 155 | public ConfigException(String msg) { 156 | super(msg); 157 | } 158 | 159 | public ConfigException(String msg, Exception e) { 160 | super(msg, e); 161 | } 162 | } 163 | 164 | static class Location { 165 | 166 | @JsonProperty("path") 167 | private String path; 168 | 169 | @JsonProperty("proxy_pass") 170 | private String proxypass; 171 | 172 | public String path() { 173 | return path; 174 | } 175 | 176 | public String proxypass() { 177 | return proxypass; 178 | } 179 | } 180 | 181 | static class Upstream { 182 | // the maximum number of idle keepalive connections to upstream servers 183 | // that are preserved in the cache of each worker process 184 | @JsonProperty("keepalive") 185 | private int keepalive; 186 | 187 | @JsonProperty("servers") 188 | private List servers; 189 | 190 | public int keepAlive() { 191 | return keepalive; 192 | } 193 | 194 | public List servers() { 195 | return servers; 196 | } 197 | } 198 | 199 | public static class Server { 200 | 201 | private int keepalive; 202 | 203 | private String ip; 204 | 205 | private int port; 206 | 207 | public Server(String host, int keepalive) { 208 | this.keepalive = keepalive; 209 | int pidx = host.lastIndexOf(':'); 210 | if (pidx >= 0) { 211 | // otherwise : is at the end of the string, ignore 212 | if (pidx < host.length() - 1) { 213 | this.port = Integer.parseInt(host.substring(pidx + 1)); 214 | } 215 | this.ip = host.substring(0, pidx); 216 | } 217 | } 218 | 219 | public String getIp() { 220 | return ip; 221 | } 222 | 223 | public int getPort() { 224 | return port; 225 | } 226 | 227 | public int getKeepalive() { 228 | return keepalive; 229 | } 230 | 231 | @Override 232 | public int hashCode() { 233 | final int prime = 31; 234 | int result = 1; 235 | result = prime * result + ((ip == null) ? 0 : ip.hashCode()); 236 | result = prime * result + keepalive; 237 | result = prime * result + port; 238 | return result; 239 | } 240 | 241 | @Override 242 | public boolean equals(Object obj) { 243 | if (this == obj) 244 | return true; 245 | if (obj == null) 246 | return false; 247 | if (getClass() != obj.getClass()) 248 | return false; 249 | Server other = (Server) obj; 250 | if (ip == null) { 251 | if (other.ip != null) 252 | return false; 253 | } else if (!ip.equals(other.ip)) 254 | return false; 255 | if (keepalive != other.keepalive) 256 | return false; 257 | if (port != other.port) 258 | return false; 259 | return true; 260 | } 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /src/main/java/xproxy/core/AttributeKeys.java: -------------------------------------------------------------------------------- 1 | package xproxy.core; 2 | 3 | import io.netty.channel.Channel; 4 | import io.netty.util.AttributeKey; 5 | 6 | public class AttributeKeys { 7 | 8 | public static final AttributeKey DOWNSTREAM_CHANNEL_KEY = AttributeKey.valueOf("downstreamChannel"); 9 | 10 | public static final AttributeKey KEEP_ALIVED_KEY = AttributeKey.valueOf("keepalived"); 11 | 12 | public static final AttributeKey UPSTREAM_ACTIVE_CLOSE_KEY = AttributeKey.valueOf("upstreamActiveCloseKey"); 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/xproxy/core/Connection.java: -------------------------------------------------------------------------------- 1 | package xproxy.core; 2 | 3 | import io.netty.channel.Channel; 4 | import xproxy.conf.XproxyConfig.Server; 5 | 6 | public class Connection { 7 | 8 | private final Server server; 9 | 10 | private final Channel channel; 11 | 12 | public Connection(Server server, Channel channel) { 13 | this.server = server; 14 | this.channel = channel; 15 | } 16 | 17 | public Server getServer() { 18 | return server; 19 | } 20 | 21 | public Channel getChannel() { 22 | return channel; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/xproxy/core/Independent.java: -------------------------------------------------------------------------------- 1 | package xproxy.core; 2 | 3 | import java.util.concurrent.ThreadFactory; 4 | 5 | import io.netty.channel.Channel; 6 | import io.netty.channel.EventLoopGroup; 7 | import io.netty.channel.ServerChannel; 8 | import io.netty.channel.epoll.EpollEventLoopGroup; 9 | import io.netty.channel.epoll.EpollServerSocketChannel; 10 | import io.netty.channel.epoll.EpollSocketChannel; 11 | import io.netty.channel.kqueue.KQueueEventLoopGroup; 12 | import io.netty.channel.kqueue.KQueueServerSocketChannel; 13 | import io.netty.channel.kqueue.KQueueSocketChannel; 14 | import io.netty.channel.nio.NioEventLoopGroup; 15 | import io.netty.channel.socket.nio.NioServerSocketChannel; 16 | import io.netty.channel.socket.nio.NioSocketChannel; 17 | import xproxy.util.PlatformUtil; 18 | 19 | public class Independent { 20 | 21 | public static EventLoopGroup newEventLoopGroup(int nThreads, ThreadFactory threadFactory) { 22 | if (PlatformUtil.isLinux()) { 23 | return new EpollEventLoopGroup(nThreads, threadFactory); 24 | } else if (PlatformUtil.isMac()) { 25 | return new KQueueEventLoopGroup(nThreads, threadFactory); 26 | } else { 27 | return new NioEventLoopGroup(nThreads, threadFactory); 28 | } 29 | } 30 | 31 | public static Class channelClass() { 32 | if (PlatformUtil.isLinux()) { 33 | return EpollSocketChannel.class; 34 | } else if (PlatformUtil.isMac()) { 35 | return KQueueSocketChannel.class; 36 | } else { 37 | return NioSocketChannel.class; 38 | } 39 | } 40 | 41 | public static Class serverChannelClass() { 42 | if (PlatformUtil.isLinux()) { 43 | return EpollServerSocketChannel.class; 44 | } else if (PlatformUtil.isMac()) { 45 | return KQueueServerSocketChannel.class; 46 | } else { 47 | return NioServerSocketChannel.class; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/xproxy/core/RequestContext.java: -------------------------------------------------------------------------------- 1 | package xproxy.core; 2 | 3 | import java.util.HashMap; 4 | import java.util.LinkedList; 5 | import java.util.Map; 6 | 7 | import io.netty.handler.codec.http.DefaultFullHttpResponse; 8 | import io.netty.handler.codec.http.FullHttpResponse; 9 | import io.netty.handler.codec.http.HttpHeaderNames; 10 | import io.netty.handler.codec.http.HttpHeaderValues; 11 | import io.netty.handler.codec.http.HttpResponseStatus; 12 | import io.netty.handler.codec.http.HttpVersion; 13 | import io.netty.util.concurrent.FastThreadLocal; 14 | 15 | public class RequestContext { 16 | 17 | private static final FastThreadLocal CONTEXT = new FastThreadLocal() { 18 | @Override 19 | protected RequestContext initialValue() throws Exception { 20 | return new RequestContext(); 21 | } 22 | }; 23 | 24 | private final Map> keepAlivedConns = new HashMap<>(); 25 | 26 | private final FullHttpResponse errorResponse; 27 | 28 | private final FullHttpResponse notfoundResponse; 29 | 30 | private RequestContext() { 31 | errorResponse = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.INTERNAL_SERVER_ERROR); 32 | errorResponse.headers().set(HttpHeaderNames.CONTENT_LENGTH, errorResponse.content().readableBytes()); 33 | errorResponse.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); 34 | 35 | notfoundResponse = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NOT_FOUND); 36 | notfoundResponse.headers().set(HttpHeaderNames.CONTENT_LENGTH, notfoundResponse.content().readableBytes()); 37 | notfoundResponse.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); 38 | } 39 | 40 | public LinkedList getKeepAlivedConns(String proxypass) { 41 | LinkedList conns = keepAlivedConns.get(proxypass); 42 | if (null == conns) { 43 | conns = new LinkedList<>(); 44 | keepAlivedConns.put(proxypass, conns); 45 | } 46 | return keepAlivedConns.get(proxypass); 47 | } 48 | 49 | public FullHttpResponse getErrorResponse() { 50 | return errorResponse; 51 | } 52 | 53 | public FullHttpResponse getNotfoundResponse(){ 54 | return notfoundResponse; 55 | } 56 | 57 | public static LinkedList keepAlivedConntions(String proxypass) { 58 | return CONTEXT.get().getKeepAlivedConns(proxypass); 59 | } 60 | 61 | public static FullHttpResponse errorResponse() { 62 | return CONTEXT.get().getErrorResponse().retain(); 63 | } 64 | 65 | public static FullHttpResponse notfoundResponse() { 66 | return CONTEXT.get().getNotfoundResponse().retain(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/xproxy/downstream/DownStreamHandler.java: -------------------------------------------------------------------------------- 1 | package xproxy.downstream; 2 | 3 | import java.util.Iterator; 4 | import java.util.LinkedList; 5 | 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | 9 | import io.netty.bootstrap.Bootstrap; 10 | import io.netty.buffer.PooledByteBufAllocator; 11 | import io.netty.channel.Channel; 12 | import io.netty.channel.ChannelFuture; 13 | import io.netty.channel.ChannelFutureListener; 14 | import io.netty.channel.ChannelHandler; 15 | import io.netty.channel.ChannelHandlerContext; 16 | import io.netty.channel.ChannelOption; 17 | import io.netty.channel.SimpleChannelInboundHandler; 18 | import io.netty.channel.WriteBufferWaterMark; 19 | import io.netty.handler.codec.http.FullHttpRequest; 20 | import io.netty.handler.codec.http.HttpHeaderNames; 21 | import io.netty.handler.codec.http.HttpHeaders; 22 | import io.netty.handler.codec.http.HttpUtil; 23 | import io.netty.handler.codec.http.HttpVersion; 24 | import xproxy.conf.XproxyConfig; 25 | import xproxy.conf.XproxyConfig.Server; 26 | import xproxy.core.AttributeKeys; 27 | import xproxy.core.Connection; 28 | import xproxy.core.RequestContext; 29 | import xproxy.upstream.XproxyUpStreamChannelInitializer; 30 | import xproxy.upstream.lb.RoundRobin; 31 | import xproxy.upstream.lb.RoundRobinFactory; 32 | 33 | @ChannelHandler.Sharable 34 | public class DownStreamHandler extends SimpleChannelInboundHandler { 35 | 36 | private static final Logger logger = LoggerFactory.getLogger(DownStreamHandler.class); 37 | 38 | private static final int MAX_ATTEMPTS = 3; 39 | 40 | private final XproxyConfig config; 41 | 42 | private final RoundRobinFactory robinFactory; 43 | 44 | public DownStreamHandler(XproxyConfig config, RoundRobinFactory robinFactory) { 45 | this.config = config; 46 | this.robinFactory = robinFactory; 47 | } 48 | 49 | @Override 50 | protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception { 51 | final Channel downstream = ctx.channel(); 52 | 53 | boolean keepAlived = HttpUtil.isKeepAlive(request); 54 | HttpHeaders requestHeaders = request.headers(); 55 | 56 | // get Host header 57 | String serverName = requestHeaders.get(HttpHeaderNames.HOST); 58 | // get proxy_pass 59 | String proxyPass = config.proxyPass(serverName, request.uri()); 60 | 61 | // get roundRobin 62 | RoundRobin roundRobin = null; 63 | Server server = null; 64 | if (null == proxyPass || null == (roundRobin = robinFactory.roundRobin(proxyPass)) 65 | || null == (server = roundRobin.next())) { 66 | // return 404 67 | notFound(ctx, keepAlived); 68 | return; 69 | } 70 | 71 | // rewrite http request(keep alive to upstream) 72 | request.setProtocolVersion(HttpVersion.HTTP_1_1); 73 | requestHeaders.remove(HttpHeaderNames.CONNECTION); 74 | 75 | // increase refCount 76 | request.retain(); 77 | // proxy request 78 | proxy(server, proxyPass, downstream, request, keepAlived, MAX_ATTEMPTS); 79 | } 80 | 81 | public void proxy(Server server, String proxyPass, Channel downstream, FullHttpRequest request, boolean keepAlived, 82 | int maxAttempts) { 83 | // get connection from cache 84 | Connection connection = getConn(server, proxyPass); 85 | if (null == connection) {// need create an new connection 86 | createConnAndSendRequest(downstream, server, proxyPass, request, keepAlived, maxAttempts); 87 | } else {// use the cached connection 88 | setContextAndRequest(server, proxyPass, request, connection.getChannel(), downstream, keepAlived, false, 89 | maxAttempts); 90 | } 91 | } 92 | 93 | public void createConnAndSendRequest(Channel downstream, Server server, String proxyPass, FullHttpRequest request, 94 | boolean keepAlived, int maxAttempts) { 95 | Bootstrap b = new Bootstrap(); 96 | b.group(downstream.eventLoop()); 97 | b.channel(downstream.getClass()); 98 | 99 | b.option(ChannelOption.TCP_NODELAY, true); 100 | b.option(ChannelOption.SO_KEEPALIVE, true); 101 | // default is pooled direct 102 | // ByteBuf(io.netty.util.internal.PlatformDependent.DIRECT_BUFFER_PREFERRED) 103 | b.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT); 104 | // 32kb(for massive long connections, See 105 | // http://www.infoq.com/cn/articles/netty-million-level-push-service-design-points) 106 | // 64kb(RocketMq remoting default value) 107 | b.option(ChannelOption.SO_SNDBUF, 32 * 1024); 108 | b.option(ChannelOption.SO_RCVBUF, 32 * 1024); 109 | // temporary settings, need more tests 110 | b.option(ChannelOption.WRITE_BUFFER_WATER_MARK, new WriteBufferWaterMark(8 * 1024, 32 * 1024)); 111 | // default is true, reduce thread context switching 112 | b.option(ChannelOption.SINGLE_EVENTEXECUTOR_PER_GROUP, true); 113 | 114 | b.handler(new XproxyUpStreamChannelInitializer(server, proxyPass)); 115 | 116 | ChannelFuture connectFuture = b.connect(server.getIp(), server.getPort()); 117 | connectFuture.addListener(new ChannelFutureListener() { 118 | @Override 119 | public void operationComplete(ChannelFuture future) throws Exception { 120 | if (future.isSuccess()) { 121 | setContextAndRequest(server, proxyPass, request, future.channel(), downstream, keepAlived, true, 122 | maxAttempts); 123 | } else { 124 | if (maxAttempts > 0) { 125 | proxy(server, proxyPass, downstream, request, keepAlived, maxAttempts - 1); 126 | } else { 127 | request.release(); 128 | downstream.writeAndFlush(RequestContext.errorResponse(), downstream.voidPromise()); 129 | } 130 | } 131 | } 132 | }); 133 | } 134 | 135 | public Connection getConn(Server server, String proxyPass) { 136 | LinkedList conns = RequestContext.keepAlivedConntions(proxyPass); 137 | Connection connection = null; 138 | Connection tmp = null; 139 | 140 | for (Iterator it = conns.iterator(); it.hasNext();) { 141 | tmp = it.next(); 142 | // find the matched keepalived connection 143 | if (server.equals(tmp.getServer())) { 144 | it.remove(); 145 | connection = tmp; 146 | break; 147 | } 148 | } 149 | return connection; 150 | } 151 | 152 | public void notFound(ChannelHandlerContext ctx, boolean keepAlived) { 153 | if (keepAlived) { 154 | ctx.writeAndFlush(RequestContext.notfoundResponse(), ctx.voidPromise()); 155 | } else { 156 | ctx.writeAndFlush(RequestContext.notfoundResponse()).addListener(ChannelFutureListener.CLOSE); 157 | } 158 | } 159 | 160 | public void setContextAndRequest(Server server, String proxyPass, FullHttpRequest request, Channel upstream, 161 | Channel downstream, boolean keepAlived, final boolean newConn, final int maxAttempts) { 162 | // set request context 163 | upstream.attr(AttributeKeys.DOWNSTREAM_CHANNEL_KEY).set(downstream); 164 | upstream.attr(AttributeKeys.KEEP_ALIVED_KEY).set(keepAlived); 165 | 166 | request.retain(); 167 | upstream.writeAndFlush(request).addListener(new ChannelFutureListener() { 168 | @Override 169 | public void operationComplete(ChannelFuture future) throws Exception { 170 | if (!future.isSuccess()) { 171 | if (maxAttempts > 0) { 172 | proxy(server, proxyPass, downstream, request, keepAlived, maxAttempts - 1); 173 | } else { 174 | downstream.writeAndFlush(RequestContext.errorResponse(), downstream.voidPromise()); 175 | logger.error(String.format("%s upstream channel[%s] write to backed fail", 176 | newConn ? "new" : "cached", future.channel()), future.cause()); 177 | } 178 | } else { 179 | request.release(); 180 | } 181 | } 182 | }); 183 | } 184 | 185 | @Override 186 | public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception { 187 | logger.warn(String.format("downstream channel[%s] writability changed, isWritable: %s", ctx.channel(), 188 | ctx.channel().isWritable())); 189 | super.channelWritabilityChanged(ctx); 190 | } 191 | 192 | @Override 193 | public void channelInactive(ChannelHandlerContext ctx) throws Exception { 194 | logger.warn(String.format("downstream channel[%s] inactive", ctx.channel())); 195 | super.channelInactive(ctx); 196 | } 197 | 198 | @Override 199 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { 200 | logger.error(String.format("downstream channel[%s] exceptionCaught", ctx.channel()), cause); 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/main/java/xproxy/downstream/XproxyDownStreamChannelInitializer.java: -------------------------------------------------------------------------------- 1 | package xproxy.downstream; 2 | 3 | import io.netty.channel.Channel; 4 | import io.netty.channel.ChannelInitializer; 5 | import io.netty.channel.ChannelPipeline; 6 | import io.netty.handler.codec.http.HttpObjectAggregator; 7 | import io.netty.handler.codec.http.HttpServerCodec; 8 | import xproxy.conf.XproxyConfig; 9 | 10 | public class XproxyDownStreamChannelInitializer extends ChannelInitializer { 11 | 12 | //private final XproxyConfig config; 13 | 14 | private final DownStreamHandler downStreamHandler; 15 | 16 | public XproxyDownStreamChannelInitializer(XproxyConfig config, DownStreamHandler downStreamHandler) { 17 | //this.config = config; 18 | this.downStreamHandler = downStreamHandler; 19 | } 20 | 21 | @Override 22 | protected void initChannel(Channel ch) throws Exception { 23 | ChannelPipeline pipeline = ch.pipeline(); 24 | //pipeline.addLast(new IdleStateHandler(0, 0, config.keepaliveTimeout(), TimeUnit.SECONDS)); 25 | pipeline.addLast(new HttpServerCodec()); 26 | pipeline.addLast(new HttpObjectAggregator(512 * 1024)); 27 | pipeline.addLast(downStreamHandler); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/xproxy/upstream/UpStreamHandler.java: -------------------------------------------------------------------------------- 1 | package xproxy.upstream; 2 | 3 | import java.util.Iterator; 4 | import java.util.LinkedList; 5 | 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | 9 | import io.netty.channel.Channel; 10 | import io.netty.channel.ChannelFutureListener; 11 | import io.netty.channel.ChannelHandlerContext; 12 | import io.netty.channel.SimpleChannelInboundHandler; 13 | import io.netty.handler.codec.http.FullHttpResponse; 14 | import io.netty.handler.codec.http.HttpHeaderNames; 15 | import io.netty.handler.codec.http.HttpHeaderValues; 16 | import xproxy.conf.XproxyConfig.Server; 17 | import xproxy.core.AttributeKeys; 18 | import xproxy.core.Connection; 19 | import xproxy.core.RequestContext; 20 | 21 | public class UpStreamHandler extends SimpleChannelInboundHandler { 22 | 23 | private static final Logger logger = LoggerFactory.getLogger(UpStreamHandler.class); 24 | 25 | private final Server server; 26 | 27 | private final String proxyPass; 28 | 29 | public UpStreamHandler(Server server, String proxyPass) { 30 | this.server = server; 31 | this.proxyPass = proxyPass; 32 | } 33 | 34 | @Override 35 | protected void channelRead0(ChannelHandlerContext ctx, FullHttpResponse response) throws Exception { 36 | Channel upstream = ctx.channel(); 37 | 38 | // get context and clear 39 | Channel downstream = upstream.attr(AttributeKeys.DOWNSTREAM_CHANNEL_KEY).getAndSet(null); 40 | boolean keepAlived = upstream.attr(AttributeKeys.KEEP_ALIVED_KEY).getAndSet(null); 41 | 42 | LinkedList conns = RequestContext.keepAlivedConntions(proxyPass); 43 | 44 | if (conns.size() == server.getKeepalive()) { 45 | // the least recently used connection are closed 46 | logger.info(String.format( 47 | "[%s]cached connctions exceed the keepalive[%d], the least recently used connection are closed", 48 | proxyPass, server.getKeepalive())); 49 | Channel tmp = conns.pollFirst().getChannel(); 50 | tmp.attr(AttributeKeys.UPSTREAM_ACTIVE_CLOSE_KEY).set(true); 51 | tmp.close(); 52 | } 53 | conns.addLast(new Connection(server, upstream)); 54 | if (keepAlived) { 55 | response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); 56 | downstream.writeAndFlush(response.retain(), downstream.voidPromise()); 57 | } else {// close the downstream connection 58 | downstream.writeAndFlush(response.retain()).addListener(ChannelFutureListener.CLOSE); 59 | } 60 | } 61 | 62 | @Override 63 | public void channelInactive(ChannelHandlerContext ctx) throws Exception { 64 | boolean activeClose = false; 65 | if(ctx.channel().hasAttr(AttributeKeys.UPSTREAM_ACTIVE_CLOSE_KEY) 66 | && ctx.channel().attr(AttributeKeys.UPSTREAM_ACTIVE_CLOSE_KEY).get()){ 67 | activeClose = true; 68 | } 69 | 70 | logger.warn(String.format("upstream channel[%s] inactive, activeClose:%s", ctx.channel(), activeClose)); 71 | 72 | Channel downstream = null; 73 | Boolean keepAlived = null; 74 | if (null != (downstream = ctx.channel().attr(AttributeKeys.DOWNSTREAM_CHANNEL_KEY).get()) 75 | && null != (keepAlived = ctx.channel().attr(AttributeKeys.KEEP_ALIVED_KEY).get())) { 76 | if (keepAlived) { 77 | downstream.writeAndFlush(RequestContext.errorResponse(), downstream.voidPromise()); 78 | } else { 79 | downstream.writeAndFlush(RequestContext.errorResponse()).addListener(ChannelFutureListener.CLOSE); 80 | } 81 | }else{// remove current inactive channel from cached conns 82 | LinkedList conns = RequestContext.keepAlivedConntions(proxyPass); 83 | Connection tmp = null; 84 | 85 | for (Iterator it = conns.iterator(); it.hasNext();) { 86 | tmp = it.next(); 87 | // find the inactive connection 88 | if (server == tmp.getServer()) { 89 | it.remove(); 90 | break; 91 | } 92 | } 93 | } 94 | super.channelInactive(ctx); 95 | } 96 | 97 | @Override 98 | public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception { 99 | logger.warn(String.format("upstream channel[%s] writability changed, isWritable: %s", ctx.channel(), 100 | ctx.channel().isWritable())); 101 | super.channelWritabilityChanged(ctx); 102 | } 103 | 104 | @Override 105 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { 106 | logger.error(String.format("upstream channel[%s] exceptionCaught", ctx.channel()), cause); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/main/java/xproxy/upstream/XproxyUpStreamChannelInitializer.java: -------------------------------------------------------------------------------- 1 | package xproxy.upstream; 2 | 3 | import io.netty.channel.Channel; 4 | import io.netty.channel.ChannelInitializer; 5 | import io.netty.channel.ChannelPipeline; 6 | import io.netty.handler.codec.http.HttpClientCodec; 7 | import io.netty.handler.codec.http.HttpObjectAggregator; 8 | import xproxy.conf.XproxyConfig.Server; 9 | 10 | public class XproxyUpStreamChannelInitializer extends ChannelInitializer{ 11 | 12 | private final Server server; 13 | 14 | private final String proxyPass; 15 | 16 | public XproxyUpStreamChannelInitializer(Server server, String proxyPass) { 17 | this.server = server; 18 | this.proxyPass = proxyPass; 19 | } 20 | 21 | @Override 22 | protected void initChannel(Channel ch) throws Exception { 23 | ChannelPipeline pipeline = ch.pipeline(); 24 | pipeline.addLast(new HttpClientCodec()); 25 | pipeline.addLast(new HttpObjectAggregator(512 * 1024)); 26 | pipeline.addLast(new UpStreamHandler(server, proxyPass)); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/xproxy/upstream/lb/RoundRobin.java: -------------------------------------------------------------------------------- 1 | package xproxy.upstream.lb; 2 | 3 | import java.util.concurrent.atomic.AtomicInteger; 4 | 5 | import xproxy.conf.XproxyConfig.Server; 6 | 7 | public class RoundRobin implements ServerChooser { 8 | 9 | private final ServerChooser inner; 10 | 11 | public RoundRobin(Server[] servers) { 12 | inner = ServerChooserFactory.INSTANCE.newChooser(servers); 13 | } 14 | 15 | static class ServerChooserFactory { 16 | 17 | public static final ServerChooserFactory INSTANCE = new ServerChooserFactory(); 18 | 19 | private ServerChooserFactory() { 20 | } 21 | 22 | public ServerChooser newChooser(Server[] servers) { 23 | if (isPowerOfTwo(servers.length)) { 24 | return new PowerOfTwoEventExecutorChooser(servers); 25 | } else { 26 | return new GenericEventExecutorChooser(servers); 27 | } 28 | } 29 | 30 | private static boolean isPowerOfTwo(int val) { 31 | return (val & -val) == val; 32 | } 33 | 34 | private static final class PowerOfTwoEventExecutorChooser implements ServerChooser { 35 | private final AtomicInteger idx = new AtomicInteger(); 36 | private final Server[] servers; 37 | 38 | PowerOfTwoEventExecutorChooser(Server[] servers) { 39 | this.servers = servers; 40 | } 41 | 42 | public Server next() { 43 | return servers[idx.getAndIncrement() & servers.length - 1]; 44 | } 45 | } 46 | 47 | private static final class GenericEventExecutorChooser implements ServerChooser { 48 | private final AtomicInteger idx = new AtomicInteger(); 49 | private final Server[] servers; 50 | 51 | GenericEventExecutorChooser(Server[] servers) { 52 | this.servers = servers; 53 | } 54 | 55 | public Server next() { 56 | return servers[Math.abs(idx.getAndIncrement() % servers.length)]; 57 | } 58 | } 59 | } 60 | 61 | @Override 62 | public Server next() { 63 | return inner.next(); 64 | } 65 | } 66 | 67 | interface ServerChooser { 68 | Server next(); 69 | } -------------------------------------------------------------------------------- /src/main/java/xproxy/upstream/lb/RoundRobinFactory.java: -------------------------------------------------------------------------------- 1 | package xproxy.upstream.lb; 2 | 3 | import java.util.HashMap; 4 | import java.util.List; 5 | import java.util.Map; 6 | import java.util.Map.Entry; 7 | 8 | import xproxy.conf.XproxyConfig; 9 | import xproxy.conf.XproxyConfig.Server; 10 | 11 | public class RoundRobinFactory { 12 | 13 | private final Map robinMap = new HashMap<>(); 14 | 15 | public void init(XproxyConfig config) { 16 | Map> upstreams = config.upstreams(); 17 | if (null == upstreams || upstreams.isEmpty()) { 18 | return; 19 | } 20 | 21 | for (Entry> upstreamEntry : upstreams.entrySet()) { 22 | robinMap.put(upstreamEntry.getKey(), new RoundRobin(upstreamEntry.getValue().toArray(new Server[] {}))); 23 | } 24 | } 25 | 26 | public RoundRobin roundRobin(String proxypass) { 27 | return robinMap.get(proxypass); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/xproxy/util/AntPathMatcher.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2002-2016 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package xproxy.util; 18 | 19 | import java.util.ArrayList; 20 | import java.util.Collection; 21 | import java.util.Comparator; 22 | import java.util.LinkedHashMap; 23 | import java.util.LinkedList; 24 | import java.util.List; 25 | import java.util.Map; 26 | import java.util.StringTokenizer; 27 | import java.util.concurrent.ConcurrentHashMap; 28 | import java.util.regex.Matcher; 29 | import java.util.regex.Pattern; 30 | 31 | /** 32 | * {@link PathMatcher} implementation for Ant-style path patterns. 33 | * 34 | *

Part of this mapping code has been kindly borrowed from Apache Ant. 35 | * 36 | *

The mapping matches URLs using the following rules:
37 | *

    38 | *
  • {@code ?} matches one character
  • 39 | *
  • {@code *} matches zero or more characters
  • 40 | *
  • {@code **} matches zero or more directories in a path
  • 41 | *
  • {@code {spring:[a-z]+}} matches the regexp {@code [a-z]+} as a path variable named "spring"
  • 42 | *
43 | * 44 | *

Examples

45 | *
    46 | *
  • {@code com/t?st.jsp} — matches {@code com/test.jsp} but also 47 | * {@code com/tast.jsp} or {@code com/txst.jsp}
  • 48 | *
  • {@code com/*.jsp} — matches all {@code .jsp} files in the 49 | * {@code com} directory
  • 50 | *
  • com/**/test.jsp — matches all {@code test.jsp} 51 | * files underneath the {@code com} path
  • 52 | *
  • org/springframework/**/*.jsp — matches all 53 | * {@code .jsp} files underneath the {@code org/springframework} path
  • 54 | *
  • org/**/servlet/bla.jsp — matches 55 | * {@code org/springframework/servlet/bla.jsp} but also 56 | * {@code org/springframework/testing/servlet/bla.jsp} and {@code org/servlet/bla.jsp}
  • 57 | *
  • {@code com/{filename:\\w+}.jsp} will match {@code com/test.jsp} and assign the value {@code test} 58 | * to the {@code filename} variable
  • 59 | *
60 | * 61 | *

Note: a pattern and a path must both be absolute or must 62 | * both be relative in order for the two to match. Therefore it is recommended 63 | * that users of this implementation to sanitize patterns in order to prefix 64 | * them with "/" as it makes sense in the context in which they're used. 65 | * 66 | * @author Alef Arendsen 67 | * @author Juergen Hoeller 68 | * @author Rob Harrop 69 | * @author Arjen Poutsma 70 | * @author Rossen Stoyanchev 71 | * @author Sam Brannen 72 | * @since 16.07.2003 73 | */ 74 | public class AntPathMatcher { 75 | 76 | /** Default path separator: "/" */ 77 | public static final String DEFAULT_PATH_SEPARATOR = "/"; 78 | 79 | private static final int CACHE_TURNOFF_THRESHOLD = 65536; 80 | 81 | private static final Pattern VARIABLE_PATTERN = Pattern.compile("\\{[^/]+?\\}"); 82 | 83 | private static final char[] WILDCARD_CHARS = { '*', '?', '{' }; 84 | 85 | 86 | private String pathSeparator; 87 | 88 | private PathSeparatorPatternCache pathSeparatorPatternCache; 89 | 90 | private boolean caseSensitive = true; 91 | 92 | private boolean trimTokens = false; 93 | 94 | private volatile Boolean cachePatterns; 95 | 96 | private final Map tokenizedPatternCache = new ConcurrentHashMap(256); 97 | 98 | final Map stringMatcherCache = new ConcurrentHashMap(256); 99 | 100 | 101 | /** 102 | * Create a new instance with the {@link #DEFAULT_PATH_SEPARATOR}. 103 | */ 104 | public AntPathMatcher() { 105 | this.pathSeparator = DEFAULT_PATH_SEPARATOR; 106 | this.pathSeparatorPatternCache = new PathSeparatorPatternCache(DEFAULT_PATH_SEPARATOR); 107 | } 108 | 109 | /** 110 | * A convenient, alternative constructor to use with a custom path separator. 111 | * @param pathSeparator the path separator to use, must not be {@code null}. 112 | * @since 4.1 113 | */ 114 | public AntPathMatcher(String pathSeparator) { 115 | this.pathSeparator = pathSeparator; 116 | this.pathSeparatorPatternCache = new PathSeparatorPatternCache(pathSeparator); 117 | } 118 | 119 | 120 | /** 121 | * Set the path separator to use for pattern parsing. 122 | *

Default is "/", as in Ant. 123 | */ 124 | public void setPathSeparator(String pathSeparator) { 125 | this.pathSeparator = (pathSeparator != null ? pathSeparator : DEFAULT_PATH_SEPARATOR); 126 | this.pathSeparatorPatternCache = new PathSeparatorPatternCache(this.pathSeparator); 127 | } 128 | 129 | /** 130 | * Specify whether to perform pattern matching in a case-sensitive fashion. 131 | *

Default is {@code true}. Switch this to {@code false} for case-insensitive matching. 132 | * @since 4.2 133 | */ 134 | public void setCaseSensitive(boolean caseSensitive) { 135 | this.caseSensitive = caseSensitive; 136 | } 137 | 138 | /** 139 | * Specify whether to trim tokenized paths and patterns. 140 | *

Default is {@code false}. 141 | */ 142 | public void setTrimTokens(boolean trimTokens) { 143 | this.trimTokens = trimTokens; 144 | } 145 | 146 | /** 147 | * Specify whether to cache parsed pattern metadata for patterns passed 148 | * into this matcher's {@link #match} method. A value of {@code true} 149 | * activates an unlimited pattern cache; a value of {@code false} turns 150 | * the pattern cache off completely. 151 | *

Default is for the cache to be on, but with the variant to automatically 152 | * turn it off when encountering too many patterns to cache at runtime 153 | * (the threshold is 65536), assuming that arbitrary permutations of patterns 154 | * are coming in, with little chance for encountering a recurring pattern. 155 | * @since 4.0.1 156 | * @see #getStringMatcher(String) 157 | */ 158 | public void setCachePatterns(boolean cachePatterns) { 159 | this.cachePatterns = cachePatterns; 160 | } 161 | 162 | private void deactivatePatternCache() { 163 | this.cachePatterns = false; 164 | this.tokenizedPatternCache.clear(); 165 | this.stringMatcherCache.clear(); 166 | } 167 | 168 | 169 | public boolean isPattern(String path) { 170 | return (path.indexOf('*') != -1 || path.indexOf('?') != -1); 171 | } 172 | 173 | public boolean match(String pattern, String path) { 174 | return doMatch(pattern, path, true, null); 175 | } 176 | 177 | public boolean matchStart(String pattern, String path) { 178 | return doMatch(pattern, path, false, null); 179 | } 180 | 181 | /** 182 | * Actually match the given {@code path} against the given {@code pattern}. 183 | * @param pattern the pattern to match against 184 | * @param path the path String to test 185 | * @param fullMatch whether a full pattern match is required (else a pattern match 186 | * as far as the given base path goes is sufficient) 187 | * @return {@code true} if the supplied {@code path} matched, {@code false} if it didn't 188 | */ 189 | protected boolean doMatch(String pattern, String path, boolean fullMatch, Map uriTemplateVariables) { 190 | if (path.startsWith(this.pathSeparator) != pattern.startsWith(this.pathSeparator)) { 191 | return false; 192 | } 193 | 194 | String[] pattDirs = tokenizePattern(pattern); 195 | if (fullMatch && this.caseSensitive && !isPotentialMatch(path, pattDirs)) { 196 | return false; 197 | } 198 | 199 | String[] pathDirs = tokenizePath(path); 200 | 201 | int pattIdxStart = 0; 202 | int pattIdxEnd = pattDirs.length - 1; 203 | int pathIdxStart = 0; 204 | int pathIdxEnd = pathDirs.length - 1; 205 | 206 | // Match all elements up to the first ** 207 | while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) { 208 | String pattDir = pattDirs[pattIdxStart]; 209 | if ("**".equals(pattDir)) { 210 | break; 211 | } 212 | if (!matchStrings(pattDir, pathDirs[pathIdxStart], uriTemplateVariables)) { 213 | return false; 214 | } 215 | pattIdxStart++; 216 | pathIdxStart++; 217 | } 218 | 219 | if (pathIdxStart > pathIdxEnd) { 220 | // Path is exhausted, only match if rest of pattern is * or **'s 221 | if (pattIdxStart > pattIdxEnd) { 222 | return (pattern.endsWith(this.pathSeparator) ? path.endsWith(this.pathSeparator) : 223 | !path.endsWith(this.pathSeparator)); 224 | } 225 | if (!fullMatch) { 226 | return true; 227 | } 228 | if (pattIdxStart == pattIdxEnd && pattDirs[pattIdxStart].equals("*") && path.endsWith(this.pathSeparator)) { 229 | return true; 230 | } 231 | for (int i = pattIdxStart; i <= pattIdxEnd; i++) { 232 | if (!pattDirs[i].equals("**")) { 233 | return false; 234 | } 235 | } 236 | return true; 237 | } 238 | else if (pattIdxStart > pattIdxEnd) { 239 | // String not exhausted, but pattern is. Failure. 240 | return false; 241 | } 242 | else if (!fullMatch && "**".equals(pattDirs[pattIdxStart])) { 243 | // Path start definitely matches due to "**" part in pattern. 244 | return true; 245 | } 246 | 247 | // up to last '**' 248 | while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) { 249 | String pattDir = pattDirs[pattIdxEnd]; 250 | if (pattDir.equals("**")) { 251 | break; 252 | } 253 | if (!matchStrings(pattDir, pathDirs[pathIdxEnd], uriTemplateVariables)) { 254 | return false; 255 | } 256 | pattIdxEnd--; 257 | pathIdxEnd--; 258 | } 259 | if (pathIdxStart > pathIdxEnd) { 260 | // String is exhausted 261 | for (int i = pattIdxStart; i <= pattIdxEnd; i++) { 262 | if (!pattDirs[i].equals("**")) { 263 | return false; 264 | } 265 | } 266 | return true; 267 | } 268 | 269 | while (pattIdxStart != pattIdxEnd && pathIdxStart <= pathIdxEnd) { 270 | int patIdxTmp = -1; 271 | for (int i = pattIdxStart + 1; i <= pattIdxEnd; i++) { 272 | if (pattDirs[i].equals("**")) { 273 | patIdxTmp = i; 274 | break; 275 | } 276 | } 277 | if (patIdxTmp == pattIdxStart + 1) { 278 | // '**/**' situation, so skip one 279 | pattIdxStart++; 280 | continue; 281 | } 282 | // Find the pattern between padIdxStart & padIdxTmp in str between 283 | // strIdxStart & strIdxEnd 284 | int patLength = (patIdxTmp - pattIdxStart - 1); 285 | int strLength = (pathIdxEnd - pathIdxStart + 1); 286 | int foundIdx = -1; 287 | 288 | strLoop: 289 | for (int i = 0; i <= strLength - patLength; i++) { 290 | for (int j = 0; j < patLength; j++) { 291 | String subPat = pattDirs[pattIdxStart + j + 1]; 292 | String subStr = pathDirs[pathIdxStart + i + j]; 293 | if (!matchStrings(subPat, subStr, uriTemplateVariables)) { 294 | continue strLoop; 295 | } 296 | } 297 | foundIdx = pathIdxStart + i; 298 | break; 299 | } 300 | 301 | if (foundIdx == -1) { 302 | return false; 303 | } 304 | 305 | pattIdxStart = patIdxTmp; 306 | pathIdxStart = foundIdx + patLength; 307 | } 308 | 309 | for (int i = pattIdxStart; i <= pattIdxEnd; i++) { 310 | if (!pattDirs[i].equals("**")) { 311 | return false; 312 | } 313 | } 314 | 315 | return true; 316 | } 317 | 318 | private boolean isPotentialMatch(String path, String[] pattDirs) { 319 | if (!this.trimTokens) { 320 | char[] pathChars = path.toCharArray(); 321 | int pos = 0; 322 | for (String pattDir : pattDirs) { 323 | int skipped = skipSeparator(path, pos, this.pathSeparator); 324 | pos += skipped; 325 | skipped = skipSegment(pathChars, pos, pattDir); 326 | if (skipped < pattDir.length()) { 327 | if (skipped > 0) { 328 | return true; 329 | } 330 | return (pattDir.length() > 0) && isWildcardChar(pattDir.charAt(0)); 331 | } 332 | pos += skipped; 333 | } 334 | } 335 | return true; 336 | } 337 | 338 | private int skipSegment(char[] chars, int pos, String prefix) { 339 | int skipped = 0; 340 | for (char c : prefix.toCharArray()) { 341 | if (isWildcardChar(c)) { 342 | return skipped; 343 | } 344 | else if (pos + skipped >= chars.length) { 345 | return 0; 346 | } 347 | else if (chars[pos + skipped] == c) { 348 | skipped++; 349 | } 350 | } 351 | return skipped; 352 | } 353 | 354 | private int skipSeparator(String path, int pos, String separator) { 355 | int skipped = 0; 356 | while (path.startsWith(separator, pos + skipped)) { 357 | skipped += separator.length(); 358 | } 359 | return skipped; 360 | } 361 | 362 | private boolean isWildcardChar(char c) { 363 | for (char candidate : WILDCARD_CHARS) { 364 | if (c == candidate) { 365 | return true; 366 | } 367 | } 368 | return false; 369 | } 370 | 371 | /** 372 | * Tokenize the given path pattern into parts, based on this matcher's settings. 373 | *

Performs caching based on {@link #setCachePatterns}, delegating to 374 | * {@link #tokenizePath(String)} for the actual tokenization algorithm. 375 | * @param pattern the pattern to tokenize 376 | * @return the tokenized pattern parts 377 | */ 378 | protected String[] tokenizePattern(String pattern) { 379 | String[] tokenized = null; 380 | Boolean cachePatterns = this.cachePatterns; 381 | if (cachePatterns == null || cachePatterns.booleanValue()) { 382 | tokenized = this.tokenizedPatternCache.get(pattern); 383 | } 384 | if (tokenized == null) { 385 | tokenized = tokenizePath(pattern); 386 | if (cachePatterns == null && this.tokenizedPatternCache.size() >= CACHE_TURNOFF_THRESHOLD) { 387 | // Try to adapt to the runtime situation that we're encountering: 388 | // There are obviously too many different patterns coming in here... 389 | // So let's turn off the cache since the patterns are unlikely to be reoccurring. 390 | deactivatePatternCache(); 391 | return tokenized; 392 | } 393 | if (cachePatterns == null || cachePatterns.booleanValue()) { 394 | this.tokenizedPatternCache.put(pattern, tokenized); 395 | } 396 | } 397 | return tokenized; 398 | } 399 | 400 | /** 401 | * Tokenize the given path String into parts, based on this matcher's settings. 402 | * @param path the path to tokenize 403 | * @return the tokenized path parts 404 | */ 405 | protected String[] tokenizePath(String path) { 406 | return tokenizeToStringArray(path, this.pathSeparator, this.trimTokens, true); 407 | } 408 | 409 | public static String[] tokenizeToStringArray( 410 | String str, String delimiters, boolean trimTokens, boolean ignoreEmptyTokens) { 411 | 412 | if (str == null) { 413 | return null; 414 | } 415 | StringTokenizer st = new StringTokenizer(str, delimiters); 416 | List tokens = new ArrayList(); 417 | while (st.hasMoreTokens()) { 418 | String token = st.nextToken(); 419 | if (trimTokens) { 420 | token = token.trim(); 421 | } 422 | if (!ignoreEmptyTokens || token.length() > 0) { 423 | tokens.add(token); 424 | } 425 | } 426 | return toStringArray(tokens); 427 | } 428 | 429 | public static String[] toStringArray(Collection collection) { 430 | if (collection == null) { 431 | return null; 432 | } 433 | return collection.toArray(new String[collection.size()]); 434 | } 435 | /** 436 | * Test whether or not a string matches against a pattern. 437 | * @param pattern the pattern to match against (never {@code null}) 438 | * @param str the String which must be matched against the pattern (never {@code null}) 439 | * @return {@code true} if the string matches against the pattern, or {@code false} otherwise 440 | */ 441 | private boolean matchStrings(String pattern, String str, Map uriTemplateVariables) { 442 | return getStringMatcher(pattern).matchStrings(str, uriTemplateVariables); 443 | } 444 | 445 | /** 446 | * Build or retrieve an {@link AntPathStringMatcher} for the given pattern. 447 | *

The default implementation checks this AntPathMatcher's internal cache 448 | * (see {@link #setCachePatterns}), creating a new AntPathStringMatcher instance 449 | * if no cached copy is found. 450 | *

When encountering too many patterns to cache at runtime (the threshold is 65536), 451 | * it turns the default cache off, assuming that arbitrary permutations of patterns 452 | * are coming in, with little chance for encountering a recurring pattern. 453 | *

This method may be overridden to implement a custom cache strategy. 454 | * @param pattern the pattern to match against (never {@code null}) 455 | * @return a corresponding AntPathStringMatcher (never {@code null}) 456 | * @see #setCachePatterns 457 | */ 458 | protected AntPathStringMatcher getStringMatcher(String pattern) { 459 | AntPathStringMatcher matcher = null; 460 | Boolean cachePatterns = this.cachePatterns; 461 | if (cachePatterns == null || cachePatterns.booleanValue()) { 462 | matcher = this.stringMatcherCache.get(pattern); 463 | } 464 | if (matcher == null) { 465 | matcher = new AntPathStringMatcher(pattern, this.caseSensitive); 466 | if (cachePatterns == null && this.stringMatcherCache.size() >= CACHE_TURNOFF_THRESHOLD) { 467 | // Try to adapt to the runtime situation that we're encountering: 468 | // There are obviously too many different patterns coming in here... 469 | // So let's turn off the cache since the patterns are unlikely to be reoccurring. 470 | deactivatePatternCache(); 471 | return matcher; 472 | } 473 | if (cachePatterns == null || cachePatterns.booleanValue()) { 474 | this.stringMatcherCache.put(pattern, matcher); 475 | } 476 | } 477 | return matcher; 478 | } 479 | 480 | /** 481 | * Given a pattern and a full path, determine the pattern-mapped part.

For example:

    482 | *
  • '{@code /docs/cvs/commit.html}' and '{@code /docs/cvs/commit.html} -> ''
  • 483 | *
  • '{@code /docs/*}' and '{@code /docs/cvs/commit} -> '{@code cvs/commit}'
  • 484 | *
  • '{@code /docs/cvs/*.html}' and '{@code /docs/cvs/commit.html} -> '{@code commit.html}'
  • 485 | *
  • '{@code /docs/**}' and '{@code /docs/cvs/commit} -> '{@code cvs/commit}'
  • 486 | *
  • '{@code /docs/**\/*.html}' and '{@code /docs/cvs/commit.html} -> '{@code cvs/commit.html}'
  • 487 | *
  • '{@code /*.html}' and '{@code /docs/cvs/commit.html} -> '{@code docs/cvs/commit.html}'
  • 488 | *
  • '{@code *.html}' and '{@code /docs/cvs/commit.html} -> '{@code /docs/cvs/commit.html}'
  • 489 | *
  • '{@code *}' and '{@code /docs/cvs/commit.html} -> '{@code /docs/cvs/commit.html}'
490 | *

Assumes that {@link #match} returns {@code true} for '{@code pattern}' and '{@code path}', but 491 | * does not enforce this. 492 | */ 493 | public String extractPathWithinPattern(String pattern, String path) { 494 | String[] patternParts = tokenizeToStringArray(pattern, this.pathSeparator, this.trimTokens, true); 495 | String[] pathParts = tokenizeToStringArray(path, this.pathSeparator, this.trimTokens, true); 496 | StringBuilder builder = new StringBuilder(); 497 | boolean pathStarted = false; 498 | 499 | for (int segment = 0; segment < patternParts.length; segment++) { 500 | String patternPart = patternParts[segment]; 501 | if (patternPart.indexOf('*') > -1 || patternPart.indexOf('?') > -1) { 502 | for (; segment < pathParts.length; segment++) { 503 | if (pathStarted || (segment == 0 && !pattern.startsWith(this.pathSeparator))) { 504 | builder.append(this.pathSeparator); 505 | } 506 | builder.append(pathParts[segment]); 507 | pathStarted = true; 508 | } 509 | } 510 | } 511 | 512 | return builder.toString(); 513 | } 514 | 515 | public Map extractUriTemplateVariables(String pattern, String path) { 516 | Map variables = new LinkedHashMap(); 517 | boolean result = doMatch(pattern, path, true, variables); 518 | if (!result) { 519 | throw new IllegalStateException("Pattern \"" + pattern + "\" is not a match for \"" + path + "\""); 520 | } 521 | return variables; 522 | } 523 | 524 | /** 525 | * Combine two patterns into a new pattern. 526 | *

This implementation simply concatenates the two patterns, unless 527 | * the first pattern contains a file extension match (e.g., {@code *.html}). 528 | * In that case, the second pattern will be merged into the first. Otherwise, 529 | * an {@code IllegalArgumentException} will be thrown. 530 | *

Examples

531 | * 532 | * 533 | * 534 | * 535 | * 536 | * 537 | * 538 | * 539 | * 540 | * 541 | * 542 | * 543 | * 544 | * 545 | * 546 | *
Pattern 1Pattern 2Result
{@code null}{@code null} 
/hotels{@code null}/hotels
{@code null}/hotels/hotels
/hotels/bookings/hotels/bookings
/hotelsbookings/hotels/bookings
/hotels/*/bookings/hotels/bookings
/hotels/**/bookings/hotels/**/bookings
/hotels{hotel}/hotels/{hotel}
/hotels/*{hotel}/hotels/{hotel}
/hotels/**{hotel}/hotels/**/{hotel}
/*.html/hotels.html/hotels.html
/*.html/hotels/hotels.html
/*.html/*.txt{@code IllegalArgumentException}
547 | * @param pattern1 the first pattern 548 | * @param pattern2 the second pattern 549 | * @return the combination of the two patterns 550 | * @throws IllegalArgumentException if the two patterns cannot be combined 551 | */ 552 | public String combine(String pattern1, String pattern2) { 553 | if (!hasText(pattern1) && !hasText(pattern2)) { 554 | return ""; 555 | } 556 | if (!hasText(pattern1)) { 557 | return pattern2; 558 | } 559 | if (!hasText(pattern2)) { 560 | return pattern1; 561 | } 562 | 563 | boolean pattern1ContainsUriVar = (pattern1.indexOf('{') != -1); 564 | if (!pattern1.equals(pattern2) && !pattern1ContainsUriVar && match(pattern1, pattern2)) { 565 | // /* + /hotel -> /hotel ; "/*.*" + "/*.html" -> /*.html 566 | // However /user + /user -> /usr/user ; /{foo} + /bar -> /{foo}/bar 567 | return pattern2; 568 | } 569 | 570 | // /hotels/* + /booking -> /hotels/booking 571 | // /hotels/* + booking -> /hotels/booking 572 | if (pattern1.endsWith(this.pathSeparatorPatternCache.getEndsOnWildCard())) { 573 | return concat(pattern1.substring(0, pattern1.length() - 2), pattern2); 574 | } 575 | 576 | // /hotels/** + /booking -> /hotels/**/booking 577 | // /hotels/** + booking -> /hotels/**/booking 578 | if (pattern1.endsWith(this.pathSeparatorPatternCache.getEndsOnDoubleWildCard())) { 579 | return concat(pattern1, pattern2); 580 | } 581 | 582 | int starDotPos1 = pattern1.indexOf("*."); 583 | if (pattern1ContainsUriVar || starDotPos1 == -1 || this.pathSeparator.equals(".")) { 584 | // simply concatenate the two patterns 585 | return concat(pattern1, pattern2); 586 | } 587 | 588 | String ext1 = pattern1.substring(starDotPos1 + 1); 589 | int dotPos2 = pattern2.indexOf('.'); 590 | String file2 = (dotPos2 == -1 ? pattern2 : pattern2.substring(0, dotPos2)); 591 | String ext2 = (dotPos2 == -1 ? "" : pattern2.substring(dotPos2)); 592 | boolean ext1All = (ext1.equals(".*") || ext1.equals("")); 593 | boolean ext2All = (ext2.equals(".*") || ext2.equals("")); 594 | if (!ext1All && !ext2All) { 595 | throw new IllegalArgumentException("Cannot combine patterns: " + pattern1 + " vs " + pattern2); 596 | } 597 | String ext = (ext1All ? ext2 : ext1); 598 | return file2 + ext; 599 | } 600 | 601 | public static boolean hasText(String str) { 602 | return hasText((CharSequence) str); 603 | } 604 | 605 | public static boolean hasText(CharSequence str) { 606 | if (!hasLength(str)) { 607 | return false; 608 | } 609 | int strLen = str.length(); 610 | for (int i = 0; i < strLen; i++) { 611 | if (!Character.isWhitespace(str.charAt(i))) { 612 | return true; 613 | } 614 | } 615 | return false; 616 | } 617 | 618 | public static boolean hasLength(CharSequence str) { 619 | return (str != null && str.length() > 0); 620 | } 621 | 622 | private String concat(String path1, String path2) { 623 | boolean path1EndsWithSeparator = path1.endsWith(this.pathSeparator); 624 | boolean path2StartsWithSeparator = path2.startsWith(this.pathSeparator); 625 | 626 | if (path1EndsWithSeparator && path2StartsWithSeparator) { 627 | return path1 + path2.substring(1); 628 | } 629 | else if (path1EndsWithSeparator || path2StartsWithSeparator) { 630 | return path1 + path2; 631 | } 632 | else { 633 | return path1 + this.pathSeparator + path2; 634 | } 635 | } 636 | 637 | /** 638 | * Given a full path, returns a {@link Comparator} suitable for sorting patterns in order of 639 | * explicitness. 640 | *

This{@code Comparator} will {@linkplain java.util.Collections#sort(List, Comparator) sort} 641 | * a list so that more specific patterns (without uri templates or wild cards) come before 642 | * generic patterns. So given a list with the following patterns: 643 | *

    644 | *
  1. {@code /hotels/new}
  2. 645 | *
  3. {@code /hotels/{hotel}}
  4. {@code /hotels/*}
  5. 646 | *
647 | * the returned comparator will sort this list so that the order will be as indicated. 648 | *

The full path given as parameter is used to test for exact matches. So when the given path 649 | * is {@code /hotels/2}, the pattern {@code /hotels/2} will be sorted before {@code /hotels/1}. 650 | * @param path the full path to use for comparison 651 | * @return a comparator capable of sorting patterns in order of explicitness 652 | */ 653 | public Comparator getPatternComparator(String path) { 654 | return new AntPatternComparator(path); 655 | } 656 | 657 | 658 | /** 659 | * Tests whether or not a string matches against a pattern via a {@link Pattern}. 660 | *

The pattern may contain special characters: '*' means zero or more characters; '?' means one and 661 | * only one character; '{' and '}' indicate a URI template pattern. For example /users/{user}. 662 | */ 663 | protected static class AntPathStringMatcher { 664 | 665 | private static final Pattern GLOB_PATTERN = Pattern.compile("\\?|\\*|\\{((?:\\{[^/]+?\\}|[^/{}]|\\\\[{}])+?)\\}"); 666 | 667 | private static final String DEFAULT_VARIABLE_PATTERN = "(.*)"; 668 | 669 | private final Pattern pattern; 670 | 671 | private final List variableNames = new LinkedList(); 672 | 673 | public AntPathStringMatcher(String pattern) { 674 | this(pattern, true); 675 | } 676 | 677 | public AntPathStringMatcher(String pattern, boolean caseSensitive) { 678 | StringBuilder patternBuilder = new StringBuilder(); 679 | Matcher matcher = GLOB_PATTERN.matcher(pattern); 680 | int end = 0; 681 | while (matcher.find()) { 682 | patternBuilder.append(quote(pattern, end, matcher.start())); 683 | String match = matcher.group(); 684 | if ("?".equals(match)) { 685 | patternBuilder.append('.'); 686 | } 687 | else if ("*".equals(match)) { 688 | patternBuilder.append(".*"); 689 | } 690 | else if (match.startsWith("{") && match.endsWith("}")) { 691 | int colonIdx = match.indexOf(':'); 692 | if (colonIdx == -1) { 693 | patternBuilder.append(DEFAULT_VARIABLE_PATTERN); 694 | this.variableNames.add(matcher.group(1)); 695 | } 696 | else { 697 | String variablePattern = match.substring(colonIdx + 1, match.length() - 1); 698 | patternBuilder.append('('); 699 | patternBuilder.append(variablePattern); 700 | patternBuilder.append(')'); 701 | String variableName = match.substring(1, colonIdx); 702 | this.variableNames.add(variableName); 703 | } 704 | } 705 | end = matcher.end(); 706 | } 707 | patternBuilder.append(quote(pattern, end, pattern.length())); 708 | this.pattern = (caseSensitive ? Pattern.compile(patternBuilder.toString()) : 709 | Pattern.compile(patternBuilder.toString(), Pattern.CASE_INSENSITIVE)); 710 | } 711 | 712 | private String quote(String s, int start, int end) { 713 | if (start == end) { 714 | return ""; 715 | } 716 | return Pattern.quote(s.substring(start, end)); 717 | } 718 | 719 | /** 720 | * Main entry point. 721 | * @return {@code true} if the string matches against the pattern, or {@code false} otherwise. 722 | */ 723 | public boolean matchStrings(String str, Map uriTemplateVariables) { 724 | Matcher matcher = this.pattern.matcher(str); 725 | if (matcher.matches()) { 726 | if (uriTemplateVariables != null) { 727 | // SPR-8455 728 | if (this.variableNames.size() != matcher.groupCount()) { 729 | throw new IllegalArgumentException("The number of capturing groups in the pattern segment " + 730 | this.pattern + " does not match the number of URI template variables it defines, " + 731 | "which can occur if capturing groups are used in a URI template regex. " + 732 | "Use non-capturing groups instead."); 733 | } 734 | for (int i = 1; i <= matcher.groupCount(); i++) { 735 | String name = this.variableNames.get(i - 1); 736 | String value = matcher.group(i); 737 | uriTemplateVariables.put(name, value); 738 | } 739 | } 740 | return true; 741 | } 742 | else { 743 | return false; 744 | } 745 | } 746 | } 747 | 748 | 749 | /** 750 | * The default {@link Comparator} implementation returned by 751 | * {@link #getPatternComparator(String)}. 752 | *

In order, the most "generic" pattern is determined by the following: 753 | *

    754 | *
  • if it's null or a capture all pattern (i.e. it is equal to "/**")
  • 755 | *
  • if the other pattern is an actual match
  • 756 | *
  • if it's a catch-all pattern (i.e. it ends with "**"
  • 757 | *
  • if it's got more "*" than the other pattern
  • 758 | *
  • if it's got more "{foo}" than the other pattern
  • 759 | *
  • if it's shorter than the other pattern
  • 760 | *
761 | */ 762 | protected static class AntPatternComparator implements Comparator { 763 | 764 | private final String path; 765 | 766 | public AntPatternComparator(String path) { 767 | this.path = path; 768 | } 769 | 770 | /** 771 | * Compare two patterns to determine which should match first, i.e. which 772 | * is the most specific regarding the current path. 773 | * @return a negative integer, zero, or a positive integer as pattern1 is 774 | * more specific, equally specific, or less specific than pattern2. 775 | */ 776 | @Override 777 | public int compare(String pattern1, String pattern2) { 778 | PatternInfo info1 = new PatternInfo(pattern1); 779 | PatternInfo info2 = new PatternInfo(pattern2); 780 | 781 | if (info1.isLeastSpecific() && info2.isLeastSpecific()) { 782 | return 0; 783 | } 784 | else if (info1.isLeastSpecific()) { 785 | return 1; 786 | } 787 | else if (info2.isLeastSpecific()) { 788 | return -1; 789 | } 790 | 791 | boolean pattern1EqualsPath = pattern1.equals(path); 792 | boolean pattern2EqualsPath = pattern2.equals(path); 793 | if (pattern1EqualsPath && pattern2EqualsPath) { 794 | return 0; 795 | } 796 | else if (pattern1EqualsPath) { 797 | return -1; 798 | } 799 | else if (pattern2EqualsPath) { 800 | return 1; 801 | } 802 | 803 | if (info1.isPrefixPattern() && info2.getDoubleWildcards() == 0) { 804 | return 1; 805 | } 806 | else if (info2.isPrefixPattern() && info1.getDoubleWildcards() == 0) { 807 | return -1; 808 | } 809 | 810 | if (info1.getTotalCount() != info2.getTotalCount()) { 811 | return info1.getTotalCount() - info2.getTotalCount(); 812 | } 813 | 814 | if (info1.getLength() != info2.getLength()) { 815 | return info2.getLength() - info1.getLength(); 816 | } 817 | 818 | if (info1.getSingleWildcards() < info2.getSingleWildcards()) { 819 | return -1; 820 | } 821 | else if (info2.getSingleWildcards() < info1.getSingleWildcards()) { 822 | return 1; 823 | } 824 | 825 | if (info1.getUriVars() < info2.getUriVars()) { 826 | return -1; 827 | } 828 | else if (info2.getUriVars() < info1.getUriVars()) { 829 | return 1; 830 | } 831 | 832 | return 0; 833 | } 834 | 835 | 836 | /** 837 | * Value class that holds information about the pattern, e.g. number of 838 | * occurrences of "*", "**", and "{" pattern elements. 839 | */ 840 | private static class PatternInfo { 841 | 842 | private final String pattern; 843 | 844 | private int uriVars; 845 | 846 | private int singleWildcards; 847 | 848 | private int doubleWildcards; 849 | 850 | private boolean catchAllPattern; 851 | 852 | private boolean prefixPattern; 853 | 854 | private Integer length; 855 | 856 | public PatternInfo(String pattern) { 857 | this.pattern = pattern; 858 | if (this.pattern != null) { 859 | initCounters(); 860 | this.catchAllPattern = this.pattern.equals("/**"); 861 | this.prefixPattern = !this.catchAllPattern && this.pattern.endsWith("/**"); 862 | } 863 | if (this.uriVars == 0) { 864 | this.length = (this.pattern != null ? this.pattern.length() : 0); 865 | } 866 | } 867 | 868 | protected void initCounters() { 869 | int pos = 0; 870 | while (pos < this.pattern.length()) { 871 | if (this.pattern.charAt(pos) == '{') { 872 | this.uriVars++; 873 | pos++; 874 | } 875 | else if (this.pattern.charAt(pos) == '*') { 876 | if (pos + 1 < this.pattern.length() && this.pattern.charAt(pos + 1) == '*') { 877 | this.doubleWildcards++; 878 | pos += 2; 879 | } 880 | else if (pos > 0 && !this.pattern.substring(pos - 1).equals(".*")) { 881 | this.singleWildcards++; 882 | pos++; 883 | } 884 | else { 885 | pos++; 886 | } 887 | } 888 | else { 889 | pos++; 890 | } 891 | } 892 | } 893 | 894 | public int getUriVars() { 895 | return this.uriVars; 896 | } 897 | 898 | public int getSingleWildcards() { 899 | return this.singleWildcards; 900 | } 901 | 902 | public int getDoubleWildcards() { 903 | return this.doubleWildcards; 904 | } 905 | 906 | public boolean isLeastSpecific() { 907 | return (this.pattern == null || this.catchAllPattern); 908 | } 909 | 910 | public boolean isPrefixPattern() { 911 | return this.prefixPattern; 912 | } 913 | 914 | public int getTotalCount() { 915 | return this.uriVars + this.singleWildcards + (2 * this.doubleWildcards); 916 | } 917 | 918 | /** 919 | * Returns the length of the given pattern, where template variables are considered to be 1 long. 920 | */ 921 | public int getLength() { 922 | if (this.length == null) { 923 | this.length = VARIABLE_PATTERN.matcher(this.pattern).replaceAll("#").length(); 924 | } 925 | return this.length; 926 | } 927 | } 928 | } 929 | 930 | 931 | /** 932 | * A simple cache for patterns that depend on the configured path separator. 933 | */ 934 | private static class PathSeparatorPatternCache { 935 | 936 | private final String endsOnWildCard; 937 | 938 | private final String endsOnDoubleWildCard; 939 | 940 | public PathSeparatorPatternCache(String pathSeparator) { 941 | this.endsOnWildCard = pathSeparator + "*"; 942 | this.endsOnDoubleWildCard = pathSeparator + "**"; 943 | } 944 | 945 | public String getEndsOnWildCard() { 946 | return this.endsOnWildCard; 947 | } 948 | 949 | public String getEndsOnDoubleWildCard() { 950 | return this.endsOnDoubleWildCard; 951 | } 952 | } 953 | 954 | } 955 | -------------------------------------------------------------------------------- /src/main/java/xproxy/util/PlatformUtil.java: -------------------------------------------------------------------------------- 1 | package xproxy.util; 2 | 3 | import java.io.File; 4 | import java.io.FileInputStream; 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | import java.net.MalformedURLException; 8 | import java.net.URL; 9 | import java.security.AccessController; 10 | import java.security.PrivilegedAction; 11 | import java.util.Properties; 12 | 13 | public class PlatformUtil { 14 | 15 | 16 | // NOTE: since this class can be initialized by application code in some 17 | // cases, we must encapsulate all calls to System.getProperty("...") in 18 | // a doPrivileged block except for standard JVM properties such as 19 | // os.name, os.version, os.arch, java.vm.name, etc. 20 | 21 | private static final String os = System.getProperty("os.name"); 22 | private static final String version = System.getProperty("os.version"); 23 | private static final boolean embedded; 24 | private static final String embeddedType; 25 | private static final boolean useEGL; 26 | private static final boolean doEGLCompositing; 27 | // a property used to denote a non-default impl for this host 28 | private static String javafxPlatform; 29 | 30 | static { 31 | javafxPlatform = AccessController.doPrivileged(new PrivilegedAction() {public String run() { return System.getProperty("javafx.platform");}}); 32 | loadProperties(); 33 | embedded = AccessController.doPrivileged(new PrivilegedAction() {public Boolean run() { return Boolean.getBoolean("com.sun.javafx.isEmbedded");}}); 34 | embeddedType = AccessController.doPrivileged(new PrivilegedAction() {public String run() { return System.getProperty("embedded");}}); 35 | useEGL = AccessController.doPrivileged(new PrivilegedAction() {public Boolean run() {return Boolean.getBoolean("use.egl");}}); 36 | if (useEGL) { 37 | doEGLCompositing = AccessController.doPrivileged(new PrivilegedAction() {public Boolean run() { return Boolean.getBoolean("doNativeComposite");}}); 38 | } else 39 | doEGLCompositing = false; 40 | } 41 | 42 | private static final boolean ANDROID = "android".equals(javafxPlatform) 43 | || "Dalvik".equals(System.getProperty("java.vm.name")); 44 | private static final boolean WINDOWS = os.startsWith("Windows"); 45 | private static final boolean WINDOWS_VISTA_OR_LATER = WINDOWS && versionNumberGreaterThanOrEqualTo(6.0f); 46 | private static final boolean WINDOWS_7_OR_LATER = WINDOWS && versionNumberGreaterThanOrEqualTo(6.1f); 47 | private static final boolean MAC = os.startsWith("Mac"); 48 | private static final boolean LINUX = os.startsWith("Linux") && !ANDROID; 49 | private static final boolean SOLARIS = os.startsWith("SunOS"); 50 | private static final boolean IOS = os.startsWith("iOS"); 51 | 52 | /** 53 | * Utility method used to determine whether the version number as 54 | * reported by system properties is greater than or equal to a given 55 | * value. 56 | * 57 | * @param value The value to test against. 58 | * @return false if the version number cannot be parsed as a float, 59 | * otherwise the comparison against value. 60 | */ 61 | private static boolean versionNumberGreaterThanOrEqualTo(float value) { 62 | try { 63 | return Float.parseFloat(version) >= value; 64 | } catch (Exception e) { 65 | return false; 66 | } 67 | } 68 | 69 | /** 70 | * Returns true if the operating system is a form of Windows. 71 | */ 72 | public static boolean isWindows() { 73 | return WINDOWS; 74 | } 75 | 76 | /** 77 | * Returns true if the operating system is at least Windows Vista(v6.0). 78 | */ 79 | public static boolean isWinVistaOrLater() { 80 | return WINDOWS_VISTA_OR_LATER; 81 | } 82 | 83 | /** 84 | * Returns true if the operating system is at least Windows 7(v6.1). 85 | */ 86 | public static boolean isWin7OrLater() { 87 | return WINDOWS_7_OR_LATER; 88 | } 89 | 90 | /** 91 | * Returns true if the operating system is a form of Mac OS. 92 | */ 93 | public static boolean isMac() { 94 | return MAC; 95 | } 96 | 97 | /** 98 | * Returns true if the operating system is a form of Linux. 99 | */ 100 | public static boolean isLinux() { 101 | return LINUX; 102 | } 103 | 104 | public static boolean useEGL() { 105 | return useEGL; 106 | } 107 | 108 | public static boolean useEGLWindowComposition() { 109 | return doEGLCompositing; 110 | } 111 | 112 | public static boolean useGLES2() { 113 | String useGles2 = "false"; 114 | useGles2 = AccessController.doPrivileged(new PrivilegedAction() { 115 | public String run() {return System.getProperty("use.gles2");} 116 | }); 117 | if ("true".equals(useGles2)) 118 | return true; 119 | else 120 | return false; 121 | } 122 | 123 | /** 124 | * Returns true if the operating system is a form of Unix, including Linux. 125 | */ 126 | public static boolean isSolaris() { 127 | return SOLARIS; 128 | } 129 | 130 | /** 131 | * Returns true if the operating system is a form of Linux or Solaris 132 | */ 133 | public static boolean isUnix() { 134 | return LINUX || SOLARIS; 135 | } 136 | 137 | /** 138 | * Returns true if the platform is embedded. 139 | */ 140 | public static boolean isEmbedded() { 141 | return embedded; 142 | } 143 | 144 | /** 145 | * Returns a string with the embedded type - ie eglx11, eglfb, dfb or null. 146 | */ 147 | public static String getEmbeddedType() { 148 | return embeddedType; 149 | } 150 | 151 | /** 152 | * Returns true if the operating system is iOS 153 | */ 154 | public static boolean isIOS() { 155 | return IOS; 156 | } 157 | 158 | private static void loadPropertiesFromFile(final File file) { 159 | Properties p = new Properties(); 160 | try { 161 | InputStream in = new FileInputStream(file); 162 | p.load(in); 163 | in.close(); 164 | } catch (IOException e) { 165 | e.printStackTrace(); 166 | } 167 | if (javafxPlatform == null) { 168 | javafxPlatform = p.getProperty("javafx.platform"); 169 | } 170 | String prefix = javafxPlatform + "."; 171 | int prefixLength = prefix.length(); 172 | boolean foundPlatform = false; 173 | for (Object o : p.keySet()) { 174 | String key = (String) o; 175 | if (key.startsWith(prefix)) { 176 | foundPlatform = true; 177 | String systemKey = key.substring(prefixLength); 178 | if (System.getProperty(systemKey) == null) { 179 | String value = p.getProperty(key); 180 | System.setProperty(systemKey, value); 181 | } 182 | } 183 | } 184 | if (!foundPlatform) { 185 | System.err.println("Warning: No settings found for javafx.platform='" + javafxPlatform + "'"); 186 | } 187 | } 188 | 189 | /** Returns the directory containing the JavaFX runtime, or null 190 | * if the directory cannot be located 191 | */ 192 | private static File getRTDir() { 193 | try { 194 | String theClassFile = "PlatformUtil.class"; 195 | Class theClass = PlatformUtil.class; 196 | URL url = theClass.getResource(theClassFile); 197 | if (url == null) 198 | return null; 199 | String classUrlString = url.toString(); 200 | if (!classUrlString.startsWith("jar:file:") || classUrlString.indexOf('!') == -1) { 201 | return null; 202 | } 203 | // Strip out the "jar:" and everything after and including the "!" 204 | String s = classUrlString.substring(4, classUrlString.lastIndexOf('!')); 205 | // Strip everything after the last "/" or "\" to get rid of the jar filename 206 | int lastIndexOfSlash = Math.max(s.lastIndexOf('/'), s.lastIndexOf('\\')); 207 | return new File(new URL(s.substring(0, lastIndexOfSlash + 1)).getPath()).getParentFile(); 208 | } catch (MalformedURLException e) { 209 | return null; 210 | } 211 | } 212 | 213 | private static void loadProperties() { 214 | final String vmname = System.getProperty("java.vm.name"); 215 | final String arch = System.getProperty("os.arch"); 216 | 217 | if (!(javafxPlatform != null || (arch != null && arch.equals("arm")) 218 | || (vmname != null && vmname.indexOf("Embedded") > 0))) { 219 | return; 220 | } 221 | AccessController.doPrivileged(new PrivilegedAction() { 222 | @Override 223 | public Void run() { 224 | final File rtDir = getRTDir(); 225 | final String propertyFilename = "javafx.platform.properties"; 226 | File rtProperties = new File(rtDir, propertyFilename); 227 | // First look for javafx.platform.properties in the JavaFX runtime 228 | // Then in the installation directory of the JRE 229 | if (rtProperties.exists()) { 230 | loadPropertiesFromFile(rtProperties); 231 | return null; 232 | } 233 | String javaHome = System.getProperty("java.home"); 234 | File javaHomeProperties = new File(javaHome, "lib" + File.separator + propertyFilename); 235 | if (javaHomeProperties.exists()) { 236 | loadPropertiesFromFile(javaHomeProperties); 237 | return null; 238 | } 239 | 240 | String javafxRuntimePath = System.getProperty("javafx.runtime.path"); 241 | File javafxRuntimePathProperties = new File(javafxRuntimePath, File.separator + propertyFilename); 242 | if (javafxRuntimePathProperties.exists()) { 243 | loadPropertiesFromFile(javafxRuntimePathProperties); 244 | return null; 245 | } 246 | return null; 247 | } 248 | }); 249 | } 250 | 251 | public static boolean isAndroid() { 252 | return ANDROID; 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/main/resources/xproxy.yml: -------------------------------------------------------------------------------- 1 | # accept connections on the specified port 2 | listen: 8000 3 | 4 | # keepalive timeout for all downstream connetions, second 5 | keepalive_timeout: 65 6 | 7 | # netty worker threads(auto = cpu cores) 8 | worker_threads: auto 9 | 10 | # max connections per worker 11 | worker_connections: 102400 12 | 13 | # all virtual hosts configurations 14 | servers: 15 | localhost: 16 | - 17 | path: /* 18 | proxy_pass: http://localhost_pool 19 | 20 | # all upstream configurations 21 | upstreams: 22 | localhost_pool: 23 | keepalive: 16 # for all backends in current pool 24 | servers: 25 | - 127.0.0.1:8088 --------------------------------------------------------------------------------