Repository: blakey22/shadowsocks-java Branch: master Commit: 5a70f18d463e Files: 44 Total size: 140.5 KB Directory structure: gitextract_fryewo1t/ ├── .gitignore ├── LICENSE ├── README.md ├── pom.xml └── src/ └── main/ ├── java/ │ └── com/ │ └── stfl/ │ ├── Constant.java │ ├── Main.java │ ├── MainGui.java │ ├── misc/ │ │ ├── Config.java │ │ ├── Log.java │ │ ├── Reflection.java │ │ ├── UTF8Control.java │ │ └── Util.java │ ├── network/ │ │ ├── IServer.java │ │ ├── LocalServer.java │ │ ├── NioLocalServer.java │ │ ├── io/ │ │ │ └── PipeSocket.java │ │ ├── nio/ │ │ │ ├── ChangeRequest.java │ │ │ ├── ISocketHandler.java │ │ │ ├── PipeEvent.java │ │ │ ├── PipeWorker.java │ │ │ ├── RemoteSocketHandler.java │ │ │ └── SocketHandlerBase.java │ │ └── proxy/ │ │ ├── AutoProxy.java │ │ ├── HttpProxy.java │ │ ├── IProxy.java │ │ ├── ProxyFactory.java │ │ └── Socks5Proxy.java │ ├── ss/ │ │ ├── AesCrypt.java │ │ ├── BlowFishCrypt.java │ │ ├── CamelliaCrypt.java │ │ ├── CryptBase.java │ │ ├── CryptFactory.java │ │ ├── ICrypt.java │ │ ├── SeedCrypt.java │ │ └── ShadowSocksKey.java │ └── ui/ │ ├── LogLayoutController.java │ ├── MainLayoutController.java │ └── TextAreaLogHandler.java └── resources/ ├── META-INF/ │ └── MANIFEST.MF └── resources/ ├── bundle/ │ ├── ui_en.properties │ ├── ui_zh_CN.properties │ └── ui_zh_TW.properties └── ui/ ├── LogLayout.fxml └── MainLayout.fxml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # ignore output folders .idea/ out/ target/ # ignore files config.json *.iml ================================================ FILE: LICENSE ================================================ shadowsocks-java is distributed under the following BSD-style license: Copyright (c) 2015, Blake All Rights Reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. The name of the author may not be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: README.md ================================================ This project is being discontinued ================================== Since I am busy at work in the past few years and couldn't find time to maintain it, I believe it's time to achieve it. Thank you to everyone who has used/interested in shadowsocks-java. shadowsocks-java ================ shadowsocks-java is a pure JAVA client for [shadowsocks](https://github.com/shadowsocks/shadowsocks) project. Only tested AES encryption. ### Requirements * Bouncy Castle v1.5.2 [Release](https://www.bouncycastle.org/) * json-simple v1.1.1 [Release](https://code.google.com/p/json-simple/) ### Developers * Using Non-blocking server Config config = new Config("SS_SERVER_IP", "SS_SERVER_PORT", "LOCAL_IP", "LOCAL_PORT", "CIPHER_NAME", "PASSWORD"); NioLocalServer server = new NioLocalServer(config); new Thread(server).start(); * Using blocking server Config config = new Config("SS_SERVER_IP", "SS_SERVER_PORT", "LOCAL_IP", "LOCAL_PORT", "CIPHER_NAME", "PASSWORD"); LocalServer server = new LocalServer(config); new Thread(server).start(); ================================================ FILE: pom.xml ================================================ 4.0.0 com.stfl shadowsocks-java 0.2-SNAPSHOT UTF-8 org.apache.maven.plugins maven-compiler-plugin 3.0 1.7 1.7 org.bouncycastle bcprov-jdk15on 1.52 com.googlecode.json-simple json-simple 1.1.1 junit junit org.hamcrest hamcrest-core ================================================ FILE: src/main/java/com/stfl/Constant.java ================================================ package com.stfl; import java.util.Locale; public class Constant { public static final String PROG_NAME = "shadowsocks-java"; public static final String VERSION = "0.2"; public static final int BUFFER_SIZE = 16384; public static final String CONF_FILE = "config.json"; public static final Locale LOCALE = Locale.getDefault(); } ================================================ FILE: src/main/java/com/stfl/Main.java ================================================ package com.stfl; import com.stfl.misc.Config; import com.stfl.misc.Util; import com.stfl.network.LocalServer; import com.stfl.network.NioLocalServer; import com.stfl.network.proxy.IProxy; import com.stfl.network.proxy.ProxyFactory; import com.stfl.ss.CryptFactory; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.logging.Logger; public class Main { private static Logger logger = Logger.getLogger(Main.class.getName()); public static void main(String[] args) { if (args.length != 0) { startCommandLine(args); } else { MainGui.launch(MainGui.class); } } private static void startCommandLine(String[] args) { Config config; config = parseArgument(args); if (config == null) { printUsage(); return; } Util.saveFile(Constant.CONF_FILE, config.saveToJson()); try { //LocalServer server = new LocalServer(config); NioLocalServer server = new NioLocalServer(config); Thread t = new Thread(server); t.start(); t.join(); } catch (Exception e) { logger.warning("Unable to start server: " + e.toString()); } } private static Config parseArgument(String[] args) { Config config = new Config(); if (args.length == 2) { if (args[0].equals("--config")) { Path path = Paths.get(args[1]); try { String json = new String(Files.readAllBytes(path)); config.loadFromJson(json); } catch (IOException e) { System.out.println("Unable to read configuration file: " + args[1]); return null; } return config; } else { return null; } } if (args.length != 8) { return null; } // parse arguments for (int i = 0; i < args.length; i+=2) { String[] tempArgs; if (args[i].equals("--local")) { tempArgs = args[i+1].split(":"); if (tempArgs.length < 2) { System.out.println("Invalid argument: " + args[i]); return null; } config.setLocalIpAddress(tempArgs[0]); config.setLocalPort(Integer.parseInt(tempArgs[1])); } else if (args[i].equals("--remote")) { tempArgs = args[i+1].split(":"); if (tempArgs.length < 2) { System.out.println("Invalid argument: " + args[i]); return null; } config.setRemoteIpAddress(tempArgs[0]); config.setRemotePort(Integer.parseInt(tempArgs[1])); } else if (args[i].equals("--cipher")) { config.setMethod(args[i+1]); } else if (args[i].equals("--password")) { config.setPassword(args[i + 1]); } else if (args[i].equals("--proxy")) { config.setProxyType(args[i + 1]); } } return config; } private static void printUsage() { System.out.println("Usage: ss --[option] value --[option] value..."); System.out.println("Option:"); System.out.println(" --local [IP:PORT]"); System.out.println(" --remote [IP:PORT]"); System.out.println(" --cipher [CIPHER_NAME]"); System.out.println(" --password [PASSWORD]"); System.out.println(" --config [CONFIG_FILE]"); System.out.println(" --proxy [TYPE]"); System.out.println("Support Proxy Type:"); for (IProxy.TYPE t : ProxyFactory.getSupportedProxyTypes()) { System.out.printf(" %s\n", t.toString().toLowerCase()); } System.out.println("Support Ciphers:"); for (String s : CryptFactory.getSupportedCiphers()) { System.out.printf(" %s\n", s); } System.out.println("Example:"); System.out.println(" ss --local \"127.0.0.1:1080\" --remote \"[SS_SERVER_IP]:1080\" --cipher \"aes-256-cfb\" --password \"HelloWorld\""); System.out.println(" ss --config config.json"); } } ================================================ FILE: src/main/java/com/stfl/MainGui.java ================================================ package com.stfl; import com.stfl.misc.UTF8Control; import com.stfl.ui.MainLayoutController; import javafx.application.Application; import javafx.application.Platform; import javafx.fxml.FXMLLoader; import javafx.scene.Scene; import javafx.scene.image.Image; import javafx.scene.layout.Pane; import javafx.stage.Stage; import javax.imageio.ImageIO; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.IOException; import java.util.ResourceBundle; import java.util.logging.Logger; public class MainGui extends Application { private static Logger logger = Logger.getLogger(MainGui.class.getName()); private Stage primaryStage; private Scene rootScene; private MainLayoutController controller; private TrayIcon trayIcon; @Override public void start(Stage primaryStage) throws Exception { Platform.setImplicitExit(false); this.primaryStage = primaryStage; this.primaryStage.setTitle("Server Configuration"); try { // Load the root layout from the fxml file FXMLLoader mainLayoutLoader = new FXMLLoader(MainGui.class.getResource("/resources/ui/MainLayout.fxml")); mainLayoutLoader.setResources(ResourceBundle.getBundle("resources.bundle.ui", Constant.LOCALE, new UTF8Control())); Pane rootLayout = mainLayoutLoader.load(); rootScene = new Scene(rootLayout); primaryStage.setScene(rootScene); primaryStage.setResizable(false); controller = mainLayoutLoader.getController(); controller.setMainGui(this); addToTray(); primaryStage.getIcons().add(new Image(MainGui.class.getResource("/resources/image/icon.png").toString())); primaryStage.show(); } catch (IOException e) { // Exception gets thrown if the fxml file could not be loaded e.printStackTrace(); } } private void addToTray() { // ensure awt is initialized java.awt.Toolkit.getDefaultToolkit(); // make sure system tray is supported if (!java.awt.SystemTray.isSupported()) { logger.warning("No system tray support!"); } final java.awt.SystemTray tray = java.awt.SystemTray.getSystemTray(); try { java.awt.Image image = ImageIO.read(MainGui.class.getResource("/resources/image/icon.png")); trayIcon = new TrayIcon(image); trayIcon.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { Platform.runLater(new Runnable() { @Override public void run() { primaryStage.show(); } }); } }); java.awt.MenuItem openItem = new java.awt.MenuItem("Configuration"); openItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { Platform.runLater(new Runnable() { @Override public void run() { show(); } }); } }); java.awt.MenuItem exitItem = new java.awt.MenuItem("Exit"); exitItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { controller.closeServer(); Platform.exit(); tray.remove(trayIcon); } }); PopupMenu popup = new PopupMenu(); popup.add(openItem); popup.addSeparator(); popup.add(exitItem); trayIcon.setPopupMenu(popup); trayIcon.setToolTip("Not Connected"); tray.add(trayIcon); } catch (IOException e) { e.printStackTrace(); } catch (AWTException e) { e.printStackTrace(); } } public void show() { primaryStage.show(); } public void hide() { primaryStage.hide(); } public void setTooltip(String message) { if (trayIcon != null) { trayIcon.setToolTip(message); } } public void showNotification(String message) { trayIcon.displayMessage( "shadowsocks-java", message, java.awt.TrayIcon.MessageType.INFO ); } } ================================================ FILE: src/main/java/com/stfl/misc/Config.java ================================================ /* * Copyright (c) 2015, Blake * All Rights Reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * 3. The name of the author may not be used to endorse or promote * products derived from this software without specific prior * written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ package com.stfl.misc; import com.stfl.network.proxy.IProxy; import org.json.simple.JSONObject; import org.json.simple.JSONValue; import com.stfl.ss.AesCrypt; /** * Data class for configuration to bring up server */ public class Config { private String _ipAddr; private int _port; private String _localIpAddr; private int _localPort; private String _method; private String _password; private String _logLevel; private IProxy.TYPE _proxyType; public Config() { loadFromJson(""); } public Config(String ipAddr, int port, String localIpAddr, int localPort, String method, String password) { this(); _ipAddr = ipAddr; _port = port; _localIpAddr = localIpAddr; _localPort = localPort; _method = method; _password = password; _proxyType = IProxy.TYPE.AUTO; } public Config(String ipAddr, int port, String localIpAddr, int localPort, String method, String password, IProxy.TYPE type) { this(ipAddr, port, localIpAddr, localPort, method, password); _proxyType = type; } public void setRemoteIpAddress(String value) { _ipAddr = value; } public String getRemoteIpAddress() { return _ipAddr; } public void setLocalIpAddress(String value) { _localIpAddr = value; } public String getLocalIpAddress() { return _localIpAddr; } public void setRemotePort(int value) { _port = value; } public int getRemotePort() { return _port; } public void setLocalPort(int value) { _localPort = value; } public int getLocalPort() { return _localPort; } public void setProxyType(String value) { _proxyType = IProxy.TYPE.AUTO; if (value.toLowerCase().equals(IProxy.TYPE.HTTP.toString().toLowerCase())) { _proxyType = IProxy.TYPE.HTTP; } else if (value.toLowerCase().equals(IProxy.TYPE.SOCKS5.toString().toLowerCase())) { _proxyType = IProxy.TYPE.SOCKS5; } } public void setProxyType(IProxy.TYPE value) { _proxyType = value; } public IProxy.TYPE getProxyType() { return _proxyType; } public void setMethod(String value) { _method = value; } public String getMethod() { return _method; } public void setPassword(String value) { _password = value; } public String getPassword() { return _password; } public void setLogLevel(String value) { _logLevel = value; Log.init(getLogLevel()); } public String getLogLevel() { return _logLevel; } public void loadFromJson(String jsonStr) { if (jsonStr.length() == 0) { jsonStr = "{}"; } JSONObject jObj = (JSONObject)JSONValue.parse(jsonStr); _ipAddr = (String)jObj.getOrDefault("remoteIpAddress", ""); _port = ((Number)jObj.getOrDefault("remotePort", 1080)).intValue(); _localIpAddr = (String)jObj.getOrDefault("localIpAddress", "127.0.0.1"); _localPort = ((Number)jObj.getOrDefault("localPort", 1080)).intValue(); _method = (String)jObj.getOrDefault("method", AesCrypt.CIPHER_AES_256_CFB); _password = (String)jObj.getOrDefault("password", ""); _logLevel = (String)jObj.getOrDefault("logLevel", "INFO"); setProxyType((String) jObj.getOrDefault("proxyType", IProxy.TYPE.SOCKS5.toString().toLowerCase())); setLogLevel(_logLevel); } public String saveToJson() { JSONObject jObj = new JSONObject(); jObj.put("remoteIpAddress", _ipAddr); jObj.put("remotePort", _port); jObj.put("localIpAddress", _localIpAddr); jObj.put("localPort", _localPort); jObj.put("method", _method); jObj.put("password", _password); jObj.put("proxyType", _proxyType.toString().toLowerCase()); jObj.put("logLevel", _logLevel); return Util.prettyPrintJson(jObj); } } ================================================ FILE: src/main/java/com/stfl/misc/Log.java ================================================ /* * Copyright (c) 2015, Blake * All Rights Reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * 3. The name of the author may not be used to endorse or promote * products derived from this software without specific prior * written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ package com.stfl.misc; import java.util.Locale; import java.util.Properties; import java.util.logging.*; /** * Initialized level of root logger */ public class Log { private static boolean handlerInit = false; public static void init() { init(Level.INFO); } public static void init(Level level) { Logger rootLogger = getRootLogger(); if (handlerInit) { rootLogger.setLevel(level); for(Handler handler : rootLogger.getHandlers()) { handler.setLevel(level); } return; } // disable message localization Locale.setDefault(Locale.ENGLISH); // config log output format Properties props = System.getProperties(); props.setProperty("java.util.logging.SimpleFormatter.format", "%1$tY-%1$tb-%1$td %1$tT [%4$s] %5$s%n"); // setup root logger //Logger rootLogger = getRootLogger(); rootLogger.setUseParentHandlers(false); for(Handler handler : rootLogger.getHandlers()) { rootLogger.removeHandler(handler); } // set log level and format rootLogger.setLevel(level); ConsoleHandler handler = new ConsoleHandler(); handler.setLevel(level); rootLogger.addHandler(handler); handlerInit = true; } public static void init(String level) { Level l = Level.parse(level); init(l); } public static void addHandler(Handler handler) { Logger rootLogger = getRootLogger(); Level logLevel = Level.INFO; for (Handler h : rootLogger.getHandlers()) { logLevel = h.getLevel(); } handler.setLevel(logLevel); rootLogger.addHandler(handler); } private static Logger getRootLogger() { return Logger.getLogger("com.stfl"); } } ================================================ FILE: src/main/java/com/stfl/misc/Reflection.java ================================================ package com.stfl.misc; import java.lang.reflect.Constructor; public class Reflection { public static Object get(String className, Object... args) { Object retValue = null; try { Class c = Class.forName(className); if (args.length == 0) { retValue = c.newInstance(); } else if ((args.length & 1) == 0) { // args should come with pairs, for example // String.class, "arg1_value", String.class, "arg2_value" Class[] oParam = new Class[args.length / 2]; for (int arg_i = 0, i = 0; arg_i < args.length; arg_i+=2, i++) { oParam[i] = (Class)args[arg_i]; } Constructor constructor = c.getConstructor(oParam); Object[] paramObjs = new Object[args.length / 2]; for (int arg_i = 1, i = 0; arg_i < args.length; arg_i+=2, i++) { paramObjs[i] = args[arg_i]; } retValue = constructor.newInstance(paramObjs); } } catch (Exception e) { e.printStackTrace(); } return retValue; } } ================================================ FILE: src/main/java/com/stfl/misc/UTF8Control.java ================================================ package com.stfl.misc; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.URL; import java.net.URLConnection; import java.util.Locale; import java.util.PropertyResourceBundle; import java.util.ResourceBundle; public class UTF8Control extends ResourceBundle.Control { public ResourceBundle newBundle (String baseName, Locale locale, String format, ClassLoader loader, boolean reload) throws IllegalAccessException, InstantiationException, IOException { // below is the original implementation String bundleName = toBundleName(baseName, locale); String resourceName = toResourceName(bundleName, "properties"); ResourceBundle bundle = null; InputStream stream = null; if (reload) { URL url = loader.getResource(resourceName); if (url != null) { URLConnection connection = url.openConnection(); if (connection != null) { connection.setUseCaches(false); stream = connection.getInputStream(); } } } else { stream = loader.getResourceAsStream(resourceName); } if (stream != null) { try { // load string use UTF-8 encoding bundle = new PropertyResourceBundle(new InputStreamReader(stream, "UTF-8")); } finally { stream.close(); } } return bundle; } } ================================================ FILE: src/main/java/com/stfl/misc/Util.java ================================================ /* * Copyright (c) 2015, Blake * All Rights Reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * 3. The name of the author may not be used to endorse or promote * products derived from this software without specific prior * written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ package com.stfl.misc; import com.stfl.network.proxy.Socks5Proxy; import org.json.simple.JSONObject; import java.io.*; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.security.SecureRandom; /** * Helper class */ public class Util { public static String dumpBytes(byte[] a) { StringBuilder sb = new StringBuilder(a.length * 2); for(byte b: a) sb.append(String.format("%x", b & 0xff)); return sb.toString(); } public static byte[] randomBytes(int size) { byte[] bytes = new byte[size]; new SecureRandom().nextBytes(bytes); return bytes; } public static String getErrorMessage(Throwable e) { Writer writer = new StringWriter(); PrintWriter pWriter = new PrintWriter(writer); e.printStackTrace(pWriter); return writer.toString(); } public static String prettyPrintJson(JSONObject jObj) { String retValue; StringWriter writer = new StringWriter() { private final static String indent = " "; private final String LINE_SEP = System.getProperty("line.separator"); private int indentLevel = 0; @Override public void write(int c) { char ch = (char) c; if (ch == '[' || ch == '{') { super.write(c); super.write(LINE_SEP); indentLevel++; writeIndentation(); } else if (ch == ']' || ch == '}') { super.write(LINE_SEP); indentLevel--; writeIndentation(); super.write(c); } else if (ch == ':') { super.write(c); super.write(" "); } else if (ch == ',') { super.write(c); super.write(LINE_SEP); writeIndentation(); } else { super.write(c); } } private void writeIndentation() { for (int i = 0; i < indentLevel; i++) { super.write(indent); } } }; try { jObj.writeJSONString(writer); retValue = writer.toString(); } catch (IOException e) { // something wrong with writer, use the original method retValue = jObj.toJSONString(); } return retValue; } public static String getRequestedHostInfo(byte[] data) { String ret = ""; int port; int neededLength; switch (data[0]) { case Socks5Proxy.ATYP_IP_V4: // IP v4 Address // 4 bytes of IP, 2 bytes of port neededLength = 6; if (data.length > neededLength) { port = getPort(data[5], data[6]); ret = String.format("%d.%d.%d.%d:%d", data[1], data[2], data[3], data[4], port); } break; case Socks5Proxy.ATYP_DOMAIN_NAME: // domain neededLength = data[1]; if (data.length > neededLength + 3) { port = getPort(data[neededLength + 2], data[neededLength + 3]); String domain = bytesToString(data, 2, neededLength); ret = String.format("%s:%d", domain, port); } break; case Socks5Proxy.ATYP_IP_V6: // IP v6 Address // 16 bytes of IP, 2 bytes of port neededLength = 18; if (data.length > neededLength) { port = getPort(data[17], data[18]); ret = String.format("%x%x:%x%x:%x%x:%x%x:%x%x:%x%x:%x%x:%x%x:%d", data[1], data[2], data[3], data[4], data[5], data[6], data[7], data[8], data[9], data[10], data[11], data[12], data[13], data[14], data[15], data[16], port); } break; } return ret; } public static String bytesToString(byte[] data, int start, int length) { String str = ""; try { str = new String(data, start, length, "UTF-8"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } return str; } public static byte[] composeSSHeader(String host, int port) { // TYPE (1 byte) + LENGTH (1 byte) + HOST (var bytes) + PORT (2 bytes) byte[] respData = new byte[host.length() + 4]; respData[0] = Socks5Proxy.ATYP_DOMAIN_NAME; respData[1] = (byte)host.length(); System.arraycopy(host.getBytes(), 0, respData, 2, host.length()); respData[host.length() + 2] = (byte)(port >> 8); respData[host.length() + 3] = (byte)(port & 0xFF); return respData; } public static boolean saveFile(String fn, String content) { PrintWriter writer; try { writer = new PrintWriter(fn); writer.println(content); writer.close(); } catch (FileNotFoundException e) { return false; } return true; } public static String getFileContent(String fn) { Path path = Paths.get(fn); String content = ""; try { content = new String(Files.readAllBytes(path)); } catch (IOException e) { // do nothing } return content; } private static short byteToUnsignedByte(byte b) { return (short)(b & 0xff); } private static int getPort(byte b, byte b1) { return byteToUnsignedByte(b) << 8 | byteToUnsignedByte(b1); } } ================================================ FILE: src/main/java/com/stfl/network/IServer.java ================================================ package com.stfl.network; public interface IServer extends Runnable { void close(); } ================================================ FILE: src/main/java/com/stfl/network/LocalServer.java ================================================ /* * Copyright (c) 2015, Blake * All Rights Reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * 3. The name of the author may not be used to endorse or promote * products derived from this software without specific prior * written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ package com.stfl.network; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; import java.security.InvalidAlgorithmParameterException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.logging.Logger; import com.stfl.Constant; import com.stfl.misc.Config; import com.stfl.misc.Util; import com.stfl.network.io.PipeSocket; import com.stfl.ss.CryptFactory; /** * Blocking local server for shadowsocks */ public class LocalServer implements IServer { private Logger logger = Logger.getLogger(LocalServer.class.getName()); private Config _config; private ServerSocket _serverSocket; private Executor _executor; private List _pipes; public LocalServer(Config config) throws IOException, InvalidAlgorithmParameterException { if (!CryptFactory.isCipherExisted(config.getMethod())) { throw new InvalidAlgorithmParameterException(config.getMethod()); } _config = config; _serverSocket = new ServerSocket(config.getLocalPort(), 128); _executor = Executors.newCachedThreadPool(); _pipes = new ArrayList<>(); // print server info logger.info("Shadowsocks-Java v" + Constant.VERSION); logger.info(config.getProxyType() + " Proxy Server starts at port: " + config.getLocalPort()); } @Override public void run() { while (true) { try { Socket localSocket = _serverSocket.accept(); PipeSocket pipe = new PipeSocket(_executor, localSocket, _config); _pipes.add(pipe); _executor.execute(pipe); } catch (IOException e) { logger.warning(Util.getErrorMessage(e)); } } } public void close() { try { for (PipeSocket p : _pipes) { p.close(); } _pipes.clear(); _serverSocket.close(); } catch (IOException e) { logger.warning(Util.getErrorMessage(e)); } } } ================================================ FILE: src/main/java/com/stfl/network/NioLocalServer.java ================================================ /* * Copyright (c) 2015, Blake * All Rights Reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * 3. The name of the author may not be used to endorse or promote * products derived from this software without specific prior * written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ package com.stfl.network; import com.stfl.Constant; import com.stfl.misc.Config; import com.stfl.misc.Util; import com.stfl.network.nio.*; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.nio.channels.spi.SelectorProvider; import java.security.InvalidAlgorithmParameterException; import java.util.*; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.logging.Logger; /** * Non-blocking local server for shadowsocks */ public class NioLocalServer extends SocketHandlerBase { private Logger logger = Logger.getLogger(NioLocalServer.class.getName()); private ServerSocketChannel _serverChannel; private RemoteSocketHandler _remoteSocketHandler; private ExecutorService _executor; public NioLocalServer(Config config) throws IOException, InvalidAlgorithmParameterException { super(config); _executor = Executors.newCachedThreadPool(); // init remote socket handler _remoteSocketHandler = new RemoteSocketHandler(_config); _executor.execute(_remoteSocketHandler); // print server info logger.info("Shadowsocks-Java v" + Constant.VERSION); logger.info("Cipher: " + config.getMethod()); logger.info(config.getProxyType() + " Proxy Server starts at port: " + config.getLocalPort()); } @Override protected Selector initSelector() throws IOException { Selector socketSelector = SelectorProvider.provider().openSelector(); _serverChannel = ServerSocketChannel.open(); _serverChannel.configureBlocking(false); InetSocketAddress isa = new InetSocketAddress(_config.getLocalIpAddress(), _config.getLocalPort()); _serverChannel.socket().bind(isa); _serverChannel.register(socketSelector, SelectionKey.OP_ACCEPT); return socketSelector; } @Override protected boolean processPendingRequest(ChangeRequest request) { switch (request.type) { case ChangeRequest.CHANGE_SOCKET_OP: SelectionKey key = request.socket.keyFor(_selector); if ((key != null) && key.isValid()) { key.interestOps(request.op); } else { logger.warning("NioLocalServer::processPendingRequest (drop): " + key + request.socket); } break; case ChangeRequest.CLOSE_CHANNEL: cleanUp(request.socket); break; } return true; } @Override protected void processSelect(SelectionKey key) { // Handle event try { if (key.isAcceptable()) { accept(key); } else if (key.isReadable()) { read(key); } else if (key.isWritable()) { write(key); } } catch (IOException e) { cleanUp((SocketChannel)key.channel()); } } private void accept(SelectionKey key) throws IOException { // local socket established ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel(); SocketChannel socketChannel = serverSocketChannel.accept(); socketChannel.configureBlocking(false); socketChannel.register(_selector, SelectionKey.OP_READ); // prepare local socket write queue createWriteBuffer(socketChannel); // create pipe between local and remote socket PipeWorker pipe = _remoteSocketHandler.createPipe(this, socketChannel, _config.getRemoteIpAddress(), _config.getRemotePort()); _pipes.put(socketChannel, pipe); _executor.execute(pipe); } private void read(SelectionKey key) throws IOException { SocketChannel socketChannel = (SocketChannel) key.channel(); int readCount; PipeWorker pipe = _pipes.get(socketChannel); byte[] data; if (pipe == null) { // should not happen cleanUp(socketChannel); return; } _readBuffer.clear(); try { readCount = socketChannel.read(_readBuffer); } catch (IOException e) { cleanUp(socketChannel); return; } if (readCount == -1) { cleanUp(socketChannel); return; } data = _readBuffer.array(); pipe.processData(data, readCount, true); } private void write(SelectionKey key) throws IOException { SocketChannel socketChannel = (SocketChannel) key.channel(); List queue = (List) _pendingData.get(socketChannel); if (queue != null) { synchronized (queue) { // Write data while (!queue.isEmpty()) { ByteBuffer buf = (ByteBuffer) queue.get(0); socketChannel.write(buf); if (buf.remaining() > 0) { break; } queue.remove(0); } if (queue.isEmpty()) { key.interestOps(SelectionKey.OP_READ); } } } else { logger.warning("LocalSocket::write queue = null: " + socketChannel); return; } } @Override protected void cleanUp(SocketChannel socketChannel) { //logger.warning("LocalSocket closed: " + socketChannel); super.cleanUp(socketChannel); PipeWorker pipe = _pipes.get(socketChannel); if (pipe != null) { pipe.close(); _pipes.remove(socketChannel); logger.fine("LocalSocket closed: " + pipe.socketInfo); } else { logger.fine("LocalSocket closed (NULL): " + socketChannel); } } @Override public void close() { super.close(); _executor.shutdownNow(); try { _serverChannel.close(); _remoteSocketHandler.close(); } catch (IOException e) { logger.warning(Util.getErrorMessage(e)); } logger.info("Server closed."); } } ================================================ FILE: src/main/java/com/stfl/network/io/PipeSocket.java ================================================ /* * Copyright (c) 2015, Blake * All Rights Reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * 3. The name of the author may not be used to endorse or promote * products derived from this software without specific prior * written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ package com.stfl.network.io; import com.stfl.misc.Config; import com.stfl.misc.Util; import com.stfl.Constant; import com.stfl.network.proxy.IProxy; import com.stfl.network.proxy.ProxyFactory; import com.stfl.ss.CryptFactory; import com.stfl.ss.ICrypt; import java.io.*; import java.net.Socket; import java.net.SocketTimeoutException; import java.util.List; import java.util.concurrent.Executor; import java.util.logging.Logger; /** * Pipe local and remote sockets while server is running under blocking mode. */ public class PipeSocket implements Runnable { private Logger logger = Logger.getLogger(PipeSocket.class.getName()); private final int TIMEOUT = 10000; // 10s private ByteArrayOutputStream _remoteOutStream; private ByteArrayOutputStream _localOutStream; private Socket _remote; private Socket _local; private IProxy _proxy; private ICrypt _crypt; private boolean _isClosed; private Executor _executor; private Config _config; public PipeSocket(Executor executor, Socket socket, Config config) throws IOException { _executor = executor; _local = socket; _local.setSoTimeout(TIMEOUT); _config = config; _crypt = CryptFactory.get(_config.getMethod(), _config.getPassword()); _proxy = ProxyFactory.get(_config.getProxyType()); _remoteOutStream = new ByteArrayOutputStream(Constant.BUFFER_SIZE); _localOutStream = new ByteArrayOutputStream(Constant.BUFFER_SIZE); } @Override public void run() { try { _remote = initRemote(_config); _remote.setSoTimeout(TIMEOUT); } catch (IOException e) { close(); logger.warning(Util.getErrorMessage(e)); return; } _executor.execute(getLocalWorker()); _executor.execute(getRemoteWorker()); } private Socket initRemote(Config config) throws IOException { return new Socket(config.getRemoteIpAddress(), config.getRemotePort()); } private Runnable getLocalWorker() { return new Runnable() { @Override public void run() { BufferedInputStream stream; byte[] dataBuffer = new byte[Constant.BUFFER_SIZE]; byte[] buffer; int readCount; List sendData = null; // prepare local stream try { stream = new BufferedInputStream(_local.getInputStream()); } catch (IOException e) { logger.info(e.toString()); return; } // start to process data from local socket while (true) { try { // read data readCount = stream.read(dataBuffer); if (readCount == -1) { throw new IOException("Local socket closed (Read)!"); } // initialize proxy if (!_proxy.isReady()) { byte[] temp; buffer = new byte[readCount]; // dup dataBuffer to use in later System.arraycopy(dataBuffer, 0, buffer, 0, readCount); temp = _proxy.getResponse(buffer); if ((temp != null) && (!_sendLocal(temp, temp.length))) { throw new IOException("Local socket closed (proxy-Write)!"); } // packet for remote socket sendData = _proxy.getRemoteResponse(buffer); if (sendData == null) { continue; } logger.info("Connected to: " + Util.getRequestedHostInfo(sendData.get(0))); } else { sendData.clear(); sendData.add(dataBuffer); } for (byte[] bytes : sendData) { // send data to remote socket if (!sendRemote(bytes, bytes.length)) { throw new IOException("Remote socket closed (Write)!"); } } } catch (SocketTimeoutException e) { continue; } catch (IOException e) { logger.fine(e.toString()); break; } } close(); logger.fine(String.format("localWorker exit, Local=%s, Remote=%s", _local, _remote)); } }; } private Runnable getRemoteWorker() { return new Runnable() { @Override public void run() { BufferedInputStream stream; int readCount; byte[] dataBuffer = new byte[4096]; // prepare remote stream try { //stream = _remote.getInputStream(); stream = new BufferedInputStream (_remote.getInputStream()); } catch (IOException e) { logger.info(e.toString()); return; } // start to process data from remote socket while (true) { try { readCount = stream.read(dataBuffer); if (readCount == -1) { throw new IOException("Remote socket closed (Read)!"); } // send data to local socket if (!sendLocal(dataBuffer, readCount)) { throw new IOException("Local socket closed (Write)!"); } } catch (SocketTimeoutException e) { continue; } catch (IOException e) { logger.fine(e.toString()); break; } } close(); logger.fine(String.format("remoteWorker exit, Local=%s, Remote=%s", _local, _remote)); } }; } public void close() { if (_isClosed) { return; } _isClosed = true; try { _local.shutdownInput(); _local.shutdownOutput(); _local.close(); } catch (IOException e) { logger.fine("PipeSocket failed to close local socket (I/O exception)!"); } try { if (_remote != null) { _remote.shutdownInput(); _remote.shutdownOutput(); _remote.close(); } } catch (IOException e) { logger.fine("PipeSocket failed to close remote socket (I/O exception)!"); } } private boolean sendRemote(byte[] data, int length) { _crypt.encrypt(data, length, _remoteOutStream); byte[] sendData = _remoteOutStream.toByteArray(); return _sendRemote(sendData, sendData.length); } private boolean _sendRemote(byte[] data, int length) { try { if (length > 0) { OutputStream outStream = _remote.getOutputStream(); outStream.write(data, 0, length); } else { logger.info("Nothing to sendRemote!\n"); } } catch (IOException e) { logger.info(Util.getErrorMessage(e)); return false; } return true; } private boolean sendLocal(byte[] data, int length) { _crypt.decrypt(data, length, _localOutStream); byte[] sendData = _localOutStream.toByteArray(); return _sendLocal(sendData, sendData.length); } private boolean _sendLocal(byte[] data, int length) { try { OutputStream outStream = _local.getOutputStream(); outStream.write(data, 0, length); } catch (IOException e) { logger.info(Util.getErrorMessage(e)); return false; } return true; } } ================================================ FILE: src/main/java/com/stfl/network/nio/ChangeRequest.java ================================================ /* * Copyright (c) 2015, Blake * All Rights Reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * 3. The name of the author may not be used to endorse or promote * products derived from this software without specific prior * written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ package com.stfl.network.nio; import java.nio.channels.SocketChannel; /** * Request for nio socket handler */ public class ChangeRequest { public static final int REGISTER_CHANNEL = 1; public static final int CHANGE_SOCKET_OP = 2; public static final int CLOSE_CHANNEL = 3; public SocketChannel socket; public int type; public int op; public ChangeRequest(SocketChannel socket, int type, int op) { this.socket = socket; this.type = type; this.op = op; } public ChangeRequest(SocketChannel socket, int type) { this(socket, type, 0); } } ================================================ FILE: src/main/java/com/stfl/network/nio/ISocketHandler.java ================================================ /* * Copyright (c) 2015, Blake * All Rights Reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * 3. The name of the author may not be used to endorse or promote * products derived from this software without specific prior * written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ package com.stfl.network.nio; /** * Interface of socket handler */ public interface ISocketHandler { void send(ChangeRequest request, byte[] data); void send(ChangeRequest request); } ================================================ FILE: src/main/java/com/stfl/network/nio/PipeEvent.java ================================================ /* * Copyright (c) 2015, Blake * All Rights Reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * 3. The name of the author may not be used to endorse or promote * products derived from this software without specific prior * written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ package com.stfl.network.nio; /** * pipe event for pipe worker */ public class PipeEvent { public byte[] data; public boolean isEncrypted; public PipeEvent() {} public PipeEvent(byte[] data, boolean isEncrypted) { this.data = data; this.isEncrypted = isEncrypted; } } ================================================ FILE: src/main/java/com/stfl/network/nio/PipeWorker.java ================================================ /* * Copyright (c) 2015, Blake * All Rights Reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * 3. The name of the author may not be used to endorse or promote * products derived from this software without specific prior * written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ package com.stfl.network.nio; import com.stfl.misc.Config; import com.stfl.misc.Util; import com.stfl.Constant; import com.stfl.network.proxy.IProxy; import com.stfl.network.proxy.ProxyFactory; import com.stfl.ss.CryptFactory; import com.stfl.ss.ICrypt; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.channels.SelectionKey; import java.nio.channels.SocketChannel; import java.util.List; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.logging.Logger; public class PipeWorker implements Runnable { private Logger logger = Logger.getLogger(PipeWorker.class.getName()); private SocketChannel _localChannel; private SocketChannel _remoteChannel; private ISocketHandler _localSocketHandler; private ISocketHandler _remoteSocketHandler; private IProxy _proxy; private ICrypt _crypt; public String socketInfo; private ByteArrayOutputStream _outStream; private BlockingQueue _processQueue; private volatile boolean requestedClose; public PipeWorker(ISocketHandler localHandler, SocketChannel localChannel, ISocketHandler remoteHandler, SocketChannel remoteChannel, Config config) { _localChannel = localChannel; _remoteChannel = remoteChannel; _localSocketHandler = localHandler; _remoteSocketHandler = remoteHandler; _crypt = CryptFactory.get(config.getMethod(), config.getPassword()); _proxy = ProxyFactory.get(config.getProxyType()); _outStream = new ByteArrayOutputStream(Constant.BUFFER_SIZE); _processQueue = new LinkedBlockingQueue(); requestedClose = false; socketInfo = String.format("Local: %s, Remote: %s", localChannel, remoteChannel); } public void close() { requestedClose = true; processData(null, 0, false); } public void forceClose() { logger.fine("PipeWorker::forceClose " + socketInfo); // close socket now! try { if (_localChannel.isOpen()) { _localChannel.close(); } if (_remoteChannel.isOpen()) { _remoteChannel.close(); } } catch (IOException e) { logger.fine("PipeWorker::forceClose> " + e.toString()); } // follow the standard close steps close(); } public void processData(byte[] data, int count, boolean isEncrypted) { if (data != null) { byte[] dataCopy = new byte[count]; System.arraycopy(data, 0, dataCopy, 0, count); _processQueue.add(new PipeEvent(dataCopy, isEncrypted)); } else { _processQueue.add(new PipeEvent()); } } @Override public void run() { PipeEvent event; ISocketHandler socketHandler; SocketChannel channel; List sendData = null; while(true) { // make sure all the requests in the queue are processed if (_processQueue.isEmpty() && requestedClose) { logger.fine("PipeWorker closed ("+ _processQueue.size() + "): " + this.socketInfo); if (_localChannel.isOpen()) { _localSocketHandler.send(new ChangeRequest(_localChannel, ChangeRequest.CLOSE_CHANNEL)); } if (_remoteChannel.isOpen()) { _remoteSocketHandler.send(new ChangeRequest(_remoteChannel, ChangeRequest.CLOSE_CHANNEL)); } break; } try { event = (PipeEvent)_processQueue.take(); // if event data is null, it means this is a wake-up call // to check if any other thread is requested to close sockets if (event.data == null) { continue; } // process proxy packet if needed if (!_proxy.isReady()) { // packet for local socket byte[] temp = _proxy.getResponse(event.data); if (temp != null) { _localSocketHandler.send(new ChangeRequest(_localChannel, ChangeRequest.CHANGE_SOCKET_OP, SelectionKey.OP_WRITE), temp); } // packet for remote socket (ss payload + request) sendData = _proxy.getRemoteResponse(event.data); if (sendData == null) { continue; } // index 0 is always ss payload logger.info("Connected to: " + Util.getRequestedHostInfo(sendData.get(0))); //logger.info("Test: " + Util.bytesToString(temp, 0, temp.length)); } else { sendData.clear(); sendData.add(event.data); } for (byte[] bytes : sendData) { // empty stream for new data _outStream.reset(); if (event.isEncrypted) { _crypt.encrypt(bytes, _outStream); channel = _remoteChannel; socketHandler = _remoteSocketHandler; } else { _crypt.decrypt(bytes, _outStream); channel = _localChannel; socketHandler = _localSocketHandler; } // data is ready to send to socket ChangeRequest request = new ChangeRequest(channel, ChangeRequest.CHANGE_SOCKET_OP, SelectionKey.OP_WRITE); socketHandler.send(request, _outStream.toByteArray()); } } catch (InterruptedException e) { logger.fine(Util.getErrorMessage(e)); break; } } } } ================================================ FILE: src/main/java/com/stfl/network/nio/RemoteSocketHandler.java ================================================ /* * Copyright (c) 2015, Blake * All Rights Reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * 3. The name of the author may not be used to endorse or promote * products derived from this software without specific prior * written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ package com.stfl.network.nio; import com.stfl.misc.Config; import com.stfl.misc.Util; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.ClosedChannelException; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.SocketChannel; import java.nio.channels.spi.SelectorProvider; import java.security.InvalidAlgorithmParameterException; import java.util.List; import java.util.logging.Logger; /** * Handler for processing all IO event for remote sockets */ public class RemoteSocketHandler extends SocketHandlerBase { private Logger logger = Logger.getLogger(RemoteSocketHandler.class.getName()); public RemoteSocketHandler(Config config) throws IOException, InvalidAlgorithmParameterException { super(config); } @Override protected Selector initSelector() throws IOException { return SelectorProvider.provider().openSelector(); } @Override protected boolean processPendingRequest(ChangeRequest request) { if ((request.type != ChangeRequest.REGISTER_CHANNEL) && request.socket.isConnectionPending()) { return false; } SelectionKey key; switch (request.type) { case ChangeRequest.CHANGE_SOCKET_OP: key = request.socket.keyFor(_selector); if ((key != null) && key.isValid()) { key.interestOps(request.op); } else { logger.warning("RemoteSocketHandler::processPendingRequest (drop): " + key + request.socket); } break; case ChangeRequest.REGISTER_CHANNEL: try { request.socket.register(_selector, request.op); } catch (ClosedChannelException e) { // socket get closed by remote logger.warning(e.toString()); cleanUp(request.socket); } break; case ChangeRequest.CLOSE_CHANNEL: cleanUp(request.socket); break; } return true; } @Override protected void processSelect(SelectionKey key) { try { if (key.isConnectable()) { finishConnection(key); } else if (key.isReadable()) { read(key); } else if (key.isWritable()) { write(key); } } catch (IOException e) { cleanUp((SocketChannel) key.channel()); } } public PipeWorker createPipe(ISocketHandler localHandler, SocketChannel localChannel, String ipAddress, int port) throws IOException { // prepare remote socket SocketChannel socketChannel = SocketChannel.open(); socketChannel.configureBlocking(false); socketChannel.connect(new InetSocketAddress(ipAddress, port)); // create write buffer for specified socket createWriteBuffer(socketChannel); // create pipe worker for handling encrypt and decrypt PipeWorker pipe = new PipeWorker(localHandler, localChannel, this, socketChannel, _config); // setup pipe info //pipe.setRemoteChannel(socketChannel); _pipes.put(socketChannel, pipe); synchronized(_pendingRequest) { _pendingRequest.add(new ChangeRequest(socketChannel, ChangeRequest.REGISTER_CHANNEL, SelectionKey.OP_CONNECT)); } return pipe; } private void read(SelectionKey key) throws IOException { SocketChannel socketChannel = (SocketChannel) key.channel(); PipeWorker pipe = _pipes.get(socketChannel); if (pipe == null) { // should not happen cleanUp(socketChannel); return; } // clear read buffer for new data _readBuffer.clear(); // read data int readCount; try { readCount = socketChannel.read(_readBuffer); } catch (IOException e) { // remote socket closed cleanUp(socketChannel); return; } if (readCount == -1) { cleanUp(socketChannel); return; } // Handle the response pipe.processData(_readBuffer.array(), readCount, false); } private void write(SelectionKey key) throws IOException { SocketChannel socketChannel = (SocketChannel) key.channel(); List queue = (List) _pendingData.get(socketChannel); if (queue != null) { synchronized (queue) { // write data to socket while (!queue.isEmpty()) { ByteBuffer buf = (ByteBuffer) queue.get(0); socketChannel.write(buf); if (buf.remaining() > 0) { break; } queue.remove(0); } if (queue.isEmpty()) { key.interestOps(SelectionKey.OP_READ); } } } else { logger.warning("RemoteSocket::write queue = null: " + socketChannel); return; } } private void finishConnection(SelectionKey key) throws IOException { SocketChannel socketChannel = (SocketChannel) key.channel(); try { socketChannel.finishConnect(); } catch (IOException e) { logger.warning("RemoteSocketHandler::finishConnection I/O exception: " + e.toString()); cleanUp(socketChannel); return; } key.interestOps(SelectionKey.OP_WRITE); } @Override protected void cleanUp(SocketChannel socketChannel) { super.cleanUp(socketChannel); PipeWorker pipe = _pipes.get(socketChannel); if (pipe != null) { pipe.close(); _pipes.remove(socketChannel); logger.fine("RemoteSocket closed: " + pipe.socketInfo); } else { logger.fine("RemoteSocket closed (NULL): " + socketChannel); } } } ================================================ FILE: src/main/java/com/stfl/network/nio/SocketHandlerBase.java ================================================ /* * Copyright (c) 2015, Blake * All Rights Reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * 3. The name of the author may not be used to endorse or promote * products derived from this software without specific prior * written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ package com.stfl.network.nio; import com.stfl.misc.Config; import com.stfl.misc.Util; import com.stfl.Constant; import com.stfl.network.IServer; import com.stfl.ss.CryptFactory; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.ClosedSelectorException; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.SocketChannel; import java.security.InvalidAlgorithmParameterException; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.logging.Logger; /** * Base class of socket handler for processing all IO event for sockets */ public abstract class SocketHandlerBase implements IServer, ISocketHandler { private Logger logger = Logger.getLogger(SocketHandlerBase.class.getName()); protected Selector _selector; protected Config _config; protected final List _pendingRequest = new LinkedList(); protected final ConcurrentHashMap _pendingData = new ConcurrentHashMap(); protected ConcurrentMap _pipes = new ConcurrentHashMap<>(); protected ByteBuffer _readBuffer = ByteBuffer.allocate(Constant.BUFFER_SIZE); protected abstract Selector initSelector() throws IOException; protected abstract boolean processPendingRequest(ChangeRequest request); protected abstract void processSelect(SelectionKey key); public SocketHandlerBase(Config config) throws IOException, InvalidAlgorithmParameterException { if (!CryptFactory.isCipherExisted(config.getMethod())) { throw new InvalidAlgorithmParameterException(config.getMethod()); } _config = config; _selector = initSelector(); } @Override public void run() { while (true) { try { synchronized (_pendingRequest) { Iterator changes = _pendingRequest.iterator(); while (changes.hasNext()) { ChangeRequest change = (ChangeRequest) changes.next(); if (!processPendingRequest(change)) break; changes.remove(); } } // wait events from selected channels _selector.select(); Iterator selectedKeys = _selector.selectedKeys().iterator(); while (selectedKeys.hasNext()) { SelectionKey key = (SelectionKey) selectedKeys.next(); selectedKeys.remove(); if (!key.isValid()) { continue; } processSelect(key); } } catch (ClosedSelectorException e) { break; } catch (Exception e) { logger.warning(Util.getErrorMessage(e)); } } logger.fine(this.getClass().getName() + " Closed."); } protected void createWriteBuffer(SocketChannel socketChannel) { List queue = new ArrayList(); Object put; put = _pendingData.putIfAbsent(socketChannel, queue); if (put != null) { logger.severe("Dup write buffer creation: " + socketChannel); } } protected void cleanUp(SocketChannel socketChannel) { try { socketChannel.close(); } catch (IOException e) { logger.info(Util.getErrorMessage(e)); } SelectionKey key = socketChannel.keyFor(_selector); if (key != null) { key.cancel(); } if (_pendingData.containsKey(socketChannel)) { _pendingData.remove(socketChannel); } } @Override public void send(ChangeRequest request, byte[] data) { switch (request.type) { case ChangeRequest.CHANGE_SOCKET_OP: List queue = (List) _pendingData.get(request.socket); if (queue != null) { synchronized (queue) { // in general case, the write queue is always existed, unless, the socket has been shutdown queue.add(ByteBuffer.wrap(data)); } } else { logger.warning(Util.getErrorMessage(new Throwable("Socket is closed! dropping this request"))); } break; } synchronized (_pendingRequest) { _pendingRequest.add(request); } _selector.wakeup(); } @Override public void send(ChangeRequest request) { send(request, null); } public void close() { for (PipeWorker p : _pipes.values()) { p.forceClose(); } _pipes.clear(); try { _selector.close(); } catch (IOException e) { logger.warning(Util.getErrorMessage(e)); } } } ================================================ FILE: src/main/java/com/stfl/network/proxy/AutoProxy.java ================================================ package com.stfl.network.proxy; import com.stfl.misc.Reflection; import java.util.List; import java.util.Map; import java.util.logging.Logger; /** * Decides proxy type automatically (as soon as socket established). * Proxy class for Socks5 and Http */ public class AutoProxy implements IProxy { private Logger logger = Logger.getLogger(AutoProxy.class.getName()); private IProxy _proxy; private volatile boolean isInitialized; public AutoProxy() { isInitialized = false; } @Override public boolean isReady() { return (isInitialized && _proxy.isReady()); } @Override public TYPE getType() { return TYPE.AUTO; } @Override public byte[] getResponse(byte[] data) { if (!isInitialized) { init(data); } return _proxy.getResponse(data); } @Override public List getRemoteResponse(byte[] data) { if (!isInitialized) { init(data); } return _proxy.getRemoteResponse(data); } @Override public boolean isMine(byte[] data) { if (!isInitialized) { init(data); } return _proxy.isMine(data); } private void init(byte[] data) { Object obj; IProxy proxy; for (Map.Entry entry : ProxyFactory.proxies.entrySet()) { if (entry.getKey() == this.getType()) continue; obj = Reflection.get(entry.getValue()); proxy = (IProxy)obj; if (proxy.isMine(data)) { logger.fine("ProxyType (Auto): " + proxy.getType()); _proxy = proxy; isInitialized = true; return; } } logger.severe("Unable to determine proxy type!"); } } ================================================ FILE: src/main/java/com/stfl/network/proxy/HttpProxy.java ================================================ /* * Copyright (c) 2015, Blake * All Rights Reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * 3. The name of the author may not be used to endorse or promote * products derived from this software without specific prior * written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ package com.stfl.network.proxy; import com.stfl.Constant; import com.stfl.misc.Util; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Provide local HTTP proxy statue and required response */ public class HttpProxy implements IProxy { private static final String[] HTTP_METHODS = new String[] {"OPTIONS", "GET", "HEAD", "POST", "PUT", "DELETE", "TRACE", "CONNECT"}; private Logger logger = Logger.getLogger(HttpProxy.class.getName()); private boolean _isReady; private boolean _isHttpConnect; private Map methodCache; public HttpProxy() { _isReady = false; _isHttpConnect = false; } public TYPE getType() { return TYPE.HTTP; } public boolean isReady() { return _isReady; } public byte[] getResponse(byte[] data) { if (methodCache == null) { methodCache = getHttpMethod(data); } setHttpMethod(methodCache); if (_isHttpConnect) return String.format("HTTP/1.0 200\r\nProxy-agent: %s/%s\r\n\r\n", Constant.PROG_NAME, Constant.VERSION).getBytes(); return null; } public List getRemoteResponse(byte[] data) { List respData = new ArrayList<>(2); String host; int port = 80; // HTTP port if (methodCache == null) { methodCache = getHttpMethod(data); } String[] hostInfo = methodCache.get("host").split(":"); // get hostname and port host = hostInfo[0]; if (hostInfo.length > 1) { port = Integer.parseInt(hostInfo[1]); } byte[] ssHeader = Util.composeSSHeader(host, port); respData.add(ssHeader); if (!_isHttpConnect) { byte[] httpHeader = reconstructHttpHeader(methodCache, data); respData.add(httpHeader); } _isReady = true; return respData; } @Override public boolean isMine(byte[] data) { if (methodCache == null) { methodCache = getHttpMethod(data); } String method = methodCache.get("method"); if (method != null) { for (String s : HTTP_METHODS) { if (s.equals(method)) { return true; } } } return false; } private Map getHttpMethod(byte[] data) { String httpRequest = Util.bytesToString(data, 0, data.length); String[] httpHeaders = httpRequest.split("\\r?\\n"); boolean isHostFound = true; //Pattern pattern = Pattern.compile("^([a-zA-Z]*) [hHtTpP]{0,4}[:\\/]{0,3}(\\S[^/ ]*)"); Pattern pattern = Pattern.compile("^([a-zA-Z]*) [htps]{0,4}[:/]{0,3}(\\S[^/]*)(\\S*) (\\S*)"); Map header = new HashMap<>(); if (httpHeaders.length > 0) { logger.fine("HTTP Header: " + httpHeaders[0]); Matcher matcher = pattern.matcher(httpHeaders[0]); if (matcher.find()) { header.put("method", matcher.group(1)); if (matcher.group(2).startsWith("/")) { header.put("url", "/"); isHostFound = false; } else { header.put("host", matcher.group(2)); header.put("url", matcher.group(3)); } header.put("version", matcher.group(4)); } } if (!isHostFound) { for (String line : httpHeaders) { if (line.toLowerCase().contains("host")) { String info = line.split(":")[1].trim(); header.put("host", info); break; } } } return header; } private byte[] reconstructHttpHeader(Map method, byte[] data) { String httpRequest = Util.bytesToString(data, 0, data.length); String[] httpHeaders = httpRequest.split("\\r?\\n"); StringBuilder sb = new StringBuilder(); boolean isFirstLine = true; //logger.info("original HttpHeader:" + httpRequest); for (String line : httpHeaders) { if (isFirstLine && _isHttpConnect) { sb.append(method.get("method")); sb.append(" "); sb.append(method.get("host")); sb.append(" "); sb.append(method.get("version")); sb.append("\r\n"); sb.append("User-Agent: test/0.1\r\n"); break; } else if (isFirstLine) { sb.append(method.get("method")); sb.append(" "); sb.append(method.get("url")); sb.append(" "); sb.append(method.get("version")); isFirstLine = false; } else if (line.toLowerCase().contains("cache-control")) { sb.append("Pragma: no-cache\r\n"); sb.append("Cache-Control: no-cache"); } else if (line.toLowerCase().contains("proxy-connection")) { //Proxy-Connection String[] fields = line.split(":"); sb.append("Connection: "); sb.append(fields[1].trim()); } else if (line.toLowerCase().contains("if-none-match")) { continue; } else if (line.toLowerCase().contains("if-modified-since")) { continue; } else { sb.append(line); } sb.append("\r\n"); } sb.append("\r\n"); //logger.info("reconstructHttpHeader:" + sb.toString()); return sb.toString().getBytes(); } private void setHttpMethod(Map header) { String method = header.get("method"); if (method != null) { if (method.toUpperCase().equals("CONNECT")) { _isHttpConnect = true; } else { _isHttpConnect = false; } } } } ================================================ FILE: src/main/java/com/stfl/network/proxy/IProxy.java ================================================ /* * Copyright (c) 2015, Blake * All Rights Reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * 3. The name of the author may not be used to endorse or promote * products derived from this software without specific prior * written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ package com.stfl.network.proxy; import java.util.List; public interface IProxy { enum TYPE {SOCKS5, HTTP, AUTO} boolean isReady(); TYPE getType(); byte[] getResponse(byte[] data); List getRemoteResponse(byte[] data); boolean isMine(byte[] data); } ================================================ FILE: src/main/java/com/stfl/network/proxy/ProxyFactory.java ================================================ /* * Copyright (c) 2015, Blake * All Rights Reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * 3. The name of the author may not be used to endorse or promote * products derived from this software without specific prior * written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ package com.stfl.network.proxy; import com.stfl.misc.Reflection; import java.util.*; import java.util.logging.Logger; /** * Proxy factory */ public class ProxyFactory { public static final Map proxies = new HashMap() {{ put(IProxy.TYPE.HTTP, HttpProxy.class.getName()); put(IProxy.TYPE.SOCKS5, Socks5Proxy.class.getName()); put(IProxy.TYPE.AUTO, AutoProxy.class.getName()); }}; private static Logger logger = Logger.getLogger(ProxyFactory.class.getName()); public static boolean isProxyTypeExisted(String name) { IProxy.TYPE type = IProxy.TYPE.valueOf(name); return (proxies.get(type) != null); } public static IProxy get(IProxy.TYPE type) { try { Object obj = Reflection.get(proxies.get(type)); return (IProxy)obj; } catch (Exception e) { logger.info(com.stfl.misc.Util.getErrorMessage(e)); } return null; } public static List getSupportedProxyTypes() { List sortedKeys = new ArrayList<>(proxies.keySet()); Collections.sort(sortedKeys); return sortedKeys; } } ================================================ FILE: src/main/java/com/stfl/network/proxy/Socks5Proxy.java ================================================ /* * Copyright (c) 2015, Blake * All Rights Reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * 3. The name of the author may not be used to endorse or promote * products derived from this software without specific prior * written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ package com.stfl.network.proxy; import java.util.ArrayList; import java.util.List; import java.util.logging.Logger; /** * Provide local socks5 statue and required response */ public class Socks5Proxy implements IProxy { public final static int ATYP_IP_V4 = 0x1; public final static int ATYP_DOMAIN_NAME = 0x3; public final static int ATYP_IP_V6 = 0x4; private Logger logger = Logger.getLogger(Socks5Proxy.class.getName()); private enum STAGE {SOCK5_HELLO, SOCKS_ACK, SOCKS_READY} private STAGE _stage; public Socks5Proxy() { _stage = STAGE.SOCK5_HELLO; } public TYPE getType() { return TYPE.SOCKS5; } public boolean isReady() { return (_stage == STAGE.SOCKS_READY); } public byte[] getResponse(byte[] data) { byte[] respData = null; switch (_stage) { case SOCK5_HELLO: if (isMine(data)) { respData = new byte[] {5, 0}; } else { respData = new byte[] {0, 91}; } _stage = STAGE.SOCKS_ACK; break; case SOCKS_ACK: respData = new byte[] {5, 0, 0, 1, 0, 0, 0, 0, 0, 0}; _stage = STAGE.SOCKS_READY; break; default: // TODO: exception break; } return respData; } public List getRemoteResponse(byte[] data) { List respData = null; int dataLength = data.length; /* There are two stage of establish Sock5: 1. HELLO (3 bytes) 2. ACK (3 bytes + dst info) as Client sending ACK, it might contain dst info. In this case, server needs to send back ACK response to client and start the remote socket right away, otherwise, client will wait until timeout. */ if (_stage == STAGE.SOCKS_READY) { respData = new ArrayList<>(1); // remove socks5 header (partial) if (dataLength > 3) { dataLength -= 3; byte[] temp = new byte[dataLength]; System.arraycopy(data, 3, temp, 0, dataLength); respData.add(temp); } } return respData; } @Override public boolean isMine(byte[] data) { if (data[0] == 0x5) { return true; } return false; } } ================================================ FILE: src/main/java/com/stfl/ss/AesCrypt.java ================================================ /* * Copyright (c) 2015, Blake * All Rights Reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * 3. The name of the author may not be used to endorse or promote * products derived from this software without specific prior * written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ package com.stfl.ss; import org.bouncycastle.crypto.StreamBlockCipher; import org.bouncycastle.crypto.engines.AESEngine; import org.bouncycastle.crypto.engines.AESFastEngine; import org.bouncycastle.crypto.modes.CFBBlockCipher; import org.bouncycastle.crypto.modes.OFBBlockCipher; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import java.io.ByteArrayOutputStream; import java.security.InvalidAlgorithmParameterException; import java.util.HashMap; import java.util.Map; /** * AES Crypt implementation */ public class AesCrypt extends CryptBase { public final static String CIPHER_AES_128_CFB = "aes-128-cfb"; public final static String CIPHER_AES_192_CFB = "aes-192-cfb"; public final static String CIPHER_AES_256_CFB = "aes-256-cfb"; public final static String CIPHER_AES_128_OFB = "aes-128-ofb"; public final static String CIPHER_AES_192_OFB = "aes-192-ofb"; public final static String CIPHER_AES_256_OFB = "aes-256-ofb"; public static Map getCiphers() { Map ciphers = new HashMap<>(); ciphers.put(CIPHER_AES_128_CFB, AesCrypt.class.getName()); ciphers.put(CIPHER_AES_192_CFB, AesCrypt.class.getName()); ciphers.put(CIPHER_AES_256_CFB, AesCrypt.class.getName()); ciphers.put(CIPHER_AES_128_OFB, AesCrypt.class.getName()); ciphers.put(CIPHER_AES_192_OFB, AesCrypt.class.getName()); ciphers.put(CIPHER_AES_256_OFB, AesCrypt.class.getName()); return ciphers; } public AesCrypt(String name, String password) { super(name, password); } @Override public int getKeyLength() { if(_name.equals(CIPHER_AES_128_CFB) || _name.equals(CIPHER_AES_128_OFB)) { return 16; } else if (_name.equals(CIPHER_AES_192_CFB) || _name.equals(CIPHER_AES_192_OFB)) { return 24; } else if (_name.equals(CIPHER_AES_256_CFB) || _name.equals(CIPHER_AES_256_OFB)) { return 32; } return 0; } @Override protected StreamBlockCipher getCipher(boolean isEncrypted) throws InvalidAlgorithmParameterException { AESFastEngine engine = new AESFastEngine(); StreamBlockCipher cipher; if (_name.equals(CIPHER_AES_128_CFB)) { cipher = new CFBBlockCipher(engine, getIVLength() * 8); } else if (_name.equals(CIPHER_AES_192_CFB)) { cipher = new CFBBlockCipher(engine, getIVLength() * 8); } else if (_name.equals(CIPHER_AES_256_CFB)) { cipher = new CFBBlockCipher(engine, getIVLength() * 8); } else if (_name.equals(CIPHER_AES_128_OFB)) { cipher = new OFBBlockCipher(engine, getIVLength() * 8); } else if (_name.equals(CIPHER_AES_192_OFB)) { cipher = new OFBBlockCipher(engine, getIVLength() * 8); } else if (_name.equals(CIPHER_AES_256_OFB)) { cipher = new OFBBlockCipher(engine, getIVLength() * 8); } else { throw new InvalidAlgorithmParameterException(_name); } return cipher; } @Override public int getIVLength() { return 16; } @Override protected SecretKey getKey() { return new SecretKeySpec(_ssKey.getEncoded(), "AES"); } @Override protected void _encrypt(byte[] data, ByteArrayOutputStream stream) { int noBytesProcessed; byte[] buffer = new byte[data.length]; noBytesProcessed = encCipher.processBytes(data, 0, data.length, buffer, 0); stream.write(buffer, 0, noBytesProcessed); } @Override protected void _decrypt(byte[] data, ByteArrayOutputStream stream) { int noBytesProcessed; byte[] buffer = new byte[data.length]; noBytesProcessed = decCipher.processBytes(data, 0, data.length, buffer, 0); stream.write(buffer, 0, noBytesProcessed); } } ================================================ FILE: src/main/java/com/stfl/ss/BlowFishCrypt.java ================================================ /* * Copyright (c) 2015, Blake * All Rights Reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * 3. The name of the author may not be used to endorse or promote * products derived from this software without specific prior * written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ package com.stfl.ss; import org.bouncycastle.crypto.StreamBlockCipher; import org.bouncycastle.crypto.engines.BlowfishEngine; import org.bouncycastle.crypto.modes.CFBBlockCipher; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import java.io.ByteArrayOutputStream; import java.security.InvalidAlgorithmParameterException; import java.util.HashMap; import java.util.Map; /** * Blow fish cipher implementation */ public class BlowFishCrypt extends CryptBase { public final static String CIPHER_BLOWFISH_CFB = "bf-cfb"; public static Map getCiphers() { Map ciphers = new HashMap<>(); ciphers.put(CIPHER_BLOWFISH_CFB, BlowFishCrypt.class.getName()); return ciphers; } public BlowFishCrypt(String name, String password) { super(name, password); } @Override public int getKeyLength() { return 16; } @Override protected StreamBlockCipher getCipher(boolean isEncrypted) throws InvalidAlgorithmParameterException { BlowfishEngine engine = new BlowfishEngine(); StreamBlockCipher cipher; if (_name.equals(CIPHER_BLOWFISH_CFB)) { cipher = new CFBBlockCipher(engine, getIVLength() * 8); } else { throw new InvalidAlgorithmParameterException(_name); } return cipher; } @Override public int getIVLength() { return 8; } @Override protected SecretKey getKey() { return new SecretKeySpec(_ssKey.getEncoded(), "AES"); } @Override protected void _encrypt(byte[] data, ByteArrayOutputStream stream) { int noBytesProcessed; byte[] buffer = new byte[data.length]; noBytesProcessed = encCipher.processBytes(data, 0, data.length, buffer, 0); stream.write(buffer, 0, noBytesProcessed); } @Override protected void _decrypt(byte[] data, ByteArrayOutputStream stream) { int noBytesProcessed; byte[] buffer = new byte[data.length]; noBytesProcessed = decCipher.processBytes(data, 0, data.length, buffer, 0); stream.write(buffer, 0, noBytesProcessed); } } ================================================ FILE: src/main/java/com/stfl/ss/CamelliaCrypt.java ================================================ /* * Copyright (c) 2015, Blake * All Rights Reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * 3. The name of the author may not be used to endorse or promote * products derived from this software without specific prior * written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ package com.stfl.ss; import org.bouncycastle.crypto.StreamBlockCipher; import org.bouncycastle.crypto.engines.CamelliaEngine; import org.bouncycastle.crypto.modes.CFBBlockCipher; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import java.io.ByteArrayOutputStream; import java.security.InvalidAlgorithmParameterException; import java.util.HashMap; import java.util.Map; /** * Camellia cipher implementation */ public class CamelliaCrypt extends CryptBase { public final static String CIPHER_CAMELLIA_128_CFB = "camellia-128-cfb"; public final static String CIPHER_CAMELLIA_192_CFB = "camellia-192-cfb"; public final static String CIPHER_CAMELLIA_256_CFB = "camellia-256-cfb"; public static Map getCiphers() { Map ciphers = new HashMap<>(); ciphers.put(CIPHER_CAMELLIA_128_CFB, CamelliaCrypt.class.getName()); ciphers.put(CIPHER_CAMELLIA_192_CFB, CamelliaCrypt.class.getName()); ciphers.put(CIPHER_CAMELLIA_256_CFB, CamelliaCrypt.class.getName()); return ciphers; } public CamelliaCrypt(String name, String password) { super(name, password); } @Override public int getKeyLength() { if(_name.equals(CIPHER_CAMELLIA_128_CFB)) { return 16; } else if (_name.equals(CIPHER_CAMELLIA_192_CFB)) { return 24; } else if (_name.equals(CIPHER_CAMELLIA_256_CFB)) { return 32; } return 0; } @Override protected StreamBlockCipher getCipher(boolean isEncrypted) throws InvalidAlgorithmParameterException { CamelliaEngine engine = new CamelliaEngine(); StreamBlockCipher cipher; if (_name.equals(CIPHER_CAMELLIA_128_CFB)) { cipher = new CFBBlockCipher(engine, getIVLength() * 8); } else if (_name.equals(CIPHER_CAMELLIA_192_CFB)) { cipher = new CFBBlockCipher(engine, getIVLength() * 8); } else if (_name.equals(CIPHER_CAMELLIA_256_CFB)) { cipher = new CFBBlockCipher(engine, getIVLength() * 8); } else { throw new InvalidAlgorithmParameterException(_name); } return cipher; } @Override public int getIVLength() { return 16; } @Override protected SecretKey getKey() { return new SecretKeySpec(_ssKey.getEncoded(), "AES"); } @Override protected void _encrypt(byte[] data, ByteArrayOutputStream stream) { int noBytesProcessed; byte[] buffer = new byte[data.length]; noBytesProcessed = encCipher.processBytes(data, 0, data.length, buffer, 0); stream.write(buffer, 0, noBytesProcessed); } @Override protected void _decrypt(byte[] data, ByteArrayOutputStream stream) { int noBytesProcessed; byte[] buffer = new byte[data.length]; noBytesProcessed = decCipher.processBytes(data, 0, data.length, buffer, 0); stream.write(buffer, 0, noBytesProcessed); } } ================================================ FILE: src/main/java/com/stfl/ss/CryptBase.java ================================================ /* * Copyright (c) 2015, Blake * All Rights Reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * 3. The name of the author may not be used to endorse or promote * products derived from this software without specific prior * written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ package com.stfl.ss; import com.stfl.misc.Util; import org.bouncycastle.crypto.StreamBlockCipher; import org.bouncycastle.crypto.params.KeyParameter; import org.bouncycastle.crypto.params.ParametersWithIV; import javax.crypto.SecretKey; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.security.InvalidAlgorithmParameterException; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Logger; /** * Crypt base class implementation */ public abstract class CryptBase implements ICrypt { protected abstract StreamBlockCipher getCipher(boolean isEncrypted) throws InvalidAlgorithmParameterException; protected abstract SecretKey getKey(); protected abstract void _encrypt(byte[] data, ByteArrayOutputStream stream); protected abstract void _decrypt(byte[] data, ByteArrayOutputStream stream); protected final String _name; protected final SecretKey _key; protected final ShadowSocksKey _ssKey; protected final int _ivLength; protected final int _keyLength; protected boolean _encryptIVSet; protected boolean _decryptIVSet; protected byte[] _encryptIV; protected byte[] _decryptIV; protected final Lock encLock = new ReentrantLock(); protected final Lock decLock = new ReentrantLock(); protected StreamBlockCipher encCipher; protected StreamBlockCipher decCipher; private Logger logger = Logger.getLogger(CryptBase.class.getName()); public CryptBase(String name, String password) { _name = name.toLowerCase(); _ivLength = getIVLength(); _keyLength = getKeyLength(); _ssKey = new ShadowSocksKey(password, _keyLength); _key = getKey(); } protected void setIV(byte[] iv, boolean isEncrypt) { if (_ivLength == 0) { return; } if (isEncrypt) { _encryptIV = new byte[_ivLength]; System.arraycopy(iv, 0, _encryptIV, 0, _ivLength); try { encCipher = getCipher(isEncrypt); ParametersWithIV parameterIV = new ParametersWithIV(new KeyParameter(_key.getEncoded()), _encryptIV); encCipher.init(isEncrypt, parameterIV); } catch (InvalidAlgorithmParameterException e) { logger.info(e.toString()); } } else { _decryptIV = new byte[_ivLength]; System.arraycopy(iv, 0, _decryptIV, 0, _ivLength); try { decCipher = getCipher(isEncrypt); ParametersWithIV parameterIV = new ParametersWithIV(new KeyParameter(_key.getEncoded()), _decryptIV); decCipher.init(isEncrypt, parameterIV); } catch (InvalidAlgorithmParameterException e) { logger.info(e.toString()); } } } @Override public void encrypt(byte[] data, ByteArrayOutputStream stream) { synchronized (encLock) { stream.reset(); if (!_encryptIVSet) { _encryptIVSet = true; byte[] iv = Util.randomBytes(_ivLength); setIV(iv, true); try { stream.write(iv); } catch (IOException e) { logger.info(e.toString()); } } _encrypt(data, stream); } } @Override public void encrypt(byte[] data, int length, ByteArrayOutputStream stream) { byte[] d = new byte[length]; System.arraycopy(data, 0, d, 0, length); encrypt(d, stream); } @Override public void decrypt(byte[] data, ByteArrayOutputStream stream) { byte[] temp; synchronized (decLock) { stream.reset(); if (!_decryptIVSet) { _decryptIVSet = true; setIV(data, false); temp = new byte[data.length - _ivLength]; System.arraycopy(data, _ivLength, temp, 0, data.length - _ivLength); } else { temp = data; } _decrypt(temp, stream); } } @Override public void decrypt(byte[] data, int length, ByteArrayOutputStream stream) { byte[] d = new byte[length]; System.arraycopy(data, 0, d, 0, length); decrypt(d, stream); } } ================================================ FILE: src/main/java/com/stfl/ss/CryptFactory.java ================================================ /* * Copyright (c) 2015, Blake * All Rights Reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * 3. The name of the author may not be used to endorse or promote * products derived from this software without specific prior * written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ package com.stfl.ss; import com.stfl.misc.Reflection; import java.util.*; import java.util.logging.Logger; /** * Crypt factory */ public class CryptFactory { private static final Map crypts = new HashMap() {{ putAll(AesCrypt.getCiphers()); putAll(CamelliaCrypt.getCiphers()); putAll(BlowFishCrypt.getCiphers()); putAll(SeedCrypt.getCiphers()); // TODO: other crypts }}; private static Logger logger = Logger.getLogger(CryptFactory.class.getName()); public static boolean isCipherExisted(String name) { return (crypts.get(name) != null); } public static ICrypt get(String name, String password) { try { Object obj = Reflection.get(crypts.get(name), String.class, name, String.class, password); return (ICrypt)obj; } catch (Exception e) { logger.info(com.stfl.misc.Util.getErrorMessage(e)); } return null; } public static List getSupportedCiphers() { List sortedKeys = new ArrayList<>(crypts.keySet()); Collections.sort(sortedKeys); return sortedKeys; } } ================================================ FILE: src/main/java/com/stfl/ss/ICrypt.java ================================================ /* * Copyright (c) 2015, Blake * All Rights Reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * 3. The name of the author may not be used to endorse or promote * products derived from this software without specific prior * written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ package com.stfl.ss; import java.io.ByteArrayOutputStream; import java.util.Map; /** * Interface of crypt */ public interface ICrypt { void encrypt(byte[] data, ByteArrayOutputStream stream); void encrypt(byte[] data, int length, ByteArrayOutputStream stream); void decrypt(byte[] data, ByteArrayOutputStream stream); void decrypt(byte[] data, int length, ByteArrayOutputStream stream); int getIVLength(); int getKeyLength(); } ================================================ FILE: src/main/java/com/stfl/ss/SeedCrypt.java ================================================ /* * Copyright (c) 2015, Blake * All Rights Reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * 3. The name of the author may not be used to endorse or promote * products derived from this software without specific prior * written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ package com.stfl.ss; import org.bouncycastle.crypto.StreamBlockCipher; import org.bouncycastle.crypto.engines.SEEDEngine; import org.bouncycastle.crypto.modes.CFBBlockCipher; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import java.io.ByteArrayOutputStream; import java.security.InvalidAlgorithmParameterException; import java.util.HashMap; import java.util.Map; /** * Seed cipher implementation */ public class SeedCrypt extends CryptBase { public final static String CIPHER_SEED_CFB = "seed-cfb"; public static Map getCiphers() { Map ciphers = new HashMap<>(); ciphers.put(CIPHER_SEED_CFB, SeedCrypt.class.getName()); return ciphers; } public SeedCrypt(String name, String password) { super(name, password); } @Override public int getKeyLength() { return 16; } @Override protected StreamBlockCipher getCipher(boolean isEncrypted) throws InvalidAlgorithmParameterException { SEEDEngine engine = new SEEDEngine(); StreamBlockCipher cipher; if (_name.equals(CIPHER_SEED_CFB)) { cipher = new CFBBlockCipher(engine, getIVLength() * 8); } else { throw new InvalidAlgorithmParameterException(_name); } return cipher; } @Override public int getIVLength() { return 16; } @Override protected SecretKey getKey() { return new SecretKeySpec(_ssKey.getEncoded(), "AES"); } @Override protected void _encrypt(byte[] data, ByteArrayOutputStream stream) { int noBytesProcessed; byte[] buffer = new byte[data.length]; noBytesProcessed = encCipher.processBytes(data, 0, data.length, buffer, 0); stream.write(buffer, 0, noBytesProcessed); } @Override protected void _decrypt(byte[] data, ByteArrayOutputStream stream) { int noBytesProcessed; byte[] buffer = new byte[data.length]; noBytesProcessed = decCipher.processBytes(data, 0, data.length, buffer, 0); stream.write(buffer, 0, noBytesProcessed); } } ================================================ FILE: src/main/java/com/stfl/ss/ShadowSocksKey.java ================================================ /* * Copyright (c) 2015, Blake * All Rights Reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * 3. The name of the author may not be used to endorse or promote * products derived from this software without specific prior * written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ package com.stfl.ss; import com.stfl.misc.Util; import javax.crypto.SecretKey; import java.io.UnsupportedEncodingException; import java.security.MessageDigest; import java.util.logging.Logger; /** * Shadowsocks key generator */ public class ShadowSocksKey implements SecretKey { private Logger logger = Logger.getLogger(ShadowSocksKey.class.getName()); private final static int KEY_LENGTH = 32; private byte[] _key; private int _length; public ShadowSocksKey(String password) { _length = KEY_LENGTH; _key = init(password); } public ShadowSocksKey(String password, int length) { // TODO: Invalid key length _length = length; _key = init(password); } private byte[] init(String password) { MessageDigest md = null; byte[] keys = new byte[KEY_LENGTH]; byte[] temp = null; byte[] hash = null; byte[] passwordBytes = null; int i = 0; try { md = MessageDigest.getInstance("MD5"); passwordBytes = password.getBytes("UTF-8"); } catch (UnsupportedEncodingException e) { logger.info("ShadowSocksKey: Unsupported string encoding"); } catch (Exception e) { logger.info(Util.getErrorMessage(e)); return null; } while (i < keys.length) { if (i == 0) { hash = md.digest(passwordBytes); temp = new byte[passwordBytes.length+hash.length]; } else { System.arraycopy(hash, 0, temp, 0, hash.length); System.arraycopy(passwordBytes, 0, temp, hash.length, passwordBytes.length); hash = md.digest(temp); } System.arraycopy(hash, 0, keys, i, hash.length); i += hash.length; } if (_length != KEY_LENGTH) { byte[] keysl = new byte[_length]; System.arraycopy(keys, 0, keysl, 0, _length); return keysl; } return keys; } @Override public String getAlgorithm() { return "shadowsocks"; } @Override public String getFormat() { return "RAW"; } @Override public byte[] getEncoded() { return _key; } } ================================================ FILE: src/main/java/com/stfl/ui/LogLayoutController.java ================================================ package com.stfl.ui; import com.stfl.misc.Log; import javafx.fxml.FXML; import javafx.scene.control.Button; import javafx.scene.control.TextArea; import javafx.stage.Stage; public class LogLayoutController { @FXML public TextArea txtLog; @FXML private Button btnClose; @FXML private Button btnClear; private Stage stage; @FXML private void initialize() { TextAreaLogHandler handler = new TextAreaLogHandler(); handler.setTextArea(txtLog); Log.addHandler(handler); } @FXML private void handleClear() { txtLog.clear(); } @FXML private void handleClose() { stage.hide(); } public void setStage(Stage stage) { this.stage = stage; } } ================================================ FILE: src/main/java/com/stfl/ui/MainLayoutController.java ================================================ package com.stfl.ui; import com.stfl.Constant; import com.stfl.MainGui; import com.stfl.misc.Config; import com.stfl.misc.UTF8Control; import com.stfl.misc.Util; import com.stfl.network.IServer; import com.stfl.network.NioLocalServer; import com.stfl.network.proxy.IProxy; import com.stfl.network.proxy.ProxyFactory; import com.stfl.ss.CryptFactory; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.scene.Scene; import javafx.scene.control.*; import javafx.scene.image.Image; import javafx.scene.layout.Pane; import javafx.stage.Stage; import java.io.IOException; import java.security.InvalidAlgorithmParameterException; import java.util.ResourceBundle; import java.util.logging.Logger; public class MainLayoutController { @FXML private TextField txtServerIP; @FXML private TextField txtServerPort; @FXML private ComboBox cboCipher; @FXML private TextField txtPassword; @FXML private TextField txtLocalPort; @FXML private ComboBox cboProxyType; @FXML private Button btnStart; @FXML private Button btnStop; @FXML private Button btnLog; @FXML private Button btnClose; private Logger logger = Logger.getLogger(MainLayoutController.class.getName()); private MainGui gui; private IServer server; private Stage logStage; private Config config; @FXML private void initialize() { // set cipher options ObservableList ciphers = FXCollections.observableArrayList(); ciphers.addAll(CryptFactory.getSupportedCiphers()); cboCipher.setItems(ciphers); // set proxy options ObservableList proxyTypes = FXCollections.observableArrayList(); proxyTypes.addAll(ProxyFactory.getSupportedProxyTypes()); cboProxyType.setItems(proxyTypes); // prepare configuration config = new Config(); config.loadFromJson(Util.getFileContent(Constant.CONF_FILE)); txtServerIP.setText(config.getRemoteIpAddress()); txtServerPort.setText(String.valueOf(config.getRemotePort())); txtLocalPort.setText(String.valueOf(config.getLocalPort())); txtPassword.setText(config.getPassword()); cboCipher.setValue(config.getMethod()); cboProxyType.setValue(config.getProxyType()); // prepare log window Stage stage = new Stage(); try { FXMLLoader logLayoutLoader = new FXMLLoader(MainGui.class.getResource("/resources/ui/LogLayout.fxml")); logLayoutLoader.setResources(ResourceBundle.getBundle("resources.bundle.ui", Constant.LOCALE, new UTF8Control())); Pane logLayout = logLayoutLoader.load(); Scene logScene = new Scene(logLayout); stage.setTitle("Log"); stage.setScene(logScene); stage.setResizable(false); stage.getIcons().add(new Image(MainGui.class.getResource("/resources/image/icon.png").toString())); LogLayoutController controller = logLayoutLoader.getController(); controller.setStage(stage); logStage = stage; } catch (IOException e) { logger.warning("Unable to load ICON: " + e.toString()); } btnStop.setDisable(true); } @FXML private void handleStart() { boolean isValidated = false; do { if (!txtServerIP.getText().matches("[0-9]{1,4}.[0-9]{1,4}.[0-9]{1,4}.[0-9]{1,4}")) { showAlert(Constant.PROG_NAME, "Invalid IP address", Alert.AlertType.ERROR); break; } String ip = txtServerIP.getText(); if (!txtServerPort.getText().matches("[0-9]+")) { showAlert(Constant.PROG_NAME, "Invalid Port", Alert.AlertType.ERROR); break; } int port = Integer.parseInt(txtServerPort.getText()); String method = (String) cboCipher.getValue(); if (txtPassword.getText().length() == 0) { showAlert(Constant.PROG_NAME, "Please specified password", Alert.AlertType.ERROR); break; } String password = txtPassword.getText(); IProxy.TYPE type = (IProxy.TYPE) cboProxyType.getValue(); if (!txtLocalPort.getText().matches("[0-9]+")) { showAlert(Constant.PROG_NAME, "Invalid Port", Alert.AlertType.ERROR); break; } int localPort = Integer.parseInt(txtLocalPort.getText()); // create config config.setRemoteIpAddress(ip); config.setRemotePort(port); config.setLocalIpAddress("127.0.0.1"); config.setLocalPort(localPort); config.setMethod(method); config.setPassword(password); config.setProxyType(type); Util.saveFile(Constant.CONF_FILE, config.saveToJson()); isValidated = true; } while (false); if (!isValidated) return; // start start try { server = new NioLocalServer(config); Thread t = new Thread(server); t.setDaemon(true); t.start(); String message = String.format("(Connected) Server %s:%d", config.getRemoteIpAddress(), config.getRemotePort()); gui.setTooltip(message); gui.showNotification(message); } catch (IOException | InvalidAlgorithmParameterException e) { logger.warning("Unable to start server: " + e.toString()); } btnStop.setDisable(false); btnStart.setDisable(true); } @FXML private void handleStop() { if (server != null) { server.close(); String message = String.format("(Disconnected) Server %s:%d", config.getRemoteIpAddress(), config.getRemotePort()); gui.showNotification(message); gui.setTooltip("Not Connected"); } btnStop.setDisable(true); btnStart.setDisable(false); } @FXML private void handleLog() { logStage.show(); } @FXML private void handleClose() { gui.hide(); } public void setMainGui(MainGui gui) { this.gui = gui; } public void closeServer() { handleStop(); } private boolean validationInput(String pattern, String text) { return false; } private void showAlert(String title, String message, Alert.AlertType type) { Alert a = new Alert(type); a.setTitle(title); a.setHeaderText(type.name()); a.setResizable(false); a.setContentText(message); a.showAndWait(); } } ================================================ FILE: src/main/java/com/stfl/ui/TextAreaLogHandler.java ================================================ package com.stfl.ui; import javafx.application.Platform; import javafx.scene.control.TextArea; import java.util.logging.LogRecord; import java.util.logging.StreamHandler; public class TextAreaLogHandler extends StreamHandler { TextArea textArea = null; public void setTextArea(TextArea textArea) { this.textArea = textArea; } @Override public void publish(LogRecord record) { final LogRecord lg = record; super.publish(record); flush(); if (textArea != null) { Platform.runLater(new Runnable() { @Override public void run() { // limited log size to 64k if (textArea.getText().length() > 65535) { textArea.clear(); } textArea.appendText(getFormatter().format(lg)); } }); } } } ================================================ FILE: src/main/resources/META-INF/MANIFEST.MF ================================================ Manifest-Version: 1.0 Class-Path: bcprov-jdk15on-1.52.jar json-simple-1.1.1.jar Main-Class: com.stfl.Main ================================================ FILE: src/main/resources/resources/bundle/ui_en.properties ================================================ START=Start STOP=Stop LOG=Log CLOSE=Close CLOSE1=Close CLEAR=Clear SERVER_IP=Server IP: SERVER_PORT=Server Port: CIPHER=Cipher: PASSWORD=Password: LOCAL_PORT=Local Port: PROXY_TYPE=Proxy Type: ================================================ FILE: src/main/resources/resources/bundle/ui_zh_CN.properties ================================================ START=启动 STOP=停止 LOG=日志 CLOSE=关闭 CLEAR=清除 SERVER_IP=服务器 IP: SERVER_PORT=服务器端口: CIPHER=加密方式: PASSWORD=密码: LOCAL_PORT=本地端口: PROXY_TYPE=代理服务器类型: ================================================ FILE: src/main/resources/resources/bundle/ui_zh_TW.properties ================================================ START=啟動 STOP=停止 LOG=日誌 CLOSE=關閉 CLEAR=清除 SERVER_IP=伺服器 IP: SERVER_PORT=伺服器 Port: CIPHER=加密方式: PASSWORD=密碼: LOCAL_PORT=本地 Port: PROXY_TYPE=代理伺服器類型: ================================================ FILE: src/main/resources/resources/ui/LogLayout.fxml ================================================