This commit is contained in:
chenhaodong
2023-07-05 11:06:03 +08:00
parent 8de47479c5
commit 7c55f8da61
234 changed files with 54941 additions and 18 deletions
+146
View File
@@ -0,0 +1,146 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.fengfei</groupId>
<artifactId>yxwlkjnwct</artifactId>
<version>0.1</version>
</parent>
<artifactId>proxy-server</artifactId>
<packaging>jar</packaging>
<name>proxy-server</name>
<url>http://maven.apache.org</url>
<dependencies>
<dependency>
<groupId>org.fengfei</groupId>
<artifactId>proxy-common</artifactId>
<version>0.1</version>
</dependency>
<dependency>
<groupId>org.fengfei</groupId>
<artifactId>proxy-protocol</artifactId>
<version>0.1</version>
</dependency>
</dependencies>
<build>
<finalName>proxy-server-${project.version}</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.4</version>
<configuration>
<excludes>
<exclude>*.properties</exclude>
<exclude>*.sh</exclude>
<exclude>*.bat</exclude>
<exclude>*.jks</exclude>
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>2.2</version>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>../distribution/proxy-server-${project.version}/lib</outputDirectory>
<overWriteReleases>false</overWriteReleases>
<overWriteSnapshots>false</overWriteSnapshots>
<overWriteIfNewer>true</overWriteIfNewer>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>2.6</version>
<executions>
<execution>
<id>copy-config</id>
<phase>package</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<encoding>UTF-8</encoding>
<outputDirectory>../distribution/proxy-server-${project.version}/conf</outputDirectory>
<resources>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>*.properties</include>
<include>*.json</include>
<include>*.jks</include>
</includes>
</resource>
</resources>
</configuration>
</execution>
<execution>
<id>copy-webpages</id>
<phase>package</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<encoding>UTF-8</encoding>
<outputDirectory>../distribution/proxy-server-${project.version}/webpages</outputDirectory>
<resources>
<resource>
<directory>webpages</directory>
</resource>
</resources>
</configuration>
</execution>
<execution>
<id>copy-sh</id>
<phase>package</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<encoding>UTF-8</encoding>
<outputDirectory>../distribution/proxy-server-${project.version}/bin</outputDirectory>
<resources>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>*.sh</include>
<include>*.bat</include>
</includes>
</resource>
</resources>
</configuration>
</execution>
<execution>
<id>copy-dist-jar</id>
<phase>package</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<encoding>UTF-8</encoding>
<outputDirectory>../distribution/proxy-server-${project.version}/lib</outputDirectory>
<resources>
<resource>
<directory>target</directory>
<includes>
<include>*.jar</include>
</includes>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,292 @@
package org.fengfei.lanproxy.server;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.fengfei.lanproxy.protocol.Constants;
import org.fengfei.lanproxy.server.config.ProxyConfig;
import org.fengfei.lanproxy.server.config.ProxyConfig.ConfigChangedListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.netty.channel.Channel;
import io.netty.util.AttributeKey;
/**
* 代理服务连接管理(代理客户端连接+用户请求连接)
*
* @author fengfei
*
*/
public class ProxyChannelManager {
private static Logger logger = LoggerFactory.getLogger(ProxyChannelManager.class);
private static final AttributeKey<Map<String, Channel>> USER_CHANNELS = AttributeKey.newInstance("user_channels");
private static final AttributeKey<String> REQUEST_LAN_INFO = AttributeKey.newInstance("request_lan_info");
private static final AttributeKey<List<Integer>> CHANNEL_PORT = AttributeKey.newInstance("channel_port");
private static final AttributeKey<String> CHANNEL_CLIENT_KEY = AttributeKey.newInstance("channel_client_key");
private static Map<Integer, Channel> portCmdChannelMapping = new ConcurrentHashMap<Integer, Channel>();
private static Map<String, Channel> cmdChannels = new ConcurrentHashMap<String, Channel>();
static {
ProxyConfig.getInstance().addConfigChangedListener(new ConfigChangedListener() {
/**
* 代理配置发生变化时回调
*/
@Override
public synchronized void onChanged() {
Iterator<Entry<String, Channel>> ite = cmdChannels.entrySet().iterator();
while (ite.hasNext()) {
Channel proxyChannel = ite.next().getValue();
String clientKey = proxyChannel.attr(CHANNEL_CLIENT_KEY).get();
// 去除已经去掉的clientKey配置
Set<String> clientKeySet = ProxyConfig.getInstance().getClientKeySet();
if (!clientKeySet.contains(clientKey)) {
removeCmdChannel(proxyChannel);
continue;
}
if (proxyChannel.isActive()) {
List<Integer> inetPorts = new ArrayList<Integer>(ProxyConfig.getInstance().getClientInetPorts(clientKey));
Set<Integer> inetPortSet = new HashSet<Integer>(inetPorts);
List<Integer> channelInetPorts = new ArrayList<Integer>(proxyChannel.attr(CHANNEL_PORT).get());
synchronized (portCmdChannelMapping) {
// 移除旧的连接映射关系
for (int chanelInetPort : channelInetPorts) {
Channel channel = portCmdChannelMapping.get(chanelInetPort);
if (channel == null) {
continue;
}
// 判断是否是同一个连接对象,有可能之前已经更换成其他client的连接了
if (proxyChannel == channel) {
if (!inetPortSet.contains(chanelInetPort)) {
// 移除新配置中不包含的端口
portCmdChannelMapping.remove(chanelInetPort);
proxyChannel.attr(CHANNEL_PORT).get().remove(new Integer(chanelInetPort));
} else {
// 端口已经在改proxyChannel中使用了
inetPorts.remove(new Integer(chanelInetPort));
}
}
}
// 将新配置中增加的外网端口写入到映射配置中
for (int inetPort : inetPorts) {
portCmdChannelMapping.put(inetPort, proxyChannel);
proxyChannel.attr(CHANNEL_PORT).get().add(inetPort);
}
checkAndClearUserChannels(proxyChannel);
}
}
}
ite = cmdChannels.entrySet().iterator();
while (ite.hasNext()) {
Entry<String, Channel> entry = ite.next();
Channel proxyChannel = entry.getValue();
logger.info("proxyChannel config, {}, {}, {} ,{}", entry.getKey(), proxyChannel, getUserChannels(proxyChannel).size(), proxyChannel.attr(CHANNEL_PORT).get());
}
}
/**
* 检测连接配置是否与当前配置一致,不一致则关闭
*
* @param proxyChannel
*/
private void checkAndClearUserChannels(Channel proxyChannel) {
Map<String, Channel> userChannels = getUserChannels(proxyChannel);
Iterator<Entry<String, Channel>> userChannelIte = userChannels.entrySet().iterator();
while (userChannelIte.hasNext()) {
Entry<String, Channel> entry = userChannelIte.next();
Channel userChannel = entry.getValue();
String requestLanInfo = getUserChannelRequestLanInfo(userChannel);
InetSocketAddress sa = (InetSocketAddress) userChannel.localAddress();
String lanInfo = ProxyConfig.getInstance().getLanInfo(sa.getPort());
// 判断当前配置中对应外网端口的lan信息是否与正在运行的连接中的lan信息是否一致
if (lanInfo == null || !lanInfo.equals(requestLanInfo)) {
userChannel.close();
// ConcurrentHashMap不会报ConcurrentModificationException异常
userChannels.remove(entry.getKey());
}
}
}
});
}
/**
* 增加代理服务器端口与代理控制客户端连接的映射关系
*
* @param ports
* @param channel
*/
public static void addCmdChannel(List<Integer> ports, String clientKey, Channel channel) {
if (ports == null) {
throw new IllegalArgumentException("port can not be null");
}
// 客户端(proxy-client)相对较少,这里同步的比较重
// 保证服务器对外端口与客户端到服务器的连接关系在临界情况时调用removeChannel(Channel channel)时不出问题
synchronized (portCmdChannelMapping) {
for (int port : ports) {
portCmdChannelMapping.put(port, channel);
}
}
channel.attr(CHANNEL_PORT).set(ports);
channel.attr(CHANNEL_CLIENT_KEY).set(clientKey);
channel.attr(USER_CHANNELS).set(new ConcurrentHashMap<String, Channel>());
cmdChannels.put(clientKey, channel);
}
/**
* 代理客户端连接断开后清除关系
*
* @param channel
*/
public static void removeCmdChannel(Channel channel) {
logger.warn("channel closed, clear user channels, {}", channel);
if (channel.attr(CHANNEL_PORT).get() == null) {
return;
}
String clientKey = channel.attr(CHANNEL_CLIENT_KEY).get();
Channel channel0 = cmdChannels.remove(clientKey);
if (channel != channel0) {
cmdChannels.put(clientKey, channel);
}
List<Integer> ports = channel.attr(CHANNEL_PORT).get();
for (int port : ports) {
Channel proxyChannel = portCmdChannelMapping.remove(port);
if (proxyChannel == null) {
continue;
}
// 在执行断连之前新的连接已经连上来了
if (proxyChannel != channel) {
portCmdChannelMapping.put(port, proxyChannel);
}
}
if (channel.isActive()) {
logger.info("disconnect proxy channel {}", channel);
channel.close();
}
Map<String, Channel> userChannels = getUserChannels(channel);
Iterator<String> ite = userChannels.keySet().iterator();
while (ite.hasNext()) {
Channel userChannel = userChannels.get(ite.next());
if (userChannel.isActive()) {
userChannel.close();
logger.info("disconnect user channel {}", userChannel);
}
}
}
public static Channel getCmdChannel(Integer port) {
return portCmdChannelMapping.get(port);
}
public static Channel getCmdChannel(String clientKey) {
return cmdChannels.get(clientKey);
}
/**
* 增加用户连接与代理客户端连接关系
*
* @param proxyChannel
* @param userId
* @param userChannel
*/
public static void addUserChannelToCmdChannel(Channel cmdChannel, String userId, Channel userChannel) {
InetSocketAddress sa = (InetSocketAddress) userChannel.localAddress();
String lanInfo = ProxyConfig.getInstance().getLanInfo(sa.getPort());
userChannel.attr(Constants.USER_ID).set(userId);
userChannel.attr(REQUEST_LAN_INFO).set(lanInfo);
cmdChannel.attr(USER_CHANNELS).get().put(userId, userChannel);
}
/**
* 删除用户连接与代理客户端连接关系
*
* @param proxyChannel
* @param userId
* @return
*/
public static Channel removeUserChannelFromCmdChannel(Channel cmdChannel, String userId) {
if (cmdChannel.attr(USER_CHANNELS).get() == null) {
return null;
}
synchronized (cmdChannel) {
return cmdChannel.attr(USER_CHANNELS).get().remove(userId);
}
}
/**
* 根据代理客户端连接与用户编号获取用户连接
*
* @param proxyChannel
* @param userId
* @return
*/
public static Channel getUserChannel(Channel cmdChannel, String userId) {
return cmdChannel.attr(USER_CHANNELS).get().get(userId);
}
/**
* 获取用户编号
*
* @param userChannel
* @return
*/
public static String getUserChannelUserId(Channel userChannel) {
return userChannel.attr(Constants.USER_ID).get();
}
/**
* 获取用户请求的内网IP端口信息
*
* @param userChannel
* @return
*/
public static String getUserChannelRequestLanInfo(Channel userChannel) {
return userChannel.attr(REQUEST_LAN_INFO).get();
}
/**
* 获取代理控制客户端连接绑定的所有用户连接
*
* @param cmdChannel
* @return
*/
public static Map<String, Channel> getUserChannels(Channel cmdChannel) {
return cmdChannel.attr(USER_CHANNELS).get();
}
}
@@ -0,0 +1,177 @@
package org.fengfei.lanproxy.server;
import java.net.BindException;
import java.util.Arrays;
import java.util.List;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import org.fengfei.lanproxy.common.Config;
import org.fengfei.lanproxy.common.container.Container;
import org.fengfei.lanproxy.common.container.ContainerHelper;
import org.fengfei.lanproxy.protocol.IdleCheckHandler;
import org.fengfei.lanproxy.protocol.ProxyMessageDecoder;
import org.fengfei.lanproxy.protocol.ProxyMessageEncoder;
import org.fengfei.lanproxy.server.config.ProxyConfig;
import org.fengfei.lanproxy.server.config.ProxyConfig.ConfigChangedListener;
import org.fengfei.lanproxy.server.config.web.WebConfigContainer;
import org.fengfei.lanproxy.server.handlers.ServerChannelHandler;
import org.fengfei.lanproxy.server.handlers.UserChannelHandler;
import org.fengfei.lanproxy.server.metrics.handler.BytesMetricsHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.ssl.SslHandler;
public class ProxyServerContainer implements Container, ConfigChangedListener {
/**
* max packet is 6M.
*/
private static final int MAX_FRAME_LENGTH = 6 * 1024 * 1024;
private static final int LENGTH_FIELD_OFFSET = 0;
private static final int LENGTH_FIELD_LENGTH = 4;
private static final int INITIAL_BYTES_TO_STRIP = 0;
private static final int LENGTH_ADJUSTMENT = 0;
private static Logger logger = LoggerFactory.getLogger(ProxyServerContainer.class);
private NioEventLoopGroup serverWorkerGroup;
private NioEventLoopGroup serverBossGroup;
public ProxyServerContainer() {
serverBossGroup = new NioEventLoopGroup();
serverWorkerGroup = new NioEventLoopGroup();
ProxyConfig.getInstance().addConfigChangedListener(this);
}
@Override
public void start() {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(serverBossGroup, serverWorkerGroup).channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new ProxyMessageDecoder(MAX_FRAME_LENGTH, LENGTH_FIELD_OFFSET, LENGTH_FIELD_LENGTH, LENGTH_ADJUSTMENT, INITIAL_BYTES_TO_STRIP));
ch.pipeline().addLast(new ProxyMessageEncoder());
ch.pipeline().addLast(new IdleCheckHandler(IdleCheckHandler.READ_IDLE_TIME, IdleCheckHandler.WRITE_IDLE_TIME, 0));
ch.pipeline().addLast(new ServerChannelHandler());
}
});
try {
bootstrap.bind(ProxyConfig.getInstance().getServerBind(), ProxyConfig.getInstance().getServerPort()).get();
logger.info("proxy server start on port " + ProxyConfig.getInstance().getServerPort());
} catch (Exception ex) {
throw new RuntimeException(ex);
}
if (Config.getInstance().getBooleanValue("server.ssl.enable", false)) {
String host = Config.getInstance().getStringValue("server.ssl.bind", "0.0.0.0");
int port = Config.getInstance().getIntValue("server.ssl.port");
initializeSSLTCPTransport(host, port, new SslContextCreator().initSSLContext());
}
startUserPort();
}
private void initializeSSLTCPTransport(String host, int port, final SSLContext sslContext) {
ServerBootstrap b = new ServerBootstrap();
b.group(serverBossGroup, serverWorkerGroup).channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
try {
pipeline.addLast("ssl", createSslHandler(sslContext, Config.getInstance().getBooleanValue("server.ssl.needsClientAuth", false)));
ch.pipeline().addLast(new ProxyMessageDecoder(MAX_FRAME_LENGTH, LENGTH_FIELD_OFFSET, LENGTH_FIELD_LENGTH, LENGTH_ADJUSTMENT, INITIAL_BYTES_TO_STRIP));
ch.pipeline().addLast(new ProxyMessageEncoder());
ch.pipeline().addLast(new IdleCheckHandler(IdleCheckHandler.READ_IDLE_TIME, IdleCheckHandler.WRITE_IDLE_TIME, 0));
ch.pipeline().addLast(new ServerChannelHandler());
} catch (Throwable th) {
logger.error("Severe error during pipeline creation", th);
throw th;
}
}
});
try {
// Bind and start to accept incoming connections.
ChannelFuture f = b.bind(host, port);
f.sync();
logger.info("proxy ssl server start on port {}", port);
} catch (InterruptedException ex) {
logger.error("An interruptedException was caught while initializing server", ex);
}
}
private void startUserPort() {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(serverBossGroup, serverWorkerGroup).channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addFirst(new BytesMetricsHandler());
ch.pipeline().addLast(new UserChannelHandler());
}
});
List<Integer> ports = ProxyConfig.getInstance().getUserPorts();
for (int port : ports) {
try {
bootstrap.bind(port).get();
logger.info("bind user port " + port);
} catch (Exception ex) {
// BindException表示该端口已经绑定过
if (!(ex.getCause() instanceof BindException)) {
throw new RuntimeException(ex);
}
}
}
}
@Override
public void onChanged() {
startUserPort();
}
@Override
public void stop() {
serverBossGroup.shutdownGracefully();
serverWorkerGroup.shutdownGracefully();
}
private ChannelHandler createSslHandler(SSLContext sslContext, boolean needsClientAuth) {
SSLEngine sslEngine = sslContext.createSSLEngine();
sslEngine.setUseClientMode(false);
if (needsClientAuth) {
sslEngine.setNeedClientAuth(true);
}
return new SslHandler(sslEngine);
}
public static void main(String[] args) {
ContainerHelper.start(Arrays.asList(new Container[] { new ProxyServerContainer(), new WebConfigContainer() }));
}
}
@@ -0,0 +1,115 @@
package org.fengfei.lanproxy.server;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import org.fengfei.lanproxy.common.Config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class SslContextCreator {
private static Logger logger = LoggerFactory.getLogger(SslContextCreator.class);
public SSLContext initSSLContext() {
logger.info("Checking SSL configuration properties...");
final String jksPath = Config.getInstance().getStringValue("server.ssl.jksPath");
logger.info("Initializing SSL context. KeystorePath = {}.", jksPath);
if (jksPath == null || jksPath.isEmpty()) {
// key_store_password or key_manager_password are empty
logger.warn("The keystore path is null or empty. The SSL context won't be initialized.");
return null;
}
// if we have the port also the jks then keyStorePassword and
// keyManagerPassword
// has to be defined
final String keyStorePassword = Config.getInstance().getStringValue("server.ssl.keyStorePassword");
final String keyManagerPassword = Config.getInstance().getStringValue("server.ssl.keyManagerPassword");
if (keyStorePassword == null || keyStorePassword.isEmpty()) {
// key_store_password or key_manager_password are empty
logger.warn("The keystore password is null or empty. The SSL context won't be initialized.");
return null;
}
if (keyManagerPassword == null || keyManagerPassword.isEmpty()) {
// key_manager_password or key_manager_password are empty
logger.warn("The key manager password is null or empty. The SSL context won't be initialized.");
return null;
}
// if client authentification is enabled a trustmanager needs to be
// added to the ServerContext
boolean needsClientAuth = Config.getInstance().getBooleanValue("server.ssl.needsClientAuth", false);
try {
logger.info("Loading keystore. KeystorePath = {}.", jksPath);
InputStream jksInputStream = jksDatastore(jksPath);
SSLContext serverContext = SSLContext.getInstance("TLS");
final KeyStore ks = KeyStore.getInstance("JKS");
ks.load(jksInputStream, keyStorePassword.toCharArray());
logger.info("Initializing key manager...");
final KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(ks, keyManagerPassword.toCharArray());
TrustManager[] trustManagers = null;
if (needsClientAuth) {
logger.warn(
"Client authentication is enabled. The keystore will be used as a truststore. KeystorePath = {}.",
jksPath);
// use keystore as truststore, as server needs to trust
// certificates signed by the
// server certificates
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(ks);
trustManagers = tmf.getTrustManagers();
}
// init sslContext
logger.info("Initializing SSL context...");
serverContext.init(kmf.getKeyManagers(), trustManagers, null);
logger.info("The SSL context has been initialized successfully.");
return serverContext;
} catch (NoSuchAlgorithmException | UnrecoverableKeyException | CertificateException | KeyStoreException
| KeyManagementException | IOException ex) {
logger.error("Unable to initialize SSL context. Cause = {}, errorMessage = {}.", ex.getCause(),
ex.getMessage());
return null;
}
}
private InputStream jksDatastore(String jksPath) throws FileNotFoundException {
URL jksUrl = getClass().getClassLoader().getResource(jksPath);
if (jksUrl != null) {
logger.info("Starting with jks at {}, jks normal {}", jksUrl.toExternalForm(), jksUrl);
return getClass().getClassLoader().getResourceAsStream(jksPath);
}
logger.warn("No keystore has been found in the bundled resources. Scanning filesystem...");
File jksFile = new File(jksPath);
if (jksFile.exists()) {
logger.info("Loading external keystore. Url = {}.", jksFile.getAbsolutePath());
return new FileInputStream(jksFile);
}
logger.warn("The keystore file does not exist. Url = {}.", jksFile.getAbsolutePath());
return null;
}
}
@@ -0,0 +1,412 @@
package org.fengfei.lanproxy.server.config;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.Serializable;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.fengfei.lanproxy.common.Config;
import org.fengfei.lanproxy.common.JsonUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.reflect.TypeToken;
/**
* server config
*
* @author fengfei
*
*/
public class ProxyConfig implements Serializable {
private static final long serialVersionUID = 1L;
/** 配置文件为config.json */
public static final String CONFIG_FILE;
private static Logger logger = LoggerFactory.getLogger(ProxyConfig.class);
static {
// 代理配置信息存放在用户根目录下
String dataPath = System.getProperty("user.home") + "/" + ".lanproxy/";
File file = new File(dataPath);
if (!file.isDirectory()) {
file.mkdir();
}
CONFIG_FILE = dataPath + "/config.json";
}
/** 代理服务器绑定主机host */
private String serverBind;
/** 代理服务器与代理客户端通信端口 */
private Integer serverPort;
/** 配置服务绑定主机host */
private String configServerBind;
/** 配置服务端口 */
private Integer configServerPort;
/** 配置服务管理员用户名 */
private String configAdminUsername;
/** 配置服务管理员密码 */
private String configAdminPassword;
/** 代理客户端,支持多个客户端 */
private List<Client> clients;
/** 更新配置后保证在其他线程即时生效 */
private static ProxyConfig instance = new ProxyConfig();;
/** 代理服务器为各个代理客户端(key)开启对应的端口列表(value) */
private volatile Map<String, List<Integer>> clientInetPortMapping = new HashMap<String, List<Integer>>();
/** 代理服务器上的每个对外端口(key)对应的代理客户端背后的真实服务器信息(value) */
private volatile Map<Integer, String> inetPortLanInfoMapping = new HashMap<Integer, String>();
/** 配置变化监听器 */
private List<ConfigChangedListener> configChangedListeners = new ArrayList<ConfigChangedListener>();
private ProxyConfig() {
// 代理服务器主机和端口配置初始化
this.serverPort = Config.getInstance().getIntValue("server.port");
this.serverBind = Config.getInstance().getStringValue("server.bind", "0.0.0.0");
// 配置服务器主机和端口配置初始化
this.configServerPort = Config.getInstance().getIntValue("config.server.port");
this.configServerBind = Config.getInstance().getStringValue("config.server.bind", "0.0.0.0");
// 配置服务器管理员登录认证信息
this.configAdminUsername = Config.getInstance().getStringValue("config.admin.username");
this.configAdminPassword = Config.getInstance().getStringValue("config.admin.password");
logger.info(
"config init serverBind {}, serverPort {}, configServerBind {}, configServerPort {}, configAdminUsername {}, configAdminPassword {}",
serverBind, serverPort, configServerBind, configServerPort, configAdminUsername, configAdminPassword);
update(null);
}
public Integer getServerPort() {
return this.serverPort;
}
public String getServerBind() {
return serverBind;
}
public void setServerBind(String serverBind) {
this.serverBind = serverBind;
}
public String getConfigServerBind() {
return configServerBind;
}
public void setConfigServerBind(String configServerBind) {
this.configServerBind = configServerBind;
}
public Integer getConfigServerPort() {
return configServerPort;
}
public void setConfigServerPort(Integer configServerPort) {
this.configServerPort = configServerPort;
}
public String getConfigAdminUsername() {
return configAdminUsername;
}
public void setConfigAdminUsername(String configAdminUsername) {
this.configAdminUsername = configAdminUsername;
}
public String getConfigAdminPassword() {
return configAdminPassword;
}
public void setConfigAdminPassword(String configAdminPassword) {
this.configAdminPassword = configAdminPassword;
}
public void setServerPort(Integer serverPort) {
this.serverPort = serverPort;
}
public List<Client> getClients() {
return clients;
}
/**
* 解析配置文件
*/
public void update(String proxyMappingConfigJson) {
File file = new File(CONFIG_FILE);
try {
if (proxyMappingConfigJson == null && file.exists()) {
InputStream in = new FileInputStream(file);
byte[] buf = new byte[1024];
ByteArrayOutputStream out = new ByteArrayOutputStream();
int readIndex;
while ((readIndex = in.read(buf)) != -1) {
out.write(buf, 0, readIndex);
}
in.close();
proxyMappingConfigJson = new String(out.toByteArray(), Charset.forName("UTF-8"));
}
} catch (Exception e) {
throw new RuntimeException(e);
}
List<Client> clients = JsonUtil.json2object(proxyMappingConfigJson, new TypeToken<List<Client>>() {
});
if (clients == null) {
clients = new ArrayList<Client>();
}
Map<String, List<Integer>> clientInetPortMapping = new HashMap<String, List<Integer>>();
Map<Integer, String> inetPortLanInfoMapping = new HashMap<Integer, String>();
// 构造端口映射关系
for (Client client : clients) {
String clientKey = client.getClientKey();
if (clientInetPortMapping.containsKey(clientKey)) {
throw new IllegalArgumentException("密钥同时作为客户端标识,不能重复: " + clientKey);
}
List<ClientProxyMapping> mappings = client.getProxyMappings();
List<Integer> ports = new ArrayList<Integer>();
clientInetPortMapping.put(clientKey, ports);
for (ClientProxyMapping mapping : mappings) {
Integer port = mapping.getInetPort();
ports.add(port);
if (inetPortLanInfoMapping.containsKey(port)) {
throw new IllegalArgumentException("一个公网端口只能映射一个后端信息,不能重复: " + port);
}
inetPortLanInfoMapping.put(port, mapping.getLan());
}
}
// 替换之前的配置关系
this.clientInetPortMapping = clientInetPortMapping;
this.inetPortLanInfoMapping = inetPortLanInfoMapping;
this.clients = clients;
if (proxyMappingConfigJson != null) {
try {
FileOutputStream out = new FileOutputStream(file);
out.write(proxyMappingConfigJson.getBytes(Charset.forName("UTF-8")));
out.flush();
out.close();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
notifyconfigChangedListeners();
}
/**
* 配置更新通知
*/
private void notifyconfigChangedListeners() {
List<ConfigChangedListener> changedListeners = new ArrayList<ConfigChangedListener>(configChangedListeners);
for (ConfigChangedListener changedListener : changedListeners) {
changedListener.onChanged();
}
}
/**
* 添加配置变化监听器
*
* @param configChangedListener
*/
public void addConfigChangedListener(ConfigChangedListener configChangedListener) {
configChangedListeners.add(configChangedListener);
}
/**
* 移除配置变化监听器
*
* @param configChangedListener
*/
public void removeConfigChangedListener(ConfigChangedListener configChangedListener) {
configChangedListeners.remove(configChangedListener);
}
/**
* 获取代理客户端对应的代理服务器端口
*
* @param clientKey
* @return
*/
public List<Integer> getClientInetPorts(String clientKey) {
return clientInetPortMapping.get(clientKey);
}
/**
* 获取所有的clientKey
*
* @return
*/
public Set<String> getClientKeySet() {
return clientInetPortMapping.keySet();
}
/**
* 根据代理服务器端口获取后端服务器代理信息
*
* @param port
* @return
*/
public String getLanInfo(Integer port) {
return inetPortLanInfoMapping.get(port);
}
/**
* 返回需要绑定在代理服务器的端口(用于用户请求)
*
* @return
*/
public List<Integer> getUserPorts() {
List<Integer> ports = new ArrayList<Integer>();
Iterator<Integer> ite = inetPortLanInfoMapping.keySet().iterator();
while (ite.hasNext()) {
ports.add(ite.next());
}
return ports;
}
public static ProxyConfig getInstance() {
return instance;
}
/**
* 代理客户端
*
* @author fengfei
*
*/
public static class Client implements Serializable {
private static final long serialVersionUID = 1L;
/** 客户端备注名称 */
private String name;
/** 代理客户端唯一标识key */
private String clientKey;
/** 代理客户端与其后面的真实服务器映射关系 */
private List<ClientProxyMapping> proxyMappings;
private int status;
public String getClientKey() {
return clientKey;
}
public void setClientKey(String clientKey) {
this.clientKey = clientKey;
}
public List<ClientProxyMapping> getProxyMappings() {
return proxyMappings;
}
public void setProxyMappings(List<ClientProxyMapping> proxyMappings) {
this.proxyMappings = proxyMappings;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
}
}
/**
* 代理客户端与其后面真实服务器映射关系
*
* @author fengfei
*
*/
public static class ClientProxyMapping {
/** 代理服务器端口 */
private Integer inetPort;
/** 需要代理的网络信息(代理客户端能够访问),格式 192.168.1.99:80 (必须带端口) */
private String lan;
/** 备注名称 */
private String name;
public Integer getInetPort() {
return inetPort;
}
public void setInetPort(Integer inetPort) {
this.inetPort = inetPort;
}
public String getLan() {
return lan;
}
public void setLan(String lan) {
this.lan = lan;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
/**
* 配置更新回调
*
* @author fengfei
*
*/
public static interface ConfigChangedListener {
void onChanged();
}
}
@@ -0,0 +1,94 @@
package org.fengfei.lanproxy.server.config.web;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.fengfei.lanproxy.server.config.web.exception.ContextException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.netty.handler.codec.http.FullHttpRequest;
/**
* 接口路由管理
*
* @author fengfei
*
*/
public class ApiRoute {
private static Logger logger = LoggerFactory.getLogger(ApiRoute.class);
/** 接口路由 */
private static Map<String, RequestHandler> routes = new ConcurrentHashMap<String, RequestHandler>();
/** 拦截器,初始化后不会在变化 */
private static List<RequestMiddleware> middlewares = new ArrayList<RequestMiddleware>();
/**
* 增加接口请求路由
*
* @param uri
* @param requestHandler
*/
public static void addRoute(String uri, RequestHandler requestHandler) {
if (routes.containsKey(uri)) {
throw new IllegalArgumentException("Duplicate uri:" + uri);
}
logger.info("add route {}", uri);
routes.put(uri, requestHandler);
}
/**
* 增加拦截器
*
* @param requestMiddleware
*/
public static void addMiddleware(RequestMiddleware requestMiddleware) {
if (middlewares.contains(requestMiddleware)) {
throw new IllegalArgumentException("Duplicate RequestMiddleware:" + requestMiddleware);
}
logger.info("add requestMiddleware {}", requestMiddleware);
middlewares.add(requestMiddleware);
}
/**
* 请求执行
*
* @param request
* @return
*/
public static ResponseInfo run(FullHttpRequest request) {
try {
// 拦截器中如果不能通过以异常的方式进行反馈
for (RequestMiddleware middleware : middlewares) {
middleware.preRequest(request);
}
URI uri = new URI(request.getUri());
RequestHandler handler = routes.get(uri.getPath());
ResponseInfo responseInfo = null;
if (handler != null) {
responseInfo = handler.request(request);
} else {
responseInfo = ResponseInfo.build(ResponseInfo.CODE_API_NOT_FOUND, "api not found");
}
return responseInfo;
} catch (Exception ex) {
if (ex instanceof ContextException) {
return ResponseInfo.build(((ContextException) ex).getCode(), ex.getMessage());
}
logger.error("request error", ex);
}
return null;
}
}
@@ -0,0 +1,137 @@
package org.fengfei.lanproxy.server.config.web;
import java.io.File;
import java.io.RandomAccessFile;
import java.net.URI;
import java.nio.charset.Charset;
import org.fengfei.lanproxy.common.JsonUtil;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.DefaultFileRegion;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.DefaultHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpHeaders.Names;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.handler.ssl.SslHandler;
import io.netty.handler.stream.ChunkedNioFile;
public class HttpRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
private static final String PAGE_FOLDER = System.getProperty("app.home", System.getProperty("user.dir"))
+ "/webpages";
private static final String SERVER_VS = "LPS-0.1";
@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
// GET返回页面;POST请求接口
if (request.getMethod() != HttpMethod.POST) {
outputPages(ctx, request);
return;
}
ResponseInfo responseInfo = ApiRoute.run(request);
// 错误码规则:除100取整为http状态码
outputContent(ctx, request, responseInfo.getCode() / 100, JsonUtil.object2json(responseInfo),
"Application/json;charset=utf-8");
}
private void outputContent(ChannelHandlerContext ctx, FullHttpRequest request, int code, String content,
String mimeType) {
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.valueOf(code),
Unpooled.wrappedBuffer(content.getBytes(Charset.forName("UTF-8"))));
response.headers().set(Names.CONTENT_TYPE, mimeType);
response.headers().set(Names.CONTENT_LENGTH, response.content().readableBytes());
response.headers().set(Names.SERVER, SERVER_VS);
ChannelFuture future = ctx.writeAndFlush(response);
if (!HttpHeaders.isKeepAlive(request)) {
future.addListener(ChannelFutureListener.CLOSE);
}
}
/**
* 输出静态资源数据
*
* @param ctx
* @param request
* @throws Exception
*/
private void outputPages(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
HttpResponseStatus status = HttpResponseStatus.OK;
URI uri = new URI(request.getUri());
String uriPath = uri.getPath();
uriPath = uriPath.equals("/") ? "/index.html" : uriPath;
String path = PAGE_FOLDER + uriPath;
File rfile = new File(path);
if (rfile.isDirectory()) {
path = path + "/index.html";
rfile = new File(path);
}
if (!rfile.exists()) {
status = HttpResponseStatus.NOT_FOUND;
outputContent(ctx, request, status.code(), status.toString(), "text/html");
return;
}
if (HttpHeaders.is100ContinueExpected(request)) {
send100Continue(ctx);
}
String mimeType = MimeType.getMimeType(MimeType.parseSuffix(path));
long length = 0;
RandomAccessFile raf = null;
try {
raf = new RandomAccessFile(rfile, "r");
length = raf.length();
} finally {
if (length < 0 && raf != null) {
raf.close();
}
}
HttpResponse response = new DefaultHttpResponse(request.getProtocolVersion(), status);
response.headers().set(HttpHeaders.Names.CONTENT_TYPE, mimeType);
boolean keepAlive = HttpHeaders.isKeepAlive(request);
if (keepAlive) {
response.headers().set(HttpHeaders.Names.CONTENT_LENGTH, length);
response.headers().set(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.KEEP_ALIVE);
}
response.headers().set(Names.SERVER, SERVER_VS);
ctx.write(response);
if (ctx.pipeline().get(SslHandler.class) == null) {
ctx.write(new DefaultFileRegion(raf.getChannel(), 0, length));
} else {
ctx.write(new ChunkedNioFile(raf.getChannel()));
}
ChannelFuture future = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
if (!keepAlive) {
future.addListener(ChannelFutureListener.CLOSE);
}
}
private static void send100Continue(ChannelHandlerContext ctx) {
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE);
ctx.writeAndFlush(response);
}
}
@@ -0,0 +1,228 @@
package org.fengfei.lanproxy.server.config.web;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class MimeType {
final static Pattern pattern = Pattern.compile("\\S*[?]\\S*");
private static Map<String, String> h = new HashMap<String, String>();
static {
h.put("", "application/octet-stream");
h.put("323", "text/h323");
h.put("acx", "application/internet-property-stream");
h.put("ai", "application/postscript");
h.put("aif", "audio/x-aiff");
h.put("aifc", "audio/x-aiff");
h.put("aiff", "audio/x-aiff");
h.put("asf", "video/x-ms-asf");
h.put("asr", "video/x-ms-asf");
h.put("asx", "video/x-ms-asf");
h.put("au", "audio/basic");
h.put("avi", "video/x-msvideo");
h.put("axs", "application/olescript");
h.put("bas", "text/plain");
h.put("bcpio", "application/x-bcpio");
h.put("bin", "application/octet-stream");
h.put("bmp", "image/bmp");
h.put("c", "text/plain");
h.put("cat", "application/vnd.ms-pkiseccat");
h.put("cdf", "application/x-cdf");
h.put("cer", "application/x-x509-ca-cert");
h.put("class", "application/octet-stream");
h.put("clp", "application/x-msclip");
h.put("cmx", "image/x-cmx");
h.put("cod", "image/cis-cod");
h.put("cpio", "application/x-cpio");
h.put("crd", "application/x-mscardfile");
h.put("crl", "application/pkix-crl");
h.put("crt", "application/x-x509-ca-cert");
h.put("csh", "application/x-csh");
h.put("css", "text/css");
h.put("dcr", "application/x-director");
h.put("der", "application/x-x509-ca-cert");
h.put("dir", "application/x-director");
h.put("dll", "application/x-msdownload");
h.put("dms", "application/octet-stream");
h.put("doc", "application/msword");
h.put("dot", "application/msword");
h.put("dvi", "application/x-dvi");
h.put("dxr", "application/x-director");
h.put("eps", "application/postscript");
h.put("etx", "text/x-setext");
h.put("evy", "application/envoy");
h.put("exe", "application/octet-stream");
h.put("fif", "application/fractals");
h.put("flr", "x-world/x-vrml");
h.put("gif", "image/gif");
h.put("gtar", "application/x-gtar");
h.put("gz", "application/x-gzip");
h.put("h", "text/plain");
h.put("hdf", "applicatin/x-hdf");
h.put("hlp", "application/winhlp");
h.put("hqx", "application/mac-binhex40");
h.put("hta", "application/hta");
h.put("htc", "text/x-component");
h.put("htm", "text/html");
h.put("html", "text/html");
h.put("htt", "text/webviewhtml");
h.put("ico", "image/x-icon");
h.put("ief", "image/ief");
h.put("iii", "application/x-iphone");
h.put("ins", "application/x-internet-signup");
h.put("isp", "application/x-internet-signup");
h.put("jfif", "image/pipeg");
h.put("jpe", "image/jpeg");
h.put("jpeg", "image/jpeg");
h.put("jpg", "image/jpeg");
h.put("js", "application/x-javascript");
h.put("latex", "application/x-latex");
h.put("lha", "application/octet-stream");
h.put("lsf", "video/x-la-asf");
h.put("lsx", "video/x-la-asf");
h.put("lzh", "application/octet-stream");
h.put("m13", "application/x-msmediaview");
h.put("m14", "application/x-msmediaview");
h.put("m3u", "audio/x-mpegurl");
h.put("man", "application/x-troff-man");
h.put("mdb", "application/x-msaccess");
h.put("me", "application/x-troff-me");
h.put("mht", "message/rfc822");
h.put("mhtml", "message/rfc822");
h.put("mid", "audio/mid");
h.put("mny", "application/x-msmoney");
h.put("mov", "video/quicktime");
h.put("movie", "video/x-sgi-movie");
h.put("mp2", "video/mpeg");
h.put("mp3", "audio/mpeg");
h.put("mpa", "video/mpeg");
h.put("mpe", "video/mpeg");
h.put("mpeg", "video/mpeg");
h.put("mpg", "video/mpeg");
h.put("mpp", "application/vnd.ms-project");
h.put("mpv2", "video/mpeg");
h.put("ms", "application/x-troff-ms");
h.put("mvb", "application/x-msmediaview");
h.put("nws", "message/rfc822");
h.put("oda", "application/oda");
h.put("p10", "application/pkcs10");
h.put("p12", "application/x-pkcs12");
h.put("p7b", "application/x-pkcs7-certificates");
h.put("p7c", "application/x-pkcs7-mime");
h.put("p7m", "application/x-pkcs7-mime");
h.put("p7r", "application/x-pkcs7-certreqresp");
h.put("p7s", "application/x-pkcs7-signature");
h.put("pbm", "image/x-portable-bitmap");
h.put("pdf", "application/pdf");
h.put("pfx", "application/x-pkcs12");
h.put("pgm", "image/x-portable-graymap");
h.put("pko", "application/ynd.ms-pkipko");
h.put("pma", "application/x-perfmon");
h.put("pmc", "application/x-perfmon");
h.put("pml", "application/x-perfmon");
h.put("pmr", "application/x-perfmon");
h.put("pmw", "application/x-perfmon");
h.put("pnm", "image/x-portable-anymap");
h.put("pot,", "application/vnd.ms-powerpoint");
h.put("ppm", "image/x-portable-pixmap");
h.put("pps", "application/vnd.ms-powerpoint");
h.put("ppt", "application/vnd.ms-powerpoint");
h.put("prf", "application/pics-rules");
h.put("ps", "application/postscript");
h.put("pub", "application/x-mspublisher");
h.put("qt", "video/quicktime");
h.put("ra", "audio/x-pn-realaudio");
h.put("ram", "audio/x-pn-realaudio");
h.put("ras", "image/x-cmu-raster");
h.put("rgb", "image/x-rgb");
h.put("rmi", "audio/mid");
h.put("roff", "application/x-troff");
h.put("rtf", "application/rtf");
h.put("rtx", "text/richtext");
h.put("scd", "application/x-msschedule");
h.put("sct", "text/scriptlet");
h.put("setpay", "application/set-payment-initiation");
h.put("setreg", "application/set-registration-initiation");
h.put("sh", "application/x-sh");
h.put("shar", "application/x-shar");
h.put("sit", "application/x-stuffit");
h.put("snd", "audio/basic");
h.put("spc", "application/x-pkcs7-certificates");
h.put("spl", "application/futuresplash");
h.put("src", "application/x-wais-source");
h.put("sst", "application/vnd.ms-pkicertstore");
h.put("stl", "application/vnd.ms-pkistl");
h.put("stm", "text/html");
h.put("svg", "image/svg+xml");
h.put("sv4cpio", "application/x-sv4cpio");
h.put("sv4crc", "application/x-sv4crc");
h.put("swf", "application/x-shockwave-flash");
h.put("t", "application/x-troff");
h.put("tar", "application/x-tar");
h.put("tcl", "application/x-tcl");
h.put("tex", "application/x-tex");
h.put("texi", "application/x-texinfo");
h.put("texinfo", "application/x-texinfo");
h.put("tgz", "application/x-compressed");
h.put("tif", "image/tiff");
h.put("tiff", "image/tiff");
h.put("tr", "application/x-troff");
h.put("trm", "application/x-msterminal");
h.put("tsv", "text/tab-separated-values");
h.put("txt", "text/plain");
h.put("uls", "text/iuls");
h.put("ustar", "application/x-ustar");
h.put("vcf", "text/x-vcard");
h.put("vrml", "x-world/x-vrml");
h.put("wav", "audio/x-wav");
h.put("wcm", "application/vnd.ms-works");
h.put("wdb", "application/vnd.ms-works");
h.put("wks", "application/vnd.ms-works");
h.put("wmf", "application/x-msmetafile");
h.put("wps", "application/vnd.ms-works");
h.put("wri", "application/x-mswrite");
h.put("wrl", "x-world/x-vrml");
h.put("wrz", "x-world/x-vrml");
h.put("xaf", "x-world/x-vrml");
h.put("xbm", "image/x-xbitmap");
h.put("xla", "application/vnd.ms-excel");
h.put("xlc", "application/vnd.ms-excel");
h.put("xlm", "application/vnd.ms-excel");
h.put("xls", "application/vnd.ms-excel");
h.put("xlt", "application/vnd.ms-excel");
h.put("xlw", "application/vnd.ms-excel");
h.put("xof", "x-world/x-vrml");
h.put("xpm", "image/x-xpixmap");
h.put("xwd", "image/x-xwindowdump");
h.put("z", "application/x-compress");
h.put("zip", "application/zip");
}
public static String getMimeType(String docType) {
String mime = h.get(docType);
if (mime == null) {
mime = "application/octet-stream";
}
return mime;
}
public static String parseSuffix(String url) {
try {
Matcher matcher = pattern.matcher(url);
String[] spUrl = url.toString().split("/");
int len = spUrl.length;
String endUrl = spUrl[len - 1];
if (matcher.find()) {
String[] spEndUrl = endUrl.split("\\?");
endUrl = spEndUrl[0];
}
String[] endUrlArr = endUrl.split("\\.");
return endUrlArr[endUrlArr.length - 1];
} catch (Exception e) {
return "";
}
}
}
@@ -0,0 +1,20 @@
package org.fengfei.lanproxy.server.config.web;
import io.netty.handler.codec.http.FullHttpRequest;
/**
* 接口请求处理
*
* @author fengfei
*
*/
public interface RequestHandler {
/**
* 请求处理
*
* @param request
* @return
*/
ResponseInfo request(FullHttpRequest request);
}
@@ -0,0 +1,19 @@
package org.fengfei.lanproxy.server.config.web;
import io.netty.handler.codec.http.FullHttpRequest;
/**
* 请求拦截器
*
* @author fengfei
*
*/
public interface RequestMiddleware {
/**
* 请求预处理
*
* @param request
*/
void preRequest(FullHttpRequest request);
}
@@ -0,0 +1,75 @@
package org.fengfei.lanproxy.server.config.web;
import java.io.Serializable;
public class ResponseInfo implements Serializable {
private static final long serialVersionUID = 1L;
public static final int CODE_OK = 20000;
public static final int CODE_INVILID_PARAMS = 40000;
public static final int CODE_UNAUTHORIZED = 40100;
public static final int CODE_API_NOT_FOUND = 40400;
public static final int CODE_SYSTEM_ERROR = 50000;
private int code;
private String message;
private Object data;
public static ResponseInfo build(int code, String message, Object data) {
return new ResponseInfo(code, message, data);
}
public static ResponseInfo build(int code, String message) {
return new ResponseInfo(code, message);
}
public static ResponseInfo build(Object data) {
return new ResponseInfo(data);
}
private ResponseInfo(int code, String message, Object data) {
this.code = code;
this.message = message;
this.data = data;
}
private ResponseInfo(int code, String message) {
this(code, message, null);
}
private ResponseInfo(Object data) {
this(ResponseInfo.CODE_OK, "success", data);
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}
@@ -0,0 +1,69 @@
package org.fengfei.lanproxy.server.config.web;
import org.fengfei.lanproxy.common.container.Container;
import org.fengfei.lanproxy.server.config.ProxyConfig;
import org.fengfei.lanproxy.server.config.web.routes.RouteConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.stream.ChunkedWriteHandler;
public class WebConfigContainer implements Container {
private static Logger logger = LoggerFactory.getLogger(WebConfigContainer.class);
private NioEventLoopGroup serverWorkerGroup;
private NioEventLoopGroup serverBossGroup;
public WebConfigContainer() {
// 配置管理,并发处理很小,使用单线程处理网络事件
serverBossGroup = new NioEventLoopGroup(1);
serverWorkerGroup = new NioEventLoopGroup(1);
}
@Override
public void start() {
ServerBootstrap httpServerBootstrap = new ServerBootstrap();
httpServerBootstrap.group(serverBossGroup, serverWorkerGroup).channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new HttpServerCodec());
pipeline.addLast(new HttpObjectAggregator(64 * 1024));
pipeline.addLast(new ChunkedWriteHandler());
pipeline.addLast(new HttpRequestHandler());
}
});
try {
httpServerBootstrap.bind(ProxyConfig.getInstance().getConfigServerBind(),
ProxyConfig.getInstance().getConfigServerPort()).get();
logger.info("http server start on port " + ProxyConfig.getInstance().getConfigServerPort());
} catch (Exception ex) {
throw new RuntimeException(ex);
}
RouteConfig.init();
}
@Override
public void stop() {
serverBossGroup.shutdownGracefully();
serverWorkerGroup.shutdownGracefully();
}
}
@@ -0,0 +1,22 @@
package org.fengfei.lanproxy.server.config.web.exception;
public class ContextException extends RuntimeException {
private static final long serialVersionUID = 1L;
private int code;
public ContextException(int code, String message) {
super(message);
this.code = code;
}
public ContextException(int code) {
super();
this.code = code;
}
public int getCode() {
return code;
}
}
@@ -0,0 +1,175 @@
package org.fengfei.lanproxy.server.config.web.routes;
import java.nio.charset.Charset;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.fengfei.lanproxy.common.JsonUtil;
import org.fengfei.lanproxy.server.ProxyChannelManager;
import org.fengfei.lanproxy.server.config.ProxyConfig;
import org.fengfei.lanproxy.server.config.ProxyConfig.Client;
import org.fengfei.lanproxy.server.config.web.ApiRoute;
import org.fengfei.lanproxy.server.config.web.RequestHandler;
import org.fengfei.lanproxy.server.config.web.RequestMiddleware;
import org.fengfei.lanproxy.server.config.web.ResponseInfo;
import org.fengfei.lanproxy.server.config.web.exception.ContextException;
import org.fengfei.lanproxy.server.metrics.MetricsCollector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.reflect.TypeToken;
import io.netty.channel.Channel;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpHeaders;
/**
* 接口实现
*
* @author fengfei
*
*/
public class RouteConfig {
protected static final String AUTH_COOKIE_KEY = "token";
private static Logger logger = LoggerFactory.getLogger(RouteConfig.class);
/** 管理员不能同时在多个地方登录 */
private static String token;
public static void init() {
ApiRoute.addMiddleware(new RequestMiddleware() {
@Override
public void preRequest(FullHttpRequest request) {
String cookieHeader = request.headers().get(HttpHeaders.Names.COOKIE);
boolean authenticated = false;
if (cookieHeader != null) {
String[] cookies = cookieHeader.split(";");
for (String cookie : cookies) {
String[] cookieArr = cookie.split("=");
if (AUTH_COOKIE_KEY.equals(cookieArr[0].trim())) {
if (cookieArr.length == 2 && cookieArr[1].equals(token)) {
authenticated = true;
}
}
}
}
String auth = request.headers().get(HttpHeaders.Names.AUTHORIZATION);
if (!authenticated && auth != null) {
String[] authArr = auth.split(" ");
if (authArr.length == 2 && authArr[0].equals(ProxyConfig.getInstance().getConfigAdminUsername()) && authArr[1].equals(ProxyConfig.getInstance().getConfigAdminPassword())) {
authenticated = true;
}
}
if (!request.getUri().equals("/login") && !authenticated) {
throw new ContextException(ResponseInfo.CODE_UNAUTHORIZED);
}
logger.info("handle request for api {}", request.getUri());
}
});
// 获取配置详细信息
ApiRoute.addRoute("/config/detail", new RequestHandler() {
@Override
public ResponseInfo request(FullHttpRequest request) {
List<Client> clients = ProxyConfig.getInstance().getClients();
for (Client client : clients) {
Channel channel = ProxyChannelManager.getCmdChannel(client.getClientKey());
if (channel != null) {
client.setStatus(1);// online
} else {
client.setStatus(0);// offline
}
}
return ResponseInfo.build(ProxyConfig.getInstance().getClients());
}
});
// 更新配置
ApiRoute.addRoute("/config/update", new RequestHandler() {
@Override
public ResponseInfo request(FullHttpRequest request) {
byte[] buf = new byte[request.content().readableBytes()];
request.content().readBytes(buf);
String config = new String(buf, Charset.forName("UTF-8"));
List<Client> clients = JsonUtil.json2object(config, new TypeToken<List<Client>>() {
});
if (clients == null) {
return ResponseInfo.build(ResponseInfo.CODE_INVILID_PARAMS, "Error json config");
}
try {
ProxyConfig.getInstance().update(config);
} catch (Exception ex) {
logger.error("config update error", ex);
return ResponseInfo.build(ResponseInfo.CODE_INVILID_PARAMS, ex.getMessage());
}
return ResponseInfo.build(ResponseInfo.CODE_OK, "success");
}
});
ApiRoute.addRoute("/login", new RequestHandler() {
@Override
public ResponseInfo request(FullHttpRequest request) {
byte[] buf = new byte[request.content().readableBytes()];
request.content().readBytes(buf);
String config = new String(buf);
Map<String, String> loginParams = JsonUtil.json2object(config, new TypeToken<Map<String, String>>() {
});
if (loginParams == null) {
return ResponseInfo.build(ResponseInfo.CODE_INVILID_PARAMS, "Error login info");
}
String username = loginParams.get("username");
String password = loginParams.get("password");
if (username == null || password == null) {
return ResponseInfo.build(ResponseInfo.CODE_INVILID_PARAMS, "Error username or password");
}
if (username.equals(ProxyConfig.getInstance().getConfigAdminUsername()) && password.equals(ProxyConfig.getInstance().getConfigAdminPassword())) {
token = UUID.randomUUID().toString().replace("-", "");
return ResponseInfo.build(token);
}
return ResponseInfo.build(ResponseInfo.CODE_INVILID_PARAMS, "Error username or password");
}
});
ApiRoute.addRoute("/logout", new RequestHandler() {
@Override
public ResponseInfo request(FullHttpRequest request) {
token = null;
return ResponseInfo.build(ResponseInfo.CODE_OK, "success");
}
});
ApiRoute.addRoute("/metrics/get", new RequestHandler() {
@Override
public ResponseInfo request(FullHttpRequest request) {
return ResponseInfo.build(MetricsCollector.getAllMetrics());
}
});
ApiRoute.addRoute("/metrics/getandreset", new RequestHandler() {
@Override
public ResponseInfo request(FullHttpRequest request) {
return ResponseInfo.build(MetricsCollector.getAndResetAllMetrics());
}
});
}
}
@@ -0,0 +1,191 @@
package org.fengfei.lanproxy.server.handlers;
import java.util.List;
import org.fengfei.lanproxy.protocol.Constants;
import org.fengfei.lanproxy.protocol.ProxyMessage;
import org.fengfei.lanproxy.server.ProxyChannelManager;
import org.fengfei.lanproxy.server.config.ProxyConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelOption;
import io.netty.channel.SimpleChannelInboundHandler;
/**
*
* @author fengfei
*
*/
public class ServerChannelHandler extends SimpleChannelInboundHandler<ProxyMessage> {
private static Logger logger = LoggerFactory.getLogger(ServerChannelHandler.class);
@Override
protected void channelRead0(ChannelHandlerContext ctx, ProxyMessage proxyMessage) throws Exception {
logger.debug("ProxyMessage received {}", proxyMessage.getType());
switch (proxyMessage.getType()) {
case ProxyMessage.TYPE_HEARTBEAT:
handleHeartbeatMessage(ctx, proxyMessage);
break;
case ProxyMessage.C_TYPE_AUTH:
handleAuthMessage(ctx, proxyMessage);
break;
case ProxyMessage.TYPE_CONNECT:
handleConnectMessage(ctx, proxyMessage);
break;
case ProxyMessage.TYPE_DISCONNECT:
handleDisconnectMessage(ctx, proxyMessage);
break;
case ProxyMessage.P_TYPE_TRANSFER:
handleTransferMessage(ctx, proxyMessage);
break;
default:
break;
}
}
private void handleTransferMessage(ChannelHandlerContext ctx, ProxyMessage proxyMessage) {
Channel userChannel = ctx.channel().attr(Constants.NEXT_CHANNEL).get();
if (userChannel != null) {
ByteBuf buf = ctx.alloc().buffer(proxyMessage.getData().length);
buf.writeBytes(proxyMessage.getData());
userChannel.writeAndFlush(buf);
}
}
private void handleDisconnectMessage(ChannelHandlerContext ctx, ProxyMessage proxyMessage) {
String clientKey = ctx.channel().attr(Constants.CLIENT_KEY).get();
// 代理连接没有连上服务器由控制连接发送用户端断开连接消息
if (clientKey == null) {
String userId = proxyMessage.getUri();
Channel userChannel = ProxyChannelManager.removeUserChannelFromCmdChannel(ctx.channel(), userId);
if (userChannel != null) {
// 数据发送完成后再关闭连接,解决http1.0数据传输问题
userChannel.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
}
return;
}
Channel cmdChannel = ProxyChannelManager.getCmdChannel(clientKey);
if (cmdChannel == null) {
logger.warn("ConnectMessage:error cmd channel key {}", ctx.channel().attr(Constants.CLIENT_KEY).get());
return;
}
Channel userChannel = ProxyChannelManager.removeUserChannelFromCmdChannel(cmdChannel, ctx.channel().attr(Constants.USER_ID).get());
if (userChannel != null) {
// 数据发送完成后再关闭连接,解决http1.0数据传输问题
userChannel.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
ctx.channel().attr(Constants.NEXT_CHANNEL).remove();
ctx.channel().attr(Constants.CLIENT_KEY).remove();
ctx.channel().attr(Constants.USER_ID).remove();
}
}
private void handleConnectMessage(ChannelHandlerContext ctx, ProxyMessage proxyMessage) {
String uri = proxyMessage.getUri();
if (uri == null) {
ctx.channel().close();
logger.warn("ConnectMessage:null uri");
return;
}
String[] tokens = uri.split("@");
if (tokens.length != 2) {
ctx.channel().close();
logger.warn("ConnectMessage:error uri");
return;
}
Channel cmdChannel = ProxyChannelManager.getCmdChannel(tokens[1]);
if (cmdChannel == null) {
ctx.channel().close();
logger.warn("ConnectMessage:error cmd channel key {}", tokens[1]);
return;
}
Channel userChannel = ProxyChannelManager.getUserChannel(cmdChannel, tokens[0]);
if (userChannel != null) {
ctx.channel().attr(Constants.USER_ID).set(tokens[0]);
ctx.channel().attr(Constants.CLIENT_KEY).set(tokens[1]);
ctx.channel().attr(Constants.NEXT_CHANNEL).set(userChannel);
userChannel.attr(Constants.NEXT_CHANNEL).set(ctx.channel());
// 代理客户端与后端服务器连接成功,修改用户连接为可读状态
userChannel.config().setOption(ChannelOption.AUTO_READ, true);
}
}
private void handleHeartbeatMessage(ChannelHandlerContext ctx, ProxyMessage proxyMessage) {
ProxyMessage heartbeatMessage = new ProxyMessage();
heartbeatMessage.setSerialNumber(heartbeatMessage.getSerialNumber());
heartbeatMessage.setType(ProxyMessage.TYPE_HEARTBEAT);
logger.debug("response heartbeat message {}", ctx.channel());
ctx.channel().writeAndFlush(heartbeatMessage);
}
private void handleAuthMessage(ChannelHandlerContext ctx, ProxyMessage proxyMessage) {
String clientKey = proxyMessage.getUri();
List<Integer> ports = ProxyConfig.getInstance().getClientInetPorts(clientKey);
if (ports == null) {
logger.info("error clientKey {}, {}", clientKey, ctx.channel());
ctx.channel().close();
return;
}
Channel channel = ProxyChannelManager.getCmdChannel(clientKey);
if (channel != null) {
logger.warn("exist channel for key {}, {}", clientKey, channel);
ctx.channel().close();
return;
}
logger.info("set port => channel, {}, {}, {}", clientKey, ports, ctx.channel());
ProxyChannelManager.addCmdChannel(ports, clientKey, ctx.channel());
}
@Override
public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
Channel userChannel = ctx.channel().attr(Constants.NEXT_CHANNEL).get();
if (userChannel != null) {
userChannel.config().setOption(ChannelOption.AUTO_READ, ctx.channel().isWritable());
}
super.channelWritabilityChanged(ctx);
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
Channel userChannel = ctx.channel().attr(Constants.NEXT_CHANNEL).get();
if (userChannel != null && userChannel.isActive()) {
String clientKey = ctx.channel().attr(Constants.CLIENT_KEY).get();
String userId = ctx.channel().attr(Constants.USER_ID).get();
Channel cmdChannel = ProxyChannelManager.getCmdChannel(clientKey);
if (cmdChannel != null) {
ProxyChannelManager.removeUserChannelFromCmdChannel(cmdChannel, userId);
} else {
logger.warn("null cmdChannel, clientKey is {}", clientKey);
}
// 数据发送完成后再关闭连接,解决http1.0数据传输问题
userChannel.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
userChannel.close();
} else {
ProxyChannelManager.removeCmdChannel(ctx.channel());
}
super.channelInactive(ctx);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
logger.error("exception caught", cause);
super.exceptionCaught(ctx, cause);
}
}
@@ -0,0 +1,142 @@
package org.fengfei.lanproxy.server.handlers;
import java.net.InetSocketAddress;
import java.util.concurrent.atomic.AtomicLong;
import org.fengfei.lanproxy.protocol.Constants;
import org.fengfei.lanproxy.protocol.ProxyMessage;
import org.fengfei.lanproxy.server.ProxyChannelManager;
import org.fengfei.lanproxy.server.config.ProxyConfig;
import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelOption;
import io.netty.channel.SimpleChannelInboundHandler;
/**
* 处理服务端 channel.
*/
public class UserChannelHandler extends SimpleChannelInboundHandler<ByteBuf> {
private static AtomicLong userIdProducer = new AtomicLong(0);
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
// 当出现异常就关闭连接
ctx.close();
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf buf) throws Exception {
// 通知代理客户端
Channel userChannel = ctx.channel();
Channel proxyChannel = userChannel.attr(Constants.NEXT_CHANNEL).get();
if (proxyChannel == null) {
// 该端口还没有代理客户端
ctx.channel().close();
} else {
byte[] bytes = new byte[buf.readableBytes()];
buf.readBytes(bytes);
String userId = ProxyChannelManager.getUserChannelUserId(userChannel);
ProxyMessage proxyMessage = new ProxyMessage();
proxyMessage.setType(ProxyMessage.P_TYPE_TRANSFER);
proxyMessage.setUri(userId);
proxyMessage.setData(bytes);
proxyChannel.writeAndFlush(proxyMessage);
}
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
Channel userChannel = ctx.channel();
InetSocketAddress sa = (InetSocketAddress) userChannel.localAddress();
Channel cmdChannel = ProxyChannelManager.getCmdChannel(sa.getPort());
if (cmdChannel == null) {
// 该端口还没有代理客户端
ctx.channel().close();
} else {
String userId = newUserId();
String lanInfo = ProxyConfig.getInstance().getLanInfo(sa.getPort());
// 用户连接到代理服务器时,设置用户连接不可读,等待代理后端服务器连接成功后再改变为可读状态
userChannel.config().setOption(ChannelOption.AUTO_READ, false);
ProxyChannelManager.addUserChannelToCmdChannel(cmdChannel, userId, userChannel);
ProxyMessage proxyMessage = new ProxyMessage();
proxyMessage.setType(ProxyMessage.TYPE_CONNECT);
proxyMessage.setUri(userId);
proxyMessage.setData(lanInfo.getBytes());
cmdChannel.writeAndFlush(proxyMessage);
}
super.channelActive(ctx);
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
// 通知代理客户端
Channel userChannel = ctx.channel();
InetSocketAddress sa = (InetSocketAddress) userChannel.localAddress();
Channel cmdChannel = ProxyChannelManager.getCmdChannel(sa.getPort());
if (cmdChannel == null) {
// 该端口还没有代理客户端
ctx.channel().close();
} else {
// 用户连接断开,从控制连接中移除
String userId = ProxyChannelManager.getUserChannelUserId(userChannel);
ProxyChannelManager.removeUserChannelFromCmdChannel(cmdChannel, userId);
Channel proxyChannel = userChannel.attr(Constants.NEXT_CHANNEL).get();
if (proxyChannel != null && proxyChannel.isActive()) {
proxyChannel.attr(Constants.NEXT_CHANNEL).remove();
proxyChannel.attr(Constants.CLIENT_KEY).remove();
proxyChannel.attr(Constants.USER_ID).remove();
proxyChannel.config().setOption(ChannelOption.AUTO_READ, true);
// 通知客户端,用户连接已经断开
ProxyMessage proxyMessage = new ProxyMessage();
proxyMessage.setType(ProxyMessage.TYPE_DISCONNECT);
proxyMessage.setUri(userId);
proxyChannel.writeAndFlush(proxyMessage);
}
}
super.channelInactive(ctx);
}
@Override
public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
// 通知代理客户端
Channel userChannel = ctx.channel();
InetSocketAddress sa = (InetSocketAddress) userChannel.localAddress();
Channel cmdChannel = ProxyChannelManager.getCmdChannel(sa.getPort());
if (cmdChannel == null) {
// 该端口还没有代理客户端
ctx.channel().close();
} else {
Channel proxyChannel = userChannel.attr(Constants.NEXT_CHANNEL).get();
if (proxyChannel != null) {
proxyChannel.config().setOption(ChannelOption.AUTO_READ, userChannel.isWritable());
}
}
super.channelWritabilityChanged(ctx);
}
/**
* 为用户连接产生ID
*
* @return
*/
private static String newUserId() {
return String.valueOf(userIdProducer.incrementAndGet());
}
}
@@ -0,0 +1,79 @@
package org.fengfei.lanproxy.server.metrics;
import java.io.Serializable;
public class Metrics implements Serializable {
private static final long serialVersionUID = 1L;
private int port;
private long readBytes;
private long wroteBytes;
private long readMsgs;
private long wroteMsgs;
private int channels;
private long timestamp;
public long getReadBytes() {
return readBytes;
}
public void setReadBytes(long readBytes) {
this.readBytes = readBytes;
}
public long getWroteBytes() {
return wroteBytes;
}
public void setWroteBytes(long wroteBytes) {
this.wroteBytes = wroteBytes;
}
public long getReadMsgs() {
return readMsgs;
}
public void setReadMsgs(long readMsgs) {
this.readMsgs = readMsgs;
}
public long getWroteMsgs() {
return wroteMsgs;
}
public void setWroteMsgs(long wroteMsgs) {
this.wroteMsgs = wroteMsgs;
}
public int getChannels() {
return channels;
}
public void setChannels(int channels) {
this.channels = channels;
}
public long getTimestamp() {
return timestamp;
}
public void setTimestamp(long timestamp) {
this.timestamp = timestamp;
}
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
}
@@ -0,0 +1,121 @@
package org.fengfei.lanproxy.server.metrics;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
public class MetricsCollector {
private static Map<Integer, MetricsCollector> metricsCollectors = new ConcurrentHashMap<Integer, MetricsCollector>();
private Integer port;
private AtomicLong readBytes = new AtomicLong();
private AtomicLong wroteBytes = new AtomicLong();
private AtomicLong readMsgs = new AtomicLong();
private AtomicLong wroteMsgs = new AtomicLong();
private AtomicInteger channels = new AtomicInteger();
private MetricsCollector() {
}
public static MetricsCollector getCollector(Integer port) {
MetricsCollector collector = metricsCollectors.get(port);
if (collector == null) {
synchronized (metricsCollectors) {
collector = metricsCollectors.get(port);
if (collector == null) {
collector = new MetricsCollector();
collector.setPort(port);
metricsCollectors.put(port, collector);
}
}
}
return collector;
}
public static List<Metrics> getAndResetAllMetrics() {
List<Metrics> allMetrics = new ArrayList<Metrics>();
Iterator<Entry<Integer, MetricsCollector>> ite = metricsCollectors.entrySet().iterator();
while (ite.hasNext()) {
allMetrics.add(ite.next().getValue().getAndResetMetrics());
}
return allMetrics;
}
public static List<Metrics> getAllMetrics() {
List<Metrics> allMetrics = new ArrayList<Metrics>();
Iterator<Entry<Integer, MetricsCollector>> ite = metricsCollectors.entrySet().iterator();
while (ite.hasNext()) {
allMetrics.add(ite.next().getValue().getMetrics());
}
return allMetrics;
}
public Metrics getAndResetMetrics() {
Metrics metrics = new Metrics();
metrics.setChannels(channels.get());
metrics.setPort(port);
metrics.setReadBytes(readBytes.getAndSet(0));
metrics.setWroteBytes(wroteBytes.getAndSet(0));
metrics.setTimestamp(System.currentTimeMillis());
metrics.setReadMsgs(readMsgs.getAndSet(0));
metrics.setWroteMsgs(wroteMsgs.getAndSet(0));
return metrics;
}
public Metrics getMetrics() {
Metrics metrics = new Metrics();
metrics.setChannels(channels.get());
metrics.setPort(port);
metrics.setReadBytes(readBytes.get());
metrics.setWroteBytes(wroteBytes.get());
metrics.setTimestamp(System.currentTimeMillis());
metrics.setReadMsgs(readMsgs.get());
metrics.setWroteMsgs(wroteMsgs.get());
return metrics;
}
public void incrementReadBytes(long bytes) {
readBytes.addAndGet(bytes);
}
public void incrementWroteBytes(long bytes) {
wroteBytes.addAndGet(bytes);
}
public void incrementReadMsgs(long msgs) {
readMsgs.addAndGet(msgs);
}
public void incrementWroteMsgs(long msgs) {
wroteMsgs.addAndGet(msgs);
}
public AtomicInteger getChannels() {
return channels;
}
public Integer getPort() {
return port;
}
public void setPort(Integer port) {
this.port = port;
}
}
@@ -0,0 +1,46 @@
package org.fengfei.lanproxy.server.metrics.handler;
import java.net.InetSocketAddress;
import org.fengfei.lanproxy.server.metrics.MetricsCollector;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
public class BytesMetricsHandler extends ChannelDuplexHandler {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
InetSocketAddress sa = (InetSocketAddress) ctx.channel().localAddress();
MetricsCollector metricsCollector = MetricsCollector.getCollector(sa.getPort());
metricsCollector.incrementReadBytes(((ByteBuf) msg).readableBytes());
metricsCollector.incrementReadMsgs(1);
ctx.fireChannelRead(msg);
}
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
InetSocketAddress sa = (InetSocketAddress) ctx.channel().localAddress();
MetricsCollector metricsCollector = MetricsCollector.getCollector(sa.getPort());
metricsCollector.incrementWroteBytes(((ByteBuf) msg).readableBytes());
metricsCollector.incrementWroteMsgs(1);
super.write(ctx, msg, promise);
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
InetSocketAddress sa = (InetSocketAddress) ctx.channel().localAddress();
MetricsCollector.getCollector(sa.getPort()).getChannels().incrementAndGet();
super.channelActive(ctx);
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
InetSocketAddress sa = (InetSocketAddress) ctx.channel().localAddress();
MetricsCollector.getCollector(sa.getPort()).getChannels().decrementAndGet();
super.channelInactive(ctx);
}
}
@@ -0,0 +1,18 @@
server.bind=0.0.0.0
server.port=4900
# 服务启动接收请求端口4900
# ssl是否开启,默认不开启
server.ssl.enable=false
server.ssl.bind=0.0.0.0
server.ssl.port=4993
server.ssl.jksPath=test.jks
server.ssl.keyStorePassword=123456
server.ssl.keyManagerPassword=123456
server.ssl.needsClientAuth=false
# 8090服务端启动访问端口,账号密码
config.server.bind=0.0.0.0
config.server.port=8090
config.admin.username=admin
config.admin.password=qq755141
@@ -0,0 +1,13 @@
log4j.rootLogger=info,R
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - <%m>%n
log4j.appender.R=org.apache.log4j.DailyRollingFileAppender
log4j.appender.R.DatePattern='_'yyyy-MM-dd'.log'
log4j.appender.R.File=${app.home}/logs/server.log
log4j.appender.R.layout=org.apache.log4j.PatternLayout
log4j.appender.R.layout.ConversionPattern=%d %p [%c] - <%m>%n
log4j.logger.io.netty=warn
@@ -0,0 +1,11 @@
@echo off & setlocal enabledelayedexpansion
title lanproxy-server
cd %~dp0
set LIB_JARS=""
cd ..\lib
for %%i in (*) do set LIB_JARS=!LIB_JARS!;..\lib\%%i
cd ..\bin
java -Dapp.home=../ -Xms64m -Xmx1024m -classpath ..\conf;%LIB_JARS% org.fengfei.lanproxy.server.ProxyServerContainer
goto end
@@ -0,0 +1,43 @@
#!/bin/bash
cd `dirname $0`
cd ..
DEPLOY_DIR=`pwd`
CONF_DIR=$DEPLOY_DIR/conf
LOGS_DIR=$DEPLOY_DIR/logs
APP_MAINCLASS=org.fengfei.lanproxy.server.ProxyServerContainer
PIDS=`ps -ef | grep -v grep | grep "$CONF_DIR" |awk '{print $2}'`
if [ -n "$PIDS" ]; then
echo "ERROR: already started!"
echo "PID: $PIDS"
exit 1
fi
if [ ! -d $LOGS_DIR ]; then
mkdir $LOGS_DIR
fi
STDOUT_FILE=$LOGS_DIR/stdout.log
CLOG_FILE=$LOGS_DIR/gc.log
LIB_DIR=$DEPLOY_DIR/lib
LIB_JARS=`ls $LIB_DIR|grep .jar|awk '{print "'$LIB_DIR'/"$0}'| xargs | sed "s/ /:/g"`
JAVA_OPTS=" -Djava.awt.headless=true -Djava.net.preferIPv4Stack=true "
JAVA_DEBUG_OPTS=""
if [ "$1" = "debug" ]; then
JAVA_DEBUG_OPTS=" -Xdebug -Xnoagent -Djava.compiler=NONE -Xrunjdwp:transport=dt_socket,address=8000,server=y,suspend=n "
fi
JAVA_JMX_OPTS=""
if [ "$1" = "jmx" ]; then
JAVA_JMX_OPTS=" -Dcom.sun.management.jmxremote.port=1099 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false "
fi
JAVA_MEM_OPTS=""
#JAVA_MEM_OPTS="-server -Xms5120M -Xmx5120M -Xmn1024M -Xnoclassgc -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+CMSClassUnloadingEnabled -XX:+CMSParallelRemarkEnabled -XX:CMSInitiatingOccupancyFraction=80 -XX:SoftRefLRUPolicyMSPerMB=0 -XX:+PrintClassHistogram -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintHeapAtGC -Xloggc:$CLOG_FILE"
echo -e "Starting the proxy server ...\c"
nohup java -Dapp.home=$DEPLOY_DIR $JAVA_OPTS $JAVA_MEM_OPTS $JAVA_DEBUG_OPTS $JAVA_JMX_OPTS -classpath $CONF_DIR:$LIB_JARS $APP_MAINCLASS >$STDOUT_FILE 2>&1 &
sleep 1
echo "started"
PIDS=`ps -ef | grep java | grep "$DEPLOY_DIR" | awk '{print $2}'`
echo "PID: $PIDS"
+35
View File
@@ -0,0 +1,35 @@
#!/bin/bash
cd `dirname $0`
BIN_DIR=`pwd`
cd ..
DEPLOY_DIR=`pwd`
LOGS_DIR=$DEPLOY_DIR/logs
if [ ! -d $LOGS_DIR ]; then
mkdir $LOGS_DIR
fi
STDOUT_FILE=$LOGS_DIR/stdout.log
PID=`ps -ef | grep -v grep | grep "$DEPLOY_DIR/conf" | awk '{print $2}'`
echo "PID: $PID"
if [ -z "$PID" ]; then
echo "ERROR: The proxy server does not started!"
exit 1
fi
echo -e "Stopping the proxy server ...\c"
kill $PID > $STDOUT_FILE 2>&1
COUNT=0
while [ $COUNT -lt 1 ]; do
echo -e ".\c"
sleep 1
COUNT=1
PID_EXIST=`ps -f -p $PID | grep java`
if [ -n "$PID_EXIST" ]; then
COUNT=0
fi
done
echo "stopped"
echo "PID: $PID"
Binary file not shown.
@@ -0,0 +1,11 @@
package org.fengfei.lanproxy.server.test;
import org.fengfei.lanproxy.server.ProxyServerContainer;
public class ServerMainTest {
public static void main(String[] args) {
ProxyServerContainer.main(args);
}
}
@@ -0,0 +1,15 @@
server.bind=0.0.0.0
server.port=4900
server.ssl.enable=true
server.ssl.bind=0.0.0.0
server.ssl.port=4993
server.ssl.jksPath=test.jks
server.ssl.keyStorePassword=123456
server.ssl.keyManagerPassword=123456
server.ssl.needsClientAuth=false
config.server.bind=0.0.0.0
config.server.port=8099
config.admin.username=admin
config.admin.password=admin
@@ -0,0 +1,7 @@
log4j.rootLogger=info,stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - <%m>%n
#log4j.logger.io.netty=warn
Binary file not shown.
@@ -0,0 +1,18 @@
server.bind=0.0.0.0
server.port=4900
# 服务启动接收请求端口4900
# ssl是否开启,默认不开启
server.ssl.enable=false
server.ssl.bind=0.0.0.0
server.ssl.port=4993
server.ssl.jksPath=test.jks
server.ssl.keyStorePassword=123456
server.ssl.keyManagerPassword=123456
server.ssl.needsClientAuth=false
# 8090服务端启动访问端口,账号密码
config.server.bind=0.0.0.0
config.server.port=8090
config.admin.username=admin
config.admin.password=qq755141
@@ -0,0 +1,13 @@
log4j.rootLogger=info,R
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - <%m>%n
log4j.appender.R=org.apache.log4j.DailyRollingFileAppender
log4j.appender.R.DatePattern='_'yyyy-MM-dd'.log'
log4j.appender.R.File=${app.home}/logs/server.log
log4j.appender.R.layout=org.apache.log4j.PatternLayout
log4j.appender.R.layout.ConversionPattern=%d %p [%c] - <%m>%n
log4j.logger.io.netty=warn
+11
View File
@@ -0,0 +1,11 @@
@echo off & setlocal enabledelayedexpansion
title lanproxy-server
cd %~dp0
set LIB_JARS=""
cd ..\lib
for %%i in (*) do set LIB_JARS=!LIB_JARS!;..\lib\%%i
cd ..\bin
java -Dapp.home=../ -Xms64m -Xmx1024m -classpath ..\conf;%LIB_JARS% org.fengfei.lanproxy.server.ProxyServerContainer
goto end
+43
View File
@@ -0,0 +1,43 @@
#!/bin/bash
cd `dirname $0`
cd ..
DEPLOY_DIR=`pwd`
CONF_DIR=$DEPLOY_DIR/conf
LOGS_DIR=$DEPLOY_DIR/logs
APP_MAINCLASS=org.fengfei.lanproxy.server.ProxyServerContainer
PIDS=`ps -ef | grep -v grep | grep "$CONF_DIR" |awk '{print $2}'`
if [ -n "$PIDS" ]; then
echo "ERROR: already started!"
echo "PID: $PIDS"
exit 1
fi
if [ ! -d $LOGS_DIR ]; then
mkdir $LOGS_DIR
fi
STDOUT_FILE=$LOGS_DIR/stdout.log
CLOG_FILE=$LOGS_DIR/gc.log
LIB_DIR=$DEPLOY_DIR/lib
LIB_JARS=`ls $LIB_DIR|grep .jar|awk '{print "'$LIB_DIR'/"$0}'| xargs | sed "s/ /:/g"`
JAVA_OPTS=" -Djava.awt.headless=true -Djava.net.preferIPv4Stack=true "
JAVA_DEBUG_OPTS=""
if [ "$1" = "debug" ]; then
JAVA_DEBUG_OPTS=" -Xdebug -Xnoagent -Djava.compiler=NONE -Xrunjdwp:transport=dt_socket,address=8000,server=y,suspend=n "
fi
JAVA_JMX_OPTS=""
if [ "$1" = "jmx" ]; then
JAVA_JMX_OPTS=" -Dcom.sun.management.jmxremote.port=1099 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false "
fi
JAVA_MEM_OPTS=""
#JAVA_MEM_OPTS="-server -Xms5120M -Xmx5120M -Xmn1024M -Xnoclassgc -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+CMSClassUnloadingEnabled -XX:+CMSParallelRemarkEnabled -XX:CMSInitiatingOccupancyFraction=80 -XX:SoftRefLRUPolicyMSPerMB=0 -XX:+PrintClassHistogram -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintHeapAtGC -Xloggc:$CLOG_FILE"
echo -e "Starting the proxy server ...\c"
nohup java -Dapp.home=$DEPLOY_DIR $JAVA_OPTS $JAVA_MEM_OPTS $JAVA_DEBUG_OPTS $JAVA_JMX_OPTS -classpath $CONF_DIR:$LIB_JARS $APP_MAINCLASS >$STDOUT_FILE 2>&1 &
sleep 1
echo "started"
PIDS=`ps -ef | grep java | grep "$DEPLOY_DIR" | awk '{print $2}'`
echo "PID: $PIDS"
+35
View File
@@ -0,0 +1,35 @@
#!/bin/bash
cd `dirname $0`
BIN_DIR=`pwd`
cd ..
DEPLOY_DIR=`pwd`
LOGS_DIR=$DEPLOY_DIR/logs
if [ ! -d $LOGS_DIR ]; then
mkdir $LOGS_DIR
fi
STDOUT_FILE=$LOGS_DIR/stdout.log
PID=`ps -ef | grep -v grep | grep "$DEPLOY_DIR/conf" | awk '{print $2}'`
echo "PID: $PID"
if [ -z "$PID" ]; then
echo "ERROR: The proxy server does not started!"
exit 1
fi
echo -e "Stopping the proxy server ...\c"
kill $PID > $STDOUT_FILE 2>&1
COUNT=0
while [ $COUNT -lt 1 ]; do
echo -e ".\c"
sleep 1
COUNT=1
PID_EXIST=`ps -f -p $PID | grep java`
if [ -n "$PID_EXIST" ]; then
COUNT=0
fi
done
echo "stopped"
echo "PID: $PID"
Binary file not shown.
@@ -0,0 +1,5 @@
#Generated by Maven
#Wed Jul 05 11:03:48 CST 2023
groupId=org.fengfei
artifactId=proxy-server
version=0.1
@@ -0,0 +1,36 @@
org\fengfei\lanproxy\server\config\web\HttpRequestHandler.class
org\fengfei\lanproxy\server\config\web\RequestMiddleware.class
org\fengfei\lanproxy\server\config\web\ResponseInfo.class
org\fengfei\lanproxy\server\config\web\WebConfigContainer$1.class
org\fengfei\lanproxy\server\config\ProxyConfig$1.class
org\fengfei\lanproxy\server\config\web\routes\RouteConfig$1.class
org\fengfei\lanproxy\server\config\web\routes\RouteConfig$3$1.class
org\fengfei\lanproxy\server\config\ProxyConfig$Client.class
org\fengfei\lanproxy\server\ProxyServerContainer.class
org\fengfei\lanproxy\server\SslContextCreator.class
org\fengfei\lanproxy\server\metrics\handler\BytesMetricsHandler.class
org\fengfei\lanproxy\server\config\web\routes\RouteConfig$3.class
org\fengfei\lanproxy\server\ProxyServerContainer$2.class
org\fengfei\lanproxy\server\config\web\exception\ContextException.class
org\fengfei\lanproxy\server\config\web\ApiRoute.class
org\fengfei\lanproxy\server\config\web\routes\RouteConfig$5.class
org\fengfei\lanproxy\server\metrics\Metrics.class
org\fengfei\lanproxy\server\config\web\RequestHandler.class
org\fengfei\lanproxy\server\handlers\ServerChannelHandler.class
org\fengfei\lanproxy\server\config\ProxyConfig.class
org\fengfei\lanproxy\server\config\web\routes\RouteConfig$2.class
org\fengfei\lanproxy\server\config\web\routes\RouteConfig$4$1.class
org\fengfei\lanproxy\server\config\web\WebConfigContainer.class
org\fengfei\lanproxy\server\ProxyChannelManager.class
org\fengfei\lanproxy\server\ProxyServerContainer$3.class
org\fengfei\lanproxy\server\config\ProxyConfig$ConfigChangedListener.class
org\fengfei\lanproxy\server\config\web\routes\RouteConfig$7.class
org\fengfei\lanproxy\server\handlers\UserChannelHandler.class
org\fengfei\lanproxy\server\ProxyChannelManager$1.class
org\fengfei\lanproxy\server\ProxyServerContainer$1.class
org\fengfei\lanproxy\server\config\ProxyConfig$ClientProxyMapping.class
org\fengfei\lanproxy\server\config\web\routes\RouteConfig$4.class
org\fengfei\lanproxy\server\config\web\MimeType.class
org\fengfei\lanproxy\server\metrics\MetricsCollector.class
org\fengfei\lanproxy\server\config\web\routes\RouteConfig.class
org\fengfei\lanproxy\server\config\web\routes\RouteConfig$6.class
@@ -0,0 +1,18 @@
E:\yx\lanproxy-0.1\proxy-server\src\main\java\org\fengfei\lanproxy\server\metrics\MetricsCollector.java
E:\yx\lanproxy-0.1\proxy-server\src\main\java\org\fengfei\lanproxy\server\config\web\RequestMiddleware.java
E:\yx\lanproxy-0.1\proxy-server\src\main\java\org\fengfei\lanproxy\server\handlers\ServerChannelHandler.java
E:\yx\lanproxy-0.1\proxy-server\src\main\java\org\fengfei\lanproxy\server\metrics\handler\BytesMetricsHandler.java
E:\yx\lanproxy-0.1\proxy-server\src\main\java\org\fengfei\lanproxy\server\config\ProxyConfig.java
E:\yx\lanproxy-0.1\proxy-server\src\main\java\org\fengfei\lanproxy\server\config\web\RequestHandler.java
E:\yx\lanproxy-0.1\proxy-server\src\main\java\org\fengfei\lanproxy\server\metrics\Metrics.java
E:\yx\lanproxy-0.1\proxy-server\src\main\java\org\fengfei\lanproxy\server\config\web\routes\RouteConfig.java
E:\yx\lanproxy-0.1\proxy-server\src\main\java\org\fengfei\lanproxy\server\handlers\UserChannelHandler.java
E:\yx\lanproxy-0.1\proxy-server\src\main\java\org\fengfei\lanproxy\server\config\web\ApiRoute.java
E:\yx\lanproxy-0.1\proxy-server\src\main\java\org\fengfei\lanproxy\server\config\web\MimeType.java
E:\yx\lanproxy-0.1\proxy-server\src\main\java\org\fengfei\lanproxy\server\ProxyChannelManager.java
E:\yx\lanproxy-0.1\proxy-server\src\main\java\org\fengfei\lanproxy\server\config\web\exception\ContextException.java
E:\yx\lanproxy-0.1\proxy-server\src\main\java\org\fengfei\lanproxy\server\config\web\HttpRequestHandler.java
E:\yx\lanproxy-0.1\proxy-server\src\main\java\org\fengfei\lanproxy\server\config\web\ResponseInfo.java
E:\yx\lanproxy-0.1\proxy-server\src\main\java\org\fengfei\lanproxy\server\config\web\WebConfigContainer.java
E:\yx\lanproxy-0.1\proxy-server\src\main\java\org\fengfei\lanproxy\server\SslContextCreator.java
E:\yx\lanproxy-0.1\proxy-server\src\main\java\org\fengfei\lanproxy\server\ProxyServerContainer.java
@@ -0,0 +1 @@
org\fengfei\lanproxy\server\test\ServerMainTest.class
@@ -0,0 +1 @@
E:\yx\lanproxy-0.1\proxy-server\src\test\java\org\fengfei\lanproxy\server\test\ServerMainTest.java
@@ -0,0 +1,15 @@
server.bind=0.0.0.0
server.port=4900
server.ssl.enable=true
server.ssl.bind=0.0.0.0
server.ssl.port=4993
server.ssl.jksPath=test.jks
server.ssl.keyStorePassword=123456
server.ssl.keyManagerPassword=123456
server.ssl.needsClientAuth=false
config.server.bind=0.0.0.0
config.server.port=8099
config.admin.username=admin
config.admin.password=admin
@@ -0,0 +1,7 @@
log4j.rootLogger=info,stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - <%m>%n
#log4j.logger.io.netty=warn
Binary file not shown.
+9
View File
@@ -0,0 +1,9 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>404 - LanProxy</title>
</head>
<body>404
</body>
</html>
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+150
View File
@@ -0,0 +1,150 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title>配置编辑</title>
<!-- load a custom version of Ace editor -->
<script src="/ace/ace.js"></script>
<!-- load the minimalist version of JSONEditor, which doesn't have Ace embedded -->
<link href="/jsoneditor/dist/jsoneditor.css" rel="stylesheet"
type="text/css">
<script src="/jsoneditor/dist/jsoneditor-minimalist.js"></script>
<style type="text/css">
#jsoneditor {
width: 98%;
height: 600px;
margin: 0 auto;
}
.op {
width: 98%;
margin: 10px auto;
}
.op a {
float: right;
}
body, html {
font-family: "DejaVu Sans", sans-serif;
}
p, li {
width: 98%;
font-size: 10.5pt;
}
code {
background: #f5f5f5;
}
div.jsoneditor {
border: 1px solid #50a3a2;
}
div.jsoneditor-menu {
background-color: #50a3a2;
border-bottom: 1px solid #50a3a2;
}
button {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
outline: 0;
background-color: #50a3a2;
border: 0;
padding: 8px 15px;
color: white;
border-radius: 3px;
width: 180px;
cursor: pointer;
font-size: 14px;
-webkit-transition-duration: 0.25s;
transition-duration: 0.25s;
}
</style>
</head>
<body>
<div id="jsoneditor"></div>
<div class="op">
<button class="update-btn">提交更新</button>
<a href="javascript:;" class="logout">退出登录</a>
</div>
<script src="/jquery/jquery-3.1.1.min.js"></script>
<script>
// create the editor
var container = document.getElementById('jsoneditor');
var options = {
mode : 'code',
ace : ace
};
var editor = new JSONEditor(container, options, {});
$.ajax({
type : "POST",
url : "/config/detail",
contentType : "application/json; charset=utf-8",
data : JSON.stringify(editor.get()),
dataType : "json",
success : function(data) {
editor.set(data.data);
},
error : function(e) {
if (e.responseJSON.code == 40100) {
location.href = "/";
}
}
});
$(".update-btn").click(function() {
$.ajax({
type : "POST",
url : "/config/update",
contentType : "application/json; charset=utf-8",
data : JSON.stringify(editor.get()),
dataType : "json",
success : function(data) {
if (data.code == 20000) {
alert("配置更新成功");
window.location.href = "/config.html";
}
},
error : function(e) {
if (e.responseJSON.code == 40100) {
location.href = "/";
} else {
alert(e.responseJSON.message);
}
}
});
});
$(".logout").click(function() {
$.ajax({
type : "POST",
url : "/logout",
contentType : "application/json; charset=utf-8",
data : JSON.stringify(editor.get()),
dataType : "json",
success : function(data) {
if (data.code == 20000) {
window.location.href = "/";
}
},
error : function(e) {
if (e.responseJSON.code == 40100) {
location.href = "/";
} else {
alert(e.responseJSON.message);
}
}
});
});
</script>
</body>
</html>
@@ -0,0 +1,57 @@
login=Sign in
logout=Sign out
username=Username
password=Password
title=yxwlkj - help you expose a local server behind a NAT or firewall to the internet
menu.client.list=Client list
menu.client.add=New client
menu.client.config=Client config
menu.client.statistics=Statistics
public.confirm.delete=Delete?
public.options=Options
public.edit=Edit
public.submit=Submit
public.delete=Delete
public.ok=Ok
public.cancel=Cancel
public.back=Back
public.notice.updatesuccess=Update successfully
public.notice.addsuccess=Add successfully
public.tips=Tips
client.list=Client list
client.name=Client name
client.key=Client key
client.add=New client
client.name.placeholder=
client.key.placeholder=
client.randomkey=Generate a random key
client.notice.inputname=Client name is required
client.notice.inputkey=Client key is required
client.notice.addsuccess=New client has been added successfully
client.edit=Edit client
client.status=Status
client.status.online=Up
client.status.offline=Down
lan.proxyconfig=Proxy config
lan.addnewconfig=New proxy config
lan.editconfig=Edit proxy config
lan.name=Proxy name
lan.inetport=Internet port
lan.inetport.placeholder=
lan.ip=Backend ip
lan.ip.placeholder=127.0.0.1:80
lan.notice.inputname=Proxy config name is required
lan.notice.inputinetport=Internet port is required
lan.notice.inputlan=Backend server ip:port is required
lan.notice.errorlan=Error backend info, should be ip:port
lan.notice.errorport=Error port
statistics.inetport=Internet port
statistics.inflow=Inflow data
statistics.outflow=Outflow data
statistics.channels=Current channels
@@ -0,0 +1,57 @@
login=登录
logout=退出
username=用户名
password=密码
title=言逍网络科技-内网穿透工具
menu.client.list=客户端管理
menu.client.add=添加客户端
menu.client.config=配置管理
menu.client.statistics=数据统计
public.confirm.delete=确定删除?
public.options=操作
public.submit=提交
public.edit=编辑
public.delete=删除
public.ok=确定
public.cancel=取消
public.back=返回
public.notice.updatesuccess=更新成功
public.notice.addsuccess=添加成功
public.tips=提示
client.list=客户端列表
client.name=客户端名称
client.key=客户端密钥
client.add=添加客户端
client.name.placeholder=请输入客户端名称
client.key.placeholder=请输入客户端密钥
client.randomkey=生成随机密钥
client.notice.inputname=请输入客户端备注名称
client.notice.inputkey=请输入客户端通信密钥
client.notice.addsuccess=客户端添加成功
client.edit=编辑客户端
client.status=状态
client.status.online=在线
client.status.offline=离线
lan.proxyconfig=代理配置
lan.addnewconfig=添加配置
lan.editconfig=编辑配置
lan.name=代理名称
lan.inetport=公网端口
lan.inetport.placeholder=请输入公网出口端口,请确保端口没有被其他程序占用
lan.ip=后端IP端口
lan.ip.placeholder=请输入后端代理信息,格式:127.0.0.1:80
lan.notice.inputname=请输入代理信息备注名称
lan.notice.inputinetport=请输入公网出口端口
lan.notice.inputlan=请输入后端代理信息
lan.notice.errorlan=后端代理信息格式错误
lan.notice.errorport=端口错误
statistics.inetport=出口端口
statistics.inflow=流入数据
statistics.outflow=流出数据
statistics.channels=当前连接
+241
View File
@@ -0,0 +1,241 @@
<!doctype html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>登录</title>
<style type="text/css">
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-weight: 300;
}
body {
font-family: 'Source Sans Pro', sans-serif;
color: white;
font-weight: 300;
}
body ::-webkit-input-placeholder {
/* WebKit browsers */
font-family: 'Source Sans Pro', sans-serif;
color: white;
font-weight: 300;
}
body :-moz-placeholder {
/* Mozilla Firefox 4 to 18 */
font-family: 'Source Sans Pro', sans-serif;
color: white;
opacity: 1;
font-weight: 300;
}
body ::-moz-placeholder {
/* Mozilla Firefox 19+ */
font-family: 'Source Sans Pro', sans-serif;
color: white;
opacity: 1;
font-weight: 300;
}
body :-ms-input-placeholder {
/* Internet Explorer 10+ */
font-family: 'Source Sans Pro', sans-serif;
color: white;
font-weight: 300;
}
.wrapper {
background: #393D49 !important;
background: -webkit-linear-gradient(top left, #50a3a2 0%, #53e3a6 100%);
background: linear-gradient(to bottom right, #50a3a2 0%, #53e3a6 100%);
opacity: 0.8;
position: absolute;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
.wrapper.form-success .container h1 {
-webkit-transform: translateY(85px);
-ms-transform: translateY(85px);
transform: translateY(85px);
}
.container {
max-width: 600px;
margin: 0 auto;
padding: 80px 0;
height: 400px;
text-align: center;
}
.container h1 {
font-size: 40px;
-webkit-transition-duration: 1s;
transition-duration: 1s;
-webkit-transition-timing-function: ease-in-put;
transition-timing-function: ease-in-put;
font-weight: 200;
}
.form {
padding: 20px 0;
position: relative;
z-index: 2;
}
.form input {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
outline: 0;
border: 1px solid rgba(255, 255, 255, 0.4);
background-color: rgba(255, 255, 255, 0.2);
width: 250px;
border-radius: 3px;
padding: 10px 15px;
margin: 0 auto 10px auto;
display: block;
text-align: center;
font-size: 18px;
color: white;
-webkit-transition-duration: 0.25s;
transition-duration: 0.25s;
font-weight: 300;
}
.form input:hover {
background-color: rgba(255, 255, 255, 0.4);
}
.form input:focus {
background-color: white;
width: 300px;
color: #53e3a6;
}
.form button {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
outline: 0;
background-color: white;
border: 0;
padding: 10px 15px;
color: #53e3a6;
border-radius: 3px;
width: 250px;
cursor: pointer;
font-size: 18px;
-webkit-transition-duration: 0.25s;
transition-duration: 0.25s;
}
.form button:hover {
background-color: #f5f7f9;
}
</style>
</head>
<body>
<!-- 代码 开始 -->
<div class="wrapper">
<div class="container">
<h1>言逍网络科技</h1>
<div class="form">
<input type="text" class="username" placeholder="">
<input type="password" class="password" placeholder="">
<button type="submit" id="login-button"></button>
</div>
<h4>内网穿透后台</h4>
</div>
</div>
<script src="jquery/jquery-3.1.1.min.js" type="text/javascript"></script>
<script src="jquery/jquery.i18n.properties.min.js" type="text/javascript"></script>
<script>
$('#login-button').click(function(event) {
$.ajax({
type : "POST",
url : "/login",
contentType : "application/json; charset=utf-8",
data : JSON.stringify({
username : $(".username").val(),
password : $(".password").val()
}),
dataType : "json",
success : function(data) {
if (data.code == 20000) {
setCookie("token", data.data);
window.location.href = "/lanproxy-config/";
}
},
error : function(e) {
alert(e.responseJSON.message);
}
});
});
function setCookie(name, value) {
var Days = 30;
var exp = new Date();
exp.setTime(exp.getTime() + Days * 24 * 60 * 60 * 1000);
document.cookie = name + "=" + escape(value) + ";expires="
+ exp.toGMTString();
}
$(document).keyup(function(event) {
if (event.keyCode == 13) {
$("#login-button").trigger("click");
}
});
/**
* 设置语言类型: 默认为英文
*/
var i18nLanguage = "en";
/*
设置一下网站支持的语言种类
zh-CN(中文简体)、en(英语)
*/
var webLanguage = [ 'zh-CN', 'en' ];
function initWebLanguage() {
var navLanguage = navigator.language;
//console.log("user set browser language is " + navLanguage);
if (navLanguage) {
var charSize = $.inArray(navLanguage, webLanguage);
if (charSize > -1) {
i18nLanguage = navLanguage;
}
}
}
initWebLanguage();
jQuery.i18n.properties({
name : 'lang', //资源文件名称
path : '/i18n/', //资源文件路径
mode : 'map', //用Map的方式使用资源文件中的值
language : i18nLanguage,
encoding: 'UTF-8',
callback : function() {//加载成功后设置显示内容
$('title').html($.i18n.prop('login')+" - LanProxy");
$('#login-button').html($.i18n.prop('login'));
$(".username").attr("placeholder", $.i18n.prop('username'));
$(".password").attr("placeholder", $.i18n.prop('password'));
}
});
</script>
<!-- 代码 结束 -->
</body>
</html>
File diff suppressed because one or more lines are too long
@@ -0,0 +1,9 @@
(function(k){function t(a){a.debug&&(g("callbackIfComplete()"),g("totalFiles: "+a.totalFiles),g("filesLoaded: "+a.filesLoaded));a.async&&a.filesLoaded===a.totalFiles&&a.callback&&a.callback()}function n(a,d){d.debug&&g("loadAndParseFiles");null!==a&&0<a.length?u(a[0],d,function(){a.shift();n(a,d)}):t(d)}function u(a,d,b){d.debug&&(g("loadAndParseFile('"+a+"')"),g("totalFiles: "+d.totalFiles),g("filesLoaded: "+d.filesLoaded));null!==a&&"undefined"!==typeof a&&k.ajax({url:a,async:d.async,cache:d.cache,
dataType:"text",success:function(h,c){d.debug&&(g("Succeeded in downloading "+a+"."),g(h));v(h,d);b()},error:function(h,c,e){d.debug&&g("Failed to download or parse "+a+". errorThrown: "+e);404===h.status&&--d.totalFiles;b()}})}function v(a,d){var b="",h=a.split(/\n/),c=/(\{\d+})/g,e=/\{(\d+)}/g,g=/(\\u.{4})/ig;h.forEach(function(a,w){a=a.trim();if(0<a.length&&"#"!=a.match("^#")){var l=a.split("=");if(0<l.length){for(var m=decodeURI(l[0]).trim(),f=1==l.length?"":l[1];"\\"===f.match(/\\$/);)f=f.substring(0,
f.length-1),f+=h[++w].trimRight();for(var p=2;p<l.length;p++)f+="="+l[p];f=f.trim();if("map"==d.mode||"both"==d.mode)(l=f.match(g))&&l.forEach(function(a){f=f.replace(a,x(a))}),k.i18n.map[m]=f;if("vars"==d.mode||"both"==d.mode)if(f=f.replace(/"/g,'\\"'),y(m),c.test(f)){var n=!0,q="",r=[];f.split(c).forEach(function(a){!c.test(a)||0!==r.length&&-1!=r.indexOf(a)||(n||(q+=","),q+=a.replace(e,"v$1"),r.push(a),n=!1)});b+=m+"=function("+q+"){";m='"'+f.replace(e,'"+v$1+"')+'"';b+="return "+m+";};"}else b+=
m+'="'+f+'";'}}});eval(b);d.filesLoaded+=1}function y(a){if(/\./.test(a)){var d="";a.split(/\./).forEach(function(a,h){0<h&&(d+=".");d+=a;eval("typeof "+d+' == "undefined"')&&eval(d+"={};")})}}function x(a){var d=[];a=parseInt(a.substr(2),16);0<=a&&a<Math.pow(2,16)&&d.push(a);return d.reduce(function(a,d){return a+String.fromCharCode(d)},"")}k.i18n={};k.i18n.map={};var g=function(a){window.console&&console.log("i18n::"+a)};k.i18n.properties=function(a){a=k.extend({name:"Messages",language:"",path:"",
mode:"vars",cache:!1,debug:!1,encoding:"UTF-8",async:!1,callback:null},a);a.path.match(/\/$/)||(a.path+="/");a.language=this.normaliseLanguageCode(a.language);var d=a.name&&a.name.constructor===Array?a.name:[a.name];a.totalFiles=2*d.length+(5<=a.language.length?d.length:0);a.debug&&g("totalFiles: "+a.totalFiles);a.filesLoaded=0;d.forEach(function(b){var d=a.path+b+".properties";var c=a.language.substring(0,2);c=a.path+b+"_"+c+".properties";if(5<=a.language.length){var e=a.language.substring(0,5);
b=a.path+b+"_"+e+".properties";d=[d,c,b]}else d=[d,c];n(d,a)});a.callback&&!a.async&&a.callback()};k.i18n.prop=function(a){var d,b=k.i18n.map[a];if(null===b)return"["+a+"]";var h;2==arguments.length&&k.isArray(arguments[1])&&(h=arguments[1]);var c;if("string"==typeof b){for(c=0;-1!=(c=b.indexOf("\\",c));)b="t"==b.charAt(c+1)?b.substring(0,c)+"\t"+b.substring(c++ +2):"r"==b.charAt(c+1)?b.substring(0,c)+"\r"+b.substring(c++ +2):"n"==b.charAt(c+1)?b.substring(0,c)+"\n"+b.substring(c++ +2):"f"==b.charAt(c+
1)?b.substring(0,c)+"\f"+b.substring(c++ +2):"\\"==b.charAt(c+1)?b.substring(0,c)+"\\"+b.substring(c++ +2):b.substring(0,c)+b.substring(c+1);var e=[];for(c=0;c<b.length;)if("'"==b.charAt(c))if(c==b.length-1)b=b.substring(0,c);else if("'"==b.charAt(c+1))b=b.substring(0,c)+b.substring(++c);else{for(d=c+2;-1!=(d=b.indexOf("'",d));)if(d==b.length-1||"'"!=b.charAt(d+1)){b=b.substring(0,c)+b.substring(c+1,d)+b.substring(d+1);c=d-1;break}else b=b.substring(0,d)+b.substring(++d);-1==d&&(b=b.substring(0,c)+
b.substring(c+1))}else if("{"==b.charAt(c))if(d=b.indexOf("}",c+1),-1==d)c++;else{var g=parseInt(b.substring(c+1,d));!isNaN(g)&&0<=g?(c=b.substring(0,c),""!==c&&e.push(c),e.push(g),c=0,b=b.substring(d+1)):c=d+1}else c++;""!==b&&e.push(b);b=e;k.i18n.map[a]=e}if(0===b.length)return"";if(1==b.length&&"string"==typeof b[0])return b[0];e="";c=0;for(d=b.length;c<d;c++)e="string"==typeof b[c]?e+b[c]:h&&b[c]<h.length?e+h[b[c]]:!h&&b[c]+1<arguments.length?e+arguments[b[c]+1]:e+("{"+b[c]+"}");return e};k.i18n.normaliseLanguageCode=
function(a){if(!a||2>a.length)a=navigator.languages&&0<navigator.languages.length?navigator.languages[0]:navigator.language||navigator.userLanguage||"en";a=a.toLowerCase();a=a.replace(/-/,"_");3<a.length&&(a=a.substring(0,3)+a.substring(3).toUpperCase());return a}})(jQuery);
@@ -0,0 +1,893 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="216"
height="144"
id="svg4136"
version="1.1"
inkscape:version="0.91 r"
sodipodi:docname="jsoneditor-icons.svg">
<title
id="title6512">JSON Editor Icons</title>
<metadata
id="metadata4148">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title>JSON Editor Icons</dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs4146" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1028"
id="namedview4144"
showgrid="true"
inkscape:zoom="4"
inkscape:cx="97.217248"
inkscape:cy="59.950227"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg4136"
showguides="false"
borderlayer="false"
inkscape:showpageshadow="true"
showborder="true">
<inkscape:grid
type="xygrid"
id="grid4640"
empspacing="24" />
</sodipodi:namedview>
<!-- Created with SVG-edit - http://svg-edit.googlecode.com/ -->
<g
id="g4394">
<rect
x="4"
y="4"
width="16"
height="16"
id="svg_1"
style="fill:#1aae1c;fill-opacity:1;stroke:none;stroke-width:0" />
<rect
style="fill:#ec3f29;fill-opacity:0.94117647;stroke:none;stroke-width:0"
x="28.000006"
y="3.999995"
width="16"
height="16"
id="svg_1-7" />
<rect
id="rect4165"
height="16"
width="16"
y="3.999995"
x="52.000004"
style="fill:#4c4c4c;fill-opacity:1;stroke:none;stroke-width:0" />
<rect
style="fill:#4c4c4c;fill-opacity:1;stroke:none;stroke-width:0"
x="172.00002"
y="3.9999852"
width="16"
height="16"
id="rect4175" />
<rect
style="fill:#4c4c4c;fill-opacity:1;stroke:none;stroke-width:0"
x="196"
y="3.999995"
width="16"
height="16"
id="rect4175-3" />
<g
style="stroke:none"
id="g4299">
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0"
id="svg_1-1"
height="1.9999986"
width="9.9999924"
y="10.999998"
x="7.0000048" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0"
id="svg_1-1-1"
height="9.9999838"
width="1.9999955"
y="7.0000114"
x="11.000005" />
</g>
<g
style="stroke:none"
transform="matrix(0.70710678,-0.70710678,0.70710678,0.70710678,19.029435,12.000001)"
id="g4299-3">
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0"
id="svg_1-1-0"
height="1.9999986"
width="9.9999924"
y="10.999998"
x="7.0000048" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0"
id="svg_1-1-1-9"
height="9.9999838"
width="1.9999955"
y="7.0000114"
x="11.000005" />
</g>
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0"
x="55.000004"
y="7.0000048"
width="6.9999909"
height="6.9999905"
id="svg_1-7-5" />
<rect
id="rect4354"
height="6.9999905"
width="6.9999909"
y="10.00001"
x="58"
style="fill:#ffffff;fill-opacity:1;stroke:#4c4c4c;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#3c80df;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.94117647"
x="58.000004"
y="10.000005"
width="6.9999909"
height="6.9999905"
id="svg_1-7-5-7" />
<g
id="g4378">
<rect
id="svg_1-7-5-3"
height="1.9999965"
width="7.9999909"
y="10.999999"
x="198"
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0"
x="198"
y="7.0000005"
width="11.999995"
height="1.9999946"
id="rect4374" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0"
x="198"
y="14.999996"
width="3.9999928"
height="1.9999995"
id="rect4376" />
</g>
<g
id="g4383"
transform="matrix(1,0,0,-1,-23.999995,23.999995)">
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0"
x="198"
y="10.999999"
width="7.9999909"
height="1.9999965"
id="rect4385" />
<rect
id="rect4387"
height="1.9999946"
width="11.999995"
y="7.0000005"
x="198"
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0" />
<rect
id="rect4389"
height="1.9999995"
width="3.9999928"
y="14.999996"
x="198"
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0" />
</g>
<rect
y="3.9999199"
x="76"
height="16"
width="16"
id="rect3754-4"
style="fill:#4c4c4c;fill-opacity:1;stroke:none" />
<path
sodipodi:nodetypes="cccccccc"
inkscape:connector-curvature="0"
id="path4351"
d="m 85.10447,6.0157384 -0.0156,1.4063 c 3.02669,-0.2402 0.33008,3.6507996 2.48438,4.5780996 -2.18694,1.0938 0.49191,4.9069 -2.45313,4.5781 l -0.0156,1.4219 c 5.70828,0.559 1.03264,-5.1005 4.70313,-5.2656 l 0,-1.4063 c -3.61303,-0.027 1.11893,-5.7069996 -4.70313,-5.3124996 z"
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:0.2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
sodipodi:nodetypes="cccccccc"
inkscape:connector-curvature="0"
id="path4351-9"
d="m 82.78125,5.9984384 0.0156,1.4063 c -3.02668,-0.2402 -0.33007,3.6506996 -2.48437,4.5780996 2.18694,1.0938 -0.49192,4.9069 2.45312,4.5781 l 0.0156,1.4219 c -5.70827,0.559 -1.03263,-5.1004 -4.70312,-5.2656 l 0,-1.4063 c 3.61303,-0.027 -1.11894,-5.7070996 4.70312,-5.3124996 z"
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:0.2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
y="3.9999199"
x="100"
height="16"
width="16"
id="rect3754-25"
style="fill:#4c4c4c;fill-opacity:1;stroke:none" />
<path
inkscape:connector-curvature="0"
id="path2987"
d="m 103.719,5.6719384 0,12.7187996 3.03125,0 0,-1.5313 -1.34375,0 0,-9.6249996 1.375,0 0,-1.5625 z"
style="fill:#ffffff;fill-opacity:1;stroke:none" />
<path
inkscape:connector-curvature="0"
id="path2987-1"
d="m 112.2185,5.6721984 0,12.7187996 -3.03125,0 0,-1.5313 1.34375,0 0,-9.6249996 -1.375,0 0,-1.5625 z"
style="fill:#ffffff;fill-opacity:1;stroke:none" />
<rect
y="3.9999199"
x="124"
height="16"
width="16"
id="rect3754-73"
style="fill:#4c4c4c;fill-opacity:1;stroke:none" />
<path
sodipodi:nodetypes="ccccccccc"
inkscape:connector-curvature="0"
id="path3780"
d="m 126.2824,17.602938 1.78957,0 1.14143,-2.8641 5.65364,0 1.14856,2.8641 1.76565,0 -4.78687,-11.1610996 -1.91903,0 z"
style="fill:#ffffff;fill-opacity:1;stroke:none" />
<path
inkscape:connector-curvature="0"
id="path3782"
d="m 129.72704,13.478838 4.60852,0.01 -2.30426,-5.5497996 z"
style="fill:#4c4c4c;fill-opacity:1;stroke:none" />
<rect
y="3.9999199"
x="148"
height="16"
width="16"
id="rect3754-35"
style="fill:#4c4c4c;fill-opacity:1;stroke:none" />
<path
sodipodi:nodetypes="ccccccc"
inkscape:connector-curvature="0"
id="path5008-2"
d="m 156.47655,5.8917384 0,2.1797 0.46093,2.3983996 1.82813,0 0.39844,-2.3983996 0,-2.1797 z"
style="fill:#ffffff;fill-opacity:1;stroke:none" />
<path
sodipodi:nodetypes="ccccccc"
inkscape:connector-curvature="0"
id="path5008-2-8"
d="m 152.51561,5.8906384 0,2.1797 0.46094,2.3983996 1.82812,0 0.39844,-2.3983996 0,-2.1797 z"
style="fill:#ffffff;fill-opacity:1;stroke:none" />
</g>
<rect
x="4"
y="27.999994"
width="16"
height="16"
id="rect4432"
style="fill:#d3d3d3;fill-opacity:1;stroke:none;stroke-width:0" />
<rect
style="fill:#d3d3d3;fill-opacity:1;stroke:none;stroke-width:0"
x="28.000006"
y="27.99999"
width="16"
height="16"
id="rect4434" />
<rect
id="rect4436"
height="16"
width="16"
y="27.99999"
x="52.000004"
style="fill:#d3d3d3;fill-opacity:1;stroke:#000000;stroke-width:0" />
<rect
style="fill:#d3d3d3;stroke:#000000;stroke-width:0"
x="172.00002"
y="27.999981"
width="16"
height="16"
id="rect4446" />
<rect
style="fill:#d3d3d3;stroke:#000000;stroke-width:0"
x="196"
y="27.99999"
width="16"
height="16"
id="rect4448" />
<g
id="g4466"
style="stroke:none"
transform="translate(0,23.999995)">
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0"
id="rect4468"
height="1.9999986"
width="9.9999924"
y="10.999998"
x="7.0000048" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0"
id="rect4470"
height="9.9999838"
width="1.9999955"
y="7.0000114"
x="11.000005" />
</g>
<g
transform="matrix(0.70710678,-0.70710678,0.70710678,0.70710678,19.029435,35.999996)"
id="g4472"
style="stroke:none">
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0"
id="rect4474"
height="1.9999986"
width="9.9999924"
y="10.999998"
x="7.0000048" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0"
id="rect4476"
height="9.9999838"
width="1.9999955"
y="7.0000114"
x="11.000005" />
</g>
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0"
x="55.000004"
y="31"
width="6.9999909"
height="6.9999905"
id="rect4478" />
<rect
id="rect4480"
height="6.9999905"
width="6.9999909"
y="34.000008"
x="58"
style="fill:#ffffff;fill-opacity:1;stroke:#d3d3d3;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#d3d3d3;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none"
x="58.000004"
y="34.000004"
width="6.9999909"
height="6.9999905"
id="rect4482" />
<g
id="g4484"
transform="translate(0,23.999995)">
<rect
id="rect4486"
height="1.9999965"
width="7.9999909"
y="10.999999"
x="198"
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0"
x="198"
y="7.0000005"
width="11.999995"
height="1.9999946"
id="rect4488" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0"
x="198"
y="14.999996"
width="3.9999928"
height="1.9999995"
id="rect4490" />
</g>
<g
id="g4492"
transform="matrix(1,0,0,-1,-23.999995,47.99999)">
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0"
x="198"
y="10.999999"
width="7.9999909"
height="1.9999965"
id="rect4494" />
<rect
id="rect4496"
height="1.9999946"
width="11.999995"
y="7.0000005"
x="198"
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0" />
<rect
id="rect4498"
height="1.9999995"
width="3.9999928"
y="14.999996"
x="198"
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0" />
</g>
<rect
style="fill:#d3d3d3;fill-opacity:1;stroke:none"
id="rect3754-8"
width="16"
height="16"
x="76"
y="27.99992" />
<path
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:0.2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 85.10448,30.015537 -0.0156,1.4063 c 3.02668,-0.2402 0.33007,3.6508 2.48438,4.5781 -2.18695,1.0938 0.49191,4.90688 -2.45313,4.57808 l -0.0156,1.4219 c 5.70827,0.559 1.03263,-5.10048 4.70313,-5.26558 l 0,-1.4063 c -3.61304,-0.027 1.11893,-5.707 -4.70313,-5.3125 z"
id="path4351-1"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccccccc" />
<path
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:0.2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 82.78126,29.998237 0.0156,1.4063 c -3.02668,-0.2402 -0.33008,3.6507 -2.48438,4.5781 2.18694,1.0938 -0.49191,4.90688 2.45313,4.57808 l 0.0156,1.4219 c -5.70828,0.559 -1.03264,-5.10038 -4.70313,-5.26558 l 0,-1.4063 c 3.61303,-0.027 -1.11893,-5.7071 4.70313,-5.3125 z"
id="path4351-9-5"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccccccc" />
<rect
style="fill:#d3d3d3;fill-opacity:1;stroke:none"
id="rect3754-65"
width="16"
height="16"
x="100"
y="27.99992" />
<path
style="fill:#ffffff;fill-opacity:1;stroke:none"
d="m 103.719,29.671937 0,12.71878 3.03125,0 0,-1.5313 -1.34375,0 0,-9.62498 1.375,0 0,-1.5625 z"
id="path2987-8"
inkscape:connector-curvature="0" />
<path
style="fill:#ffffff;fill-opacity:1;stroke:none"
d="m 112.2185,29.671937 0,12.71878 -3.03125,0 0,-1.5313 1.34375,0 0,-9.62498 -1.375,0 0,-1.5625 z"
id="path2987-1-9"
inkscape:connector-curvature="0" />
<rect
style="fill:#d3d3d3;fill-opacity:1;stroke:none"
id="rect3754-92"
width="16"
height="16"
x="124"
y="27.99992" />
<path
style="fill:#ffffff;fill-opacity:1;stroke:none"
d="m 126.2824,41.602917 1.78957,0 1.14143,-2.86408 5.65364,0 1.14856,2.86408 1.76565,0 -4.78687,-11.16108 -1.91902,0 z"
id="path3780-9"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccccc" />
<path
style="fill:#d3d3d3;fill-opacity:1;stroke:none"
d="m 129.72704,37.478837 4.60852,0.01 -2.30426,-5.5498 z"
id="path3782-2"
inkscape:connector-curvature="0" />
<rect
style="fill:#d3d3d3;fill-opacity:1;stroke:none"
id="rect3754-47"
width="16"
height="16"
x="148"
y="27.99992" />
<path
style="fill:#ffffff;fill-opacity:1;stroke:none"
d="m 156.47656,29.891737 0,2.1797 0.46093,2.3984 1.82813,0 0.39844,-2.3984 0,-2.1797 z"
id="path5008-2-1"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccc" />
<path
style="fill:#ffffff;fill-opacity:1;stroke:none"
d="m 152.51562,29.890637 0,2.1797 0.46094,2.3984 1.82812,0 0.39844,-2.3984 0,-2.1797 z"
id="path5008-2-8-8"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccc" />
<rect
id="svg_1-7-2"
height="1.9999961"
width="11.999996"
y="64"
x="54"
style="fill:#4c4c4c;fill-opacity:0.98431373;stroke:none;stroke-width:0" />
<rect
id="svg_1-7-2-2"
height="2.9999905"
width="2.9999907"
y="52"
x="80.000008"
style="fill:#4c4c4c;fill-opacity:0.98431373;stroke:none;stroke-width:0" />
<rect
style="fill:#4c4c4c;fill-opacity:0.98431373;stroke:none;stroke-width:0"
x="85.000008"
y="52"
width="2.9999907"
height="2.9999905"
id="rect4561" />
<rect
style="fill:#4c4c4c;fill-opacity:0.98431373;stroke:none;stroke-width:0"
x="80.000008"
y="58"
width="2.9999907"
height="2.9999905"
id="rect4563" />
<rect
id="rect4565"
height="2.9999905"
width="2.9999907"
y="58"
x="85.000008"
style="fill:#4c4c4c;fill-opacity:0.98431373;stroke:none;stroke-width:0" />
<rect
id="rect4567"
height="2.9999905"
width="2.9999907"
y="64"
x="80.000008"
style="fill:#4c4c4c;fill-opacity:0.98431373;stroke:none;stroke-width:0" />
<rect
style="fill:#4c4c4c;fill-opacity:0.98431373;stroke:none;stroke-width:0"
x="85.000008"
y="64"
width="2.9999907"
height="2.9999905"
id="rect4569" />
<circle
style="opacity:1;fill:none;fill-opacity:1;stroke:#4c4c4c;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
id="path4571"
cx="110.06081"
cy="57.939209"
r="4.7438836" />
<rect
style="fill:#4c4c4c;fill-opacity:0.98431373;stroke:none;stroke-width:0"
x="116.64566"
y="-31.79752"
width="4.229713"
height="6.4053884"
id="rect4563-2"
transform="matrix(0.70710678,0.70710678,-0.70710678,0.70710678,0,0)" />
<path
style="fill:#4c4c4c;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 125,56 138.77027,56.095 132,64 Z"
id="path4613"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccc" />
<path
sodipodi:nodetypes="cccc"
inkscape:connector-curvature="0"
id="path4615"
d="M 149,64 162.77027,63.905 156,56 Z"
style="fill:#4c4c4c;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
style="fill:#4c4c4c;fill-opacity:0.98431373;stroke:none;stroke-width:0"
x="54"
y="53"
width="11.999996"
height="1.9999961"
id="rect4638" />
<rect
id="svg_1-7-2-24"
height="1.9999957"
width="12.99999"
y="-56"
x="53"
style="fill:#4c4c4c;fill-opacity:0.98431373;stroke:none;stroke-width:0"
transform="matrix(0,1,-1,0,0,0)" />
<rect
transform="matrix(0,1,-1,0,0,0)"
style="fill:#4c4c4c;fill-opacity:0.98431373;stroke:none;stroke-width:0"
x="53"
y="-66"
width="12.99999"
height="1.9999957"
id="rect4657" />
<rect
id="rect4659"
height="0.99999291"
width="11.999999"
y="57"
x="54"
style="fill:#4c4c4c;fill-opacity:0.98431373;stroke:none;stroke-width:0" />
<rect
style="fill:#d3d3d3;fill-opacity:1;stroke:none;stroke-width:0;stroke-opacity:1"
x="54"
y="88.000122"
width="11.999996"
height="1.9999961"
id="rect4661" />
<rect
style="fill:#d3d3d3;fill-opacity:1;stroke:none;stroke-width:0;stroke-opacity:1"
x="80.000008"
y="76.000122"
width="2.9999907"
height="2.9999905"
id="rect4663" />
<rect
id="rect4665"
height="2.9999905"
width="2.9999907"
y="76.000122"
x="85.000008"
style="fill:#d3d3d3;fill-opacity:1;stroke:none;stroke-width:0;stroke-opacity:1" />
<rect
id="rect4667"
height="2.9999905"
width="2.9999907"
y="82.000122"
x="80.000008"
style="fill:#d3d3d3;fill-opacity:1;stroke:none;stroke-width:0;stroke-opacity:1" />
<rect
style="fill:#d3d3d3;fill-opacity:1;stroke:none;stroke-width:0;stroke-opacity:1"
x="85.000008"
y="82.000122"
width="2.9999907"
height="2.9999905"
id="rect4669" />
<rect
style="fill:#d3d3d3;fill-opacity:1;stroke:none;stroke-width:0;stroke-opacity:1"
x="80.000008"
y="88.000122"
width="2.9999907"
height="2.9999905"
id="rect4671" />
<rect
id="rect4673"
height="2.9999905"
width="2.9999907"
y="88.000122"
x="85.000008"
style="fill:#d3d3d3;fill-opacity:1;stroke:none;stroke-width:0;stroke-opacity:1" />
<circle
r="4.7438836"
cy="81.939331"
cx="110.06081"
id="circle4675"
style="opacity:1;fill:none;fill-opacity:1;stroke:#d3d3d3;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
transform="matrix(0.70710678,0.70710678,-0.70710678,0.70710678,0,0)"
id="rect4677"
height="6.4053884"
width="4.229713"
y="-14.826816"
x="133.6163"
style="fill:#d3d3d3;fill-opacity:1;stroke:#d3d3d3;stroke-width:0;stroke-opacity:1" />
<path
sodipodi:nodetypes="cccc"
inkscape:connector-curvature="0"
id="path4679"
d="m 125,80.000005 13.77027,0.09499 L 132,87.999992 Z"
style="fill:#d3d3d3;fill-opacity:1;fill-rule:evenodd;stroke:#d3d3d3;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
style="fill:#d3d3d3;fill-opacity:1;fill-rule:evenodd;stroke:#d3d3d3;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 149,88.0002 162.77027,87.9052 156,80.0002 Z"
id="path4681"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccc" />
<rect
id="rect4683"
height="1.9999961"
width="11.999996"
y="77.000122"
x="54"
style="fill:#d3d3d3;fill-opacity:1;stroke:none;stroke-width:0;stroke-opacity:1" />
<rect
transform="matrix(0,1,-1,0,0,0)"
style="fill:#d3d3d3;fill-opacity:1;stroke:none;stroke-width:0;stroke-opacity:1"
x="77.000122"
y="-56"
width="12.99999"
height="1.9999957"
id="rect4685" />
<rect
id="rect4687"
height="1.9999957"
width="12.99999"
y="-66"
x="77.000122"
style="fill:#d3d3d3;fill-opacity:1;stroke:none;stroke-width:0;stroke-opacity:1"
transform="matrix(0,1,-1,0,0,0)" />
<rect
style="fill:#d3d3d3;fill-opacity:1;stroke:none;stroke-width:0;stroke-opacity:1"
x="54"
y="81.000122"
width="11.999999"
height="0.99999291"
id="rect4689" />
<rect
id="rect4761-1"
height="1.9999945"
width="15.99999"
y="101"
x="76.000008"
style="fill:#ffffff;fill-opacity:0.80000007;stroke:none;stroke-width:0" />
<rect
id="rect4761-0"
height="1.9999945"
width="15.99999"
y="105"
x="76.000008"
style="fill:#ffffff;fill-opacity:0.80000007;stroke:none;stroke-width:0" />
<rect
id="rect4761-7"
height="1.9999945"
width="9"
y="109"
x="76.000008"
style="fill:#ffffff;fill-opacity:0.80000007;stroke:none;stroke-width:0" />
<rect
id="rect4761-1-1"
height="1.9999945"
width="12"
y="125"
x="76.000008"
style="fill:#ffffff;fill-opacity:0.80000007;stroke:none;stroke-width:0" />
<rect
id="rect4761-1-1-4"
height="1.9999945"
width="10"
y="137"
x="76.000008"
style="fill:#ffffff;fill-opacity:0.80000007;stroke:none;stroke-width:0" />
<rect
id="rect4761-1-1-4-4"
height="1.9999945"
width="10"
y="129"
x="82"
style="fill:#ffffff;fill-opacity:0.80000007;stroke:none;stroke-width:0" />
<rect
id="rect4761-1-1-4-4-3"
height="1.9999945"
width="9"
y="133"
x="82"
style="fill:#ffffff;fill-opacity:0.80000007;stroke:none;stroke-width:0" />
<path
inkscape:connector-curvature="0"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:0.8;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.66157866;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 36.398438,100.0254 c -0.423362,-0.013 -0.846847,0.01 -1.265626,0.062 -1.656562,0.2196 -3.244567,0.9739 -4.507812,2.2266 L 29,100.5991 l -2.324219,7.7129 7.826172,-1.9062 -1.804687,-1.9063 c 1.597702,-1.5308 4.048706,-1.8453 5.984375,-0.7207 1.971162,1.1452 2.881954,3.3975 2.308593,5.5508 -0.573361,2.1533 -2.533865,3.6953 -4.830078,3.6953 l 0,3.0742 c 3.550756,0 6.710442,-2.4113 7.650391,-5.9414 0.939949,-3.5301 -0.618463,-7.2736 -3.710938,-9.0703 -1.159678,-0.6738 -2.431087,-1.0231 -3.701171,-1.0625 z"
id="path4138" />
<path
inkscape:connector-curvature="0"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:0.8;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.66157866;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 59.722656,99.9629 c -1.270084,0.039 -2.541493,0.3887 -3.701172,1.0625 -3.092475,1.7967 -4.650886,5.5402 -3.710937,9.0703 0.939949,3.5301 4.09768,5.9414 7.648437,5.9414 l 0,-3.0742 c -2.296214,0 -4.256717,-1.542 -4.830078,-3.6953 -0.573361,-2.1533 0.337432,-4.4056 2.308594,-5.5508 1.935731,-1.1246 4.38863,-0.8102 5.986326,0.7207 l -1.806638,1.9063 7.828128,1.9062 -2.32422,-7.7129 -1.62696,1.7168 c -1.26338,-1.2531 -2.848917,-2.0088 -4.505855,-2.2285 -0.418778,-0.055 -0.842263,-0.076 -1.265625,-0.062 z"
id="path4138-1" />
<path
inkscape:connector-curvature="0"
style="opacity:0.8;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1.966;stroke-miterlimit:4;stroke-dasharray:none"
d="m 10.5,100 0,2 -2.4999996,0 L 12,107 l 4,-5 -2.5,0 0,-2 -3,0 z"
id="path3055-0-77" />
<path
style="opacity:0.8;fill:none;stroke:#ffffff;stroke-width:1.966;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 4.9850574,108.015 14.0298856,-0.03"
id="path5244-5-0-5"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cc" />
<path
style="opacity:0.8;fill:none;stroke:#ffffff;stroke-width:1.966;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 4.9849874,132.015 14.0298866,-0.03"
id="path5244-5-0-5-8"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cc" />
<path
inkscape:connector-curvature="0"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:0.4;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#4d4d4d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.66157866;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 36.398438,123.9629 c -0.423362,-0.013 -0.846847,0.01 -1.265626,0.062 -1.656562,0.2196 -3.244567,0.9739 -4.507812,2.2266 L 29,124.5366 l -2.324219,7.7129 7.826172,-1.9062 -1.804687,-1.9063 c 1.597702,-1.5308 4.048706,-1.8453 5.984375,-0.7207 1.971162,1.1453 2.881954,3.3975 2.308593,5.5508 -0.573361,2.1533 -2.533864,3.6953 -4.830078,3.6953 l 0,3.0742 c 3.550757,0 6.710442,-2.4093 7.650391,-5.9394 0.939949,-3.5301 -0.618463,-7.2756 -3.710938,-9.0723 -1.159678,-0.6737 -2.431087,-1.0231 -3.701171,-1.0625 z"
id="path4138-12" />
<path
inkscape:connector-curvature="0"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:0.4;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#4d4d4d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.66157866;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 59.722656,123.9629 c -1.270084,0.039 -2.541493,0.3888 -3.701172,1.0625 -3.092475,1.7967 -4.650886,5.5422 -3.710937,9.0723 0.939949,3.5301 4.09768,5.9394 7.648437,5.9394 l 0,-3.0742 c -2.296214,0 -4.256717,-1.542 -4.830078,-3.6953 -0.573361,-2.1533 0.337432,-4.4055 2.308594,-5.5508 1.935731,-1.1246 4.38863,-0.8102 5.986326,0.7207 l -1.806638,1.9063 7.828128,1.9062 -2.32422,-7.7129 -1.62696,1.7168 c -1.26338,-1.2531 -2.848917,-2.0088 -4.505855,-2.2285 -0.418778,-0.055 -0.842263,-0.076 -1.265625,-0.062 z"
id="path4138-1-3" />
<path
id="path6191"
d="m 10.5,116 0,-2 -2.4999996,0 L 12,109 l 4,5 -2.5,0 0,2 -3,0 z"
style="opacity:0.8;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1.966;stroke-miterlimit:4;stroke-dasharray:none"
inkscape:connector-curvature="0" />
<path
inkscape:connector-curvature="0"
style="opacity:0.8;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1.966;stroke-miterlimit:4;stroke-dasharray:none"
d="m 10.5,129 0,-2 -2.4999996,0 L 12,122 l 4,5 -2.5,0 0,2 -3,0 z"
id="path6193" />
<path
id="path6195"
d="m 10.5,135 0,2 -2.4999996,0 L 12,142 l 4,-5 -2.5,0 0,-2 -3,0 z"
style="opacity:0.8;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1.966;stroke-miterlimit:4;stroke-dasharray:none"
inkscape:connector-curvature="0" />
<path
sodipodi:type="star"
style="fill:#4d4d4d;fill-opacity:0.90196078;stroke:#d3d3d3;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
id="path4500"
sodipodi:sides="3"
sodipodi:cx="11.55581"
sodipodi:cy="60.073242"
sodipodi:r1="5.1116104"
sodipodi:r2="2.5558052"
sodipodi:arg1="0"
sodipodi:arg2="1.0471976"
inkscape:flatsided="false"
inkscape:rounded="0"
inkscape:randomized="0"
d="m 16.66742,60.073242 -3.833708,2.213392 -3.8337072,2.213393 0,-4.426785 0,-4.426784 3.8337082,2.213392 z"
inkscape:transform-center-x="-1.2779026" />
<path
inkscape:transform-center-x="1.277902"
d="m -31.500004,60.073242 -3.833708,2.213392 -3.833707,2.213393 0,-4.426785 0,-4.426784 3.833707,2.213392 z"
inkscape:randomized="0"
inkscape:rounded="0"
inkscape:flatsided="false"
sodipodi:arg2="1.0471976"
sodipodi:arg1="0"
sodipodi:r2="2.5558052"
sodipodi:r1="5.1116104"
sodipodi:cy="60.073242"
sodipodi:cx="-36.611614"
sodipodi:sides="3"
id="path4502"
style="fill:#4d4d4d;fill-opacity:0.90196078;stroke:#d3d3d3;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
sodipodi:type="star"
transform="scale(-1,1)" />
<path
d="m 16.66742,60.073212 -3.833708,2.213392 -3.8337072,2.213392 0,-4.426784 0,-4.426785 3.8337082,2.213392 z"
inkscape:randomized="0"
inkscape:rounded="0"
inkscape:flatsided="false"
sodipodi:arg2="1.0471976"
sodipodi:arg1="0"
sodipodi:r2="2.5558052"
sodipodi:r1="5.1116104"
sodipodi:cy="60.073212"
sodipodi:cx="11.55581"
sodipodi:sides="3"
id="path4504"
style="fill:#4d4d4d;fill-opacity:0.90196078;stroke:#d3d3d3;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
sodipodi:type="star"
transform="matrix(0,1,-1,0,72.0074,71.7877)"
inkscape:transform-center-y="1.2779029" />
<path
inkscape:transform-center-y="-1.2779026"
transform="matrix(0,-1,-1,0,96,96)"
sodipodi:type="star"
style="fill:#4d4d4d;fill-opacity:0.90196078;stroke:#d3d3d3;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
id="path4506"
sodipodi:sides="3"
sodipodi:cx="11.55581"
sodipodi:cy="60.073212"
sodipodi:r1="5.1116104"
sodipodi:r2="2.5558052"
sodipodi:arg1="0"
sodipodi:arg2="1.0471976"
inkscape:flatsided="false"
inkscape:rounded="0"
inkscape:randomized="0"
d="m 16.66742,60.073212 -3.833708,2.213392 -3.8337072,2.213392 0,-4.426784 0,-4.426785 3.8337082,2.213392 z" />
<path
sodipodi:nodetypes="cccc"
inkscape:connector-curvature="0"
id="path4615-5"
d="m 171.82574,65.174193 16.34854,0 -8.17427,-13.348454 z"
style="fill:#fbb917;fill-opacity:1;fill-rule:evenodd;stroke:#fbb917;stroke-width:1.65161395;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 179,55 0,6 2,0 0,-6"
id="path4300"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccc" />
<path
style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 179,62 0,2 2,0 0,-2"
id="path4300-6"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccc" />
</svg>

After

Width:  |  Height:  |  Size: 35 KiB

File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+929
View File
@@ -0,0 +1,929 @@
/* reset styling (prevent conflicts with bootstrap, materialize.css, etc.) */
div.jsoneditor .jsoneditor-search input {
height: auto;
border: inherit;
}
div.jsoneditor .jsoneditor-search input:focus {
border: none !important;
box-shadow: none !important;
}
div.jsoneditor table {
border-collapse: collapse;
width: auto;
}
div.jsoneditor td,
div.jsoneditor th {
padding: 0;
display: table-cell;
text-align: left;
vertical-align: inherit;
border-radius: inherit;
}
div.jsoneditor-field,
div.jsoneditor-value,
div.jsoneditor-readonly {
border: 1px solid transparent;
min-height: 16px;
min-width: 32px;
padding: 2px;
margin: 1px;
word-wrap: break-word;
float: left;
}
/* adjust margin of p elements inside editable divs, needed for Opera, IE */
div.jsoneditor-field p,
div.jsoneditor-value p {
margin: 0;
}
div.jsoneditor-value {
word-break: break-word;
}
div.jsoneditor-readonly {
min-width: 16px;
color: gray;
}
div.jsoneditor-empty {
border-color: lightgray;
border-style: dashed;
border-radius: 2px;
}
div.jsoneditor-field.jsoneditor-empty::after,
div.jsoneditor-value.jsoneditor-empty::after {
pointer-events: none;
color: lightgray;
font-size: 8pt;
}
div.jsoneditor-field.jsoneditor-empty::after {
content: "field";
}
div.jsoneditor-value.jsoneditor-empty::after {
content: "value";
}
div.jsoneditor-value.jsoneditor-url,
a.jsoneditor-value.jsoneditor-url {
color: green;
text-decoration: underline;
}
a.jsoneditor-value.jsoneditor-url {
display: inline-block;
padding: 2px;
margin: 2px;
}
a.jsoneditor-value.jsoneditor-url:hover,
a.jsoneditor-value.jsoneditor-url:focus {
color: #ee422e;
}
div.jsoneditor td.jsoneditor-separator {
padding: 3px 0;
vertical-align: top;
color: gray;
}
div.jsoneditor-field[contenteditable=true]:focus,
div.jsoneditor-field[contenteditable=true]:hover,
div.jsoneditor-value[contenteditable=true]:focus,
div.jsoneditor-value[contenteditable=true]:hover,
div.jsoneditor-field.jsoneditor-highlight,
div.jsoneditor-value.jsoneditor-highlight {
background-color: #FFFFAB;
border: 1px solid yellow;
border-radius: 2px;
}
div.jsoneditor-field.jsoneditor-highlight-active,
div.jsoneditor-field.jsoneditor-highlight-active:focus,
div.jsoneditor-field.jsoneditor-highlight-active:hover,
div.jsoneditor-value.jsoneditor-highlight-active,
div.jsoneditor-value.jsoneditor-highlight-active:focus,
div.jsoneditor-value.jsoneditor-highlight-active:hover {
background-color: #ffee00;
border: 1px solid #ffc700;
border-radius: 2px;
}
div.jsoneditor-value.jsoneditor-string {
color: #008000;
}
div.jsoneditor-value.jsoneditor-object,
div.jsoneditor-value.jsoneditor-array {
min-width: 16px;
color: #808080;
}
div.jsoneditor-value.jsoneditor-number {
color: #ee422e;
}
div.jsoneditor-value.jsoneditor-boolean {
color: #ff8c00;
}
div.jsoneditor-value.jsoneditor-null {
color: #004ED0;
}
div.jsoneditor-value.jsoneditor-invalid {
color: #000000;
}
div.jsoneditor-tree button {
width: 24px;
height: 24px;
padding: 0;
margin: 0;
border: none;
cursor: pointer;
background: transparent url("img/jsoneditor-icons.svg");
}
div.jsoneditor-mode-view tr.jsoneditor-expandable td.jsoneditor-tree,
div.jsoneditor-mode-form tr.jsoneditor-expandable td.jsoneditor-tree {
cursor: pointer;
}
div.jsoneditor-tree button.jsoneditor-collapsed {
background-position: 0 -48px;
}
div.jsoneditor-tree button.jsoneditor-expanded {
background-position: 0 -72px;
}
div.jsoneditor-tree button.jsoneditor-contextmenu {
background-position: -48px -72px;
}
div.jsoneditor-tree button.jsoneditor-contextmenu:hover,
div.jsoneditor-tree button.jsoneditor-contextmenu:focus,
div.jsoneditor-tree button.jsoneditor-contextmenu.jsoneditor-selected,
tr.jsoneditor-selected.jsoneditor-first button.jsoneditor-contextmenu {
background-position: -48px -48px;
}
div.jsoneditor-tree *:focus {
outline: none;
}
div.jsoneditor-tree button:focus {
/* TODO: nice outline for buttons with focus
outline: #97B0F8 solid 2px;
box-shadow: 0 0 8px #97B0F8;
*/
background-color: #f5f5f5;
outline: #e5e5e5 solid 1px;
}
div.jsoneditor-tree button.jsoneditor-invisible {
visibility: hidden;
background: none;
}
div.jsoneditor {
color: #1A1A1A;
border: 1px solid #3883fa;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
width: 100%;
height: 100%;
position: relative;
padding: 0;
line-height: 100%;
}
div.jsoneditor-tree table.jsoneditor-tree {
border-collapse: collapse;
border-spacing: 0;
width: 100%;
margin: 0;
}
div.jsoneditor-outer {
position: static;
width: 100%;
height: 100%;
margin: -35px 0 0 0;
padding: 35px 0 0 0;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
textarea.jsoneditor-text,
.ace-jsoneditor {
min-height: 150px;
}
div.jsoneditor-tree {
width: 100%;
height: 100%;
position: relative;
overflow: auto;
}
textarea.jsoneditor-text {
width: 100%;
height: 100%;
margin: 0;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
outline-width: 0;
border: none;
background-color: white;
resize: none;
}
tr.jsoneditor-highlight,
tr.jsoneditor-selected {
background-color: #e6e6e6;
}
tr.jsoneditor-selected button.jsoneditor-dragarea,
tr.jsoneditor-selected button.jsoneditor-contextmenu {
visibility: hidden;
}
tr.jsoneditor-selected.jsoneditor-first button.jsoneditor-dragarea,
tr.jsoneditor-selected.jsoneditor-first button.jsoneditor-contextmenu {
visibility: visible;
}
div.jsoneditor-tree button.jsoneditor-dragarea {
background: url("img/jsoneditor-icons.svg") -72px -72px;
cursor: move;
}
div.jsoneditor-tree button.jsoneditor-dragarea:hover,
div.jsoneditor-tree button.jsoneditor-dragarea:focus,
tr.jsoneditor-selected.jsoneditor-first button.jsoneditor-dragarea {
background-position: -72px -48px;
}
div.jsoneditor tr,
div.jsoneditor th,
div.jsoneditor td {
padding: 0;
margin: 0;
}
div.jsoneditor td {
vertical-align: top;
}
div.jsoneditor td.jsoneditor-tree {
vertical-align: top;
}
div.jsoneditor-field,
div.jsoneditor-value,
div.jsoneditor td,
div.jsoneditor th,
div.jsoneditor textarea,
.jsoneditor-schema-error {
font-family: droid sans mono, consolas, monospace, courier new, courier, sans-serif;
font-size: 10pt;
color: #1A1A1A;
}
/* popover */
.jsoneditor-schema-error {
cursor: default;
display: inline-block;
/*font-family: arial, sans-serif;*/
height: 24px;
line-height: 24px;
position: relative;
text-align: center;
width: 24px;
}
div.jsoneditor-tree .jsoneditor-schema-error {
width: 24px;
height: 24px;
padding: 0;
margin: 0 4px 0 0;
background: url("img/jsoneditor-icons.svg") -168px -48px;
}
.jsoneditor-schema-error .jsoneditor-popover {
background-color: #4c4c4c;
border-radius: 3px;
box-shadow: 0 0 5px rgba(0,0,0,0.4);
color: #fff;
display: none;
padding: 7px 10px;
position: absolute;
width: 200px;
z-index: 4;
}
.jsoneditor-schema-error .jsoneditor-popover.jsoneditor-above {
bottom: 32px;
left: -98px;
}
.jsoneditor-schema-error .jsoneditor-popover.jsoneditor-below {
top: 32px;
left: -98px;
}
.jsoneditor-schema-error .jsoneditor-popover.jsoneditor-left {
top: -7px;
right: 32px;
}
.jsoneditor-schema-error .jsoneditor-popover.jsoneditor-right {
top: -7px;
left: 32px;
}
.jsoneditor-schema-error .jsoneditor-popover:before {
border-right: 7px solid transparent;
border-left: 7px solid transparent;
content: '';
display: block;
left: 50%;
margin-left: -7px;
position: absolute;
}
.jsoneditor-schema-error .jsoneditor-popover.jsoneditor-above:before {
border-top: 7px solid #4c4c4c;
bottom: -7px;
}
.jsoneditor-schema-error .jsoneditor-popover.jsoneditor-below:before {
border-bottom: 7px solid #4c4c4c;
top: -7px;
}
.jsoneditor-schema-error .jsoneditor-popover.jsoneditor-left:before {
border-left: 7px solid #4c4c4c;
border-top: 7px solid transparent;
border-bottom: 7px solid transparent;
content: '';
top: 19px;
right: -14px;
left: inherit;
margin-left: inherit;
margin-top: -7px;
position: absolute;
}
.jsoneditor-schema-error .jsoneditor-popover.jsoneditor-right:before {
border-right: 7px solid #4c4c4c;
border-top: 7px solid transparent;
border-bottom: 7px solid transparent;
content: '';
top: 19px;
left: -14px;
margin-left: inherit;
margin-top: -7px;
position: absolute;
}
.jsoneditor-schema-error:hover .jsoneditor-popover,
.jsoneditor-schema-error:focus .jsoneditor-popover {
display: block;
-webkit-animation: fade-in .3s linear 1, move-up .3s linear 1;
-moz-animation: fade-in .3s linear 1, move-up .3s linear 1;
-ms-animation: fade-in .3s linear 1, move-up .3s linear 1;
}
@-webkit-keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@-moz-keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@-ms-keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
/*@-webkit-keyframes move-up {*/
/*from { bottom: 24px; }*/
/*to { bottom: 32px; }*/
/*}*/
/*@-moz-keyframes move-up {*/
/*from { bottom: 24px; }*/
/*to { bottom: 32px; }*/
/*}*/
/*@-ms-keyframes move-up {*/
/*from { bottom: 24px; }*/
/*to { bottom: 32px; }*/
/*}*/
/* JSON schema errors displayed at the bottom of the editor in mode text and code */
.jsoneditor .jsoneditor-text-errors {
width: 100%;
border-collapse: collapse;
background-color: #ffef8b;
border-top: 1px solid #ffd700;
}
.jsoneditor .jsoneditor-text-errors td {
padding: 3px 6px;
vertical-align: middle;
}
.jsoneditor-text-errors .jsoneditor-schema-error {
border: none;
width: 24px;
height: 24px;
padding: 0;
margin: 0 4px 0 0;
background: url("img/jsoneditor-icons.svg") -168px -48px;
}
/* ContextMenu - main menu */
div.jsoneditor-contextmenu-root {
position: relative;
width: 0;
height: 0;
}
div.jsoneditor-contextmenu {
position: absolute;
box-sizing: content-box;
z-index: 99999;
}
div.jsoneditor-contextmenu ul,
div.jsoneditor-contextmenu li {
box-sizing: content-box;
}
div.jsoneditor-contextmenu ul {
position: relative;
left: 0;
top: 0;
width: 124px;
background: white;
border: 1px solid #d3d3d3;
box-shadow: 2px 2px 12px rgba(128, 128, 128, 0.3);
list-style: none;
margin: 0;
padding: 0;
}
div.jsoneditor-contextmenu ul li button {
padding: 0;
margin: 0;
width: 124px;
height: 24px;
border: none;
cursor: pointer;
color: #4d4d4d;
background: transparent;
font-size: 10pt;
font-family: arial, sans-serif;
box-sizing: border-box;
line-height: 26px;
text-align: left;
}
/* Fix button padding in firefox */
div.jsoneditor-contextmenu ul li button::-moz-focus-inner {
padding: 0;
border: 0;
}
div.jsoneditor-contextmenu ul li button:hover,
div.jsoneditor-contextmenu ul li button:focus {
color: #1a1a1a;
background-color: #f5f5f5;
outline: none;
}
div.jsoneditor-contextmenu ul li button.jsoneditor-default {
width: 92px;
}
div.jsoneditor-contextmenu ul li button.jsoneditor-expand {
float: right;
width: 32px;
height: 24px;
border-left: 1px solid #e5e5e5;
}
div.jsoneditor-contextmenu div.jsoneditor-icon {
float: left;
width: 24px;
height: 24px;
border: none;
padding: 0;
margin: 0;
background-image: url("img/jsoneditor-icons.svg");
}
div.jsoneditor-contextmenu ul li button div.jsoneditor-expand {
float: right;
width: 24px;
height: 24px;
padding: 0;
margin: 0 4px 0 0;
background: url("img/jsoneditor-icons.svg") 0 -72px;
opacity: 0.4;
}
div.jsoneditor-contextmenu ul li button:hover div.jsoneditor-expand,
div.jsoneditor-contextmenu ul li button:focus div.jsoneditor-expand,
div.jsoneditor-contextmenu ul li.jsoneditor-selected div.jsoneditor-expand,
div.jsoneditor-contextmenu ul li button.jsoneditor-expand:hover div.jsoneditor-expand,
div.jsoneditor-contextmenu ul li button.jsoneditor-expand:focus div.jsoneditor-expand {
opacity: 1;
}
div.jsoneditor-contextmenu div.jsoneditor-separator {
height: 0;
border-top: 1px solid #e5e5e5;
padding-top: 5px;
margin-top: 5px;
}
div.jsoneditor-contextmenu button.jsoneditor-remove > div.jsoneditor-icon {
background-position: -24px -24px;
}
div.jsoneditor-contextmenu button.jsoneditor-remove:hover > div.jsoneditor-icon,
div.jsoneditor-contextmenu button.jsoneditor-remove:focus > div.jsoneditor-icon {
background-position: -24px 0;
}
div.jsoneditor-contextmenu button.jsoneditor-append > div.jsoneditor-icon {
background-position: 0 -24px;
}
div.jsoneditor-contextmenu button.jsoneditor-append:hover > div.jsoneditor-icon,
div.jsoneditor-contextmenu button.jsoneditor-append:focus > div.jsoneditor-icon {
background-position: 0 0;
}
div.jsoneditor-contextmenu button.jsoneditor-insert > div.jsoneditor-icon {
background-position: 0 -24px;
}
div.jsoneditor-contextmenu button.jsoneditor-insert:hover > div.jsoneditor-icon,
div.jsoneditor-contextmenu button.jsoneditor-insert:focus > div.jsoneditor-icon {
background-position: 0 0;
}
div.jsoneditor-contextmenu button.jsoneditor-duplicate > div.jsoneditor-icon {
background-position: -48px -24px;
}
div.jsoneditor-contextmenu button.jsoneditor-duplicate:hover > div.jsoneditor-icon,
div.jsoneditor-contextmenu button.jsoneditor-duplicate:focus > div.jsoneditor-icon {
background-position: -48px 0;
}
div.jsoneditor-contextmenu button.jsoneditor-sort-asc > div.jsoneditor-icon {
background-position: -168px -24px;
}
div.jsoneditor-contextmenu button.jsoneditor-sort-asc:hover > div.jsoneditor-icon,
div.jsoneditor-contextmenu button.jsoneditor-sort-asc:focus > div.jsoneditor-icon {
background-position: -168px 0;
}
div.jsoneditor-contextmenu button.jsoneditor-sort-desc > div.jsoneditor-icon {
background-position: -192px -24px;
}
div.jsoneditor-contextmenu button.jsoneditor-sort-desc:hover > div.jsoneditor-icon,
div.jsoneditor-contextmenu button.jsoneditor-sort-desc:focus > div.jsoneditor-icon {
background-position: -192px 0;
}
/* ContextMenu - sub menu */
div.jsoneditor-contextmenu ul li button.jsoneditor-selected,
div.jsoneditor-contextmenu ul li button.jsoneditor-selected:hover,
div.jsoneditor-contextmenu ul li button.jsoneditor-selected:focus {
color: white;
background-color: #ee422e;
}
div.jsoneditor-contextmenu ul li {
overflow: hidden;
}
div.jsoneditor-contextmenu ul li ul {
display: none;
position: relative;
left: -10px;
top: 0;
border: none;
box-shadow: inset 0 0 10px rgba(128, 128, 128, 0.5);
padding: 0 10px;
/* TODO: transition is not supported on IE8-9 */
-webkit-transition: all 0.3s ease-out;
-moz-transition: all 0.3s ease-out;
-o-transition: all 0.3s ease-out;
transition: all 0.3s ease-out;
}
div.jsoneditor-contextmenu ul li ul li button {
padding-left: 24px;
animation: all ease-in-out 1s;
}
div.jsoneditor-contextmenu ul li ul li button:hover,
div.jsoneditor-contextmenu ul li ul li button:focus {
background-color: #f5f5f5;
}
div.jsoneditor-contextmenu button.jsoneditor-type-string > div.jsoneditor-icon {
background-position: -144px -24px;
}
div.jsoneditor-contextmenu button.jsoneditor-type-string:hover > div.jsoneditor-icon,
div.jsoneditor-contextmenu button.jsoneditor-type-string:focus > div.jsoneditor-icon,
div.jsoneditor-contextmenu button.jsoneditor-type-string.jsoneditor-selected > div.jsoneditor-icon {
background-position: -144px 0;
}
div.jsoneditor-contextmenu button.jsoneditor-type-auto > div.jsoneditor-icon {
background-position: -120px -24px;
}
div.jsoneditor-contextmenu button.jsoneditor-type-auto:hover > div.jsoneditor-icon,
div.jsoneditor-contextmenu button.jsoneditor-type-auto:focus > div.jsoneditor-icon,
div.jsoneditor-contextmenu button.jsoneditor-type-auto.jsoneditor-selected > div.jsoneditor-icon {
background-position: -120px 0;
}
div.jsoneditor-contextmenu button.jsoneditor-type-object > div.jsoneditor-icon {
background-position: -72px -24px;
}
div.jsoneditor-contextmenu button.jsoneditor-type-object:hover > div.jsoneditor-icon,
div.jsoneditor-contextmenu button.jsoneditor-type-object:focus > div.jsoneditor-icon,
div.jsoneditor-contextmenu button.jsoneditor-type-object.jsoneditor-selected > div.jsoneditor-icon {
background-position: -72px 0;
}
div.jsoneditor-contextmenu button.jsoneditor-type-array > div.jsoneditor-icon {
background-position: -96px -24px;
}
div.jsoneditor-contextmenu button.jsoneditor-type-array:hover > div.jsoneditor-icon,
div.jsoneditor-contextmenu button.jsoneditor-type-array:focus > div.jsoneditor-icon,
div.jsoneditor-contextmenu button.jsoneditor-type-array.jsoneditor-selected > div.jsoneditor-icon {
background-position: -96px 0;
}
div.jsoneditor-contextmenu button.jsoneditor-type-modes > div.jsoneditor-icon {
background-image: none;
width: 6px;
}
div.jsoneditor-menu {
width: 100%;
height: 35px;
padding: 2px;
margin: 0;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
color: white;
background-color: #3883fa;
border-bottom: 1px solid #3883fa;
}
div.jsoneditor-menu > button,
div.jsoneditor-menu > div.jsoneditor-modes > button {
width: 26px;
height: 26px;
margin: 2px;
padding: 0;
border-radius: 2px;
border: 1px solid transparent;
background: transparent url("img/jsoneditor-icons.svg");
color: white;
opacity: 0.8;
font-family: arial, sans-serif;
font-size: 10pt;
float: left;
}
div.jsoneditor-menu > button:hover,
div.jsoneditor-menu > div.jsoneditor-modes > button:hover {
background-color: rgba(255,255,255,0.2);
border: 1px solid rgba(255,255,255,0.4);
}
div.jsoneditor-menu > button:focus,
div.jsoneditor-menu > button:active,
div.jsoneditor-menu > div.jsoneditor-modes > button:focus,
div.jsoneditor-menu > div.jsoneditor-modes > button:active {
background-color: rgba(255,255,255,0.3);
}
div.jsoneditor-menu > button:disabled,
div.jsoneditor-menu > div.jsoneditor-modes > button:disabled {
opacity: 0.5;
}
div.jsoneditor-menu > button.jsoneditor-collapse-all {
background-position: 0 -96px;
}
div.jsoneditor-menu > button.jsoneditor-expand-all {
background-position: 0 -120px;
}
div.jsoneditor-menu > button.jsoneditor-undo {
background-position: -24px -96px;
}
div.jsoneditor-menu > button.jsoneditor-undo:disabled {
background-position: -24px -120px;
}
div.jsoneditor-menu > button.jsoneditor-redo {
background-position: -48px -96px;
}
div.jsoneditor-menu > button.jsoneditor-redo:disabled {
background-position: -48px -120px;
}
div.jsoneditor-menu > button.jsoneditor-compact {
background-position: -72px -96px;
}
div.jsoneditor-menu > button.jsoneditor-format {
background-position: -72px -120px;
}
div.jsoneditor-menu > div.jsoneditor-modes {
display: inline-block;
float: left;
}
div.jsoneditor-menu > div.jsoneditor-modes > button {
background-image: none;
width: auto;
padding-left: 6px;
padding-right: 6px;
}
div.jsoneditor-menu > button.jsoneditor-separator,
div.jsoneditor-menu > div.jsoneditor-modes > button.jsoneditor-separator {
margin-left: 10px;
}
div.jsoneditor-menu a {
font-family: arial, sans-serif;
font-size: 10pt;
color: white;
opacity: 0.8;
vertical-align: middle;
}
div.jsoneditor-menu a:hover {
opacity: 1;
}
div.jsoneditor-menu a.jsoneditor-poweredBy {
font-size: 8pt;
position: absolute;
right: 0;
top: 0;
padding: 10px;
}
table.jsoneditor-search input,
table.jsoneditor-search div.jsoneditor-results {
font-family: arial, sans-serif;
font-size: 10pt;
color: #1A1A1A;
background: transparent;
/* For Firefox */
}
table.jsoneditor-search div.jsoneditor-results {
color: white;
padding-right: 5px;
line-height: 24px;
}
table.jsoneditor-search {
position: absolute;
right: 4px;
top: 4px;
border-collapse: collapse;
border-spacing: 0;
}
table.jsoneditor-search div.jsoneditor-frame {
border: 1px solid transparent;
background-color: white;
padding: 0 2px;
margin: 0;
}
table.jsoneditor-search div.jsoneditor-frame table {
border-collapse: collapse;
}
table.jsoneditor-search input {
width: 120px;
border: none;
outline: none;
margin: 1px;
line-height: 20px;
}
table.jsoneditor-search button {
width: 16px;
height: 24px;
padding: 0;
margin: 0;
border: none;
background: url("img/jsoneditor-icons.svg");
vertical-align: top;
}
table.jsoneditor-search button:hover {
background-color: transparent;
}
table.jsoneditor-search button.jsoneditor-refresh {
width: 18px;
background-position: -99px -73px;
}
table.jsoneditor-search button.jsoneditor-next {
cursor: pointer;
background-position: -124px -73px;
}
table.jsoneditor-search button.jsoneditor-next:hover {
background-position: -124px -49px;
}
table.jsoneditor-search button.jsoneditor-previous {
cursor: pointer;
background-position: -148px -73px;
margin-right: 2px;
}
table.jsoneditor-search button.jsoneditor-previous:hover {
background-position: -148px -49px;
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,86 @@
<div class="layui-tab layui-tab-brief">
<ul class="layui-tab-title site-demo-title">
<li class="layui-this add-client-title"></li>
</ul>
<div class="main-content">
<div class="layui-form-item">
<label class="layui-form-label i18n-client-name"></label>
<div class="layui-input-inline">
<input type="text" name="name" autocomplete="off" placeholder="" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label i18n-client-key"></label>
<div class="layui-input-inline">
<input type="text" name="clientKey" autocomplete="off" placeholder="" class="layui-input">
</div>
<div class="layui-form-mid"><a class="layui-btn layui-btn-mini random-key random"></a></div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button class="layui-btn add"></button>
</div>
</div>
</div>
</div>
<script>
$(".add-client-title").html($.i18n.prop('client.add'));
$(".i18n-client-name").html($.i18n.prop('client.name'));
$(".i18n-client-key").html($.i18n.prop('client.key'));
$("input[name='name']").attr("placeholder", $.i18n.prop('client.name.placeholder'));
$("input[name='clientKey']").attr("placeholder", $.i18n.prop('client.key.placeholder'));
$(".random").html($.i18n.prop('client.randomkey'));
$(".add").html($.i18n.prop('public.submit'));
$(".random").click(function(){
$("input[name='clientKey']").val(uuid());
});
$(".add").click(function(){
var name = $("input[name='name']").val();
if(name == ""){
layer.alert($.i18n.prop('client.notice.inputname'), {
title: $.i18n.prop('public.tips')
});
return;
}
var clientKey = $("input[name='clientKey']").val();
if(clientKey == ""){
layer.alert($.i18n.prop('client.notice.inputkey'), {
title: $.i18n.prop('public.tips')
});
return;
}
clientList.push({
name:name,
clientKey:clientKey,
proxyMappings:[]
});
api_invoke("/config/update", clientList, function(data) {
if (data.code == 20000) {
layer.alert($.i18n.prop('client.notice.addsuccess'), {title: $.i18n.prop('public.tips')}, function(index){
layer.close(index);
location.reload();
});
}
});
});
$(".back").click(function(){
load_page("html/client/list.html");
});
function uuid() {
var s = [];
var hexDigits = "0123456789abcdef";
for (var i = 0; i < 36; i++) {
s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
}
s[14] = "4"; // bits 12-15 of the time_hi_and_version field to 0010
s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1); // bits 6-7 of the clock_seq_hi_and_reserved to 01
s[8] = s[13] = s[18] = s[23] = "";
var uuid = s.join("");
return uuid;
}
</script>
@@ -0,0 +1,73 @@
<div class="layui-tab layui-tab-brief">
<ul class="layui-tab-title site-demo-title">
<li class="layui-this edit-client-title"></li>
</ul>
<div class="main-content">
<div class="layui-form-item">
<label class="layui-form-label i18n-client-name"></label>
<div class="layui-input-inline">
<input type="text" name="name" autocomplete="off" placeholder="" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label i18n-client-key"></label>
<div class="layui-input-inline">
<input type="text" name="clientKey" autocomplete="off" placeholder="" class="layui-input">
</div>
<div class="layui-form-mid"><a class="layui-btn layui-btn-mini random-key random"></a></div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button class="layui-btn update"></button>
<button class="layui-btn layui-btn-primary back"></button>
</div>
</div>
</div>
</div>
<script>
$(".edit-client-title").html($.i18n.prop('client.edit'));
$(".i18n-client-name").html($.i18n.prop('client.name'));
$(".i18n-client-key").html($.i18n.prop('client.key'));
$("input[name='name']").attr("placeholder", $.i18n.prop('client.name.placeholder'));
$("input[name='clientKey']").attr("placeholder", $.i18n.prop('client.key.placeholder'));
$(".random").html($.i18n.prop('client.randomkey'));
$(".update").html($.i18n.prop('public.submit'));
$(".back").html($.i18n.prop('public.back'));
$("input[name='name']").val(clientList[clientIndex].name);
$("input[name='clientKey']").val(clientList[clientIndex].clientKey);
$(".random").click(function(){
$("input[name='clientKey']").val(uuid());
});
$(".update").click(function(){
clientList[clientIndex]['name'] = $("input[name='name']").val();
clientList[clientIndex]['clientKey'] = $("input[name='clientKey']").val();
api_invoke("/config/update", clientList, function(data) {
if (data.code == 20000) {
layer.alert($.i18n.prop('public.notice.updatesuccess'), {title: $.i18n.prop('public.tips')}, function(index){
layer.close(index);
location.reload();
});
}
});
});
$(".back").click(function(){
load_page("html/client/list.html");
});
function uuid() {
var s = [];
var hexDigits = "0123456789abcdef";
for (var i = 0; i < 36; i++) {
s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
}
s[14] = "4"; // bits 12-15 of the time_hi_and_version field to 0010
s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1); // bits 6-7 of the clock_seq_hi_and_reserved to 01
s[8] = s[13] = s[18] = s[23] = "";
var uuid = s.join("");
return uuid;
}
</script>
@@ -0,0 +1,75 @@
<div class="layui-tab layui-tab-brief">
<ul class="layui-tab-title site-demo-title">
<li class="layui-this detail-page"></li>
</ul>
<div class="main-content"></div>
</div>
<script>
$(".layui-this.detail-page").html($.i18n.prop('client.list'));
window.clientList = [];
api_invoke("/config/detail", {}, function(data) {
if (data.code == 20000) {
clientList = data.data;
var html = template($("#client-tpl").html(), data);
$(".main-content").html(html);
$(".mapping-config").click(function() {
window.clientIndex = $(this).attr("data-index");
load_page("html/lan/list.html");
});
$(".client-edit").click(function() {
window.clientIndex = $(this).attr("data-index");
load_page("html/client/edit.html");
});
$(".client-delete").click(function() {
window.clientIndex = $(this).attr("data-index");
layer.confirm($.i18n.prop('public.confirm.delete'), {
title: $.i18n.prop('public.tips'),
btn : [ $.i18n.prop('public.ok'), $.i18n.prop('public.cancel') ]
}, function(i) {
layer.close(i);
clientList.splice(clientIndex, 1);
api_invoke("/config/update", clientList, function(data) {
if (data.code != 20000) {
layer.alert(data.message);
} else {
location.reload();
}
})
});
});
} else {
alert(data.message);
}
});
</script>
<script id="client-tpl" type="text/html">
<table class="layui-table" lay-skin="line">
<thead>
<tr>
<th class="th-client-name"><%:=$.i18n.prop('client.name')%></th>
<th class="th-client-key"><%:=$.i18n.prop('client.key')%></th>
<th class="th-status"><%:=$.i18n.prop('client.status')%></th>
<th class="th-options"><%:=$.i18n.prop('public.options')%></th>
</tr>
</thead>
<tbody>
<%for(var i = 0; i < data.length; i++) {%>
<tr>
<td><%:=data[i].name%></td>
<td><%:=data[i].clientKey%></td>
<td>
<% if(data[i].status == 1){ %>
<span class="layui-badge layui-bg-green"><%:=$.i18n.prop('client.status.online')%></span>
<% } else { %>
<span class="layui-badge layui-bg-gray"><%:=$.i18n.prop('client.status.offline')%></span>
<% }%>
</td>
<td>
<a data-index="<%:=i%>" class="layui-btn layui-btn-mini client-edit"><%:=$.i18n.prop('public.edit')%></a>
<a data-index="<%:=i%>" class="layui-btn layui-btn-danger layui-btn-mini client-delete"><%:=$.i18n.prop('public.delete')%></a>
</td>
</tr>
<%}%>
</tbody>
</table>
</script>
@@ -0,0 +1,86 @@
<div class="layui-tab layui-tab-brief">
<ul class="layui-tab-title site-demo-title">
<li class="layui-this i18n-new-proxy"></li>
</ul>
<div class="main-content">
<div class="layui-form-item">
<label class="layui-form-label i18n-lan-name"></label>
<div class="layui-input-block">
<input type="text" name="name" autocomplete="off" placeholder="" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label i18n-lan-inetport"></label>
<div class="layui-input-block">
<input type="text" name="inetPort" autocomplete="off" placeholder="" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label i18n-lan-ip"></label>
<div class="layui-input-block">
<input type="text" name="lan" autocomplete="off" placeholder="" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button class="layui-btn add"></button>
<button class="layui-btn layui-btn-primary back"></button>
</div>
</div>
</div>
</div>
<script>
$(".i18n-new-proxy").html($.i18n.prop('lan.addnewconfig'));
$(".i18n-lan-name").html($.i18n.prop('lan.name'));
$(".i18n-lan-inetport").html($.i18n.prop('lan.inetport'));
$(".i18n-lan-ip").html($.i18n.prop('lan.ip'));
$("input[name='inetPort']").attr("placeholder", $.i18n.prop('lan.inetport.placeholder'));
$("input[name='lan']").attr("placeholder", $.i18n.prop('lan.ip.placeholder'));
$(".add").html($.i18n.prop('public.submit'));
$(".back").html($.i18n.prop('public.back'));
$(".add").click(function(){
var name = $("input[name='name']").val();
if(name == ""){
layer.alert($.i18n.prop('lan.notice.inputname'), {title:"Tips"});
return;
}
var inetPort = $("input[name='inetPort']").val();
if(inetPort == ""){
layer.alert($.i18n.prop('lan.notice.inputinetport'), {title:"Tips"});
return;
}
if(!check_port(inetPort)){
layer.alert($.i18n.prop('lan.notice.errorport'), {title:"Tips"});
return;
}
var lan = $("input[name='lan']").val();
if(lan == ""){
layer.alert($.i18n.prop('lan.notice.inputlan'), {title:"Tips"});
return;
}
if(!check_lan(lan)){
layer.alert($.i18n.prop('lan.notice.errorlan'), {title:"Tips"});
return;
}
clientList[clientIndex].proxyMappings.push({
name:name,
inetPort:inetPort,
lan:lan
});
api_invoke("/config/update", clientList, function(data) {
if (data.code == 20000) {
layer.alert($.i18n.prop('public.notice.addsuccess'), {title:"Tips"}, function(index){
layer.close(index);
load_page("html/lan/list.html");
});
}
});
});
$(".back").click(function(){
load_page("html/lan/list.html");
});
</script>
@@ -0,0 +1,91 @@
<div class="layui-tab layui-tab-brief">
<ul class="layui-tab-title site-demo-title">
<li class="layui-this i18n-lan-title"></li>
</ul>
<div class="main-content">
<div class="layui-form-item">
<label class="layui-form-label i18n-lan-name"></label>
<div class="layui-input-block">
<input type="text" name="name" autocomplete="off" placeholder="" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label i18n-lan-inetport"></label>
<div class="layui-input-block">
<input type="text" name="inetPort" autocomplete="off" placeholder="" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label i18n-lan-ip"></label>
<div class="layui-input-block">
<input type="text" name="lan" autocomplete="off" placeholder="" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button class="layui-btn update"></button>
<button class="layui-btn layui-btn-primary back"></button>
</div>
</div>
</div>
</div>
<script>
$(function(){
$("input[name='name']").val(clientList[clientIndex].proxyMappings[mappingIndex].name);
$("input[name='inetPort']").val(clientList[clientIndex].proxyMappings[mappingIndex].inetPort);
$("input[name='lan']").val(clientList[clientIndex].proxyMappings[mappingIndex].lan);
$(".i18n-lan-title").html($.i18n.prop('lan.editconfig'));
$(".i18n-lan-name").html($.i18n.prop('lan.name'));
$(".i18n-lan-inetport").html($.i18n.prop('lan.inetport'));
$(".i18n-lan-ip").html($.i18n.prop('lan.ip'));
$("input[name='inetPort']").attr("placeholder", $.i18n.prop('lan.inetport.placeholder'));
$("input[name='lan']").attr("placeholder", $.i18n.prop('lan.ip.placeholder'));
$(".update").html($.i18n.prop('public.submit'));
$(".back").html($.i18n.prop('public.back'));
});
$(".update").click(function(){
var name = $("input[name='name']").val();
if(name == ""){
layer.alert($.i18n.prop('lan.notice.inputname'), {title:"Tips"});
return;
}
var inetPort = $("input[name='inetPort']").val();
if(inetPort == ""){
layer.alert($.i18n.prop('lan.notice.inputinetport'), {title:"Tips"});
return;
}
if(!check_port(inetPort)){
layer.alert($.i18n.prop('lan.notice.errorport'), {title:"Tips"});
return;
}
var lan = $("input[name='lan']").val();
if(lan == ""){
layer.alert($.i18n.prop('lan.notice.inputlan'), {title:"Tips"});
return;
}
if(!check_lan(lan)){
layer.alert($.i18n.prop('lan.notice.errorlan'), {title:"Tips"});
return;
}
clientList[clientIndex].proxyMappings[mappingIndex]= {
name:name,
inetPort:inetPort,
lan:lan
};
api_invoke("/config/update", clientList, function(data) {
if (data.code == 20000) {
layer.alert($.i18n.prop('public.notice.updatesuccess'), {title:"Tips"}, function(index){
layer.close(index);
load_page("html/lan/list.html");
});
}
});
});
$(".back").click(function(){
load_page("html/lan/list.html");
});
</script>
@@ -0,0 +1,80 @@
<div class="layui-tab layui-tab-brief">
<ul class="layui-tab-title site-demo-title">
<li class="layui-this tab-title"></li>
</ul>
<div class="main-content"></div>
<div class="layui-input-block" style="float: left; margin-left: 15px;">
<button class="layui-btn mapping-add"></button>
</div>
</div>
<script id="mapping-tpl" type="text/html">
<table class="layui-table" lay-skin="line">
<thead>
<tr>
<th class="i18n-lan-name"></th>
<th class="i18n-lan-inetport"></th>
<th class="i18n-lan-ip"></th>
<th class="i18n-lan-options"></th>
</tr>
</thead>
<tbody>
<%for(var i = 0; i < data.length; i++) {%>
<tr>
<td><%:=data[i].name%></td>
<td><%:=data[i].inetPort%></td>
<td><%:=data[i].lan%></td>
<td>
<a data-index="<%:=i%>" class="layui-btn layui-btn-mini mapping-edit"></a>
<a data-index="<%:=i%>" class="layui-btn layui-btn-danger layui-btn-mini mapping-delete"></a>
</td>
</tr>
<%}%>
</tbody>
</table>
</script>
<script>
$(".tab-title").html(clientList[clientIndex].name + " - " + $.i18n.prop('lan.proxyconfig'));
$(".mapping-add").html($.i18n.prop('lan.addnewconfig'));
var html = template($("#mapping-tpl").html(), {
data : clientList[clientIndex].proxyMappings
});
$(".main-content").html(html);
$(".i18n-lan-name").html($.i18n.prop('lan.name'));
$(".i18n-lan-inetport").html($.i18n.prop('lan.inetport'));
$(".i18n-lan-ip").html($.i18n.prop('lan.ip'));
$(".i18n-lan-options").html($.i18n.prop('public.options'));
$(".mapping-edit").html($.i18n.prop('public.edit'));
$(".mapping-delete").html($.i18n.prop('public.delete'));
$(".mapping-config").click(function() {
window.clientIndex = $(this).attr("data-index");
load_page("html/lan/list.html");
});
$(".mapping-edit").click(function() {
window.mappingIndex = $(this).attr("data-index");
load_page("html/lan/edit.html");
});
$(".mapping-delete").click(function() {
var mappingIndex = $(this).attr("data-index");
layer.confirm($.i18n.prop('public.confirm.delete'), {
title: $.i18n.prop('public.tips'),
btn : [ $.i18n.prop('public.ok'), $.i18n.prop('public.cancel') ]
}, function(i) {
layer.close(i);
clientList[clientIndex].proxyMappings.splice(mappingIndex, 1);
api_invoke("/config/update", clientList, function(data) {
if (data.code != 20000) {
layer.alert(data.message);
} else {
load_page("html/lan/list.html");
}
})
});
});
$(".back").click(function() {
load_page("html/client/list.html");
});
$(".mapping-add").click(function() {
load_page("html/lan/add.html");
});
</script>
@@ -0,0 +1,51 @@
<div class="layui-tab layui-tab-brief">
<ul class="layui-tab-title site-demo-title">
<li class="layui-this tab-title"></li>
</ul>
<div class="main-content"></div>
</div>
<script id="stat-tpl" type="text/html">
<table class="layui-table" lay-skin="line">
<thead>
<tr>
<th class="i18n-inetport"></th>
<th class="i18n-inflow"></th>
<th class="i18n-outflow"></th>
<th class="i18n-channels"></th>
</tr>
</thead>
<tbody>
<%for(var i = 0; i < data.length; i++) {%>
<tr>
<td><%:=data[i].port%></td>
<td><%:=bytesToSize(data[i].readBytes)%></td>
<td><%:=bytesToSize(data[i].wroteBytes)%></td>
<td><%:=data[i].channels%></td>
</tr>
<%}%>
</tbody>
</table>
</script>
<script>
$(".tab-title").html($.i18n.prop('menu.client.statistics'));
function bytesToSize(bytes) {
if (bytes === 0)
return '0 B';
var k = 1000, // or 1024
sizes = [ 'B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB' ], i = Math
.floor(Math.log(bytes) / Math.log(k));
return (bytes / Math.pow(k, i)).toPrecision(3) + ' ' + sizes[i];
}
api_invoke("/metrics/get", {}, function(data) {
var html = template($("#stat-tpl").html(), {
data : data.data
});
$(".main-content").html(html);
$(".i18n-inetport").html($.i18n.prop('statistics.inetport'));
$(".i18n-inflow").html($.i18n.prop('statistics.inflow'));
$(".i18n-outflow").html($.i18n.prop('statistics.outflow'));
$(".i18n-channels").html($.i18n.prop('statistics.channels'));
});
</script>
@@ -0,0 +1,205 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1">
<title></title>
<link rel="stylesheet" href="/lanproxy-config/layui/css/layui.css">
<style type="text/css">
.main-content {
padding: 15px;
}
</style>
</head>
<body>
<div class="layui-layout layui-layout-admin">
<div class="layui-header">
<div class="layui-logo">言逍网络科技 配置控制台</div>
<ul class="layui-nav layui-layout-right">
<li class="layui-nav-item"><a href="javascript:;"
class="logout"></a></li>
</ul>
</div>
<div class="layui-side layui-bg-black">
<div class="layui-side-scroll"></div>
</div>
<div class="layui-body"></div>
<div class="layui-footer"></div>
</div>
<script src="/lanproxy-config/layui/layui.js"></script>
<script src="/jquery/jquery-3.1.1.min.js"></script>
<script src="/jquery/jquery.i18n.properties.min.js" type="text/javascript"></script>
<script src="/template/template.js"></script>
<script>
window.clientList = [];
function api_invoke(uri, params, callback) {
var index = layer.load(1, {
shade : [ 0.1, '#fff' ]
});
$.ajax({
url : uri,
data : JSON.stringify(params),
type : 'POST',
cache : false,
dataType : 'json',
contentType : "application/json; charset=utf-8",
success : function(data) {
layer.close(index);
callback(data);
},
error : function(XMLHttpRequest, textStatus, errorThrown) {
layer.close(index);
if ("undefined" == typeof (XMLHttpRequest.responseJSON)) {
layer.alert("System error, please try again later.");
return;
}
if (40100 == XMLHttpRequest.responseJSON.code) {
location.href = "/";
} else {
layer.alert(XMLHttpRequest.responseJSON.message);
api_invoke("/config/detail", {}, function(data) {
clientList = data.data;
});
}
}
});
}
function load_page(pageUrl) {
$(".layui-body").load(pageUrl);
}
function check_lan(ip) {
var re = /^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5]):([0-9]|[1-9]\d{1,3}|[1-5]\d{4}|6[0-5]{2}[0-3][0-5])$/
return re.test(ip);
}
function check_port(port) {
if (port > 65535) {
return false;
}
var re = /^[1-9]+[0-9]*]*$/
return re.test(port);
}
$(function() {
if (window.history && window.history.pushState) {
$(window).on('popstate', function() {
window.history.pushState('forward', null, '#');
window.history.forward(1);
});
}
if ('pushState' in history) {
window.history.pushState('forward', null, '#');
window.history.forward(1);
} else {
History.pushState('forward', null, '?state=2');
window.history.forward(1);
}
window.onhashchange = function() {
History.pushState('forward', null, '?state=1');
}
$(".logout").click(function() {
api_invoke("/logout", {}, function(data) {
location.href = "/";
})
});
});
function update_menu() {
api_invoke("/config/detail", {}, function(data) {
if (data.code == 20000) {
clientList = data.data;
var html = template($("#menu-tpl").html(), {
list : clientList
});
$(".layui-side-scroll").html(html);
layui.use('element', function() {
var element = layui.element;
});
$(".client-list").html($.i18n.prop('menu.client.list'));
$(".client-list-sub").html($.i18n.prop('menu.client.list'));
$(".client-add").html($.i18n.prop('menu.client.add'));
$(".client-config").html($.i18n.prop('menu.client.config'));
$(".statistics").html($.i18n.prop('menu.client.statistics'));
$(".menu-item").click(function() {
window.clientIndex = $(this).attr("data-index");
load_page("html/lan/list.html");
});
} else {
alert(data.message);
}
});
}
var i18nLanguage = "en";
var webLanguage = [ 'zh-CN', 'en' ];
function initWebLanguage() {
var navLanguage = navigator.language;
if (navLanguage) {
var charSize = $.inArray(navLanguage, webLanguage);
if (charSize > -1) {
i18nLanguage = navLanguage;
}
}
}
initWebLanguage();
jQuery.i18n.properties({
name : 'lang', //资源文件名称
path : '/i18n/', //资源文件路径
mode : 'map', //用Map的方式使用资源文件中的值
language : i18nLanguage,
encoding: 'UTF-8',
callback : function() {//加载成功后设置显示内容
$('title').html($.i18n.prop('title'));
$(".layui-footer").html($.i18n.prop('title'));
$(".logout").html($.i18n.prop('logout'));
layui.use('layer', function() {
window.layer = layui.layer;
update_menu();
load_page("html/client/list.html");
});
}
});
</script>
<script id="menu-tpl" type="text/html">
<ul class="layui-nav layui-nav-tree">
<li class="layui-nav-item layui-nav-itemed"><a class="client-list"
href="javascript:;"></a>
<dl class="layui-nav-child">
<dd class="layui-this">
<a href="javascript:load_page('html/client/list.html');" class="client-list-sub"></a>
</dd>
<dd>
<a href="javascript:load_page('html/client/add.html');" class="client-add"></a>
</dd>
</dl>
</li>
<li class="layui-nav-item layui-nav-itemed">
<a class="client-config" href="javascript:;"></a>
<dl class="layui-nav-child">
<%for(var i = 0; i < list.length; i++) {%>
<dd>
<a href="javascript:;" class="menu-item" data-index="<%:=i%>"><%:=list[i].name%></a>
</dd>
<%}%>
</dl>
</li>
<li class="layui-nav-item layui-nav-itemed"><a class="statistics"
href="javascript:load_page('html/statistics/list.html');"></a>
</li>
</ul>
</script>
</body>
</html>
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,2 @@
/** layui-v2.0.2 MIT License By http://www.layui.com */
html #layuicss-skincodecss{display:none;position:absolute;width:1989px}.layui-code-h3,.layui-code-view{position:relative;font-size:12px}.layui-code-view{display:block;margin:10px 0;padding:0;border:1px solid #e2e2e2;border-left-width:6px;background-color:#F2F2F2;color:#333;font-family:Courier New}.layui-code-h3{padding:0 10px;height:32px;line-height:32px;border-bottom:1px solid #e2e2e2}.layui-code-h3 a{position:absolute;right:10px;top:0;color:#999}.layui-code-view .layui-code-ol{position:relative;overflow:auto}.layui-code-view .layui-code-ol li{position:relative;margin-left:45px;line-height:20px;padding:0 5px;border-left:1px solid #e2e2e2;list-style-type:decimal-leading-zero;*list-style-type:decimal;background-color:#fff}.layui-code-view pre{margin:0}.layui-code-notepad{border:1px solid #0C0C0C;border-left-color:#3F3F3F;background-color:#0C0C0C;color:#C2BE9E}.layui-code-notepad .layui-code-h3{border-bottom:none}.layui-code-notepad .layui-code-ol li{background-color:#3F3F3F;border-left:none}
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 701 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Some files were not shown because too many files have changed in this diff Show More