From 97635546abb5f3f79e4a45b804848e6ae88d56a2 Mon Sep 17 00:00:00 2001
From: ubuntu <ubuntu@ubuntu.com>
Date: Tue, 26 Jun 2018 11:28:43 +0800
Subject: [PATCH] MMP, Cancel fork, because i do not know how to merge to the
 original foreign author

---
 .gitignore                    |    1 +
 README.md                     |  109 ++
 clients/client-linux.py       |  240 ++++
 clients/client-psutil.py      |  209 ++++
 server/.gitignore             |    2 +
 server/Makefile               |   34 +
 server/config.json            |   37 +
 server/include/argparse.h     |  139 +++
 server/include/detect.h       |  149 +++
 server/include/json.h         |  269 +++++
 server/include/system.h       | 1299 +++++++++++++++++++++
 server/obj/.gitignore         |    1 +
 server/src/argparse.c         |  322 ++++++
 server/src/json.c             |  949 ++++++++++++++++
 server/src/main.cpp           |  468 ++++++++
 server/src/main.h             |   93 ++
 server/src/netban.cpp         |  463 ++++++++
 server/src/netban.h           |  179 +++
 server/src/network.cpp        |  144 +++
 server/src/network.h          |   87 ++
 server/src/network_client.cpp |  184 +++
 server/src/server.cpp         |  204 ++++
 server/src/server.h           |   46 +
 server/src/system.c           | 2001 +++++++++++++++++++++++++++++++++
 web/css/dark.css              |   49 +
 web/css/light.css             |   46 +
 web/favicon.ico               |  Bin 0 -> 45470 bytes
 web/img/dark.png              |  Bin 0 -> 600 bytes
 web/img/light.png             |  Bin 0 -> 19452 bytes
 web/index.html                |  112 ++
 web/js/serverstatus.js        |  393 +++++++
 web/json/.gitignore           |    2 +
 32 files changed, 8231 insertions(+)
 create mode 100644 .gitignore
 create mode 100644 README.md
 create mode 100755 clients/client-linux.py
 create mode 100755 clients/client-psutil.py
 create mode 100644 server/.gitignore
 create mode 100644 server/Makefile
 create mode 100644 server/config.json
 create mode 100644 server/include/argparse.h
 create mode 100644 server/include/detect.h
 create mode 100644 server/include/json.h
 create mode 100644 server/include/system.h
 create mode 100644 server/obj/.gitignore
 create mode 100644 server/src/argparse.c
 create mode 100644 server/src/json.c
 create mode 100644 server/src/main.cpp
 create mode 100644 server/src/main.h
 create mode 100644 server/src/netban.cpp
 create mode 100644 server/src/netban.h
 create mode 100644 server/src/network.cpp
 create mode 100644 server/src/network.h
 create mode 100644 server/src/network_client.cpp
 create mode 100644 server/src/server.cpp
 create mode 100644 server/src/server.h
 create mode 100644 server/src/system.c
 create mode 100644 web/css/dark.css
 create mode 100644 web/css/light.css
 create mode 100644 web/favicon.ico
 create mode 100644 web/img/dark.png
 create mode 100644 web/img/light.png
 create mode 100644 web/index.html
 create mode 100644 web/js/serverstatus.js
 create mode 100644 web/json/.gitignore

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..cd5a000
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+default.sublime-workspace
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..0b949fe
--- /dev/null
+++ b/README.md
@@ -0,0 +1,109 @@
+# ServerStatus中文版:   
+
+* ServerStatus中文版是一个酷炫高逼格的云探针、云监控、服务器云监控、多服务器探针~。
+* 在线演示:https://tz.cloudcpp.com    
+
+# 目录介绍:
+
+* clients  客户端文件
+* server   服务端文件
+* web      网站文件  
+
+# 更新说明:
+
+* 20180314, 调整前端,置默认密码为,设置ip和user即可上线      
+* 20180312, 加入失联(被照顾)检测【正常:MH361, 屏蔽:MH370】,校准虚拟化(container)流量统计异常      
+* 20170807, 更新平均1,5,15负载
+* 20170607, 去掉无用的IPV6信息,增加服务器总流量监控                           
+
+# 安装教程:     
+   
+【克隆代码】:
+```
+git clone https://github.com/cppla/ServerStatus.git
+```
+
+【服务端配置】(服务端程序在ServerStatus/web下):  
+          
+一、生成服务端程序              
+```
+cd ServerStatus/server
+make
+./sergate
+```
+如果没错误提示,OK,ctrl+c关闭;如果有错误提示,检查35601端口是否被占用    
+
+二、修改配置文件         
+修改config.json文件,注意username, password的值需要和客户端对应一致                 
+```
+{"servers":
+	[
+		{
+			"username": "s01",
+			"name": "Mainserver 1",
+			"type": "Dedicated Server",
+			"host": "GenericServerHost123",
+			"location": "Austria",
+			"password": "some-hard-to-guess-copy-paste-password"
+		},
+	]
+}       
+```
+
+三、拷贝ServerStatus/status到你的网站目录        
+例如:
+```
+sudo cp -r ServerStatus/web/* /home/wwwroot/default
+```
+
+四、运行服务端:             
+web-dir参数为上一步设置的网站根目录,务必修改成自己网站的路径   
+```
+./sergate --config=config.json --web-dir=/home/wwwroot/default   
+```
+
+【客户端配置】(客户端程序在ServerStatus/clients下):          
+客户端有两个版本,client-linux为普通linux,client-psutil为跨平台版,普通版不成功,换成跨平台版即可。        
+
+一、client-linux版配置:       
+1、vim client-linux.py, 修改SERVER地址,username帐号, password密码        
+2、python client-linux.py 运行即可。      
+
+二、client-psutil版配置:                
+1、安装psutil跨平台依赖库      
+2、vim client-psutil.py, 修改SERVER地址,username帐号, password密码       
+3、python client-psutil.py 运行即可。           
+```
+### for Centos:
+sudo yum -y install epel-release
+sudo yum -y install python-pip
+sudo yum clean all
+sudo yum -y install gcc
+sudo yum -y install python-devel
+sudo pip install psutil
+### for Ubuntu/Debian:
+sudo root
+apt-get -y install python-setuptools python-dev build-essential
+apt-get -y install python-pip
+pip install psutil
+### for Windows:
+打开网址:https://pypi.python.org/pypi?:action=display&name=psutil#downloads
+下载psutil for windows程序包
+安装即可
+```
+
+打开云探针页面,就可以正常的监控。接下来把服务器和客户端脚本自行加入开机启动,或者进程守护,或以后台方式运行即可!例如: nohup python client-linux.py &      
+
+# 为什么会有ServerStatus中文版:
+
+* 有些功能确实没用
+* 原版本部署,英文说明复杂
+* 不符合中文版的习惯
+* 没有一次又一次的轮子,哪来如此优秀的云探针
+
+# 相关开源项目,感谢: 
+
+* ServerStatus:https://github.com/BotoX/ServerStatus
+* mojeda: https://github.com/mojeda 
+* mojeda's ServerStatus: https://github.com/mojeda/ServerStatus
+* BlueVM's project: http://www.lowendtalk.com/discussion/comment/169690#Comment_169690
diff --git a/clients/client-linux.py b/clients/client-linux.py
new file mode 100755
index 0000000..7a810c0
--- /dev/null
+++ b/clients/client-linux.py
@@ -0,0 +1,240 @@
+# -*- coding: utf-8 -*-
+# Update by : https://github.com/cppla/ServerStatus
+# 支持Python版本:2.7 to 3.5
+# 支持操作系统: Linux, OSX, FreeBSD, OpenBSD and NetBSD, both 32-bit and 64-bit architectures
+# 时间: 20180312
+
+
+SERVER = "127.0.0.1"
+PORT = 35601
+USER = "s01"
+PASSWORD = "USER_DEFAULT_PASSWORD"
+INTERVAL = 1 #更新间隔
+
+
+import socket
+import time
+import string
+import math
+import re
+import os
+import json
+import subprocess
+import collections
+
+def get_uptime():
+	f = open('/proc/uptime', 'r')
+	uptime = f.readline()
+	f.close()
+	uptime = uptime.split('.', 2)
+	time = int(uptime[0])
+	return int(time)
+
+def get_memory():
+	re_parser = re.compile(r'^(?P<key>\S*):\s*(?P<value>\d*)\s*kB')
+	result = dict()
+	for line in open('/proc/meminfo'):
+		match = re_parser.match(line)
+		if not match:
+			continue;
+		key, value = match.groups(['key', 'value'])
+		result[key] = int(value)
+
+	MemTotal = float(result['MemTotal'])
+	MemFree = float(result['MemFree'])
+	Cached = float(result['Cached'])
+	MemUsed = MemTotal - (Cached + MemFree)
+	SwapTotal = float(result['SwapTotal'])
+	SwapFree = float(result['SwapFree'])
+	return int(MemTotal), int(MemUsed), int(SwapTotal), int(SwapFree)
+
+def get_hdd():
+	p = subprocess.check_output(['df', '-Tlm', '--total', '-t', 'ext4', '-t', 'ext3', '-t', 'ext2', '-t', 'reiserfs', '-t', 'jfs', '-t', 'ntfs', '-t', 'fat32', '-t', 'btrfs', '-t', 'fuseblk', '-t', 'zfs', '-t', 'simfs', '-t', 'xfs']).decode("Utf-8")
+	total = p.splitlines()[-1]
+	used = total.split()[3]
+	size = total.split()[2]
+	return int(size), int(used)
+
+def get_time():
+	stat_file = file("/proc/stat", "r")
+	time_list = stat_file.readline().split(' ')[2:6]
+	stat_file.close()
+	for i in range(len(time_list))  :
+		time_list[i] = int(time_list[i])
+	return time_list
+def delta_time():
+	x = get_time()
+	time.sleep(INTERVAL)
+	y = get_time()
+	for i in range(len(x)):
+		y[i]-=x[i]
+	return y
+def get_cpu():
+	t = delta_time()
+	st = sum(t)
+	if st == 0:
+		st = 1
+	result = 100-(t[len(t)-1]*100.00/st)
+	return round(result)
+
+class Traffic:
+	def __init__(self):
+		self.rx = collections.deque(maxlen=10)
+		self.tx = collections.deque(maxlen=10)
+	def get(self):
+		f = open('/proc/net/dev', 'r')
+		net_dev = f.readlines()
+		f.close()
+		avgrx = 0; avgtx = 0
+
+		for dev in net_dev[2:]:
+			dev = dev.split(':')
+			if dev[0].strip() == "lo" or dev[0].find("tun") > -1 \
+					or dev[0].find("docker") > -1 or dev[0].find("veth") > -1 \
+					or dev[0].find("br-") > -1:
+				continue
+			dev = dev[1].split()
+			avgrx += int(dev[0])
+			avgtx += int(dev[8])
+
+		self.rx.append(avgrx)
+		self.tx.append(avgtx)
+		avgrx = 0; avgtx = 0
+
+		l = len(self.rx)
+		for x in range(l - 1):
+			avgrx += self.rx[x+1] - self.rx[x]
+			avgtx += self.tx[x+1] - self.tx[x]
+
+		avgrx = int(avgrx / l / INTERVAL)
+		avgtx = int(avgtx / l / INTERVAL)
+
+		return avgrx, avgtx
+
+def liuliang():
+    NET_IN = 0
+    NET_OUT = 0
+    with open('/proc/net/dev') as f:
+        for line in f.readlines():
+            netinfo = re.findall('([^\s]+):[\s]{0,}(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)', line)
+            if netinfo:
+                if netinfo[0][0] == 'lo' or 'tun' in netinfo[0][0] \
+						or 'docker' in netinfo[0][0] or 'veth' in netinfo[0][0] \
+						or 'br-' in netinfo[0][0] \
+						or netinfo[0][1]=='0' or netinfo[0][9]=='0':
+                    continue
+                else:
+                    NET_IN += int(netinfo[0][1])
+                    NET_OUT += int(netinfo[0][9])
+    return NET_IN, NET_OUT
+
+# todo: 不确定是否要用多线程or多进程:  效率? 资源? 
+def ip_status():
+	object_check = ['www.10010.com', 'www.189.cn', 'www.10086.cn']
+	ip_check = 0
+	for i in object_check:
+		s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+		s.settimeout(1)
+		try:
+			s.connect((i, 80))
+		except:
+			ip_check += 1
+		s.close()
+		del s
+	if ip_check >= 2:
+		return False
+	else:
+		return True
+
+def get_network(ip_version):
+	if(ip_version == 4):
+		HOST = "ipv4.google.com"
+	elif(ip_version == 6):
+		HOST = "ipv6.google.com"
+	try:
+		s = socket.create_connection((HOST, 80), 2)
+		return True
+	except:
+		pass
+	return False
+
+if __name__ == '__main__':
+	socket.setdefaulttimeout(30)
+	while 1:
+		try:
+			print("Connecting...")
+			s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+			s.connect((SERVER, PORT))
+			data = s.recv(1024)
+			if data.find("Authentication required") > -1:
+				s.send(USER + ':' + PASSWORD + '\n')
+				data = s.recv(1024)
+				if data.find("Authentication successful") < 0:
+					print(data)
+					raise socket.error
+			else:
+				print(data)
+				raise socket.error
+
+			print(data)
+			data = s.recv(1024)
+			print(data)
+
+			timer = 0
+			check_ip = 0
+			if data.find("IPv4") > -1:
+				check_ip = 6
+			elif data.find("IPv6") > -1:
+				check_ip = 4
+			else:
+				print(data)
+				raise socket.error
+
+			traffic = Traffic()
+			traffic.get()
+			while 1:
+				CPU = get_cpu()
+				NetRx, NetTx = traffic.get()
+				NET_IN, NET_OUT = liuliang()
+				Uptime = get_uptime()
+				Load_1, Load_5, Load_15 = os.getloadavg()
+				MemoryTotal, MemoryUsed, SwapTotal, SwapFree = get_memory()
+				HDDTotal, HDDUsed = get_hdd()
+				IP_STATUS = ip_status()
+
+				array = {}
+				if not timer:
+					array['online' + str(check_ip)] = get_network(check_ip)
+					timer = 10
+				else:
+					timer -= 1*INTERVAL
+
+				array['uptime'] = Uptime
+				array['load_1'] = Load_1
+				array['load_5'] = Load_5
+				array['load_15'] = Load_15
+				array['memory_total'] = MemoryTotal
+				array['memory_used'] = MemoryUsed
+				array['swap_total'] = SwapTotal
+				array['swap_used'] = SwapTotal - SwapFree
+				array['hdd_total'] = HDDTotal
+				array['hdd_used'] = HDDUsed
+				array['cpu'] = CPU
+				array['network_rx'] = NetRx
+				array['network_tx'] = NetTx
+				array['network_in'] = NET_IN
+				array['network_out'] = NET_OUT
+				array['ip_status'] = IP_STATUS
+
+				s.send("update " + json.dumps(array) + "\n")
+		except KeyboardInterrupt:
+			raise
+		except socket.error:
+			print("Disconnected...")
+			# keep on trying after a disconnect
+			s.close()
+			time.sleep(3)
+		except Exception as e:
+			print("Caught Exception:", e)
+			s.close()
+			time.sleep(3)
diff --git a/clients/client-psutil.py b/clients/client-psutil.py
new file mode 100755
index 0000000..6c501b2
--- /dev/null
+++ b/clients/client-psutil.py
@@ -0,0 +1,209 @@
+# -*- coding: utf-8 -*-
+# Update by : https://github.com/cppla/ServerStatus
+# 依赖于psutil跨平台库:
+# 支持Python版本:2.6 to 3.5 (users of Python 2.4 and 2.5 may use 2.1.3 version)
+# 支持操作系统: Linux, Windows, OSX, Sun Solaris, FreeBSD, OpenBSD and NetBSD, both 32-bit and 64-bit architectures
+# 时间: 20180312
+
+SERVER = "127.0.0.1"
+PORT = 35601
+USER = "s01"
+PASSWORD = "USER_DEFAULT_PASSWORD"
+INTERVAL = 1 # 更新间隔
+
+
+import socket
+import time
+import string
+import math
+import os
+import json
+import collections
+import psutil
+import sys
+
+def get_uptime():
+	return int(time.time() - psutil.boot_time())
+
+def get_memory():
+	Mem = psutil.virtual_memory()
+	try:
+		MemUsed = Mem.total - (Mem.cached + Mem.free)
+	except:
+		MemUsed = Mem.total - Mem.free
+	return int(Mem.total/1024.0), int(MemUsed/1024.0)
+
+def get_swap():
+	Mem = psutil.swap_memory()
+	return int(Mem.total/1024.0), int(Mem.used/1024.0)
+
+def get_hdd():
+	valid_fs = [ "ext4", "ext3", "ext2", "reiserfs", "jfs", "btrfs", "fuseblk", "zfs", "simfs", "ntfs", "fat32", "exfat", "xfs" ]
+	disks = dict()
+	size = 0
+	used = 0
+	for disk in psutil.disk_partitions():
+		if not disk.device in disks and disk.fstype.lower() in valid_fs:
+			disks[disk.device] = disk.mountpoint
+	for disk in disks.itervalues():
+		usage = psutil.disk_usage(disk)
+		size += usage.total
+		used += usage.used
+	return int(size/1024.0/1024.0), int(used/1024.0/1024.0)
+
+def get_cpu():
+	return psutil.cpu_percent(interval=INTERVAL)
+
+class Traffic:
+	def __init__(self):
+		self.rx = collections.deque(maxlen=10)
+		self.tx = collections.deque(maxlen=10)
+	def get(self):
+		avgrx = 0; avgtx = 0
+		for name, stats in psutil.net_io_counters(pernic=True).iteritems():
+			if name == "lo" or name.find("tun") > -1 \
+					or name.find("docker") > -1 or name.find("veth") > -1 \
+					or name.find("br-") > -1:
+				continue
+			avgrx += stats.bytes_recv
+			avgtx += stats.bytes_sent
+
+		self.rx.append(avgrx)
+		self.tx.append(avgtx)
+		avgrx = 0; avgtx = 0
+
+		l = len(self.rx)
+		for x in range(l - 1):
+			avgrx += self.rx[x+1] - self.rx[x]
+			avgtx += self.tx[x+1] - self.tx[x]
+
+		avgrx = int(avgrx / l / INTERVAL)
+		avgtx = int(avgtx / l / INTERVAL)
+
+		return avgrx, avgtx
+
+def liuliang():
+    NET_IN = 0
+    NET_OUT = 0
+    net = psutil.net_io_counters(pernic=True)
+    for k, v in net.items():
+        if k == 'lo' or 'tun' in k \
+				or 'br-' in k \
+				or 'docker' in k or 'veth' in k:
+            continue
+        else:
+            NET_IN += v[1]
+            NET_OUT += v[0]
+    return NET_IN, NET_OUT
+
+# todo: 不确定是否要用多线程or多进程:  效率? 资源? 
+def ip_status():
+	object_check = ['www.10010.com', 'www.189.cn', 'www.10086.cn']
+	ip_check = 0
+	for i in object_check:
+		s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+		s.settimeout(1)
+		try:
+			s.connect((i, 80))
+		except:
+			ip_check += 1
+		s.close()
+		del s
+	if ip_check >= 2:
+		return False
+	else:
+		return True
+
+def get_network(ip_version):
+	if(ip_version == 4):
+		HOST = "ipv4.google.com"
+	elif(ip_version == 6):
+		HOST = "ipv6.google.com"
+	try:
+		s = socket.create_connection((HOST, 80), 2)
+		return True
+	except:
+		pass
+	return False
+
+if __name__ == '__main__':
+	socket.setdefaulttimeout(30)
+	while 1:
+		try:
+			print("Connecting...")
+			s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+			s.connect((SERVER, PORT))
+			data = s.recv(1024)
+			if data.find("Authentication required") > -1:
+				s.send(USER + ':' + PASSWORD + '\n')
+				data = s.recv(1024)
+				if data.find("Authentication successful") < 0:
+					print(data)
+					raise socket.error
+			else:
+				print(data)
+				raise socket.error
+
+			print(data)
+			data = s.recv(1024)
+			print(data)
+
+			timer = 0
+			check_ip = 0
+			if data.find("IPv4") > -1:
+				check_ip = 6
+			elif data.find("IPv6") > -1:
+				check_ip = 4
+			else:
+				print(data)
+				raise socket.error
+
+			traffic = Traffic()
+			traffic.get()
+			while 1:
+				CPU = get_cpu()
+				NetRx, NetTx = traffic.get()
+				NET_IN, NET_OUT = liuliang()
+				Uptime = get_uptime()
+				Load_1, Load_5, Load_15 = os.getloadavg() if 'linux' in sys.platform else (0.0, 0.0, 0.0)
+				MemoryTotal, MemoryUsed = get_memory()
+				SwapTotal, SwapUsed = get_swap()
+				HDDTotal, HDDUsed = get_hdd()
+				IP_STATUS = ip_status()
+
+				array = {}
+				if not timer:
+					array['online' + str(check_ip)] = get_network(check_ip)
+					timer = 10
+				else:
+					timer -= 1*INTERVAL
+
+				array['uptime'] = Uptime
+				array['load_1'] = Load_1
+				array['load_5'] = Load_5
+				array['load_15'] = Load_15
+				array['memory_total'] = MemoryTotal
+				array['memory_used'] = MemoryUsed
+				array['swap_total'] = SwapTotal
+				array['swap_used'] = SwapUsed
+				array['hdd_total'] = HDDTotal
+				array['hdd_used'] = HDDUsed
+				array['cpu'] = CPU
+				array['network_rx'] = NetRx
+				array['network_tx'] = NetTx
+				array['network_in'] = NET_IN
+				array['network_out'] = NET_OUT
+				array['ip_status'] = IP_STATUS
+
+				s.send("update " + json.dumps(array) + "\n")
+		except KeyboardInterrupt:
+			raise
+		except socket.error:
+			print("Disconnected...")
+			# keep on trying after a disconnect
+			s.close()
+			time.sleep(3)
+		except Exception as e:
+			print("Caught Exception:", e)
+			s.close()
+			time.sleep(3)
diff --git a/server/.gitignore b/server/.gitignore
new file mode 100644
index 0000000..6d7633f
--- /dev/null
+++ b/server/.gitignore
@@ -0,0 +1,2 @@
+sergate
+.tags*
diff --git a/server/Makefile b/server/Makefile
new file mode 100644
index 0000000..3ca07ff
--- /dev/null
+++ b/server/Makefile
@@ -0,0 +1,34 @@
+OUT = sergate
+
+#CC = clang
+CC = gcc
+CFLAGS = -Wall -O2
+
+#CXX = clang++
+CXX = g++
+CXXFLAGS = -Wall -O2
+
+ODIR = obj
+SDIR = src
+LIBS = -pthread -lm
+INC = -Iinclude
+
+C_SRCS := $(wildcard $(SDIR)/*.c)
+CXX_SRCS := $(wildcard $(SDIR)/*.cpp)
+C_OBJS := $(patsubst $(SDIR)/%.c,$(ODIR)/%.o,$(C_SRCS))
+CXX_OBJS := $(patsubst $(SDIR)/%.cpp,$(ODIR)/%.o,$(CXX_SRCS))
+OBJS := $(C_OBJS) $(CXX_OBJS)
+
+$(ODIR)/%.o: $(SDIR)/%.c
+	$(CC) -c $(INC) $(CFLAGS) $< -o $@
+
+$(ODIR)/%.o: $(SDIR)/%.cpp
+	$(CXX) -c $(INC) $(CXXFLAGS) $< -o $@
+
+$(OUT): $(OBJS)
+	$(CXX) $(LIBS) $^ -o $(OUT)
+
+.PHONY: clean
+
+clean:
+	rm -f $(ODIR)/*.o $(OUT)
diff --git a/server/config.json b/server/config.json
new file mode 100644
index 0000000..d5e823b
--- /dev/null
+++ b/server/config.json
@@ -0,0 +1,37 @@
+{"servers":
+	[
+		{
+			"username": "s01",
+			"name": "node1",
+			"type": "xen",
+			"host": "host1",
+			"location": "cn",
+			"password": "USER_DEFAULT_PASSWORD"
+		},
+		{
+			"username": "s02",
+			"name": "node2",
+			"type": "vmware",
+			"host": "host2",
+			"location": "jp",
+			"password": "USER_DEFAULT_PASSWORD"
+		},
+		{
+			"disabled": true,
+			"username": "s03",
+			"name": "node3",
+			"type": "Nothing",
+			"host": "host3",
+			"location": "fr",
+			"password": "USER_DEFAULT_PASSWORD"
+		},
+		{
+			"username": "s04",
+			"name": "node4",
+			"type": "kvm",
+			"host": "host4",
+			"location": "kr",
+			"password": "USER_DEFAULT_PASSWORD"
+		}
+	]
+}
\ No newline at end of file
diff --git a/server/include/argparse.h b/server/include/argparse.h
new file mode 100644
index 0000000..4430f94
--- /dev/null
+++ b/server/include/argparse.h
@@ -0,0 +1,139 @@
+#ifndef ARGPARSE_H
+#define ARGPARSE_H
+
+/**
+ * Command-line arguments parsing library.
+ *
+ * This module is inspired by parse-options.c (git) and python's argparse
+ * module.
+ *
+ * Arguments parsing is common task in cli program, but traditional `getopt`
+ * libraries are not easy to use. This library provides high-level arguments
+ * parsing solutions.
+ *
+ * The program defines what arguments it requires, and `argparse` will figure
+ * out how to parse those out of `argc` and `argv`, it also automatically
+ * generates help and usage messages and issues errors when users give the
+ * program invalid arguments.
+ *
+ * Reserved namespaces:
+ *  argparse
+ *  OPT
+ * Author: Yecheng Fu <cofyc.jackson@gmail.com>
+ */
+
+#include <assert.h>
+#include <math.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct argparse;
+struct argparse_option;
+
+typedef int argparse_callback(struct argparse *this_,
+                              const struct argparse_option *option);
+
+enum argparse_flag {
+    ARGPARSE_STOP_AT_NON_OPTION = 1,
+};
+
+enum argparse_option_type {
+    /* special */
+    ARGPARSE_OPT_END,
+    /* options with no arguments */
+    ARGPARSE_OPT_BOOLEAN,
+    ARGPARSE_OPT_BIT,
+    /* options with arguments (optional or required) */
+    ARGPARSE_OPT_INTEGER,
+    ARGPARSE_OPT_STRING,
+};
+
+enum argparse_option_flags {
+    OPT_NONEG = 1,              /* Negation disabled. */
+};
+
+/*
+ *  Argparse option struct.
+ *
+ *  `type`:
+ *    holds the type of the option, you must have an ARGPARSE_OPT_END last in your
+ *    array.
+ *
+ *  `short_name`:
+ *    the character to use as a short option name, '\0' if none.
+ *
+ *  `long_name`:
+ *    the long option name, without the leading dash, NULL if none.
+ *
+ *  `value`:
+ *    stores pointer to the value to be filled.
+ *
+ *  `help`:
+ *    the short help message associated to what the option does.
+ *    Must never be NULL (except for ARGPARSE_OPT_END).
+ *
+ *  `callback`:
+ *    function is called when corresponding argument is parsed.
+ *
+ *  `data`:
+ *    associated data. Callbacks can use it like they want.
+ *
+ *  `flags`:
+ *    option flags.
+ *
+ */
+struct argparse_option {
+    enum argparse_option_type type;
+    const char short_name;
+    const char *long_name;
+    void *value;
+    const char *help;
+    argparse_callback *callback;
+    intptr_t data;
+    int flags;
+};
+
+/*
+ * argpparse
+ */
+struct argparse {
+    // user supplied
+    const struct argparse_option *options;
+    const char *usage;
+    int flags;
+    // internal context
+    int argc;
+    const char **argv;
+    const char **out;
+    int cpidx;
+    const char *optvalue;       // current option value
+};
+
+// builtin callbacks
+int argparse_help_cb(struct argparse *this_,
+                     const struct argparse_option *option);
+
+// builtin option macros
+#define OPT_END()          { ARGPARSE_OPT_END, 0 }
+#define OPT_BOOLEAN(...)   { ARGPARSE_OPT_BOOLEAN, __VA_ARGS__ }
+#define OPT_BIT(...)       { ARGPARSE_OPT_BIT, __VA_ARGS__ }
+#define OPT_INTEGER(...)   { ARGPARSE_OPT_INTEGER, __VA_ARGS__ }
+#define OPT_STRING(...)    { ARGPARSE_OPT_STRING, __VA_ARGS__ }
+#define OPT_HELP()         OPT_BOOLEAN('h', "help", 0, "Show this help message and exit", argparse_help_cb)
+
+int argparse_init(struct argparse *this_, struct argparse_option *options,
+                  const char *usage, int flags);
+int argparse_parse(struct argparse *this_, int argc, const char **argv);
+void argparse_usage(struct argparse *this_);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/server/include/detect.h b/server/include/detect.h
new file mode 100644
index 0000000..0e2ef86
--- /dev/null
+++ b/server/include/detect.h
@@ -0,0 +1,149 @@
+/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
+/* If you are missing that file, acquire a complete release at teeworlds.com.                */
+#ifndef BASE_DETECT_H
+#define BASE_DETECT_H
+
+/*
+	this file detected the family, platform and architecture
+	to compile for.
+*/
+
+/* platforms */
+
+/* windows Family */
+#if defined(WIN64) || defined(_WIN64)
+	/* Hmm, is this IA64 or x86-64? */
+	#define CONF_FAMILY_WINDOWS 1
+	#define CONF_FAMILY_STRING "windows"
+	#define CONF_PLATFORM_WIN64 1
+	#define CONF_PLATFORM_STRING "win64"
+#elif defined(WIN32) || defined(_WIN32) || defined(__CYGWIN32__) || defined(__MINGW32__)
+	#define CONF_FAMILY_WINDOWS 1
+	#define CONF_FAMILY_STRING "windows"
+	#define CONF_PLATFORM_WIN32 1
+	#define CONF_PLATFORM_STRING "win32"
+#endif
+
+/* unix family */
+#if defined(__FreeBSD__) || defined(__FreeBSD_kernel__)
+	#define CONF_FAMILY_UNIX 1
+	#define CONF_FAMILY_STRING "unix"
+	#define CONF_PLATFORM_FREEBSD 1
+	#define CONF_PLATFORM_STRING "freebsd"
+#endif
+
+#if defined(__OpenBSD__)
+	#define CONF_FAMILY_UNIX 1
+	#define CONF_FAMILY_STRING "unix"
+	#define CONF_PLATFORM_OPENBSD 1
+	#define CONF_PLATFORM_STRING "openbsd"
+#endif
+
+#if defined(__LINUX__) || defined(__linux__)
+	#define CONF_FAMILY_UNIX 1
+	#define CONF_FAMILY_STRING "unix"
+	#define CONF_PLATFORM_LINUX 1
+	#define CONF_PLATFORM_STRING "linux"
+#endif
+
+#if defined(__GNU__) || defined(__gnu__)
+	#define CONF_FAMILY_UNIX 1
+	#define CONF_FAMILY_STRING "unix"
+	#define CONF_PLATFORM_HURD 1
+	#define CONF_PLATFORM_STRING "gnu"
+#endif
+
+#if defined(MACOSX) || defined(__APPLE__) || defined(__DARWIN__)
+	#define CONF_FAMILY_UNIX 1
+	#define CONF_FAMILY_STRING "unix"
+	#define CONF_PLATFORM_MACOSX 1
+	#define CONF_PLATFORM_STRING "macosx"
+#endif
+
+#if defined(__sun)
+	#define CONF_FAMILY_UNIX 1
+	#define CONF_FAMILY_STRING "unix"
+	#define CONF_PLATFORM_SOLARIS 1
+	#define CONF_PLATFORM_STRING "solaris"
+#endif
+
+/* beos family */
+#if defined(__BeOS) || defined(__BEOS__)
+	#define CONF_FAMILY_BEOS 1
+	#define CONF_FAMILY_STRING "beos"
+	#define CONF_PLATFORM_BEOS 1
+	#define CONF_PLATFORM_STRING "beos"
+#endif
+
+
+/* use gcc endianness definitions when available */
+#if defined(__GNUC__) && !defined(__APPLE__) && !defined(__MINGW32__) && !defined(__sun)
+	#if defined(__FreeBSD__) || defined(__OpenBSD__)
+		#include <sys/endian.h>
+	#else
+		#include <endian.h>
+	#endif
+
+	#if __BYTE_ORDER == __LITTLE_ENDIAN
+		#define CONF_ARCH_ENDIAN_LITTLE 1
+	#elif __BYTE_ORDER == __BIG_ENDIAN
+		#define CONF_ARCH_ENDIAN_BIG 1
+	#endif
+#endif
+
+
+/* architectures */
+#if defined(i386) || defined(__i386__) || defined(__x86__) || defined(CONF_PLATFORM_WIN32)
+	#define CONF_ARCH_IA32 1
+	#define CONF_ARCH_STRING "ia32"
+	#if !defined(CONF_ARCH_ENDIAN_LITTLE) && !defined(CONF_ARCH_ENDIAN_BIG)
+		#define CONF_ARCH_ENDIAN_LITTLE 1
+	#endif
+#endif
+
+#if defined(__ia64__) || defined(_M_IA64)
+	#define CONF_ARCH_IA64 1
+	#define CONF_ARCH_STRING "ia64"
+	#if !defined(CONF_ARCH_ENDIAN_LITTLE) && !defined(CONF_ARCH_ENDIAN_BIG)
+		#define CONF_ARCH_ENDIAN_LITTLE 1
+	#endif
+#endif
+
+#if defined(__amd64__) || defined(__x86_64__) || defined(_M_X64)
+	#define CONF_ARCH_AMD64 1
+	#define CONF_ARCH_STRING "amd64"
+	#if !defined(CONF_ARCH_ENDIAN_LITTLE) && !defined(CONF_ARCH_ENDIAN_BIG)
+		#define CONF_ARCH_ENDIAN_LITTLE 1
+	#endif
+#endif
+
+#if defined(__powerpc__) || defined(__ppc__)
+	#define CONF_ARCH_PPC 1
+	#define CONF_ARCH_STRING "ppc"
+	#if !defined(CONF_ARCH_ENDIAN_LITTLE) && !defined(CONF_ARCH_ENDIAN_BIG)
+		#define CONF_ARCH_ENDIAN_BIG 1
+	#endif
+#endif
+
+#if defined(__sparc__)
+	#define CONF_ARCH_SPARC 1
+	#define CONF_ARCH_STRING "sparc"
+	#if !defined(CONF_ARCH_ENDIAN_LITTLE) && !defined(CONF_ARCH_ENDIAN_BIG)
+		#define CONF_ARCH_ENDIAN_BIG 1
+	#endif
+#endif
+
+
+#ifndef CONF_FAMILY_STRING
+#define CONF_FAMILY_STRING "unknown"
+#endif
+
+#ifndef CONF_PLATFORM_STRING
+#define CONF_PLATFORM_STRING "unknown"
+#endif
+
+#ifndef CONF_ARCH_STRING
+#define CONF_ARCH_STRING "unknown"
+#endif
+
+#endif
diff --git a/server/include/json.h b/server/include/json.h
new file mode 100644
index 0000000..ed1f175
--- /dev/null
+++ b/server/include/json.h
@@ -0,0 +1,269 @@
+
+/* vim: set et ts=3 sw=3 sts=3 ft=c:
+ *
+ * Copyright (C) 2012, 2013, 2014 James McLaughlin et al.  All rights reserved.
+ * https://github.com/udp/json-parser
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#ifndef _JSON_H
+#define _JSON_H
+
+#ifndef json_char
+   #define json_char char
+#endif
+
+#ifndef json_int_t
+   #ifndef _MSC_VER
+      #include <inttypes.h>
+      #define json_int_t int64_t
+   #else
+      #define json_int_t __int64
+   #endif
+#endif
+
+#include <stdlib.h>
+
+#ifdef __cplusplus
+
+   #include <string.h>
+
+   extern "C"
+   {
+
+#endif
+
+typedef struct
+{
+   unsigned long max_memory;
+   int settings;
+
+   /* Custom allocator support (leave null to use malloc/free)
+    */
+
+   void * (* mem_alloc) (size_t, int zero, void * user_data);
+   void (* mem_free) (void *, void * user_data);
+
+   void * user_data;  /* will be passed to mem_alloc and mem_free */
+
+} json_settings;
+
+#define json_enable_comments  0x01
+
+typedef enum
+{
+   json_none,
+   json_object,
+   json_array,
+   json_integer,
+   json_double,
+   json_string,
+   json_boolean,
+   json_null
+
+} json_type;
+
+extern const struct _json_value json_value_none;
+
+typedef struct _json_value
+{
+   struct _json_value * parent;
+
+   json_type type;
+
+   union
+   {
+      int boolean;
+      json_int_t integer;
+      double dbl;
+
+      struct
+      {
+         unsigned int length;
+         json_char * ptr; /* null terminated */
+
+      } string;
+
+      struct
+      {
+         unsigned int length;
+
+         struct
+         {
+            json_char * name;
+            unsigned int name_length;
+
+            struct _json_value * value;
+
+         } * values;
+
+         #if defined(__cplusplus) && __cplusplus >= 201103L
+         decltype(values) begin () const
+         {  return values;
+         }
+         decltype(values) end () const
+         {  return values + length;
+         }
+         #endif
+
+      } object;
+
+      struct
+      {
+         unsigned int length;
+         struct _json_value ** values;
+
+         #if defined(__cplusplus) && __cplusplus >= 201103L
+         decltype(values) begin () const
+         {  return values;
+         }
+         decltype(values) end () const
+         {  return values + length;
+         }
+         #endif
+
+      } array;
+
+   } u;
+
+   union
+   {
+      struct _json_value * next_alloc;
+      void * object_mem;
+
+   } _reserved;
+
+
+   /* Some C++ operator sugar */
+
+   #ifdef __cplusplus
+
+      public:
+
+         inline _json_value ()
+         {  memset (this, 0, sizeof (_json_value));
+         }
+
+         inline const struct _json_value &operator [] (int index) const
+         {
+            if (type != json_array || index < 0
+                     || ((unsigned int) index) >= u.array.length)
+            {
+               return json_value_none;
+            }
+
+            return *u.array.values [index];
+         }
+
+         inline const struct _json_value &operator [] (const char * index) const
+         { 
+            if (type != json_object)
+               return json_value_none;
+
+            for (unsigned int i = 0; i < u.object.length; ++ i)
+               if (!strcmp (u.object.values [i].name, index))
+                  return *u.object.values [i].value;
+
+            return json_value_none;
+         }
+
+         inline operator const char * () const
+         {  
+            switch (type)
+            {
+               case json_string:
+                  return u.string.ptr;
+
+               default:
+                  return "";
+            };
+         }
+
+         inline operator json_int_t () const
+         {  
+            switch (type)
+            {
+               case json_integer:
+                  return u.integer;
+
+               case json_double:
+                  return (json_int_t) u.dbl;
+
+               default:
+                  return 0;
+            };
+         }
+
+         inline operator bool () const
+         {  
+            if (type != json_boolean)
+               return false;
+
+            return u.boolean != 0;
+         }
+
+         inline operator double () const
+         {  
+            switch (type)
+            {
+               case json_integer:
+                  return (double) u.integer;
+
+               case json_double:
+                  return u.dbl;
+
+               default:
+                  return 0;
+            };
+         }
+
+   #endif
+
+} json_value;
+
+json_value * json_parse (const json_char * json,
+                         size_t length);
+
+#define json_error_max 128
+json_value * json_parse_ex (json_settings * settings,
+                            const json_char * json,
+                            size_t length,
+                            char * error);
+
+void json_value_free (json_value *);
+
+
+/* Not usually necessary, unless you used a custom mem_alloc and now want to
+ * use a custom mem_free.
+ */
+void json_value_free_ex (json_settings * settings,
+                         json_value *);
+
+
+#ifdef __cplusplus
+   } /* extern "C" */
+#endif
+
+#endif
diff --git a/server/include/system.h b/server/include/system.h
new file mode 100644
index 0000000..7ba0c0a
--- /dev/null
+++ b/server/include/system.h
@@ -0,0 +1,1299 @@
+/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
+/* If you are missing that file, acquire a complete release at teeworlds.com.                */
+
+/*
+	Title: OS Abstraction
+*/
+
+#ifndef BASE_SYSTEM_H
+#define BASE_SYSTEM_H
+
+#include "detect.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* Group: Debug */
+/*
+	Function: dbg_assert
+		Breaks into the debugger based on a test.
+
+	Parameters:
+		test - Result of the test.
+		msg - Message that should be printed if the test fails.
+
+	Remarks:
+		Does nothing in release version of the library.
+
+	See Also:
+		<dbg_break>
+*/
+void dbg_assert(int test, const char *msg);
+#define dbg_assert(test,msg) dbg_assert_imp(__FILE__, __LINE__, test, msg)
+void dbg_assert_imp(const char *filename, int line, int test, const char *msg);
+
+
+#ifdef __clang_analyzer__
+#include <assert.h>
+#undef dbg_assert
+#define dbg_assert(test,msg) assert(test)
+#endif
+
+/*
+	Function: dbg_break
+		Breaks into the debugger.
+
+	Remarks:
+		Does nothing in release version of the library.
+
+	See Also:
+		<dbg_assert>
+*/
+void dbg_break();
+
+/*
+	Function: dbg_msg
+
+	Prints a debug message.
+
+	Parameters:
+		sys - A string that describes what system the message belongs to
+		fmt - A printf styled format string.
+
+	Remarks:
+		Does nothing in release version of the library.
+
+	See Also:
+		<dbg_assert>
+*/
+void dbg_msg(const char *sys, const char *fmt, ...);
+
+/* Group: Memory */
+
+/*
+	Function: mem_alloc
+		Allocates memory.
+
+	Parameters:
+		size - Size of the needed block.
+		alignment - Alignment for the block.
+
+	Returns:
+		Returns a pointer to the newly allocated block. Returns a
+		null pointer if the memory couldn't be allocated.
+
+	Remarks:
+		- Passing 0 to size will allocated the smallest amount possible
+		and return a unique pointer.
+
+	See Also:
+		<mem_free>
+*/
+void *mem_alloc_debug(const char *filename, int line, unsigned size, unsigned alignment);
+#define mem_alloc(s,a) mem_alloc_debug(__FILE__, __LINE__, (s), (a))
+
+/*
+	Function: mem_free
+		Frees a block allocated through <mem_alloc>.
+
+	Remarks:
+		- In the debug version of the library the function will assert if
+		a non-valid block is passed, like a null pointer or a block that
+		isn't allocated.
+
+	See Also:
+		<mem_alloc>
+*/
+void mem_free(void *block);
+
+/*
+	Function: mem_copy
+		Copies a a memory block.
+
+	Parameters:
+		dest - Destination.
+		source - Source to copy.
+		size - Size of the block to copy.
+
+	Remarks:
+		- This functions DOES NOT handles cases where source and
+		destination is overlapping.
+
+	See Also:
+		<mem_move>
+*/
+void mem_copy(void *dest, const void *source, unsigned size);
+
+/*
+	Function: mem_move
+		Copies a a memory block
+
+	Parameters:
+		dest - Destination
+		source - Source to copy
+		size - Size of the block to copy
+
+	Remarks:
+		- This functions handles cases where source and destination
+		is overlapping
+
+	See Also:
+		<mem_copy>
+*/
+void mem_move(void *dest, const void *source, unsigned size);
+
+/*
+	Function: mem_zero
+		Sets a complete memory block to 0
+
+	Parameters:
+		block - Pointer to the block to zero out
+		size - Size of the block
+*/
+void mem_zero(void *block, unsigned size);
+
+/*
+	Function: mem_comp
+		Compares two blocks of memory
+
+	Parameters:
+		a - First block of data
+		b - Second block of data
+		size - Size of the data to compare
+
+	Returns:
+		<0 - Block a is lesser then block b
+		0 - Block a is equal to block b
+		>0 - Block a is greater then block b
+*/
+int mem_comp(const void *a, const void *b, int size);
+
+/*
+	Function: mem_check
+		Validates the heap
+		Will trigger a assert if memory has failed.
+*/
+int mem_check_imp();
+#define mem_check() dbg_assert_imp(__FILE__, __LINE__, mem_check_imp(), "Memory check failed")
+
+/* Group: File IO */
+enum {
+	IOFLAG_READ = 1,
+	IOFLAG_WRITE = 2,
+	IOFLAG_RANDOM = 4,
+
+	IOSEEK_START = 0,
+	IOSEEK_CUR = 1,
+	IOSEEK_END = 2
+};
+
+typedef struct IOINTERNAL *IOHANDLE;
+
+/*
+	Function: io_open
+		Opens a file.
+
+	Parameters:
+		filename - File to open.
+		flags - A set of flags. IOFLAG_READ, IOFLAG_WRITE, IOFLAG_RANDOM.
+
+	Returns:
+		Returns a handle to the file on success and 0 on failure.
+
+*/
+IOHANDLE io_open(const char *filename, int flags);
+
+/*
+	Function: io_read
+		Reads data into a buffer from a file.
+
+	Parameters:
+		io - Handle to the file to read data from.
+		buffer - Pointer to the buffer that will recive the data.
+		size - Number of bytes to read from the file.
+
+	Returns:
+		Number of bytes read.
+
+*/
+unsigned io_read(IOHANDLE io, void *buffer, unsigned size);
+
+/*
+	Function: io_skip
+		Skips data in a file.
+
+	Parameters:
+		io - Handle to the file.
+		size - Number of bytes to skip.
+
+	Returns:
+		Number of bytes skipped.
+*/
+unsigned io_skip(IOHANDLE io, int size);
+
+/*
+	Function: io_write
+		Writes data from a buffer to file.
+
+	Parameters:
+		io - Handle to the file.
+		buffer - Pointer to the data that should be written.
+		size - Number of bytes to write.
+
+	Returns:
+		Number of bytes written.
+*/
+unsigned io_write(IOHANDLE io, const void *buffer, unsigned size);
+
+/*
+	Function: io_write_newline
+		Writes newline to file.
+
+	Parameters:
+		io - Handle to the file.
+
+	Returns:
+		Number of bytes written.
+*/
+unsigned io_write_newline(IOHANDLE io);
+
+/*
+	Function: io_seek
+		Seeks to a specified offset in the file.
+
+	Parameters:
+		io - Handle to the file.
+		offset - Offset from pos to stop.
+		origin - Position to start searching from.
+
+	Returns:
+		Returns 0 on success.
+*/
+int io_seek(IOHANDLE io, int offset, int origin);
+
+/*
+	Function: io_tell
+		Gets the current position in the file.
+
+	Parameters:
+		io - Handle to the file.
+
+	Returns:
+		Returns the current position. -1L if an error occured.
+*/
+long int io_tell(IOHANDLE io);
+
+/*
+	Function: io_length
+		Gets the total length of the file. Resetting cursor to the beginning
+
+	Parameters:
+		io - Handle to the file.
+
+	Returns:
+		Returns the total size. -1L if an error occured.
+*/
+long int io_length(IOHANDLE io);
+
+/*
+	Function: io_close
+		Closes a file.
+
+	Parameters:
+		io - Handle to the file.
+
+	Returns:
+		Returns 0 on success.
+*/
+int io_close(IOHANDLE io);
+
+/*
+	Function: io_flush
+		Empties all buffers and writes all pending data.
+
+	Parameters:
+		io - Handle to the file.
+
+	Returns:
+		Returns 0 on success.
+*/
+int io_flush(IOHANDLE io);
+
+
+/*
+	Function: io_stdin
+		Returns an <IOHANDLE> to the standard input.
+*/
+IOHANDLE io_stdin();
+
+/*
+	Function: io_stdout
+		Returns an <IOHANDLE> to the standard output.
+*/
+IOHANDLE io_stdout();
+
+/*
+	Function: io_stderr
+		Returns an <IOHANDLE> to the standard error.
+*/
+IOHANDLE io_stderr();
+
+
+/* Group: Threads */
+
+/*
+	Function: thread_sleep
+		Suspends the current thread for a given period.
+
+	Parameters:
+		milliseconds - Number of milliseconds to sleep.
+*/
+void thread_sleep(int milliseconds);
+
+/*
+	Function: thread_create
+		Creates a new thread.
+
+	Parameters:
+		threadfunc - Entry point for the new thread.
+		user - Pointer to pass to the thread.
+
+*/
+void *thread_create(void (*threadfunc)(void *), void *user);
+
+/*
+	Function: thread_wait
+		Waits for a thread to be done or destroyed.
+
+	Parameters:
+		thread - Thread to wait for.
+*/
+void thread_wait(void *thread);
+
+/*
+	Function: thread_destroy
+		Destroys a thread.
+
+	Parameters:
+		thread - Thread to destroy.
+*/
+void thread_destroy(void *thread);
+
+/*
+	Function: thread_yeild
+		Yeild the current threads execution slice.
+*/
+void thread_yield();
+
+/*
+	Function: thread_detach
+		Puts the thread in the detached thread, guaranteeing that
+		resources of the thread will be freed immediately when the
+		thread terminates.
+
+	Parameters:
+		thread - Thread to detach
+*/
+void thread_detach(void *thread);
+
+/* Group: Locks */
+typedef void* LOCK;
+
+LOCK lock_create();
+void lock_destroy(LOCK lock);
+
+int lock_try(LOCK lock);
+void lock_wait(LOCK lock);
+void lock_release(LOCK lock);
+
+
+/* Group: Semaphores */
+
+#if !defined(CONF_PLATFORM_MACOSX)
+	#if defined(CONF_FAMILY_UNIX)
+		#include <semaphore.h>
+		typedef sem_t SEMAPHORE;
+	#elif defined(CONF_FAMILY_WINDOWS)
+		typedef void* SEMAPHORE;
+	#else
+		#error missing sempahore implementation
+	#endif
+
+	void semaphore_init(SEMAPHORE *sem);
+	void semaphore_wait(SEMAPHORE *sem);
+	void semaphore_signal(SEMAPHORE *sem);
+	void semaphore_destroy(SEMAPHORE *sem);
+#endif
+
+/* Group: Timer */
+#ifdef __GNUC__
+/* if compiled with -pedantic-errors it will complain about long
+	not being a C90 thing.
+*/
+__extension__ typedef long long int64;
+#else
+typedef long long int64;
+#endif
+/*
+	Function: time_get
+		Fetches a sample from a high resolution timer.
+
+	Returns:
+		Current value of the timer.
+
+	Remarks:
+		To know how fast the timer is ticking, see <time_freq>.
+*/
+int64 time_get();
+
+/*
+	Function: time_freq
+		Returns the frequency of the high resolution timer.
+
+	Returns:
+		Returns the frequency of the high resolution timer.
+*/
+int64 time_freq();
+
+/*
+	Function: time_timestamp
+		Retrives the current time as a UNIX timestamp
+
+	Returns:
+		The time as a UNIX timestamp
+*/
+int time_timestamp();
+
+/* Group: Network General */
+typedef struct
+{
+	int type;
+	int ipv4sock;
+	int ipv6sock;
+} NETSOCKET;
+
+enum
+{
+	NETADDR_MAXSTRSIZE = 1+(8*4+7)+1+1+5+1, // [XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX]:XXXXX
+
+	NETTYPE_INVALID = 0,
+	NETTYPE_IPV4 = 1,
+	NETTYPE_IPV6 = 2,
+	NETTYPE_LINK_BROADCAST = 4,
+	NETTYPE_ALL = NETTYPE_IPV4|NETTYPE_IPV6
+};
+
+typedef struct
+{
+	unsigned int type;
+	unsigned char ip[16];
+	unsigned short port;
+} NETADDR;
+
+/*
+	Function: net_init
+		Initiates network functionallity.
+
+	Returns:
+		Returns 0 on success,
+
+	Remarks:
+		You must call this function before using any other network
+		functions.
+*/
+int net_init();
+
+/*
+	Function: net_host_lookup
+		Does a hostname lookup by name and fills out the passed
+		NETADDR struct with the recieved details.
+
+	Returns:
+		0 on success.
+*/
+int net_host_lookup(const char *hostname, NETADDR *addr, int types);
+
+/*
+	Function: net_addr_comp
+		Compares two network addresses.
+
+	Parameters:
+		a - Address to compare
+		b - Address to compare to.
+
+	Returns:
+		<0 - Address a is lesser then address b
+		0 - Address a is equal to address b
+		>0 - Address a is greater then address b
+*/
+int net_addr_comp(const NETADDR *a, const NETADDR *b);
+
+/*
+	Function: net_addr_str
+		Turns a network address into a representive string.
+
+	Parameters:
+		addr - Address to turn into a string.
+		string - Buffer to fill with the string.
+		max_length - Maximum size of the string.
+		add_port - add port to string or not
+
+	Remarks:
+		- The string will always be zero terminated
+
+*/
+void net_addr_str(const NETADDR *addr, char *string, int max_length, int add_port);
+
+/*
+	Function: net_addr_from_str
+		Turns string into a network address.
+
+	Returns:
+		0 on success
+
+	Parameters:
+		addr - Address to fill in.
+		string - String to parse.
+*/
+int net_addr_from_str(NETADDR *addr, const char *string);
+
+/* Group: Network UDP */
+
+/*
+	Function: net_udp_create
+		Creates a UDP socket and binds it to a port.
+
+	Parameters:
+		bindaddr - Address to bind the socket to.
+
+	Returns:
+		On success it returns an handle to the socket. On failure it
+		returns NETSOCKET_INVALID.
+*/
+NETSOCKET net_udp_create(NETADDR bindaddr);
+
+/*
+	Function: net_udp_send
+		Sends a packet over an UDP socket.
+
+	Parameters:
+		sock - Socket to use.
+		addr - Where to send the packet.
+		data - Pointer to the packet data to send.
+		size - Size of the packet.
+
+	Returns:
+		On success it returns the number of bytes sent. Returns -1
+		on error.
+*/
+int net_udp_send(NETSOCKET sock, const NETADDR *addr, const void *data, int size);
+
+/*
+	Function: net_udp_recv
+		Recives a packet over an UDP socket.
+
+	Parameters:
+		sock - Socket to use.
+		addr - Pointer to an NETADDR that will recive the address.
+		data - Pointer to a buffer that will recive the data.
+		maxsize - Maximum size to recive.
+
+	Returns:
+		On success it returns the number of bytes recived. Returns -1
+		on error.
+*/
+int net_udp_recv(NETSOCKET sock, NETADDR *addr, void *data, int maxsize);
+
+/*
+	Function: net_udp_close
+		Closes an UDP socket.
+
+	Parameters:
+		sock - Socket to close.
+
+	Returns:
+		Returns 0 on success. -1 on error.
+*/
+int net_udp_close(NETSOCKET sock);
+
+
+/* Group: Network TCP */
+
+/*
+	Function: net_tcp_create
+		Creates a TCP socket.
+
+	Parameters:
+		bindaddr - Address to bind the socket to.
+
+	Returns:
+		On success it returns an handle to the socket. On failure it returns NETSOCKET_INVALID.
+*/
+NETSOCKET net_tcp_create(NETADDR bindaddr);
+
+/*
+	Function: net_tcp_listen
+		Makes the socket start listening for new connections.
+
+	Parameters:
+		sock - Socket to start listen to.
+		backlog - Size of the queue of incomming connections to keep.
+
+	Returns:
+		Returns 0 on success.
+*/
+int net_tcp_listen(NETSOCKET sock, int backlog);
+
+/*
+	Function: net_tcp_accept
+		Polls a listning socket for a new connection.
+
+	Parameters:
+		sock - Listning socket to poll.
+		new_sock - Pointer to a socket to fill in with the new socket.
+		addr - Pointer to an address that will be filled in the remote address (optional, can be NULL).
+
+	Returns:
+		Returns a non-negative integer on success. Negative integer on failure.
+*/
+int net_tcp_accept(NETSOCKET sock, NETSOCKET *new_sock, NETADDR *addr);
+
+/*
+	Function: net_tcp_connect
+		Connects one socket to another.
+
+	Parameters:
+		sock - Socket to connect.
+		addr - Address to connect to.
+
+	Returns:
+		Returns 0 on success.
+
+*/
+int net_tcp_connect(NETSOCKET sock, const NETADDR *addr);
+
+/*
+	Function: net_tcp_send
+		Sends data to a TCP stream.
+
+	Parameters:
+		sock - Socket to send data to.
+		data - Pointer to the data to send.
+		size - Size of the data to send.
+
+	Returns:
+		Number of bytes sent. Negative value on failure.
+*/
+int net_tcp_send(NETSOCKET sock, const void *data, int size);
+
+/*
+	Function: net_tcp_recv
+		Recvives data from a TCP stream.
+
+	Parameters:
+		sock - Socket to recvive data from.
+		data - Pointer to a buffer to write the data to
+		max_size - Maximum of data to write to the buffer.
+
+	Returns:
+		Number of bytes recvived. Negative value on failure. When in
+		non-blocking mode, it returns 0 when there is no more data to
+		be fetched.
+*/
+int net_tcp_recv(NETSOCKET sock, void *data, int maxsize);
+
+/*
+	Function: net_tcp_close
+		Closes a TCP socket.
+
+	Parameters:
+		sock - Socket to close.
+
+	Returns:
+		Returns 0 on success. Negative value on failure.
+*/
+int net_tcp_close(NETSOCKET sock);
+
+/* Group: Strings */
+
+/*
+	Function: str_append
+		Appends a string to another.
+
+	Parameters:
+		dst - Pointer to a buffer that contains a string.
+		src - String to append.
+		dst_size - Size of the buffer of the dst string.
+
+	Remarks:
+		- The strings are treated as zero-termineted strings.
+		- Garantees that dst string will contain zero-termination.
+*/
+void str_append(char *dst, const char *src, int dst_size);
+
+/*
+	Function: str_copy
+		Copies a string to another.
+
+	Parameters:
+		dst - Pointer to a buffer that shall recive the string.
+		src - String to be copied.
+		dst_size - Size of the buffer dst.
+
+	Remarks:
+		- The strings are treated as zero-termineted strings.
+		- Garantees that dst string will contain zero-termination.
+*/
+void str_copy(char *dst, const char *src, int dst_size);
+
+/*
+	Function: str_length
+		Returns the length of a zero terminated string.
+
+	Parameters:
+		str - Pointer to the string.
+
+	Returns:
+		Length of string in bytes excluding the zero termination.
+*/
+int str_length(const char *str);
+
+/*
+	Function: str_format
+		Performs printf formating into a buffer.
+
+	Parameters:
+		buffer - Pointer to the buffer to recive the formated string.
+		buffer_size - Size of the buffer.
+		format - printf formating string.
+		... - Parameters for the formating.
+
+	Remarks:
+		- See the C manual for syntax for the printf formating string.
+		- The strings are treated as zero-termineted strings.
+		- Garantees that dst string will contain zero-termination.
+*/
+void str_format(char *buffer, int buffer_size, const char *format, ...);
+
+/*
+	Function: str_sanitize_strong
+		Replaces all characters below 32 and above 127 with whitespace.
+
+	Parameters:
+		str - String to sanitize.
+
+	Remarks:
+		- The strings are treated as zero-termineted strings.
+*/
+void str_sanitize_strong(char *str);
+
+/*
+	Function: str_sanitize_cc
+		Replaces all characters below 32 with whitespace.
+
+	Parameters:
+		str - String to sanitize.
+
+	Remarks:
+		- The strings are treated as zero-termineted strings.
+*/
+void str_sanitize_cc(char *str);
+
+/*
+	Function: str_sanitize
+		Replaces all characters below 32 with whitespace with
+		exception to \t, \n and \r.
+
+	Parameters:
+		str - String to sanitize.
+
+	Remarks:
+		- The strings are treated as zero-termineted strings.
+*/
+void str_sanitize(char *str);
+
+/*
+	Function: str_skip_to_whitespace
+		Skips leading non-whitespace characters(all but ' ', '\t', '\n', '\r').
+
+	Parameters:
+		str - Pointer to the string.
+
+	Returns:
+		Pointer to the first whitespace character found
+		within the string.
+
+	Remarks:
+		- The strings are treated as zero-termineted strings.
+*/
+char *str_skip_to_whitespace(char *str);
+
+/*
+	Function: str_skip_whitespaces
+		Skips leading whitespace characters(' ', '\t', '\n', '\r').
+
+	Parameters:
+		str - Pointer to the string.
+
+	Returns:
+		Pointer to the first non-whitespace character found
+		within the string.
+
+	Remarks:
+		- The strings are treated as zero-termineted strings.
+*/
+char *str_skip_whitespaces(char *str);
+
+/*
+	Function: str_comp_nocase
+		Compares to strings case insensitive.
+
+	Parameters:
+		a - String to compare.
+		b - String to compare.
+
+	Returns:
+		<0 - String a is lesser then string b
+		0 - String a is equal to string b
+		>0 - String a is greater then string b
+
+	Remarks:
+		- Only garanted to work with a-z/A-Z.
+		- The strings are treated as zero-termineted strings.
+*/
+int str_comp_nocase(const char *a, const char *b);
+
+/*
+	Function: str_comp_nocase_num
+		Compares up to num characters of two strings case insensitive.
+
+	Parameters:
+		a - String to compare.
+		b - String to compare.
+		num - Maximum characters to compare
+
+	Returns:
+		<0 - String a is lesser than string b
+		0 - String a is equal to string b
+		>0 - String a is greater than string b
+
+	Remarks:
+		- Only garanted to work with a-z/A-Z.
+		- The strings are treated as zero-termineted strings.
+*/
+int str_comp_nocase_num(const char *a, const char *b, const int num);
+
+/*
+	Function: str_comp
+		Compares to strings case sensitive.
+
+	Parameters:
+		a - String to compare.
+		b - String to compare.
+
+	Returns:
+		<0 - String a is lesser then string b
+		0 - String a is equal to string b
+		>0 - String a is greater then string b
+
+	Remarks:
+		- The strings are treated as zero-termineted strings.
+*/
+int str_comp(const char *a, const char *b);
+
+/*
+	Function: str_comp_num
+		Compares up to num characters of two strings case sensitive.
+
+	Parameters:
+		a - String to compare.
+		b - String to compare.
+		num - Maximum characters to compare
+
+	Returns:
+		<0 - String a is lesser then string b
+		0 - String a is equal to string b
+		>0 - String a is greater then string b
+
+	Remarks:
+		- The strings are treated as zero-termineted strings.
+*/
+int str_comp_num(const char *a, const char *b, const int num);
+
+/*
+	Function: str_comp_filenames
+		Compares two strings case sensitive, digit chars will be compared as numbers.
+
+	Parameters:
+		a - String to compare.
+		b - String to compare.
+
+	Returns:
+		<0 - String a is lesser then string b
+		0 - String a is equal to string b
+		>0 - String a is greater then string b
+
+	Remarks:
+		- The strings are treated as zero-termineted strings.
+*/
+int str_comp_filenames(const char *a, const char *b);
+
+/*
+	Function: str_find_nocase
+		Finds a string inside another string case insensitive.
+
+	Parameters:
+		haystack - String to search in
+		needle - String to search for
+
+	Returns:
+		A pointer into haystack where the needle was found.
+		Returns NULL of needle could not be found.
+
+	Remarks:
+		- Only garanted to work with a-z/A-Z.
+		- The strings are treated as zero-termineted strings.
+*/
+const char *str_find_nocase(const char *haystack, const char *needle);
+
+/*
+	Function: str_find
+		Finds a string inside another string case sensitive.
+
+	Parameters:
+		haystack - String to search in
+		needle - String to search for
+
+	Returns:
+		A pointer into haystack where the needle was found.
+		Returns NULL of needle could not be found.
+
+	Remarks:
+		- The strings are treated as zero-termineted strings.
+*/
+const char *str_find(const char *haystack, const char *needle);
+
+/*
+	Function: str_hex
+		Takes a datablock and generates a hexstring of it.
+
+	Parameters:
+		dst - Buffer to fill with hex data
+		dst_size - size of the buffer
+		data - Data to turn into hex
+		data - Size of the data
+
+	Remarks:
+		- The desination buffer will be zero-terminated
+*/
+void str_hex(char *dst, int dst_size, const void *data, int data_size);
+
+/*
+	Function: str_timestamp
+		Copies a time stamp in the format year-month-day_hour-minute-second to the string.
+
+	Parameters:
+		buffer - Pointer to a buffer that shall receive the time stamp string.
+		buffer_size - Size of the buffer.
+
+	Remarks:
+		- Guarantees that buffer string will contain zero-termination.
+*/
+void str_timestamp(char *buffer, int buffer_size);
+
+/* Group: Filesystem */
+
+/*
+	Function: fs_listdir
+		Lists the files in a directory
+
+	Parameters:
+		dir - Directory to list
+		cb - Callback function to call for each entry
+		type - Type of the directory
+		user - Pointer to give to the callback
+
+	Returns:
+		Always returns 0.
+*/
+typedef int (*FS_LISTDIR_CALLBACK)(const char *name, int is_dir, int dir_type, void *user);
+int fs_listdir(const char *dir, FS_LISTDIR_CALLBACK cb, int type, void *user);
+
+/*
+	Function: fs_makedir
+		Creates a directory
+
+	Parameters:
+		path - Directory to create
+
+	Returns:
+		Returns 0 on success. Negative value on failure.
+
+	Remarks:
+		Does not create several directories if needed. "a/b/c" will result
+		in a failure if b or a does not exist.
+*/
+int fs_makedir(const char *path);
+
+/*
+	Function: fs_storage_path
+		Fetches per user configuration directory.
+
+	Returns:
+		Returns 0 on success. Negative value on failure.
+
+	Remarks:
+		- Returns ~/.appname on UNIX based systems
+		- Returns ~/Library/Applications Support/appname on Mac OS X
+		- Returns %APPDATA%/Appname on Windows based systems
+*/
+int fs_storage_path(const char *appname, char *path, int max);
+
+/*
+	Function: fs_is_dir
+		Checks if directory exists
+
+	Returns:
+		Returns 1 on success, 0 on failure.
+*/
+int fs_is_dir(const char *path);
+
+/*
+	Function: fs_chdir
+		Changes current working directory
+
+	Returns:
+		Returns 0 on success, 1 on failure.
+*/
+int fs_chdir(const char *path);
+
+/*
+	Function: fs_getcwd
+		Gets the current working directory.
+
+	Returns:
+		Returns a pointer to the buffer on success, 0 on failure.
+*/
+char *fs_getcwd(char *buffer, int buffer_size);
+
+/*
+	Function: fs_parent_dir
+		Get the parent directory of a directory
+
+	Parameters:
+		path - The directory string
+
+	Returns:
+		Returns 0 on success, 1 on failure.
+
+	Remarks:
+		- The string is treated as zero-termineted string.
+*/
+int fs_parent_dir(char *path);
+
+/*
+	Function: fs_remove
+		Deletes the file with the specified name.
+
+	Parameters:
+		filename - The file to delete
+
+	Returns:
+		Returns 0 on success, 1 on failure.
+
+	Remarks:
+		- The strings are treated as zero-terminated strings.
+*/
+int fs_remove(const char *filename);
+
+/*
+	Function: fs_rename
+		Renames the file or directory. If the paths differ the file will be moved.
+
+	Parameters:
+		oldname - The actual name
+		newname - The new name
+
+	Returns:
+		Returns 0 on success, 1 on failure.
+
+	Remarks:
+		- The strings are treated as zero-terminated strings.
+*/
+int fs_rename(const char *oldname, const char *newname);
+
+/*
+	Group: Undocumented
+*/
+
+
+/*
+	Function: net_tcp_connect_non_blocking
+
+	DOCTODO: serp
+*/
+int net_tcp_connect_non_blocking(NETSOCKET sock, NETADDR bindaddr);
+
+/*
+	Function: net_set_non_blocking
+
+	DOCTODO: serp
+*/
+int net_set_non_blocking(NETSOCKET sock);
+
+/*
+	Function: net_set_non_blocking
+
+	DOCTODO: serp
+*/
+int net_set_blocking(NETSOCKET sock);
+
+/*
+	Function: net_errno
+
+	DOCTODO: serp
+*/
+int net_errno();
+
+/*
+	Function: net_would_block
+
+	DOCTODO: serp
+*/
+int net_would_block();
+
+int net_socket_read_wait(NETSOCKET sock, int time);
+
+void mem_debug_dump(IOHANDLE file);
+
+void swap_endian(void *data, unsigned elem_size, unsigned num);
+
+
+typedef void (*DBG_LOGGER)(const char *line);
+void dbg_logger(DBG_LOGGER logger);
+
+void dbg_logger_stdout();
+void dbg_logger_debugger();
+void dbg_logger_file(const char *filename);
+
+typedef struct
+{
+	int allocated;
+	int active_allocations;
+	int total_allocations;
+} MEMSTATS;
+
+const MEMSTATS *mem_stats();
+
+typedef struct
+{
+	int sent_packets;
+	int sent_bytes;
+	int recv_packets;
+	int recv_bytes;
+} NETSTATS;
+
+
+void net_stats(NETSTATS *stats);
+
+int str_toint(const char *str);
+float str_tofloat(const char *str);
+int str_isspace(char c);
+char str_uppercase(char c);
+unsigned str_quickhash(const char *str);
+
+/*
+	Function: gui_messagebox
+		Display plain OS-dependent message box
+
+	Parameters:
+		title - title of the message box
+		message - text to display
+*/
+void gui_messagebox(const char *title, const char *message);
+
+
+/*
+	Function: str_utf8_rewind
+		Moves a cursor backwards in an utf8 string
+
+	Parameters:
+		str - utf8 string
+		cursor - position in the string
+
+	Returns:
+		New cursor position.
+
+	Remarks:
+		- Won't move the cursor less then 0
+*/
+int str_utf8_rewind(const char *str, int cursor);
+
+/*
+	Function: str_utf8_forward
+		Moves a cursor forwards in an utf8 string
+
+	Parameters:
+		str - utf8 string
+		cursor - position in the string
+
+	Returns:
+		New cursor position.
+
+	Remarks:
+		- Won't move the cursor beyond the zero termination marker
+*/
+int str_utf8_forward(const char *str, int cursor);
+
+/*
+	Function: str_utf8_decode
+		Decodes an utf8 character
+
+	Parameters:
+		ptr - pointer to an utf8 string. this pointer will be moved forward
+
+	Returns:
+		Unicode value for the character. -1 for invalid characters and 0 for end of string.
+
+	Remarks:
+		- This function will also move the pointer forward.
+*/
+int str_utf8_decode(const char **ptr);
+
+/*
+	Function: str_utf8_encode
+		Encode an utf8 character
+
+	Parameters:
+		ptr - Pointer to a buffer that should recive the data. Should be able to hold at least 4 bytes.
+
+	Returns:
+		Number of bytes put into the buffer.
+
+	Remarks:
+		- Does not do zero termination of the string.
+*/
+int str_utf8_encode(char *ptr, int chr);
+
+/*
+	Function: str_utf8_check
+		Checks if a strings contains just valid utf8 characters.
+
+	Parameters:
+		str - Pointer to a possible utf8 string.
+
+	Returns:
+		0 - invalid characters found.
+		1 - only valid characters found.
+
+	Remarks:
+		- The string is treated as zero-terminated utf8 string.
+*/
+int str_utf8_check(const char *str);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/server/obj/.gitignore b/server/obj/.gitignore
new file mode 100644
index 0000000..1530978
--- /dev/null
+++ b/server/obj/.gitignore
@@ -0,0 +1 @@
+*.o
\ No newline at end of file
diff --git a/server/src/argparse.c b/server/src/argparse.c
new file mode 100644
index 0000000..b581a2f
--- /dev/null
+++ b/server/src/argparse.c
@@ -0,0 +1,322 @@
+#include "argparse.h"
+
+#if defined(__cplusplus)
+extern "C" {
+#endif
+
+#define OPT_UNSET 1
+
+static const char *
+prefix_skip(const char *str, const char *prefix)
+{
+    size_t len = strlen(prefix);
+    return strncmp(str, prefix, len) ? NULL : str + len;
+}
+
+int
+prefix_cmp(const char *str, const char *prefix)
+{
+    for (;; str++, prefix++)
+        if (!*prefix)
+            return 0;
+        else if (*str != *prefix)
+            return (unsigned char)*prefix - (unsigned char)*str;
+}
+
+static void
+argparse_error(struct argparse *this_, const struct argparse_option *opt,
+               const char *reason)
+{
+    if (!strncmp(this_->argv[0], "--", 2)) {
+        fprintf(stderr, "error: option `%s` %s\n", opt->long_name, reason);
+        exit(-1);
+    } else {
+        fprintf(stderr, "error: option `%c` %s\n", opt->short_name, reason);
+        exit(-1);
+    }
+}
+
+static int
+argparse_getvalue(struct argparse *this_, const struct argparse_option *opt,
+                  int flags)
+{
+    const char *s = NULL;
+    if (!opt->value)
+        goto skipped;
+    switch (opt->type) {
+    case ARGPARSE_OPT_BOOLEAN:
+        if (flags & OPT_UNSET) {
+            *(int *)opt->value = *(int *)opt->value - 1;
+        } else {
+            *(int *)opt->value = *(int *)opt->value + 1;
+        }
+        if (*(int *)opt->value < 0) { 
+            *(int *)opt->value = 0;
+        }
+        break;
+    case ARGPARSE_OPT_BIT:
+        if (flags & OPT_UNSET) {
+            *(int *)opt->value &= ~opt->data;
+        } else {
+            *(int *)opt->value |= opt->data;
+        }
+        break;
+    case ARGPARSE_OPT_STRING:
+        if (this_->optvalue) {
+            *(const char **)opt->value = this_->optvalue;
+            this_->optvalue = NULL;
+        } else if (this_->argc > 1) {
+            this_->argc--;
+            *(const char **)opt->value = *++this_->argv;
+        } else {
+            argparse_error(this_, opt, "requires a value");
+        }
+        break;
+    case ARGPARSE_OPT_INTEGER:
+        if (this_->optvalue) {
+            *(int *)opt->value = strtol(this_->optvalue, (char **)&s, 0);
+            this_->optvalue = NULL;
+        } else if (this_->argc > 1) {
+            this_->argc--;
+            *(int *)opt->value = strtol(*++this_->argv, (char **)&s, 0);
+        } else {
+            argparse_error(this_, opt, "requires a value");
+        }
+        if (*s)
+            argparse_error(this_, opt, "expects a numerical value");
+        break;
+    default:
+        assert(0);
+    }
+
+skipped:
+    if (opt->callback) {
+        return opt->callback(this_, opt);
+    }
+
+    return 0;
+}
+
+static void
+argparse_options_check(const struct argparse_option *options)
+{
+    for (; options->type != ARGPARSE_OPT_END; options++) {
+        switch (options->type) {
+        case ARGPARSE_OPT_END:
+        case ARGPARSE_OPT_BOOLEAN:
+        case ARGPARSE_OPT_BIT:
+        case ARGPARSE_OPT_INTEGER:
+        case ARGPARSE_OPT_STRING:
+            continue;
+        default:
+            fprintf(stderr, "wrong option type: %d", options->type);
+            break;
+        }
+    }
+}
+
+static int
+argparse_short_opt(struct argparse *this_, const struct argparse_option *options)
+{
+    for (; options->type != ARGPARSE_OPT_END; options++) {
+        if (options->short_name == *this_->optvalue) {
+            this_->optvalue = this_->optvalue[1] ? this_->optvalue + 1 : NULL;
+            return argparse_getvalue(this_, options, 0);
+        }
+    }
+    return -2;
+}
+
+static int
+argparse_long_opt(struct argparse *this_, const struct argparse_option *options)
+{
+    for (; options->type != ARGPARSE_OPT_END; options++) {
+        const char *rest;
+        int opt_flags = 0;
+        if (!options->long_name)
+            continue;
+
+        rest = prefix_skip(this_->argv[0] + 2, options->long_name);
+        if (!rest) {
+            // Negation allowed?
+            if (options->flags & OPT_NONEG) {
+                continue;
+            }
+            // Only boolean/bit allow negation.
+            if (options->type != ARGPARSE_OPT_BOOLEAN && options->type != ARGPARSE_OPT_BIT) {
+                continue;
+            }
+
+            if (!prefix_cmp(this_->argv[0] + 2, "no-")) {
+                rest = prefix_skip(this_->argv[0] + 2 + 3, options->long_name);
+                if (!rest)
+                    continue;
+                opt_flags |= OPT_UNSET;
+            } else {
+                continue;
+            }
+        }
+        if (*rest) {
+            if (*rest != '=')
+                continue;
+            this_->optvalue = rest + 1;
+        }
+        return argparse_getvalue(this_, options, opt_flags);
+    }
+    return -2;
+}
+
+int
+argparse_init(struct argparse *this_, struct argparse_option *options,
+              const char *usage, int flags)
+{
+    memset(this_, 0, sizeof(*this_));
+    this_->options = options;
+    this_->usage = usage;
+    this_->flags = flags;
+    return 0;
+}
+
+int
+argparse_parse(struct argparse *this_, int argc, const char **argv)
+{
+    this_->argc = argc - 1;
+    this_->argv = argv + 1;
+    this_->out = argv;
+
+    argparse_options_check(this_->options);
+
+    for (; this_->argc; this_->argc--, this_->argv++) {
+        const char *arg = this_->argv[0];
+        if (arg[0] != '-' || !arg[1]) {
+            if (this_->flags & ARGPARSE_STOP_AT_NON_OPTION) {
+                goto end;
+            }
+            // if it's not option or is a single char '-', copy verbatimly
+            this_->out[this_->cpidx++] = this_->argv[0];
+            continue;
+        }
+        // short option
+        if (arg[1] != '-') {
+            this_->optvalue = arg + 1;
+            switch (argparse_short_opt(this_, this_->options)) {
+            case -1:
+                break;
+            case -2:
+                goto unknown;
+            }
+            while (this_->optvalue) {
+                switch (argparse_short_opt(this_, this_->options)) {
+                case -1:
+                    break;
+                case -2:
+                    goto unknown;
+                }
+            }
+            continue;
+        }
+        // if '--' presents
+        if (!arg[2]) {
+            this_->argc--;
+            this_->argv++;
+            break;
+        }
+        // long option
+        switch (argparse_long_opt(this_, this_->options)) {
+        case -1:
+            break;
+        case -2:
+            goto unknown;
+        }
+        continue;
+
+unknown:
+        fprintf(stderr, "error: unknown option `%s`\n", this_->argv[0]);
+        argparse_usage(this_);
+        exit(0);
+    }
+
+end:
+    memmove(this_->out + this_->cpidx, this_->argv,
+            this_->argc * sizeof(*this_->out));
+    this_->out[this_->cpidx + this_->argc] = NULL;
+
+    return this_->cpidx + this_->argc;
+}
+
+void
+argparse_usage(struct argparse *this_)
+{
+    fprintf(stdout, "Usage: %s\n", this_->usage);
+    fputc('\n', stdout);
+
+    const struct argparse_option *options;
+
+    // figure out best width
+    size_t usage_opts_width = 0;
+    size_t len;
+    options = this_->options;
+    for (; options->type != ARGPARSE_OPT_END; options++) {
+        len = 0;
+        if ((options)->short_name) {
+            len += 2;
+        }
+        if ((options)->short_name && (options)->long_name) {
+            len += 2;           // separator ", "
+        }
+        if ((options)->long_name) {
+            len += strlen((options)->long_name) + 2;
+        }
+        if (options->type == ARGPARSE_OPT_INTEGER) {
+            len += strlen("=<int>");
+        } else if (options->type == ARGPARSE_OPT_STRING) {
+            len += strlen("=<str>");
+        }
+        len = ceil((float)len / 4) * 4;
+        if (usage_opts_width < len) {
+            usage_opts_width = len;
+        }
+    }
+    usage_opts_width += 4;      // 4 spaces prefix
+
+    options = this_->options;
+    for (; options->type != ARGPARSE_OPT_END; options++) {
+        size_t pos;
+        int pad;
+        pos = fprintf(stdout, "    ");
+        if (options->short_name) {
+            pos += fprintf(stdout, "-%c", options->short_name);
+        }
+        if (options->long_name && options->short_name) {
+            pos += fprintf(stdout, ", ");
+        }
+        if (options->long_name) {
+            pos += fprintf(stdout, "--%s", options->long_name);
+        }
+        if (options->type == ARGPARSE_OPT_INTEGER) {
+            pos += fprintf(stdout, "=<int>");
+        } else if (options->type == ARGPARSE_OPT_STRING) {
+            pos += fprintf(stdout, "=<str>");
+        }
+        if (pos <= usage_opts_width) {
+            pad = usage_opts_width - pos;
+        } else {
+            fputc('\n', stdout);
+            pad = usage_opts_width;
+        }
+        fprintf(stdout, "%*s%s\n", pad + 2, "", options->help);
+    }
+}
+
+int
+argparse_help_cb(struct argparse *this_, const struct argparse_option *option)
+{
+    (void)option;
+    argparse_usage(this_);
+    exit(0);
+    return 0;
+}
+
+#if defined(__cplusplus)
+}
+#endif
diff --git a/server/src/json.c b/server/src/json.c
new file mode 100644
index 0000000..ded29e0
--- /dev/null
+++ b/server/src/json.c
@@ -0,0 +1,949 @@
+/* vim: set et ts=3 sw=3 sts=3 ft=c:
+ *
+ * Copyright (C) 2012, 2013, 2014 James McLaughlin et al.  All rights reserved.
+ * https://github.com/udp/json-parser
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include "json.h"
+
+#ifdef _MSC_VER
+   #ifndef _CRT_SECURE_NO_WARNINGS
+      #define _CRT_SECURE_NO_WARNINGS
+   #endif
+#endif
+
+#ifdef __cplusplus
+   const struct _json_value json_value_none; /* zero-d by ctor */
+#else
+   const struct _json_value json_value_none = { 0 };
+#endif
+
+#include <stdio.h>
+#include <string.h>
+#include <ctype.h>
+#include <math.h>
+
+typedef unsigned short json_uchar;
+
+static unsigned char hex_value (json_char c)
+{
+   if (isdigit(c))
+      return c - '0';
+
+   switch (c) {
+      case 'a': case 'A': return 0x0A;
+      case 'b': case 'B': return 0x0B;
+      case 'c': case 'C': return 0x0C;
+      case 'd': case 'D': return 0x0D;
+      case 'e': case 'E': return 0x0E;
+      case 'f': case 'F': return 0x0F;
+      default: return 0xFF;
+   }
+}
+
+typedef struct
+{
+   unsigned long used_memory;
+
+   unsigned int uint_max;
+   unsigned long ulong_max;
+
+   json_settings settings;
+   int first_pass;
+
+} json_state;
+
+static void * default_alloc (size_t size, int zero, void * user_data)
+{
+   return zero ? calloc (1, size) : malloc (size);
+}
+
+static void default_free (void * ptr, void * user_data)
+{
+   free (ptr);
+}
+
+static void * json_alloc (json_state * state, unsigned long size, int zero)
+{
+   if ((state->ulong_max - state->used_memory) < size)
+      return 0;
+
+   if (state->settings.max_memory
+         && (state->used_memory += size) > state->settings.max_memory)
+   {
+      return 0;
+   }
+
+   return state->settings.mem_alloc (size, zero, state->settings.user_data);
+}
+
+static int new_value
+   (json_state * state, json_value ** top, json_value ** root, json_value ** alloc, json_type type)
+{
+   json_value * value;
+   int values_size;
+
+   if (!state->first_pass)
+   {
+      value = *top = *alloc;
+      *alloc = (*alloc)->_reserved.next_alloc;
+
+      if (!*root)
+         *root = value;
+
+      switch (value->type)
+      {
+         case json_array:
+
+            if (! (value->u.array.values = (json_value **) json_alloc
+               (state, value->u.array.length * sizeof (json_value *), 0)) )
+            {
+               return 0;
+            }
+
+            value->u.array.length = 0;
+            break;
+
+         case json_object:
+
+            values_size = sizeof (*value->u.object.values) * value->u.object.length;
+
+            if (! ((*(void **) &value->u.object.values) = json_alloc
+                  (state, values_size + ((unsigned long) value->u.object.values), 0)) )
+            {
+               return 0;
+            }
+
+            value->_reserved.object_mem = (*(char **) &value->u.object.values) + values_size;
+
+            value->u.object.length = 0;
+            break;
+
+         case json_string:
+
+            if (! (value->u.string.ptr = (json_char *) json_alloc
+               (state, (value->u.string.length + 1) * sizeof (json_char), 0)) )
+            {
+               return 0;
+            }
+
+            value->u.string.length = 0;
+            break;
+
+         default:
+            break;
+      };
+
+      return 1;
+   }
+
+   value = (json_value *) json_alloc (state, sizeof (json_value), 1);
+
+   if (!value)
+      return 0;
+
+   if (!*root)
+      *root = value;
+
+   value->type = type;
+   value->parent = *top;
+
+   if (*alloc)
+      (*alloc)->_reserved.next_alloc = value;
+
+   *alloc = *top = value;
+
+   return 1;
+}
+
+#define e_off \
+   ((int) (i - cur_line_begin))
+
+#define whitespace \
+   case '\n': ++ cur_line;  cur_line_begin = i; \
+   case ' ': case '\t': case '\r'
+
+#define string_add(b)  \
+   do { if (!state.first_pass) string [string_length] = b;  ++ string_length; } while (0);
+
+static const long
+   flag_next             = 1 << 0,
+   flag_reproc           = 1 << 1,
+   flag_need_comma       = 1 << 2,
+   flag_seek_value       = 1 << 3, 
+   flag_escaped          = 1 << 4,
+   flag_string           = 1 << 5,
+   flag_need_colon       = 1 << 6,
+   flag_done             = 1 << 7,
+   flag_num_negative     = 1 << 8,
+   flag_num_zero         = 1 << 9,
+   flag_num_e            = 1 << 10,
+   flag_num_e_got_sign   = 1 << 11,
+   flag_num_e_negative   = 1 << 12,
+   flag_line_comment     = 1 << 13,
+   flag_block_comment    = 1 << 14;
+
+json_value * json_parse_ex (json_settings * settings,
+                            const json_char * json,
+                            size_t length,
+                            char * error_buf)
+{
+   json_char error [json_error_max];
+   unsigned int cur_line;
+   const json_char * cur_line_begin, * i, * end;
+   json_value * top, * root, * alloc = 0;
+   json_state state = { 0 };
+   long flags;
+   long num_digits = 0, num_e = 0;
+   json_int_t num_fraction = 0;
+
+   /* Skip UTF-8 BOM
+    */
+   if (length >= 3 && ((unsigned char) json [0]) == 0xEF
+                   && ((unsigned char) json [1]) == 0xBB
+                   && ((unsigned char) json [2]) == 0xBF)
+   {
+      json += 3;
+      length -= 3;
+   }
+
+   error[0] = '\0';
+   end = (json + length);
+
+   memcpy (&state.settings, settings, sizeof (json_settings));
+
+   if (!state.settings.mem_alloc)
+      state.settings.mem_alloc = default_alloc;
+
+   if (!state.settings.mem_free)
+      state.settings.mem_free = default_free;
+
+   memset (&state.uint_max, 0xFF, sizeof (state.uint_max));
+   memset (&state.ulong_max, 0xFF, sizeof (state.ulong_max));
+
+   state.uint_max -= 8; /* limit of how much can be added before next check */
+   state.ulong_max -= 8;
+
+   for (state.first_pass = 1; state.first_pass >= 0; -- state.first_pass)
+   {
+      json_uchar uchar;
+      unsigned char uc_b1, uc_b2, uc_b3, uc_b4;
+      json_char * string = 0;
+      unsigned int string_length = 0;
+
+      top = root = 0;
+      flags = flag_seek_value;
+
+      cur_line = 1;
+      cur_line_begin = json;
+
+      for (i = json ;; ++ i)
+      {
+         json_char b = (i == end ? 0 : *i);
+         
+         if (flags & flag_string)
+         {
+            if (!b)
+            {  sprintf (error, "Unexpected EOF in string (at %d:%d)", cur_line, e_off);
+               goto e_failed;
+            }
+
+            if (string_length > state.uint_max)
+               goto e_overflow;
+
+            if (flags & flag_escaped)
+            {
+               flags &= ~ flag_escaped;
+
+               switch (b)
+               {
+                  case 'b':  string_add ('\b');  break;
+                  case 'f':  string_add ('\f');  break;
+                  case 'n':  string_add ('\n');  break;
+                  case 'r':  string_add ('\r');  break;
+                  case 't':  string_add ('\t');  break;
+                  case 'u':
+
+                    if (end - i < 4 || 
+                        (uc_b1 = hex_value (*++ i)) == 0xFF || (uc_b2 = hex_value (*++ i)) == 0xFF
+                          || (uc_b3 = hex_value (*++ i)) == 0xFF || (uc_b4 = hex_value (*++ i)) == 0xFF)
+                    {
+                        sprintf (error, "Invalid character value `%c` (at %d:%d)", b, cur_line, e_off);
+                        goto e_failed;
+                    }
+
+                    uc_b1 = uc_b1 * 16 + uc_b2;
+                    uc_b2 = uc_b3 * 16 + uc_b4;
+
+                    uchar = ((json_char) uc_b1) * 256 + uc_b2;
+
+                    if (sizeof (json_char) >= sizeof (json_uchar) || (uc_b1 == 0 && uc_b2 <= 0x7F))
+                    {
+                       string_add ((json_char) uchar);
+                       break;
+                    }
+
+                    if (uchar <= 0x7FF)
+                    {
+                        if (state.first_pass)
+                           string_length += 2;
+                        else
+                        {  string [string_length ++] = 0xC0 | ((uc_b2 & 0xC0) >> 6) | ((uc_b1 & 0x7) << 2);
+                           string [string_length ++] = 0x80 | (uc_b2 & 0x3F);
+                        }
+
+                        break;
+                    }
+
+                    if (state.first_pass)
+                       string_length += 3;
+                    else
+                    {  string [string_length ++] = 0xE0 | ((uc_b1 & 0xF0) >> 4);
+                       string [string_length ++] = 0x80 | ((uc_b1 & 0xF) << 2) | ((uc_b2 & 0xC0) >> 6);
+                       string [string_length ++] = 0x80 | (uc_b2 & 0x3F);
+                    }
+
+                    break;
+
+                  default:
+                     string_add (b);
+               };
+
+               continue;
+            }
+
+            if (b == '\\')
+            {
+               flags |= flag_escaped;
+               continue;
+            }
+
+            if (b == '"')
+            {
+               if (!state.first_pass)
+                  string [string_length] = 0;
+
+               flags &= ~ flag_string;
+               string = 0;
+
+               switch (top->type)
+               {
+                  case json_string:
+
+                     top->u.string.length = string_length;
+                     flags |= flag_next;
+
+                     break;
+
+                  case json_object:
+
+                     if (state.first_pass)
+                        (*(json_char **) &top->u.object.values) += string_length + 1;
+                     else
+                     {  
+                        top->u.object.values [top->u.object.length].name
+                           = (json_char *) top->_reserved.object_mem;
+
+                        top->u.object.values [top->u.object.length].name_length
+                           = string_length;
+
+                        (*(json_char **) &top->_reserved.object_mem) += string_length + 1;
+                     }
+
+                     flags |= flag_seek_value | flag_need_colon;
+                     continue;
+
+                  default:
+                     break;
+               };
+            }
+            else
+            {
+               string_add (b);
+               continue;
+            }
+         }
+
+         if (state.settings.settings & json_enable_comments)
+         {
+            if (flags & (flag_line_comment | flag_block_comment))
+            {
+               if (flags & flag_line_comment)
+               {
+                  if (b == '\r' || b == '\n' || !b)
+                  {
+                     flags &= ~ flag_line_comment;
+                     -- i;  /* so null can be reproc'd */
+                  }
+
+                  continue;
+               }
+
+               if (flags & flag_block_comment)
+               {
+                  if (!b)
+                  {  sprintf (error, "%d:%d: Unexpected EOF in block comment", cur_line, e_off);
+                     goto e_failed;
+                  }
+
+                  if (b == '*' && i < (end - 1) && i [1] == '/')
+                  {
+                     flags &= ~ flag_block_comment;
+                     ++ i;  /* skip closing sequence */
+                  }
+
+                  continue;
+               }
+            }
+            else if (b == '/')
+            {
+               if (! (flags & (flag_seek_value | flag_done)) && top->type != json_object)
+               {
+                  sprintf (error, "%d:%d: Comment not allowed here", cur_line, e_off);
+                  goto e_failed;
+               }
+
+               if (++ i == end)
+               {  sprintf (error, "%d:%d: EOF unexpected", cur_line, e_off);
+                  goto e_failed;
+               }
+
+               switch (b = *i)
+               {
+                  case '/':
+                     flags |= flag_line_comment;
+                     continue;
+
+                  case '*':
+                     flags |= flag_block_comment;
+                     continue;
+
+                  default:
+                     sprintf (error, "%d:%d: Unexpected `%c` in comment opening sequence", cur_line, e_off, b);
+                     goto e_failed;
+               };
+            }
+         }
+
+         if (flags & flag_done)
+         {
+            if (!b)
+               break;
+
+            switch (b)
+            {
+               whitespace:
+                  continue;
+
+               default:
+                  sprintf (error, "%d:%d: Trailing garbage: `%c`", cur_line, e_off, b);
+                  goto e_failed;
+            };
+         }
+
+         if (flags & flag_seek_value)
+         {
+            switch (b)
+            {
+               whitespace:
+                  continue;
+
+               case ']':
+
+                  if (top->type == json_array)
+                     flags = (flags & ~ (flag_need_comma | flag_seek_value)) | flag_next;
+                  else
+                  {  sprintf (error, "%d:%d: Unexpected ]", cur_line, e_off);
+                     goto e_failed;
+                  }
+
+                  break;
+
+               default:
+
+                  if (flags & flag_need_comma)
+                  {
+                     if (b == ',')
+                     {  flags &= ~ flag_need_comma;
+                        continue;
+                     }
+                     else
+                     {  sprintf (error, "%d:%d: Expected , before %c", cur_line, e_off, b);
+                        goto e_failed;
+                     }
+                  }
+
+                  if (flags & flag_need_colon)
+                  {
+                     if (b == ':')
+                     {  flags &= ~ flag_need_colon;
+                        continue;
+                     }
+                     else
+                     {  sprintf (error, "%d:%d: Expected : before %c", cur_line, e_off, b);
+                        goto e_failed;
+                     }
+                  }
+
+                  flags &= ~ flag_seek_value;
+
+                  switch (b)
+                  {
+                     case '{':
+
+                        if (!new_value (&state, &top, &root, &alloc, json_object))
+                           goto e_alloc_failure;
+
+                        continue;
+
+                     case '[':
+
+                        if (!new_value (&state, &top, &root, &alloc, json_array))
+                           goto e_alloc_failure;
+
+                        flags |= flag_seek_value;
+                        continue;
+
+                     case '"':
+
+                        if (!new_value (&state, &top, &root, &alloc, json_string))
+                           goto e_alloc_failure;
+
+                        flags |= flag_string;
+
+                        string = top->u.string.ptr;
+                        string_length = 0;
+
+                        continue;
+
+                     case 't':
+
+                        if ((end - i) < 3 || *(++ i) != 'r' || *(++ i) != 'u' || *(++ i) != 'e')
+                           goto e_unknown_value;
+
+                        if (!new_value (&state, &top, &root, &alloc, json_boolean))
+                           goto e_alloc_failure;
+
+                        top->u.boolean = 1;
+
+                        flags |= flag_next;
+                        break;
+
+                     case 'f':
+
+                        if ((end - i) < 4 || *(++ i) != 'a' || *(++ i) != 'l' || *(++ i) != 's' || *(++ i) != 'e')
+                           goto e_unknown_value;
+
+                        if (!new_value (&state, &top, &root, &alloc, json_boolean))
+                           goto e_alloc_failure;
+
+                        flags |= flag_next;
+                        break;
+
+                     case 'n':
+
+                        if ((end - i) < 3 || *(++ i) != 'u' || *(++ i) != 'l' || *(++ i) != 'l')
+                           goto e_unknown_value;
+
+                        if (!new_value (&state, &top, &root, &alloc, json_null))
+                           goto e_alloc_failure;
+
+                        flags |= flag_next;
+                        break;
+
+                     default:
+
+                        if (isdigit (b) || b == '-')
+                        {
+                           if (!new_value (&state, &top, &root, &alloc, json_integer))
+                              goto e_alloc_failure;
+
+                           if (!state.first_pass)
+                           {
+                              while (isdigit (b) || b == '+' || b == '-'
+                                        || b == 'e' || b == 'E' || b == '.')
+                              {
+                                 if ( (++ i) == end)
+                                 {
+                                    b = 0;
+                                    break;
+                                 }
+
+                                 b = *i;
+                              }
+
+                              flags |= flag_next | flag_reproc;
+                              break;
+                           }
+
+                           flags &= ~ (flag_num_negative | flag_num_e |
+                                        flag_num_e_got_sign | flag_num_e_negative |
+                                           flag_num_zero);
+
+                           num_digits = 0;
+                           num_fraction = 0;
+                           num_e = 0;
+
+                           if (b != '-')
+                           {
+                              flags |= flag_reproc;
+                              break;
+                           }
+
+                           flags |= flag_num_negative;
+                           continue;
+                        }
+                        else
+                        {  sprintf (error, "%d:%d: Unexpected %c when seeking value", cur_line, e_off, b);
+                           goto e_failed;
+                        }
+                  };
+            };
+         }
+         else
+         {
+            switch (top->type)
+            {
+            case json_object:
+               
+               switch (b)
+               {
+                  whitespace:
+                     continue;
+
+                  case '"':
+
+                     if (flags & flag_need_comma)
+                     {
+                        sprintf (error, "%d:%d: Expected , before \"", cur_line, e_off);
+                        goto e_failed;
+                     }
+
+                     flags |= flag_string;
+
+                     string = (json_char *) top->_reserved.object_mem;
+                     string_length = 0;
+
+                     break;
+                  
+                  case '}':
+
+                     flags = (flags & ~ flag_need_comma) | flag_next;
+                     break;
+
+                  case ',':
+
+                     if (flags & flag_need_comma)
+                     {
+                        flags &= ~ flag_need_comma;
+                        break;
+                     }
+
+                  default:
+
+                     sprintf (error, "%d:%d: Unexpected `%c` in object", cur_line, e_off, b);
+                     goto e_failed;
+               };
+
+               break;
+
+            case json_integer:
+            case json_double:
+
+               if (isdigit (b))
+               {
+                  ++ num_digits;
+
+                  if (top->type == json_integer || flags & flag_num_e)
+                  {
+                     if (! (flags & flag_num_e))
+                     {
+                        if (flags & flag_num_zero)
+                        {  sprintf (error, "%d:%d: Unexpected `0` before `%c`", cur_line, e_off, b);
+                           goto e_failed;
+                        }
+
+                        if (num_digits == 1 && b == '0')
+                           flags |= flag_num_zero;
+                     }
+                     else
+                     {
+                        flags |= flag_num_e_got_sign;
+                        num_e = (num_e * 10) + (b - '0');
+                        continue;
+                     }
+
+                     top->u.integer = (top->u.integer * 10) + (b - '0');
+                     continue;
+                  }
+
+                  num_fraction = (num_fraction * 10) + (b - '0');
+                  continue;
+               }
+
+               if (b == '+' || b == '-')
+               {
+                  if ( (flags & flag_num_e) && !(flags & flag_num_e_got_sign))
+                  {
+                     flags |= flag_num_e_got_sign;
+
+                     if (b == '-')
+                        flags |= flag_num_e_negative;
+
+                     continue;
+                  }
+               }
+               else if (b == '.' && top->type == json_integer)
+               {
+                  if (!num_digits)
+                  {  sprintf (error, "%d:%d: Expected digit before `.`", cur_line, e_off);
+                     goto e_failed;
+                  }
+
+                  top->type = json_double;
+                  top->u.dbl = (double) top->u.integer;
+
+                  num_digits = 0;
+                  continue;
+               }
+
+               if (! (flags & flag_num_e))
+               {
+                  if (top->type == json_double)
+                  {
+                     if (!num_digits)
+                     {  sprintf (error, "%d:%d: Expected digit after `.`", cur_line, e_off);
+                        goto e_failed;
+                     }
+
+                     top->u.dbl += ((double) num_fraction) / (pow (10, (double) num_digits));
+                  }
+
+                  if (b == 'e' || b == 'E')
+                  {
+                     flags |= flag_num_e;
+
+                     if (top->type == json_integer)
+                     {
+                        top->type = json_double;
+                        top->u.dbl = (double) top->u.integer;
+                     }
+
+                     num_digits = 0;
+                     flags &= ~ flag_num_zero;
+
+                     continue;
+                  }
+               }
+               else
+               {
+                  if (!num_digits)
+                  {  sprintf (error, "%d:%d: Expected digit after `e`", cur_line, e_off);
+                     goto e_failed;
+                  }
+
+                  top->u.dbl *= pow (10, (double) (flags & flag_num_e_negative ? - num_e : num_e));
+               }
+
+               if (flags & flag_num_negative)
+               {
+                  if (top->type == json_integer)
+                     top->u.integer = - top->u.integer;
+                  else
+                     top->u.dbl = - top->u.dbl;
+               }
+
+               flags |= flag_next | flag_reproc;
+               break;
+
+            default:
+               break;
+            };
+         }
+
+         if (flags & flag_reproc)
+         {
+            flags &= ~ flag_reproc;
+            -- i;
+         }
+
+         if (flags & flag_next)
+         {
+            flags = (flags & ~ flag_next) | flag_need_comma;
+
+            if (!top->parent)
+            {
+               /* root value done */
+
+               flags |= flag_done;
+               continue;
+            }
+
+            if (top->parent->type == json_array)
+               flags |= flag_seek_value;
+               
+            if (!state.first_pass)
+            {
+               json_value * parent = top->parent;
+
+               switch (parent->type)
+               {
+                  case json_object:
+
+                     parent->u.object.values
+                        [parent->u.object.length].value = top;
+
+                     break;
+
+                  case json_array:
+
+                     parent->u.array.values
+                           [parent->u.array.length] = top;
+
+                     break;
+
+                  default:
+                     break;
+               };
+            }
+
+            if ( (++ top->parent->u.array.length) > state.uint_max)
+               goto e_overflow;
+
+            top = top->parent;
+
+            continue;
+         }
+      }
+
+      alloc = root;
+   }
+
+   return root;
+
+e_unknown_value:
+
+   sprintf (error, "%d:%d: Unknown value", cur_line, e_off);
+   goto e_failed;
+
+e_alloc_failure:
+
+   strcpy (error, "Memory allocation failure");
+   goto e_failed;
+
+e_overflow:
+
+   sprintf (error, "%d:%d: Too long (caught overflow)", cur_line, e_off);
+   goto e_failed;
+
+e_failed:
+
+   if (error_buf)
+   {
+      if (*error)
+         strcpy (error_buf, error);
+      else
+         strcpy (error_buf, "Unknown error");
+   }
+
+   if (state.first_pass)
+      alloc = root;
+
+   while (alloc)
+   {
+      top = alloc->_reserved.next_alloc;
+      state.settings.mem_free (alloc, state.settings.user_data);
+      alloc = top;
+   }
+
+   if (!state.first_pass)
+      json_value_free_ex (&state.settings, root);
+
+   return 0;
+}
+
+json_value * json_parse (const json_char * json, size_t length)
+{
+   json_settings settings = { 0 };
+   return json_parse_ex (&settings, json, length, 0);
+}
+
+void json_value_free_ex (json_settings * settings, json_value * value)
+{
+   json_value * cur_value;
+
+   if (!value)
+      return;
+
+   value->parent = 0;
+
+   while (value)
+   {
+      switch (value->type)
+      {
+         case json_array:
+
+            if (!value->u.array.length)
+            {
+               settings->mem_free (value->u.array.values, settings->user_data);
+               break;
+            }
+
+            value = value->u.array.values [-- value->u.array.length];
+            continue;
+
+         case json_object:
+
+            if (!value->u.object.length)
+            {
+               settings->mem_free (value->u.object.values, settings->user_data);
+               break;
+            }
+
+            value = value->u.object.values [-- value->u.object.length].value;
+            continue;
+
+         case json_string:
+
+            settings->mem_free (value->u.string.ptr, settings->user_data);
+            break;
+
+         default:
+            break;
+      };
+
+      cur_value = value;
+      value = value->parent;
+      settings->mem_free (cur_value, settings->user_data);
+   }
+}
+
+void json_value_free (json_value * value)
+{
+   json_settings settings = { 0 };
+   settings.mem_free = default_free;
+   json_value_free_ex (&settings, value);
+}
diff --git a/server/src/main.cpp b/server/src/main.cpp
new file mode 100644
index 0000000..5ba7025
--- /dev/null
+++ b/server/src/main.cpp
@@ -0,0 +1,468 @@
+#define __STDC_FORMAT_MACROS
+#include <inttypes.h>
+#include <time.h>
+#include <detect.h>
+#include <system.h>
+#include <argparse.h>
+#include <json.h>
+#include "server.h"
+#include "main.h"
+
+#if defined(CONF_FAMILY_UNIX)
+	#include <signal.h>
+#endif
+
+#ifndef PRId64
+	#define PRId64 "I64d"
+#endif
+
+static volatile int gs_Running = 1;
+static volatile int gs_ReloadConfig = 0;
+
+static void ExitFunc(int Signal)
+{
+	printf("[EXIT] Caught signal %d\n", Signal);
+	gs_Running = 0;
+}
+
+static void ReloadFunc(int Signal)
+{
+	printf("[RELOAD] Caught signal %d\n", Signal);
+	gs_ReloadConfig = 1;
+}
+
+CConfig::CConfig()
+{
+	// Initialize to default values
+	m_Verbose = false; // -v, --verbose
+	str_copy(m_aConfigFile, "config.json", sizeof(m_aConfigFile)); // -c, --config
+	str_copy(m_aWebDir, "../web/", sizeof(m_aJSONFile)); // -d, --web-dir
+	str_copy(m_aTemplateFile, "template.html", sizeof(m_aTemplateFile));
+	str_copy(m_aJSONFile, "json/stats.json", sizeof(m_aJSONFile));
+	str_copy(m_aBindAddr, "", sizeof(m_aBindAddr)); // -b, --bind
+	m_Port = 35601; // -p, --port
+}
+
+CMain::CMain(CConfig Config) : m_Config(Config)
+{
+	mem_zero(m_aClients, sizeof(m_aClients));
+	for(int i = 0; i < NET_MAX_CLIENTS; i++)
+		m_aClients[i].m_ClientNetID = -1;
+}
+
+CMain::CClient *CMain::ClientNet(int ClientNetID)
+{
+	if(ClientNetID < 0 || ClientNetID >= NET_MAX_CLIENTS)
+		return 0;
+
+	for(int i = 0; i < NET_MAX_CLIENTS; i++)
+	{
+		if(Client(i)->m_ClientNetID == ClientNetID)
+			return Client(i);
+	}
+
+	return 0;
+}
+
+int CMain::ClientNetToClient(int ClientNetID)
+{
+	if(ClientNetID < 0 || ClientNetID >= NET_MAX_CLIENTS)
+		return -1;
+
+	for(int i = 0; i < NET_MAX_CLIENTS; i++)
+	{
+		if(Client(i)->m_ClientNetID == ClientNetID)
+			return i;
+	}
+
+	return -1;
+}
+
+void CMain::OnNewClient(int ClientNetID, int ClientID)
+{
+	dbg_msg("main", "OnNewClient(ncid=%d, cid=%d)", ClientNetID, ClientID);
+	Client(ClientID)->m_ClientNetID = ClientNetID;
+	Client(ClientID)->m_ClientNetType = m_Server.Network()->ClientAddr(ClientNetID)->type;
+	Client(ClientID)->m_TimeConnected = time_get();
+	Client(ClientID)->m_Connected = true;
+
+	if(Client(ClientID)->m_ClientNetType == NETTYPE_IPV4)
+		Client(ClientID)->m_Stats.m_Online4 = true;
+	else if(Client(ClientID)->m_ClientNetType == NETTYPE_IPV6)
+		Client(ClientID)->m_Stats.m_Online6 = true;
+}
+
+void CMain::OnDelClient(int ClientNetID)
+{
+	int ClientID = ClientNetToClient(ClientNetID);
+	dbg_msg("main", "OnDelClient(ncid=%d, cid=%d)", ClientNetID, ClientID);
+	if(ClientID >= 0 && ClientID < NET_MAX_CLIENTS)
+	{
+		Client(ClientID)->m_Connected = false;
+		Client(ClientID)->m_ClientNetID = -1;
+		Client(ClientID)->m_ClientNetType = NETTYPE_INVALID;
+		mem_zero(&Client(ClientID)->m_Stats, sizeof(CClient::CStats));
+	}
+}
+
+int CMain::HandleMessage(int ClientNetID, char *pMessage)
+{
+	CClient *pClient = ClientNet(ClientNetID);
+	if(!pClient)
+		return true;
+
+	if(str_comp_num(pMessage, "update", sizeof("update")-1) == 0)
+	{
+		char *pData = str_skip_whitespaces(&pMessage[sizeof("update")-1]);
+
+		// parse json data
+		json_settings JsonSettings;
+		mem_zero(&JsonSettings, sizeof(JsonSettings));
+		char aError[256];
+		json_value *pJsonData = json_parse_ex(&JsonSettings, pData, strlen(pData), aError);
+		if(!pJsonData)
+		{
+			dbg_msg("main", "JSON Error: %s", aError);
+			if(pClient->m_Stats.m_Pong)
+				m_Server.Network()->Send(ClientNetID, "1");
+			return 1;
+		}
+
+		// extract data
+		const json_value &rStart = (*pJsonData);
+		if(rStart["uptime"].type)
+			pClient->m_Stats.m_Uptime = rStart["uptime"].u.integer;
+		if(rStart["load_1"].type)
+			pClient->m_Stats.m_Load_1 = rStart["load_1"].u.dbl;
+		if(rStart["load_5"].type)
+			pClient->m_Stats.m_Load_5 = rStart["load_5"].u.dbl;
+		if(rStart["load_15"].type)
+			pClient->m_Stats.m_Load_15 = rStart["load_15"].u.dbl;
+		if(rStart["network_rx"].type)
+			pClient->m_Stats.m_NetworkRx = rStart["network_rx"].u.integer;
+		if(rStart["network_tx"].type)
+			pClient->m_Stats.m_NetworkTx = rStart["network_tx"].u.integer;
+		if(rStart["network_in"].type)
+			pClient->m_Stats.m_NetworkIN = rStart["network_in"].u.integer;
+		if(rStart["network_out"].type)
+			pClient->m_Stats.m_NetworkOUT = rStart["network_out"].u.integer;
+		if(rStart["memory_total"].type)
+			pClient->m_Stats.m_MemTotal = rStart["memory_total"].u.integer;
+		if(rStart["memory_used"].type)
+			pClient->m_Stats.m_MemUsed = rStart["memory_used"].u.integer;
+		if(rStart["swap_total"].type)
+			pClient->m_Stats.m_SwapTotal = rStart["swap_total"].u.integer;
+		if(rStart["swap_used"].type)
+			pClient->m_Stats.m_SwapUsed = rStart["swap_used"].u.integer;
+		if(rStart["hdd_total"].type)
+			pClient->m_Stats.m_HDDTotal = rStart["hdd_total"].u.integer;
+		if(rStart["hdd_used"].type)
+			pClient->m_Stats.m_HDDUsed = rStart["hdd_used"].u.integer;
+		if(rStart["cpu"].type)
+			pClient->m_Stats.m_CPU = rStart["cpu"].u.dbl;
+		if(rStart["online4"].type && pClient->m_ClientNetType == NETTYPE_IPV6)
+			pClient->m_Stats.m_Online4 = rStart["online4"].u.boolean;
+		if(rStart["online6"].type && pClient->m_ClientNetType == NETTYPE_IPV4)
+			pClient->m_Stats.m_Online6 = rStart["online6"].u.boolean;
+		if(rStart["ip_status"].type)
+			pClient->m_Stats.m_IpStatus = rStart["ip_status"].u.boolean;
+		if(rStart["custom"].type == json_string)
+			str_copy(pClient->m_Stats.m_aCustom, rStart["custom"].u.string.ptr, sizeof(pClient->m_Stats.m_aCustom));
+
+		if(m_Config.m_Verbose)
+		{
+			if(rStart["online4"].type)
+				dbg_msg("main", "Online4: %s\nUptime: %" PRId64 "\nIpStatus: %s\nLoad_1: %f\nLoad_5: %f\nLoad_15: %f\nNetworkRx: %" PRId64 "\nNetworkTx: %" PRId64 "\nNetworkIN: %" PRId64 "\nNetworkOUT: %" PRId64 "\nMemTotal: %" PRId64 "\nMemUsed: %" PRId64 "\nSwapTotal: %" PRId64 "\nSwapUsed: %" PRId64 "\nHDDTotal: %" PRId64 "\nHDDUsed: %" PRId64 "\nCPU: %f\n",
+					rStart["online4"].u.boolean ? "true" : "false",
+					pClient->m_Stats.m_Uptime,
+					pClient->m_Stats.m_IpStatus ? "true" : "false",
+					pClient->m_Stats.m_Load_1, pClient->m_Stats.m_Load_5, pClient->m_Stats.m_Load_15, pClient->m_Stats.m_NetworkRx, pClient->m_Stats.m_NetworkTx, pClient->m_Stats.m_NetworkIN, pClient->m_Stats.m_NetworkOUT, pClient->m_Stats.m_MemTotal, pClient->m_Stats.m_MemUsed, pClient->m_Stats.m_SwapTotal, pClient->m_Stats.m_SwapUsed, pClient->m_Stats.m_HDDTotal, pClient->m_Stats.m_HDDUsed, pClient->m_Stats.m_CPU);
+			else if(rStart["online6"].type)
+				dbg_msg("main", "Online6: %s\nUptime: %" PRId64 "\nIpStatus: %s\nLoad_1: %f\nLoad_5: %f\nLoad_15: %f\nNetworkRx: %" PRId64 "\nNetworkTx: %" PRId64 "\nNetworkIN: %" PRId64 "\nNetworkOUT: %" PRId64 "\nMemTotal: %" PRId64 "\nMemUsed: %" PRId64 "\nSwapTotal: %" PRId64 "\nSwapUsed: %" PRId64 "\nHDDTotal: %" PRId64 "\nHDDUsed: %" PRId64 "\nCPU: %f\n",
+					rStart["online6"].u.boolean ? "true" : "false",
+					pClient->m_Stats.m_Uptime,
+					pClient->m_Stats.m_IpStatus ? "true" : "false",
+					pClient->m_Stats.m_Load_1, pClient->m_Stats.m_Load_5, pClient->m_Stats.m_Load_15, pClient->m_Stats.m_NetworkRx, pClient->m_Stats.m_NetworkTx, pClient->m_Stats.m_NetworkIN, pClient->m_Stats.m_NetworkOUT, pClient->m_Stats.m_MemTotal, pClient->m_Stats.m_MemUsed, pClient->m_Stats.m_SwapTotal, pClient->m_Stats.m_SwapUsed, pClient->m_Stats.m_HDDTotal, pClient->m_Stats.m_HDDUsed, pClient->m_Stats.m_CPU);
+			else
+				dbg_msg("main", "Uptime: %" PRId64 "\nIpStatus: %s\nLoad_1: %f\nLoad_5: %f\nLoad_15: %f\nNetworkRx: %" PRId64 "\nNetworkTx: %" PRId64 "\nNetworkIN: %" PRId64 "\nNetworkOUT: %" PRId64 "\nMemTotal: %" PRId64 "\nMemUsed: %" PRId64 "\nSwapTotal: %" PRId64 "\nSwapUsed: %" PRId64 "\nHDDTotal: %" PRId64 "\nHDDUsed: %" PRId64 "\nCPU: %f\n",
+					pClient->m_Stats.m_Uptime,
+					pClient->m_Stats.m_IpStatus ? "true" : "false",
+					pClient->m_Stats.m_Load_1, pClient->m_Stats.m_Load_5, pClient->m_Stats.m_Load_15, pClient->m_Stats.m_NetworkRx, pClient->m_Stats.m_NetworkTx, pClient->m_Stats.m_NetworkIN, pClient->m_Stats.m_NetworkOUT, pClient->m_Stats.m_MemTotal, pClient->m_Stats.m_MemUsed, pClient->m_Stats.m_SwapTotal, pClient->m_Stats.m_SwapUsed, pClient->m_Stats.m_HDDTotal, pClient->m_Stats.m_HDDUsed, pClient->m_Stats.m_CPU);
+		}
+
+		// clean up
+		json_value_free(pJsonData);
+
+		if(pClient->m_Stats.m_Pong)
+			m_Server.Network()->Send(ClientNetID, "0");
+		return 0;
+	}
+	else if(str_comp_num(pMessage, "pong", sizeof("pong")-1) == 0)
+	{
+		char *pData = str_skip_whitespaces(&pMessage[sizeof("pong")-1]);
+
+		if(!str_comp(pData, "0") || !str_comp(pData, "off"))
+			pClient->m_Stats.m_Pong = false;
+		else if(!str_comp(pData, "1") || !str_comp(pData, "on"))
+			pClient->m_Stats.m_Pong = true;
+
+		return 0;
+	}
+
+	if(pClient->m_Stats.m_Pong)
+		m_Server.Network()->Send(ClientNetID, "1");
+
+	return 1;
+}
+
+void CMain::JSONUpdateThread(void *pUser)
+{
+	CJSONUpdateThreadData *m_pJSONUpdateThreadData = (CJSONUpdateThreadData *)pUser;
+	CClient *pClients = m_pJSONUpdateThreadData->pClients;
+	CConfig *pConfig = m_pJSONUpdateThreadData->pConfig;
+
+	while(gs_Running)
+	{
+		char aFileBuf[2048*NET_MAX_CLIENTS];
+		char *pBuf = aFileBuf;
+
+		str_format(pBuf, sizeof(aFileBuf), "{\n\"servers\": [\n");
+		pBuf += strlen(pBuf);
+
+		for(int i = 0; i < NET_MAX_CLIENTS; i++)
+		{
+			if(!pClients[i].m_Active || pClients[i].m_Disabled)
+				continue;
+
+			if(pClients[i].m_Connected)
+			{
+				// Uptime
+				char aUptime[16];
+				int Days = pClients[i].m_Stats.m_Uptime/60.0/60.0/24.0;
+				if(Days > 0)
+				{
+					if(Days > 1)
+						str_format(aUptime, sizeof(aUptime), "%d 天", Days);
+					else
+						str_format(aUptime, sizeof(aUptime), "%d 天", Days);
+				}
+				else
+					str_format(aUptime, sizeof(aUptime), "%02d:%02d:%02d", (int)(pClients[i].m_Stats.m_Uptime/60.0/60.0), (int)((pClients[i].m_Stats.m_Uptime/60)%60), (int)((pClients[i].m_Stats.m_Uptime)%60));
+
+				str_format(pBuf, sizeof(aFileBuf) - (pBuf - aFileBuf),
+				 "{ \"name\": \"%s\",\"type\": \"%s\",\"host\": \"%s\",\"location\": \"%s\",\"online4\": %s, \"online6\": %s,\"ip_status\": %s,\"uptime\": \"%s\",\"load_1\": %.2f, \"load_5\": %.2f, \"load_15\": %.2f,\"network_rx\": %" PRId64 ", \"network_tx\": %" PRId64 ", \"network_in\": %" PRId64 ", \"network_out\": %" PRId64 ", \"cpu\": %d, \"memory_total\": %" PRId64 ", \"memory_used\": %" PRId64 ", \"swap_total\": %" PRId64 ", \"swap_used\": %" PRId64 ", \"hdd_total\": %" PRId64 ", \"hdd_used\": %" PRId64 ", \"custom\": \"%s\" },\n",
+					pClients[i].m_aName,pClients[i].m_aType,pClients[i].m_aHost,pClients[i].m_aLocation,
+					pClients[i].m_Stats.m_Online4 ? "true" : "false",pClients[i].m_Stats.m_Online6 ? "true" : "false",pClients[i].m_Stats.m_IpStatus ? "true": "false",
+					aUptime, pClients[i].m_Stats.m_Load_1, pClients[i].m_Stats.m_Load_5, pClients[i].m_Stats.m_Load_15, pClients[i].m_Stats.m_NetworkRx, pClients[i].m_Stats.m_NetworkTx, pClients[i].m_Stats.m_NetworkIN, pClients[i].m_Stats.m_NetworkOUT, (int)pClients[i].m_Stats.m_CPU, pClients[i].m_Stats.m_MemTotal, pClients[i].m_Stats.m_MemUsed, pClients[i].m_Stats.m_SwapTotal, pClients[i].m_Stats.m_SwapUsed, pClients[i].m_Stats.m_HDDTotal, pClients[i].m_Stats.m_HDDUsed, pClients[i].m_Stats.m_aCustom);
+				pBuf += strlen(pBuf);
+			}
+			else
+			{
+				str_format(pBuf, sizeof(aFileBuf) - (pBuf - aFileBuf), "{ \"name\": \"%s\", \"type\": \"%s\", \"host\": \"%s\", \"location\": \"%s\", \"online4\": false, \"online6\": false },\n",
+					pClients[i].m_aName, pClients[i].m_aType, pClients[i].m_aHost, pClients[i].m_aLocation);
+				pBuf += strlen(pBuf);
+			}
+		}
+		if(!m_pJSONUpdateThreadData->m_ReloadRequired)
+			str_format(pBuf - 2, sizeof(aFileBuf) - (pBuf - aFileBuf), "\n],\n\"updated\": \"%lld\"\n}", (long long)time(/*ago*/0));
+		else
+		{
+			str_format(pBuf - 2, sizeof(aFileBuf) - (pBuf - aFileBuf), "\n],\n\"updated\": \"%lld\",\n\"reload\": true\n}", (long long)time(/*ago*/0));
+			m_pJSONUpdateThreadData->m_ReloadRequired--;
+		}
+		pBuf += strlen(pBuf);
+
+		char aJSONFileTmp[1024];
+		str_format(aJSONFileTmp, sizeof(aJSONFileTmp), "%s~", pConfig->m_aJSONFile);
+		IOHANDLE File = io_open(aJSONFileTmp, IOFLAG_WRITE);
+		if(!File)
+		{
+			dbg_msg("main", "Couldn't open %s", aJSONFileTmp);
+			exit(1);
+		}
+		io_write(File, aFileBuf, (pBuf - aFileBuf));
+		io_flush(File);
+		io_close(File);
+		fs_rename(aJSONFileTmp, pConfig->m_aJSONFile);
+		thread_sleep(1000);
+	}
+	fs_remove(pConfig->m_aJSONFile);
+}
+
+int CMain::ReadConfig()
+{
+	// read and parse config
+	IOHANDLE File = io_open(m_Config.m_aConfigFile, IOFLAG_READ);
+	if(!File)
+	{
+		dbg_msg("main", "Couldn't open %s", m_Config.m_aConfigFile);
+		return 1;
+	}
+	int FileSize = (int)io_length(File);
+	char *pFileData = (char *)mem_alloc(FileSize + 1, 1);
+
+	io_read(File, pFileData, FileSize);
+	pFileData[FileSize] = 0;
+	io_close(File);
+
+	// parse json data
+	json_settings JsonSettings;
+	mem_zero(&JsonSettings, sizeof(JsonSettings));
+	char aError[256];
+	json_value *pJsonData = json_parse_ex(&JsonSettings, pFileData, strlen(pFileData), aError);
+	if(!pJsonData)
+	{
+		dbg_msg("main", "JSON Error in file %s: %s", m_Config.m_aConfigFile, aError);
+		mem_free(pFileData);
+		return 1;
+	}
+
+	// reset clients
+	for(int i = 0; i < NET_MAX_CLIENTS; i++)
+	{
+		if(!Client(i)->m_Active || !Client(i)->m_Connected)
+			continue;
+
+		m_Server.Network()->Drop(Client(i)->m_ClientNetID, "Server reloading...");
+	}
+	mem_zero(m_aClients, sizeof(m_aClients));
+	for(int i = 0; i < NET_MAX_CLIENTS; i++)
+		m_aClients[i].m_ClientNetID = -1;
+
+	// extract data
+	int ID = 0;
+	const json_value &rStart = (*pJsonData)["servers"];
+	if(rStart.type == json_array)
+	{
+		for(unsigned i = 0; i < rStart.u.array.length; i++)
+		{
+			if(ID < 0 || ID >= NET_MAX_CLIENTS)
+				continue;
+
+			Client(ID)->m_Active = true;
+			Client(ID)->m_Disabled = rStart[i]["disabled"].u.boolean;
+			str_copy(Client(ID)->m_aName, rStart[i]["name"].u.string.ptr, sizeof(Client(ID)->m_aName));
+			str_copy(Client(ID)->m_aUsername, rStart[i]["username"].u.string.ptr, sizeof(Client(ID)->m_aUsername));
+			str_copy(Client(ID)->m_aType, rStart[i]["type"].u.string.ptr, sizeof(Client(ID)->m_aType));
+			str_copy(Client(ID)->m_aHost, rStart[i]["host"].u.string.ptr, sizeof(Client(ID)->m_aHost));
+			str_copy(Client(ID)->m_aLocation, rStart[i]["location"].u.string.ptr, sizeof(Client(ID)->m_aLocation));
+			str_copy(Client(ID)->m_aPassword, rStart[i]["password"].u.string.ptr, sizeof(Client(ID)->m_aPassword));
+
+			if(m_Config.m_Verbose)
+			{
+				if(Client(ID)->m_Disabled)
+					dbg_msg("main", "[#%d: Name: \"%s\", Username: \"%s\", Type: \"%s\", Host: \"%s\", Location: \"%s\", Password: \"%s\"]",
+						ID, Client(ID)->m_aName, Client(ID)->m_aUsername, Client(ID)->m_aType, Client(ID)->m_aHost, Client(ID)->m_aLocation, Client(ID)->m_aPassword);
+				else
+					dbg_msg("main", "#%d: Name: \"%s\", Username: \"%s\", Type: \"%s\", Host: \"%s\", Location: \"%s\", Password: \"%s\"",
+						ID, Client(ID)->m_aName, Client(ID)->m_aUsername, Client(ID)->m_aType, Client(ID)->m_aHost, Client(ID)->m_aLocation, Client(ID)->m_aPassword);
+
+			}
+			ID++;
+		}
+	}
+
+	// clean up
+	json_value_free(pJsonData);
+	mem_free(pFileData);
+
+	// tell clients to reload the page
+	m_JSONUpdateThreadData.m_ReloadRequired = 2;
+
+	return 0;
+}
+
+int CMain::Run()
+{
+	if(m_Server.Init(this, m_Config.m_aBindAddr, m_Config.m_Port))
+		return 1;
+
+	if(ReadConfig())
+		return 1;
+
+	// Start JSON Update Thread
+	m_JSONUpdateThreadData.m_ReloadRequired = 2;
+	m_JSONUpdateThreadData.pClients = m_aClients;
+	m_JSONUpdateThreadData.pConfig = &m_Config;
+	void *LoadThread = thread_create(JSONUpdateThread, &m_JSONUpdateThreadData);
+	//thread_detach(LoadThread);
+
+	while(gs_Running)
+	{
+		if(gs_ReloadConfig)
+		{
+			if(ReadConfig())
+				return 1;
+			m_Server.NetBan()->UnbanAll();
+			gs_ReloadConfig = 0;
+		}
+
+		m_Server.Update();
+
+		// wait for incomming data
+		net_socket_read_wait(*m_Server.Network()->Socket(), 10);
+	}
+
+	dbg_msg("server", "Closing.");
+	m_Server.Network()->Close();
+	thread_wait(LoadThread);
+
+	return 0;
+}
+
+int main(int argc, const char *argv[])
+{
+	int RetVal;
+	dbg_logger_stdout();
+
+	#if defined(CONF_FAMILY_UNIX)
+		signal(SIGINT, ExitFunc);
+		signal(SIGTERM, ExitFunc);
+		signal(SIGQUIT, ExitFunc);
+		signal(SIGHUP, ReloadFunc);
+	#endif
+
+	char aUsage[128];
+	CConfig Config;
+	str_format(aUsage, sizeof(aUsage), "%s [options]", argv[0]);
+	const char *pConfigFile = 0;
+	const char *pWebDir = 0;
+	const char *pBindAddr = 0;
+
+	struct argparse_option aOptions[] = {
+		OPT_HELP(),
+		OPT_BOOLEAN('v', "verbose", &Config.m_Verbose, "Verbose output", 0),
+		OPT_STRING('c', "config", &pConfigFile, "Config file to use", 0),
+		OPT_STRING('d', "web-dir", &pWebDir, "Location of the web directory", 0),
+		OPT_STRING('b', "bind", &pBindAddr, "Bind to address", 0),
+		OPT_INTEGER('p', "port", &Config.m_Port, "Listen on port", 0),
+		OPT_END(),
+	};
+	struct argparse Argparse;
+	argparse_init(&Argparse, aOptions, aUsage, 0);
+	argc = argparse_parse(&Argparse, argc, argv);
+
+	if(pConfigFile)
+		str_copy(Config.m_aConfigFile, pConfigFile, sizeof(Config.m_aConfigFile));
+	if(pWebDir)
+		str_copy(Config.m_aWebDir, pWebDir, sizeof(Config.m_aWebDir));
+	if(pBindAddr)
+		str_copy(Config.m_aBindAddr, pBindAddr, sizeof(Config.m_aBindAddr));
+
+	if(Config.m_aWebDir[strlen(Config.m_aWebDir)-1] != '/')
+		str_append(Config.m_aWebDir, "/", sizeof(Config.m_aWebDir));
+	if(!fs_is_dir(Config.m_aWebDir))
+	{
+		dbg_msg("main", "ERROR: Can't find web directory: %s", Config.m_aWebDir);
+		return 1;
+	}
+
+	char aTmp[1024];
+	str_format(aTmp, sizeof(aTmp), "%s%s", Config.m_aWebDir, Config.m_aJSONFile);
+	str_copy(Config.m_aJSONFile, aTmp, sizeof(Config.m_aJSONFile));
+
+	CMain Main(Config);
+	RetVal = Main.Run();
+
+	return RetVal;
+}
diff --git a/server/src/main.h b/server/src/main.h
new file mode 100644
index 0000000..a3b95cb
--- /dev/null
+++ b/server/src/main.h
@@ -0,0 +1,93 @@
+#ifndef MAIN_H
+#define MAIN_H
+
+#include <stdint.h>
+#include "server.h"
+
+class CConfig
+{
+public:
+	bool m_Verbose;
+	char m_aConfigFile[1024];
+	char m_aWebDir[1024];
+	char m_aTemplateFile[1024];
+	char m_aJSONFile[1024];
+	char m_aBindAddr[256];
+	int m_Port;
+
+	CConfig();
+};
+
+class CMain
+{
+	CConfig m_Config;
+	CServer m_Server;
+
+	struct CClient
+	{
+		bool m_Active;
+		bool m_Disabled;
+		bool m_Connected;
+		int m_ClientNetID;
+		int m_ClientNetType;
+		char m_aUsername[128];
+		char m_aName[128];
+		char m_aType[128];
+		char m_aHost[128];
+		char m_aLocation[128];
+		char m_aPassword[128];
+
+		int64 m_TimeConnected;
+		int64 m_LastUpdate;
+
+		struct CStats
+		{
+			bool m_Online4;
+			bool m_Online6;
+			bool m_IpStatus;    //mh361 or mh370, mourn mh370, 2014-03-0 01:20 lost from all over the world.
+			int64_t m_Uptime;
+			double m_Load_1;    //1 minutes load average
+			double m_Load_5;    //5 minutes load average
+			double m_Load_15;   //15 minutes load average
+			int64_t m_NetworkRx;
+			int64_t m_NetworkTx;
+			int64_t m_NetworkIN;
+			int64_t m_NetworkOUT;
+			int64_t m_MemTotal;
+			int64_t m_MemUsed;
+			int64_t m_SwapTotal;
+			int64_t m_SwapUsed;
+			int64_t m_HDDTotal;
+			int64_t m_HDDUsed;
+			double m_CPU;
+			char m_aCustom[512];
+			// Options
+			bool m_Pong;
+		} m_Stats;
+	} m_aClients[NET_MAX_CLIENTS];
+
+	struct CJSONUpdateThreadData
+	{
+		CClient *pClients;
+		CConfig *pConfig;
+		volatile short m_ReloadRequired;
+	} m_JSONUpdateThreadData;
+
+	static void JSONUpdateThread(void *pUser);
+public:
+	CMain(CConfig Config);
+
+	void OnNewClient(int ClienNettID, int ClientID);
+	void OnDelClient(int ClientNetID);
+	int HandleMessage(int ClientNetID, char *pMessage);
+	int ReadConfig();
+	int Run();
+
+	CClient *Client(int ClientID) { return &m_aClients[ClientID]; }
+	CClient *ClientNet(int ClientNetID);
+	const CConfig *Config() const { return &m_Config; }
+	int ClientNetToClient(int ClientNetID);
+};
+
+
+#endif
diff --git a/server/src/netban.cpp b/server/src/netban.cpp
new file mode 100644
index 0000000..0ed2d65
--- /dev/null
+++ b/server/src/netban.cpp
@@ -0,0 +1,463 @@
+#include <math.h>
+#include "netban.h"
+
+bool CNetBan::StrAllnum(const char *pStr)
+{
+	while(*pStr)
+	{
+		if(!(*pStr >= '0' && *pStr <= '9'))
+			return false;
+		pStr++;
+	}
+	return true;
+}
+
+
+CNetBan::CNetHash::CNetHash(const NETADDR *pAddr)
+{
+	if(pAddr->type==NETTYPE_IPV4)
+		m_Hash = (pAddr->ip[0]+pAddr->ip[1]+pAddr->ip[2]+pAddr->ip[3])&0xFF;
+	else
+		m_Hash = (pAddr->ip[0]+pAddr->ip[1]+pAddr->ip[2]+pAddr->ip[3]+pAddr->ip[4]+pAddr->ip[5]+pAddr->ip[6]+pAddr->ip[7]+
+			pAddr->ip[8]+pAddr->ip[9]+pAddr->ip[10]+pAddr->ip[11]+pAddr->ip[12]+pAddr->ip[13]+pAddr->ip[14]+pAddr->ip[15])&0xFF;
+	m_HashIndex = 0;
+}
+
+CNetBan::CNetHash::CNetHash(const CNetRange *pRange)
+{
+	m_Hash = 0;
+	m_HashIndex = 0;
+	for(int i = 0; pRange->m_LB.ip[i] == pRange->m_UB.ip[i]; ++i)
+	{
+		m_Hash += pRange->m_LB.ip[i];
+		++m_HashIndex;
+	}
+	m_Hash &= 0xFF;
+}
+
+int CNetBan::CNetHash::MakeHashArray(const NETADDR *pAddr, CNetHash aHash[17])
+{
+	int Length = pAddr->type==NETTYPE_IPV4 ? 4 : 16;
+	aHash[0].m_Hash = 0;
+	aHash[0].m_HashIndex = 0;
+	for(int i = 1, Sum = 0; i <= Length; ++i)
+	{
+		Sum += pAddr->ip[i-1];
+		aHash[i].m_Hash = Sum&0xFF;
+		aHash[i].m_HashIndex = i%Length;
+	}
+	return Length;
+}
+
+
+template<class T, int HashCount>
+typename CNetBan::CBan<T> *CNetBan::CBanPool<T, HashCount>::Add(const T *pData, const CBanInfo *pInfo, const CNetHash *pNetHash)
+{
+	if(!m_pFirstFree)
+		return 0;
+
+	// create new ban
+	CBan<T> *pBan = m_pFirstFree;
+	pBan->m_Data = *pData;
+	pBan->m_Info = *pInfo;
+	pBan->m_NetHash = *pNetHash;
+	if(pBan->m_pNext)
+		pBan->m_pNext->m_pPrev = pBan->m_pPrev;
+	if(pBan->m_pPrev)
+		pBan->m_pPrev->m_pNext = pBan->m_pNext;
+	else
+		m_pFirstFree = pBan->m_pNext;
+
+	// add it to the hash list
+	if(m_paaHashList[pNetHash->m_HashIndex][pNetHash->m_Hash])
+		m_paaHashList[pNetHash->m_HashIndex][pNetHash->m_Hash]->m_pHashPrev = pBan;
+	pBan->m_pHashPrev = 0;
+	pBan->m_pHashNext = m_paaHashList[pNetHash->m_HashIndex][pNetHash->m_Hash];
+	m_paaHashList[pNetHash->m_HashIndex][pNetHash->m_Hash] = pBan;
+
+	// insert it into the used list
+	if(m_pFirstUsed)
+	{
+		for(CBan<T> *p = m_pFirstUsed; ; p = p->m_pNext)
+		{
+			if(p->m_Info.m_Expires == CBanInfo::EXPIRES_NEVER || (pInfo->m_Expires != CBanInfo::EXPIRES_NEVER && pInfo->m_Expires <= p->m_Info.m_Expires))
+			{
+				// insert before
+				pBan->m_pNext = p;
+				pBan->m_pPrev = p->m_pPrev;
+				if(p->m_pPrev)
+					p->m_pPrev->m_pNext = pBan;
+				else
+					m_pFirstUsed = pBan;
+				p->m_pPrev = pBan;
+				break;
+			}
+
+			if(!p->m_pNext)
+			{
+				// last entry
+				p->m_pNext = pBan;
+				pBan->m_pPrev = p;
+				pBan->m_pNext = 0;
+				break;
+			}
+		}
+	}
+	else
+	{
+		m_pFirstUsed = pBan;
+		pBan->m_pNext = pBan->m_pPrev = 0;
+	}
+
+	// update ban count
+	++m_CountUsed;
+
+	return pBan;
+}
+
+template<class T, int HashCount>
+int CNetBan::CBanPool<T, HashCount>::Remove(CBan<T> *pBan)
+{
+	if(pBan == 0)
+		return -1;
+
+	// remove from hash list
+	if(pBan->m_pHashNext)
+		pBan->m_pHashNext->m_pHashPrev = pBan->m_pHashPrev;
+	if(pBan->m_pHashPrev)
+		pBan->m_pHashPrev->m_pHashNext = pBan->m_pHashNext;
+	else
+		m_paaHashList[pBan->m_NetHash.m_HashIndex][pBan->m_NetHash.m_Hash] = pBan->m_pHashNext;
+	pBan->m_pHashNext = pBan->m_pHashPrev = 0;
+
+	// remove from used list
+	if(pBan->m_pNext)
+		pBan->m_pNext->m_pPrev = pBan->m_pPrev;
+	if(pBan->m_pPrev)
+		pBan->m_pPrev->m_pNext = pBan->m_pNext;
+	else
+		m_pFirstUsed = pBan->m_pNext;
+
+	// add to recycle list
+	if(m_pFirstFree)
+		m_pFirstFree->m_pPrev = pBan;
+	pBan->m_pPrev = 0;
+	pBan->m_pNext = m_pFirstFree;
+	m_pFirstFree = pBan;
+
+	// update ban count
+	--m_CountUsed;
+
+	return 0;
+}
+
+template<class T, int HashCount>
+void CNetBan::CBanPool<T, HashCount>::Update(CBan<CDataType> *pBan, const CBanInfo *pInfo)
+{
+	pBan->m_Info = *pInfo;
+
+	// remove from used list
+	if(pBan->m_pNext)
+		pBan->m_pNext->m_pPrev = pBan->m_pPrev;
+	if(pBan->m_pPrev)
+		pBan->m_pPrev->m_pNext = pBan->m_pNext;
+	else
+		m_pFirstUsed = pBan->m_pNext;
+
+	// insert it into the used list
+	if(m_pFirstUsed)
+	{
+		for(CBan<T> *p = m_pFirstUsed; ; p = p->m_pNext)
+		{
+			if(p->m_Info.m_Expires == CBanInfo::EXPIRES_NEVER || (pInfo->m_Expires != CBanInfo::EXPIRES_NEVER && pInfo->m_Expires <= p->m_Info.m_Expires))
+			{
+				// insert before
+				pBan->m_pNext = p;
+				pBan->m_pPrev = p->m_pPrev;
+				if(p->m_pPrev)
+					p->m_pPrev->m_pNext = pBan;
+				else
+					m_pFirstUsed = pBan;
+				p->m_pPrev = pBan;
+				break;
+			}
+
+			if(!p->m_pNext)
+			{
+				// last entry
+				p->m_pNext = pBan;
+				pBan->m_pPrev = p;
+				pBan->m_pNext = 0;
+				break;
+			}
+		}
+	}
+	else
+	{
+		m_pFirstUsed = pBan;
+		pBan->m_pNext = pBan->m_pPrev = 0;
+	}
+}
+
+template<class T, int HashCount>
+void CNetBan::CBanPool<T, HashCount>::Reset()
+{
+	mem_zero(m_paaHashList, sizeof(m_paaHashList));
+	mem_zero(m_aBans, sizeof(m_aBans));
+	m_pFirstUsed = 0;
+	m_CountUsed = 0;
+
+	for(int i = 1; i < MAX_BANS-1; ++i)
+	{
+		m_aBans[i].m_pNext = &m_aBans[i+1];
+		m_aBans[i].m_pPrev = &m_aBans[i-1];
+	}
+
+	m_aBans[0].m_pNext = &m_aBans[1];
+	m_aBans[MAX_BANS-1].m_pPrev = &m_aBans[MAX_BANS-2];
+	m_pFirstFree = &m_aBans[0];
+}
+
+template<class T, int HashCount>
+typename CNetBan::CBan<T> *CNetBan::CBanPool<T, HashCount>::Get(int Index) const
+{
+	if(Index < 0 || Index >= Num())
+		return 0;
+
+	for(CNetBan::CBan<T> *pBan = m_pFirstUsed; pBan; pBan = pBan->m_pNext, --Index)
+	{
+		if(Index == 0)
+			return pBan;
+	}
+
+	return 0;
+}
+
+
+template<class T>
+void CNetBan::MakeBanInfo(const CBan<T> *pBan, char *pBuf, unsigned BuffSize, int Type) const
+{
+	if(pBan == 0 || pBuf == 0)
+	{
+		if(BuffSize > 0)
+			pBuf[0] = 0;
+		return;
+	}
+
+	// build type based part
+	char aBuf[256];
+	if(Type == MSGTYPE_PLAYER)
+		str_copy(aBuf, "You have been banned", sizeof(aBuf));
+	else
+	{
+		char aTemp[256];
+		switch(Type)
+		{
+		case MSGTYPE_LIST:
+			str_format(aBuf, sizeof(aBuf), "%s banned", NetToString(&pBan->m_Data, aTemp, sizeof(aTemp))); break;
+		case MSGTYPE_BANADD:
+			str_format(aBuf, sizeof(aBuf), "banned %s", NetToString(&pBan->m_Data, aTemp, sizeof(aTemp))); break;
+		case MSGTYPE_BANREM:
+			str_format(aBuf, sizeof(aBuf), "unbanned %s", NetToString(&pBan->m_Data, aTemp, sizeof(aTemp))); break;
+		default:
+			aBuf[0] = 0;
+		}
+	}
+
+	// add info part
+	if(pBan->m_Info.m_Expires != CBanInfo::EXPIRES_NEVER)
+	{
+		int Mins = ((pBan->m_Info.m_Expires-time_timestamp()) + 59) / 60;
+		if(Mins <= 1)
+			str_format(pBuf, BuffSize, "%s for 1 minute (%s)", aBuf, pBan->m_Info.m_aReason);
+		else
+			str_format(pBuf, BuffSize, "%s for %d minutes (%s)", aBuf, Mins, pBan->m_Info.m_aReason);
+	}
+	else
+		str_format(pBuf, BuffSize, "%s for life (%s)", aBuf, pBan->m_Info.m_aReason);
+}
+
+template<class T>
+int CNetBan::Ban(T *pBanPool, const typename T::CDataType *pData, int Seconds, const char *pReason)
+{
+	// do not ban localhost
+	if(NetMatch(pData, &m_LocalhostIPV4) || NetMatch(pData, &m_LocalhostIPV6))
+	{
+		dbg_msg("net_ban", "ban failed (localhost)");
+		return -1;
+	}
+
+	int Stamp = Seconds > 0 ? time_timestamp()+Seconds : CBanInfo::EXPIRES_NEVER;
+
+	// set up info
+	CBanInfo Info = {0};
+	Info.m_Expires = Stamp;
+	str_copy(Info.m_aReason, pReason, sizeof(Info.m_aReason));
+
+	// check if it already exists
+	CNetHash NetHash(pData);
+	CBan<typename T::CDataType> *pBan = pBanPool->Find(pData, &NetHash);
+	if(pBan)
+	{
+		// adjust the ban
+		pBanPool->Update(pBan, &Info);
+		char aBuf[128];
+		MakeBanInfo(pBan, aBuf, sizeof(aBuf), MSGTYPE_LIST);
+		dbg_msg("net_ban", aBuf);
+		return 1;
+	}
+
+	// add ban and print result
+	pBan = pBanPool->Add(pData, &Info, &NetHash);
+	if(pBan)
+	{
+		char aBuf[128];
+		MakeBanInfo(pBan, aBuf, sizeof(aBuf), MSGTYPE_BANADD);
+		dbg_msg("net_ban", aBuf);
+		return 0;
+	}
+	else
+		dbg_msg("net_ban", "ban failed (full banlist)");
+	return -1;
+}
+
+template<class T>
+int CNetBan::Unban(T *pBanPool, const typename T::CDataType *pData)
+{
+	CNetHash NetHash(pData);
+	CBan<typename T::CDataType> *pBan = pBanPool->Find(pData, &NetHash);
+	if(pBan)
+	{
+		char aBuf[256];
+		MakeBanInfo(pBan, aBuf, sizeof(aBuf), MSGTYPE_BANREM);
+		pBanPool->Remove(pBan);
+		dbg_msg("net_ban", aBuf);
+		return 0;
+	}
+	else
+		dbg_msg("net_ban", "unban failed (invalid entry)");
+	return -1;
+}
+
+void CNetBan::Init()
+{
+	m_BanAddrPool.Reset();
+	m_BanRangePool.Reset();
+
+	net_host_lookup("localhost", &m_LocalhostIPV4, NETTYPE_IPV4);
+	net_host_lookup("localhost", &m_LocalhostIPV6, NETTYPE_IPV6);
+}
+
+void CNetBan::Update()
+{
+	int Now = time_timestamp();
+
+	// remove expired bans
+	char aBuf[256], aNetStr[256];
+	while(m_BanAddrPool.First() && m_BanAddrPool.First()->m_Info.m_Expires != CBanInfo::EXPIRES_NEVER && m_BanAddrPool.First()->m_Info.m_Expires < Now)
+	{
+		str_format(aBuf, sizeof(aBuf), "ban %s expired", NetToString(&m_BanAddrPool.First()->m_Data, aNetStr, sizeof(aNetStr)));
+		dbg_msg("net_ban", aBuf);
+		m_BanAddrPool.Remove(m_BanAddrPool.First());
+	}
+	while(m_BanRangePool.First() && m_BanRangePool.First()->m_Info.m_Expires != CBanInfo::EXPIRES_NEVER && m_BanRangePool.First()->m_Info.m_Expires < Now)
+	{
+		str_format(aBuf, sizeof(aBuf), "ban %s expired", NetToString(&m_BanRangePool.First()->m_Data, aNetStr, sizeof(aNetStr)));
+		dbg_msg("net_ban", aBuf);
+		m_BanRangePool.Remove(m_BanRangePool.First());
+	}
+}
+
+int CNetBan::BanAddr(const NETADDR *pAddr, int Seconds, const char *pReason)
+{
+	return Ban(&m_BanAddrPool, pAddr, Seconds, pReason);
+}
+
+int CNetBan::BanRange(const CNetRange *pRange, int Seconds, const char *pReason)
+{
+	if(pRange->IsValid())
+		return Ban(&m_BanRangePool, pRange, Seconds, pReason);
+
+	dbg_msg("net_ban", "ban failed (invalid range)");
+	return -1;
+}
+
+int CNetBan::UnbanByAddr(const NETADDR *pAddr)
+{
+	return Unban(&m_BanAddrPool, pAddr);
+}
+
+int CNetBan::UnbanByRange(const CNetRange *pRange)
+{
+	if(pRange->IsValid())
+		return Unban(&m_BanRangePool, pRange);
+
+	dbg_msg("net_ban", "ban failed (invalid range)");
+	return -1;
+}
+
+int CNetBan::UnbanByIndex(int Index)
+{
+	int Result;
+	char aBuf[256];
+	CBanAddr *pBan = m_BanAddrPool.Get(Index);
+	if(pBan)
+	{
+		NetToString(&pBan->m_Data, aBuf, sizeof(aBuf));
+		Result = m_BanAddrPool.Remove(pBan);
+	}
+	else
+	{
+		CBanRange *pBan = m_BanRangePool.Get(Index-m_BanAddrPool.Num());
+		if(pBan)
+		{
+			NetToString(&pBan->m_Data, aBuf, sizeof(aBuf));
+			Result = m_BanRangePool.Remove(pBan);
+		}
+		else
+		{
+			dbg_msg("net_ban", "unban failed (invalid index)");
+			return -1;
+		}
+	}
+
+	char aMsg[256];
+	str_format(aMsg, sizeof(aMsg), "unbanned index %i (%s)", Index, aBuf);
+	dbg_msg("net_ban", aMsg);
+	return Result;
+}
+
+void CNetBan::UnbanAll()
+{
+	m_BanAddrPool.Reset();
+	m_BanRangePool.Reset();
+}
+
+bool CNetBan::IsBanned(const NETADDR *pAddr, char *pBuf, unsigned BufferSize) const
+{
+	CNetHash aHash[17];
+	int Length = CNetHash::MakeHashArray(pAddr, aHash);
+
+	// check ban adresses
+	CBanAddr *pBan = m_BanAddrPool.Find(pAddr, &aHash[Length]);
+	if(pBan)
+	{
+		MakeBanInfo(pBan, pBuf, BufferSize, MSGTYPE_PLAYER);
+		return true;
+	}
+
+	// check ban ranges
+	for(int i = Length-1; i >= 0; --i)
+	{
+		for(CBanRange *pBan = m_BanRangePool.First(&aHash[i]); pBan; pBan = pBan->m_pHashNext)
+		{
+			if(NetMatch(&pBan->m_Data, pAddr, i, Length))
+			{
+				MakeBanInfo(pBan, pBuf, BufferSize, MSGTYPE_PLAYER);
+				return true;
+			}
+		}
+	}
+	
+	return false;
+}
diff --git a/server/src/netban.h b/server/src/netban.h
new file mode 100644
index 0000000..544a4b1
--- /dev/null
+++ b/server/src/netban.h
@@ -0,0 +1,179 @@
+#ifndef NETBAN_H
+#define NETBAN_H
+
+#include <system.h>
+
+inline int NetComp(const NETADDR *pAddr1, const NETADDR *pAddr2)
+{
+	return mem_comp(pAddr1, pAddr2, pAddr1->type==NETTYPE_IPV4 ? 8 : 20);
+}
+
+class CNetRange
+{
+public:
+	NETADDR m_LB;
+	NETADDR m_UB;
+
+	bool IsValid() const { return m_LB.type == m_UB.type && NetComp(&m_LB, &m_UB) < 0; }
+};
+
+inline int NetComp(const CNetRange *pRange1, const CNetRange *pRange2)
+{
+	return NetComp(&pRange1->m_LB, &pRange2->m_LB) || NetComp(&pRange1->m_UB, &pRange2->m_UB);
+}
+
+
+class CNetBan
+{
+protected:
+	bool NetMatch(const NETADDR *pAddr1, const NETADDR *pAddr2) const
+	{
+		return NetComp(pAddr1, pAddr2) == 0;
+	}
+
+	bool NetMatch(const CNetRange *pRange, const NETADDR *pAddr, int Start, int Length) const
+	{
+		return pRange->m_LB.type == pAddr->type && (Start == 0 || mem_comp(&pRange->m_LB.ip[0], &pAddr->ip[0], Start) == 0) &&
+			mem_comp(&pRange->m_LB.ip[Start], &pAddr->ip[Start], Length-Start) <= 0 && mem_comp(&pRange->m_UB.ip[Start], &pAddr->ip[Start], Length-Start) >= 0;
+	}
+
+	bool NetMatch(const CNetRange *pRange, const NETADDR *pAddr) const
+	{
+		return NetMatch(pRange, pAddr, 0,  pRange->m_LB.type==NETTYPE_IPV4 ? 4 : 16);
+	}
+
+	const char *NetToString(const NETADDR *pData, char *pBuffer, unsigned BufferSize) const
+	{
+		char aAddrStr[NETADDR_MAXSTRSIZE];
+		net_addr_str(pData, aAddrStr, sizeof(aAddrStr), false);
+		str_format(pBuffer, BufferSize, "'%s'", aAddrStr);
+		return pBuffer;
+	}
+
+	const char *NetToString(const CNetRange *pData, char *pBuffer, unsigned BufferSize) const
+	{
+		char aAddrStr1[NETADDR_MAXSTRSIZE], aAddrStr2[NETADDR_MAXSTRSIZE];
+		net_addr_str(&pData->m_LB, aAddrStr1, sizeof(aAddrStr1), false);
+		net_addr_str(&pData->m_UB, aAddrStr2, sizeof(aAddrStr2), false);
+		str_format(pBuffer, BufferSize, "'%s' - '%s'", aAddrStr1, aAddrStr2);
+		return pBuffer;
+	}
+
+	// todo: move?
+	static bool StrAllnum(const char *pStr);
+
+	class CNetHash
+	{
+	public:
+		int m_Hash;
+		int m_HashIndex;	// matching parts for ranges, 0 for addr
+
+		CNetHash() {}	
+		CNetHash(const NETADDR *pAddr);
+		CNetHash(const CNetRange *pRange);
+
+		static int MakeHashArray(const NETADDR *pAddr, CNetHash aHash[17]);
+	};
+
+	struct CBanInfo
+	{
+		enum
+		{
+			EXPIRES_NEVER=-1,
+			REASON_LENGTH=64,
+		};
+		int m_Expires;
+		char m_aReason[REASON_LENGTH];
+	};
+
+	template<class T> struct CBan
+	{
+		T m_Data;
+		CBanInfo m_Info;
+		CNetHash m_NetHash;
+
+		// hash list
+		CBan *m_pHashNext;
+		CBan *m_pHashPrev;
+
+		// used or free list
+		CBan *m_pNext;
+		CBan *m_pPrev;
+	};
+
+	template<class T, int HashCount> class CBanPool
+	{
+	public:
+		typedef T CDataType;
+
+		CBan<CDataType> *Add(const CDataType *pData, const CBanInfo *pInfo, const CNetHash *pNetHash);
+		int Remove(CBan<CDataType> *pBan);
+		void Update(CBan<CDataType> *pBan, const CBanInfo *pInfo);
+		void Reset();
+	
+		int Num() const { return m_CountUsed; }
+		bool IsFull() const { return m_CountUsed == MAX_BANS; }
+
+		CBan<CDataType> *First() const { return m_pFirstUsed; }
+		CBan<CDataType> *First(const CNetHash *pNetHash) const { return m_paaHashList[pNetHash->m_HashIndex][pNetHash->m_Hash]; }
+		CBan<CDataType> *Find(const CDataType *pData, const CNetHash *pNetHash) const
+		{
+			for(CBan<CDataType> *pBan = m_paaHashList[pNetHash->m_HashIndex][pNetHash->m_Hash]; pBan; pBan = pBan->m_pHashNext)
+			{
+				if(NetComp(&pBan->m_Data, pData) == 0)
+					return pBan;
+			}
+
+			return 0;
+		}
+		CBan<CDataType> *Get(int Index) const;
+
+	private:
+		enum
+		{
+			MAX_BANS=1024,
+		};
+
+		CBan<CDataType> *m_paaHashList[HashCount][256];
+		CBan<CDataType> m_aBans[MAX_BANS];
+		CBan<CDataType> *m_pFirstFree;
+		CBan<CDataType> *m_pFirstUsed;
+		int m_CountUsed;
+	};
+
+	typedef CBanPool<NETADDR, 1> CBanAddrPool;
+	typedef CBanPool<CNetRange, 16> CBanRangePool;
+	typedef CBan<NETADDR> CBanAddr;
+	typedef CBan<CNetRange> CBanRange;
+	
+	template<class T> void MakeBanInfo(const CBan<T> *pBan, char *pBuf, unsigned BuffSize, int Type) const;
+	template<class T> int Ban(T *pBanPool, const typename T::CDataType *pData, int Seconds, const char *pReason);
+	template<class T> int Unban(T *pBanPool, const typename T::CDataType *pData);
+
+	CBanAddrPool m_BanAddrPool;
+	CBanRangePool m_BanRangePool;
+	NETADDR m_LocalhostIPV4, m_LocalhostIPV6;
+
+public:
+	enum
+	{
+		MSGTYPE_PLAYER=0,
+		MSGTYPE_LIST,
+		MSGTYPE_BANADD,
+		MSGTYPE_BANREM,
+	};
+
+	virtual ~CNetBan() {}
+	void Init();
+	void Update();
+
+	virtual int BanAddr(const NETADDR *pAddr, int Seconds, const char *pReason);
+	virtual int BanRange(const CNetRange *pRange, int Seconds, const char *pReason);
+	int UnbanByAddr(const NETADDR *pAddr);
+	int UnbanByRange(const CNetRange *pRange);
+	int UnbanByIndex(int Index);
+	void UnbanAll();
+	bool IsBanned(const NETADDR *pAddr, char *pBuf, unsigned BufferSize) const;
+};
+
+#endif
diff --git a/server/src/network.cpp b/server/src/network.cpp
new file mode 100644
index 0000000..a1cb1ae
--- /dev/null
+++ b/server/src/network.cpp
@@ -0,0 +1,144 @@
+#include <system.h>
+#include "netban.h"
+#include "network.h"
+
+bool CNetwork::Open(NETADDR BindAddr, CNetBan *pNetBan)
+{
+	// zero out the whole structure
+	mem_zero(this, sizeof(*this));
+	m_Socket.type = NETTYPE_INVALID;
+	m_Socket.ipv4sock = -1;
+	m_Socket.ipv6sock = -1;
+	m_pNetBan = pNetBan;
+
+	// open socket
+	m_Socket = net_tcp_create(BindAddr);
+	if(!m_Socket.type)
+		return false;
+	if(net_tcp_listen(m_Socket, NET_MAX_CLIENTS))
+		return false;
+	net_set_non_blocking(m_Socket);
+
+	for(int i = 0; i < NET_MAX_CLIENTS; i++)
+		m_aSlots[i].m_Connection.Reset();
+
+	return true;
+}
+
+void CNetwork::SetCallbacks(NETFUNC_NEWCLIENT pfnNewClient, NETFUNC_DELCLIENT pfnDelClient, void *pUser)
+{
+	m_pfnNewClient = pfnNewClient;
+	m_pfnDelClient = pfnDelClient;
+	m_UserPtr = pUser;
+}
+
+int CNetwork::Close()
+{
+	for(int i = 0; i < NET_MAX_CLIENTS; i++)
+		m_aSlots[i].m_Connection.Disconnect("Closing connection.");
+
+	net_tcp_close(m_Socket);
+
+	return 0;
+}
+
+int CNetwork::Drop(int ClientID, const char *pReason)
+{
+	if(m_pfnDelClient)
+		m_pfnDelClient(ClientID, pReason, m_UserPtr);
+
+	m_aSlots[ClientID].m_Connection.Disconnect(pReason);
+
+	return 0;
+}
+
+int CNetwork::AcceptClient(NETSOCKET Socket, const NETADDR *pAddr)
+{
+	char aError[256] = { 0 };
+	int FreeSlot = -1;
+
+	// look for free slot or multiple client
+	for(int i = 0; i < NET_MAX_CLIENTS; i++)
+	{
+		if(FreeSlot == -1 && m_aSlots[i].m_Connection.State() == NET_CONNSTATE_OFFLINE)
+			FreeSlot = i;
+		if(m_aSlots[i].m_Connection.State() != NET_CONNSTATE_OFFLINE)
+		{
+			if(net_addr_comp(pAddr, m_aSlots[i].m_Connection.PeerAddress()) == 0)
+			{
+				str_copy(aError, "Only one client per IP allowed.", sizeof(aError));
+				break;
+			}
+		}
+	}
+
+	// accept client
+	if(!aError[0] && FreeSlot != -1)
+	{
+		m_aSlots[FreeSlot].m_Connection.Init(Socket, pAddr);
+		if(m_pfnNewClient)
+			m_pfnNewClient(FreeSlot, m_UserPtr);
+		return 0;
+	}
+
+	// reject client
+	if(!aError[0])
+		str_copy(aError, "No free slot available.", sizeof(aError));
+
+	net_tcp_send(Socket, aError, str_length(aError));
+	net_tcp_close(Socket);
+
+	return -1;
+}
+
+int CNetwork::Update()
+{
+	NETSOCKET Socket;
+	NETADDR Addr;
+
+	if(net_tcp_accept(m_Socket, &Socket, &Addr) > 0)
+	{
+		// check if we should just drop the packet
+		char aBuf[128];
+		if(NetBan() && NetBan()->IsBanned(&Addr, aBuf, sizeof(aBuf)))
+		{
+			// banned, reply with a message and drop
+			net_tcp_send(Socket, aBuf, str_length(aBuf));
+			net_tcp_close(Socket);
+		}
+		else
+			AcceptClient(Socket, &Addr);
+	}
+
+	for(int i = 0; i < NET_MAX_CLIENTS; i++)
+	{
+		if(m_aSlots[i].m_Connection.State() == NET_CONNSTATE_ONLINE)
+			m_aSlots[i].m_Connection.Update();
+		if(m_aSlots[i].m_Connection.State() == NET_CONNSTATE_ERROR)
+			Drop(i, m_aSlots[i].m_Connection.ErrorString());
+	}
+
+	return 0;
+}
+
+int CNetwork::Recv(char *pLine, int MaxLength, int *pClientID)
+{
+	for(int i = 0; i < NET_MAX_CLIENTS; i++)
+	{
+		if(m_aSlots[i].m_Connection.State() == NET_CONNSTATE_ONLINE && m_aSlots[i].m_Connection.Recv(pLine, MaxLength))
+		{
+			if(pClientID)
+				*pClientID = i;
+			return 1;
+		}
+	}
+	return 0;
+}
+
+int CNetwork::Send(int ClientID, const char *pLine)
+{
+	if(m_aSlots[ClientID].m_Connection.State() == NET_CONNSTATE_ONLINE)
+		return m_aSlots[ClientID].m_Connection.Send(pLine);
+	else
+		return -1;
+}
diff --git a/server/src/network.h b/server/src/network.h
new file mode 100644
index 0000000..5dc2396
--- /dev/null
+++ b/server/src/network.h
@@ -0,0 +1,87 @@
+#ifndef NETWORK_H
+#define NETWORK_H
+
+enum
+{
+	NET_CONNSTATE_OFFLINE=0,
+	NET_CONNSTATE_CONNECT=1,
+	NET_CONNSTATE_PENDING=2,
+	NET_CONNSTATE_ONLINE=3,
+	NET_CONNSTATE_ERROR=4,
+
+	NET_MAX_PACKETSIZE = 1400,
+	NET_MAX_CLIENTS = 128
+};
+
+typedef int (*NETFUNC_DELCLIENT)(int ClientID, const char* pReason, void *pUser);
+typedef int (*NETFUNC_NEWCLIENT)(int ClientID, void *pUser);
+
+class CNetworkClient
+{
+private:
+	int m_State;
+
+	NETADDR m_PeerAddr;
+	NETSOCKET m_Socket;
+
+	char m_aBuffer[NET_MAX_PACKETSIZE];
+	int m_BufferOffset;
+
+	char m_aErrorString[256];
+
+	bool m_LineEndingDetected;
+	char m_aLineEnding[3];
+
+public:
+	void Init(NETSOCKET Socket, const NETADDR *pAddr);
+	void Disconnect(const char *pReason);
+
+	int State() const { return m_State; }
+	const NETADDR *PeerAddress() const { return &m_PeerAddr; }
+	const char *ErrorString() const { return m_aErrorString; }
+
+	void Reset();
+	int Update();
+	int Send(const char *pLine);
+	int Recv(char *pLine, int MaxLength);
+};
+
+class CNetwork
+{
+private:
+	struct CSlot
+	{
+		CNetworkClient m_Connection;
+	};
+
+	NETSOCKET m_Socket;
+	class CNetBan *m_pNetBan;
+	CSlot m_aSlots[NET_MAX_CLIENTS];
+
+	NETFUNC_NEWCLIENT m_pfnNewClient;
+	NETFUNC_DELCLIENT m_pfnDelClient;
+	void *m_UserPtr;
+
+public:
+	void SetCallbacks(NETFUNC_NEWCLIENT pfnNewClient, NETFUNC_DELCLIENT pfnDelClient, void *pUser);
+
+	//
+	bool Open(NETADDR BindAddr, CNetBan *pNetBan);
+	int Close();
+
+	//
+	int Recv(char *pLine, int MaxLength, int *pClientID = 0);
+	int Send(int ClientID, const char *pLine);
+	int Update();
+
+	//
+	int AcceptClient(NETSOCKET Socket, const NETADDR *pAddr);
+	int Drop(int ClientID, const char *pReason);
+
+	// status requests
+	const NETADDR *ClientAddr(int ClientID) const { return m_aSlots[ClientID].m_Connection.PeerAddress(); }
+	const NETSOCKET *Socket() const { return &m_Socket; }
+	class CNetBan *NetBan() const { return m_pNetBan; }
+};
+
+#endif
diff --git a/server/src/network_client.cpp b/server/src/network_client.cpp
new file mode 100644
index 0000000..0a4bce5
--- /dev/null
+++ b/server/src/network_client.cpp
@@ -0,0 +1,184 @@
+#include <system.h>
+#include "network.h"
+
+void CNetworkClient::Reset()
+{
+	m_State = NET_CONNSTATE_OFFLINE;
+	mem_zero(&m_PeerAddr, sizeof(m_PeerAddr));
+	m_aErrorString[0] = 0;
+
+	m_Socket.type = NETTYPE_INVALID;
+	m_Socket.ipv4sock = -1;
+	m_Socket.ipv6sock = -1;
+	m_aBuffer[0] = 0;
+	m_BufferOffset = 0;
+
+	m_LineEndingDetected = false;
+	#if defined(CONF_FAMILY_WINDOWS)
+		m_aLineEnding[0] = '\r';
+		m_aLineEnding[1] = '\n';
+		m_aLineEnding[2] = 0;
+	#else
+		m_aLineEnding[0] = '\n';
+		m_aLineEnding[1] = 0;
+		m_aLineEnding[2] = 0;
+	#endif
+}
+
+void CNetworkClient::Init(NETSOCKET Socket, const NETADDR *pAddr)
+{
+	Reset();
+
+	m_Socket = Socket;
+	net_set_non_blocking(m_Socket);
+
+	m_PeerAddr = *pAddr;
+	m_State = NET_CONNSTATE_ONLINE;
+}
+
+void CNetworkClient::Disconnect(const char *pReason)
+{
+	if(State() == NET_CONNSTATE_OFFLINE)
+		return;
+
+	if(pReason && pReason[0])
+		Send(pReason);
+
+	net_tcp_close(m_Socket);
+
+	Reset();
+}
+
+int CNetworkClient::Update()
+{
+	if(State() == NET_CONNSTATE_ONLINE)
+	{
+		if((int)(sizeof(m_aBuffer)) <= m_BufferOffset)
+		{
+			m_State = NET_CONNSTATE_ERROR;
+			str_copy(m_aErrorString, "too weak connection (out of buffer)", sizeof(m_aErrorString));
+			return -1;
+		}
+
+		int Bytes = net_tcp_recv(m_Socket, m_aBuffer+m_BufferOffset, (int)(sizeof(m_aBuffer))-m_BufferOffset);
+
+		if(Bytes > 0)
+		{
+			m_BufferOffset += Bytes;
+		}
+		else if(Bytes < 0)
+		{
+			if(net_would_block()) // no data received
+				return 0;
+
+			m_State = NET_CONNSTATE_ERROR; // error
+			str_copy(m_aErrorString, "connection failure", sizeof(m_aErrorString));
+			return -1;
+		}
+		else
+		{
+			m_State = NET_CONNSTATE_ERROR;
+			str_copy(m_aErrorString, "remote end closed the connection", sizeof(m_aErrorString));
+			return -1;
+		}
+	}
+
+	return 0;
+}
+
+int CNetworkClient::Recv(char *pLine, int MaxLength)
+{
+	if(State() == NET_CONNSTATE_ONLINE)
+	{
+		if(m_BufferOffset)
+		{
+			// find message start
+			int StartOffset = 0;
+			while(m_aBuffer[StartOffset] == '\r' || m_aBuffer[StartOffset] == '\n')
+			{
+				// detect clients line ending format
+				if(!m_LineEndingDetected)
+				{
+					m_aLineEnding[0] = m_aBuffer[StartOffset];
+					if(StartOffset+1 < m_BufferOffset && (m_aBuffer[StartOffset+1] == '\r' || m_aBuffer[StartOffset+1] == '\n') &&
+						m_aBuffer[StartOffset] != m_aBuffer[StartOffset+1])
+						m_aLineEnding[1] = m_aBuffer[StartOffset+1];
+					m_LineEndingDetected = true;
+				}
+
+				if(++StartOffset >= m_BufferOffset)
+				{
+					m_BufferOffset = 0;
+					return 0;
+				}
+			}
+
+			// find message end
+			int EndOffset = StartOffset;
+			while(m_aBuffer[EndOffset] != '\r' && m_aBuffer[EndOffset] != '\n')
+			{
+				if(++EndOffset >= m_BufferOffset)
+				{
+					if(StartOffset > 0)
+					{
+						mem_move(m_aBuffer, m_aBuffer+StartOffset, m_BufferOffset-StartOffset);
+						m_BufferOffset -= StartOffset;
+					}
+					return 0;
+				}
+			}
+
+			// extract message and update buffer
+			if(MaxLength-1 < EndOffset-StartOffset)
+			{
+				if(StartOffset > 0)
+				{
+					mem_move(m_aBuffer, m_aBuffer+StartOffset, m_BufferOffset-StartOffset);
+					m_BufferOffset -= StartOffset;
+				}
+				return 0;
+			}
+			mem_copy(pLine, m_aBuffer+StartOffset, EndOffset-StartOffset);
+			pLine[EndOffset-StartOffset] = 0;
+			str_sanitize_cc(pLine);
+			mem_move(m_aBuffer, m_aBuffer+EndOffset, m_BufferOffset-EndOffset);
+			m_BufferOffset -= EndOffset;
+			return 1;
+		}
+	}
+	return 0;
+}
+
+int CNetworkClient::Send(const char *pLine)
+{
+	if(State() != NET_CONNSTATE_ONLINE)
+		return -1;
+
+	char aBuf[1024];
+	str_copy(aBuf, pLine, (int)(sizeof(aBuf))-2);
+	int Length = str_length(aBuf);
+	aBuf[Length] = m_aLineEnding[0];
+	aBuf[Length+1] = m_aLineEnding[1];
+	aBuf[Length+2] = m_aLineEnding[2];
+	Length += 3;
+	const char *pData = aBuf;
+
+	while(1)
+	{
+		int Send = net_tcp_send(m_Socket, pData, Length);
+		if(Send < 0)
+		{
+			m_State = NET_CONNSTATE_ERROR;
+			str_copy(m_aErrorString, "failed to send packet", sizeof(m_aErrorString));
+			return -1;
+		}
+
+		if(Send >= Length)
+			break;
+
+		pData += Send;
+		Length -= Send;
+	}
+
+	return 0;
+}
diff --git a/server/src/server.cpp b/server/src/server.cpp
new file mode 100644
index 0000000..6996ac3
--- /dev/null
+++ b/server/src/server.cpp
@@ -0,0 +1,204 @@
+#include <system.h>
+#include "netban.h"
+#include "network.h"
+#include "main.h"
+#include "server.h"
+
+int CServer::NewClientCallback(int ClientID, void *pUser)
+{
+	CServer *pThis = (CServer *)pUser;
+
+	char aAddrStr[NETADDR_MAXSTRSIZE];
+	net_addr_str(pThis->m_Network.ClientAddr(ClientID), aAddrStr, sizeof(aAddrStr), true);
+	if(pThis->Main()->Config()->m_Verbose)
+		dbg_msg("server", "Connection accepted. ncid=%d addr=%s'", ClientID, aAddrStr);
+
+	pThis->m_aClients[ClientID].m_State = CClient::STATE_CONNECTED;
+	pThis->m_aClients[ClientID].m_TimeConnected = time_get();
+	pThis->m_Network.Send(ClientID, "Authentication required:");
+
+	return 0;
+}
+
+int CServer::DelClientCallback(int ClientID, const char *pReason, void *pUser)
+{
+	CServer *pThis = (CServer *)pUser;
+
+	char aAddrStr[NETADDR_MAXSTRSIZE];
+	net_addr_str(pThis->m_Network.ClientAddr(ClientID), aAddrStr, sizeof(aAddrStr), true);
+	if(pThis->Main()->Config()->m_Verbose)
+		dbg_msg("server", "Client dropped. ncid=%d addr=%s reason='%s'", ClientID, aAddrStr, pReason);
+
+	if(pThis->m_aClients[ClientID].m_State == CClient::STATE_AUTHED)
+		pThis->Main()->OnDelClient(ClientID);
+	pThis->m_aClients[ClientID].m_State = CClient::STATE_EMPTY;
+
+	return 0;
+}
+
+int CServer::Init(CMain *pMain, const char *Bind, int Port)
+{
+	m_pMain = pMain;
+	m_NetBan.Init();
+
+	for(int i = 0; i < NET_MAX_CLIENTS; i++)
+		m_aClients[i].m_State = CClient::STATE_EMPTY;
+
+	m_Ready = false;
+
+	if(Port == 0)
+	{
+		dbg_msg("server", "Will not bind to port 0.");
+		return 1;
+	}
+
+	NETADDR BindAddr;
+	if(Bind[0] && net_host_lookup(Bind, &BindAddr, NETTYPE_ALL) == 0)
+	{
+		// got bindaddr
+		BindAddr.type = NETTYPE_ALL;
+		BindAddr.port = Port;
+	}
+	else
+	{
+		mem_zero(&BindAddr, sizeof(BindAddr));
+		BindAddr.type = NETTYPE_ALL;
+		BindAddr.port = Port;
+	}
+
+	if(m_Network.Open(BindAddr, &m_NetBan))
+	{
+		m_Network.SetCallbacks(NewClientCallback, DelClientCallback, this);
+		m_Ready = true;
+		dbg_msg("server", "Bound to %s:%d", Bind, Port);
+		return 0;
+	}
+	else
+		dbg_msg("server", "Couldn't open socket. Port (%d) might already be in use.", Port);
+
+	return 1;
+}
+
+void CServer::Update()
+{
+	if(!m_Ready)
+		return;
+
+	m_NetBan.Update();
+	m_Network.Update();
+
+	char aBuf[NET_MAX_PACKETSIZE];
+	int ClientID;
+
+	while(m_Network.Recv(aBuf, (int)(sizeof(aBuf))-1, &ClientID))
+	{
+		dbg_assert(m_aClients[ClientID].m_State != CClient::STATE_EMPTY, "Got message from empty slot.");
+		if(m_aClients[ClientID].m_State == CClient::STATE_CONNECTED)
+		{
+			int ID = -1;
+			char aUsername[128] = {0};
+			char aPassword[128] = {0};
+			const char *pTmp;
+
+			if(!(pTmp = str_find(aBuf, ":"))
+				|| (unsigned)(pTmp - aBuf) > sizeof(aUsername) || (unsigned)(str_length(pTmp) - 1) > sizeof(aPassword))
+			{
+				m_Network.NetBan()->BanAddr(m_Network.ClientAddr(ClientID), 60, "You're an idiot, go away.");
+				m_Network.Drop(ClientID, "Fuck off.");
+				return;
+			}
+
+			str_copy(aUsername, aBuf, pTmp - aBuf + 1);
+			str_copy(aPassword, pTmp + 1, sizeof(aPassword));
+			if(!*aUsername || !*aPassword)
+			{
+				m_Network.NetBan()->BanAddr(m_Network.ClientAddr(ClientID), 60, "You're an idiot, go away.");
+				m_Network.Drop(ClientID, "Username and password must not be blank.");
+				return;
+			}
+
+			for(int i = 0; i < NET_MAX_CLIENTS; i++)
+			{
+				if(!Main()->Client(i)->m_Active)
+					continue;
+
+				if(str_comp(Main()->Client(i)->m_aUsername, aUsername) == 0 && str_comp(Main()->Client(i)->m_aPassword, aPassword) == 0)
+					ID = i;
+			}
+
+			if(ID == -1)
+			{
+				m_Network.NetBan()->BanAddr(m_Network.ClientAddr(ClientID), 60, "Wrong username and/or password.");
+				m_Network.Drop(ClientID, "Wrong username and/or password.");
+			}
+			else if(Main()->Client(ID)->m_ClientNetID != -1)
+			{
+				m_Network.Drop(ClientID, "Only one connection per user allowed.");
+			}
+			else
+			{
+				m_aClients[ClientID].m_State = CClient::STATE_AUTHED;
+				m_aClients[ClientID].m_LastReceived = time_get();
+				m_Network.Send(ClientID, "Authentication successful. Access granted.");
+
+				if(m_Network.ClientAddr(ClientID)->type == NETTYPE_IPV4)
+					m_Network.Send(ClientID, "You are connecting via: IPv4");
+				else if(m_Network.ClientAddr(ClientID)->type == NETTYPE_IPV6)
+					m_Network.Send(ClientID, "You are connecting via: IPv6");
+
+				if(Main()->Config()->m_Verbose)
+					dbg_msg("server", "ncid=%d authed", ClientID);
+				Main()->OnNewClient(ClientID, ID);
+			}
+		}
+		else if(m_aClients[ClientID].m_State == CClient::STATE_AUTHED)
+		{
+			m_aClients[ClientID].m_LastReceived = time_get();
+			if(Main()->Config()->m_Verbose)
+				dbg_msg("server", "ncid=%d cmd='%s'", ClientID, aBuf);
+
+			if(str_comp(aBuf, "logout") == 0)
+				m_Network.Drop(ClientID, "Logout. Bye Bye ~");
+			else
+				Main()->HandleMessage(ClientID, aBuf);
+		}
+	}
+
+	for(int i = 0; i < NET_MAX_CLIENTS; ++i)
+	{
+		if(m_aClients[i].m_State == CClient::STATE_CONNECTED &&
+			time_get() > m_aClients[i].m_TimeConnected + 5 * time_freq())
+		{
+			m_Network.NetBan()->BanAddr(m_Network.ClientAddr(i), 30, "Authentication timeout.");
+			m_Network.Drop(i, "Authentication timeout.");
+		}
+		else if(m_aClients[i].m_State == CClient::STATE_AUTHED &&
+			time_get() > m_aClients[i].m_LastReceived + 15 * time_freq())
+			m_Network.Drop(i, "Timeout.");
+	}
+}
+
+void CServer::Send(int ClientID, const char *pLine)
+{
+	if(!m_Ready)
+		return;
+
+	if(ClientID == -1)
+	{
+		for(int i = 0; i < NET_MAX_CLIENTS; i++)
+		{
+			if(m_aClients[i].m_State == CClient::STATE_AUTHED)
+				m_Network.Send(i, pLine);
+		}
+	}
+	else if(ClientID >= 0 && ClientID < NET_MAX_CLIENTS && m_aClients[ClientID].m_State == CClient::STATE_AUTHED)
+		m_Network.Send(ClientID, pLine);
+}
+
+void CServer::Shutdown()
+{
+	if(!m_Ready)
+		return;
+
+	m_Network.Close();
+}
diff --git a/server/src/server.h b/server/src/server.h
new file mode 100644
index 0000000..f5bf162
--- /dev/null
+++ b/server/src/server.h
@@ -0,0 +1,46 @@
+#ifndef SERVER_H
+#define SERVER_H
+
+#include "netban.h"
+#include "network.h"
+
+class CServer
+{
+	class CClient
+	{
+	public:
+		enum
+		{
+			STATE_EMPTY=0,
+			STATE_CONNECTED,
+			STATE_AUTHED,
+		};
+
+		int m_State;
+		int64 m_TimeConnected;
+		int64 m_LastReceived;
+	};
+	CClient m_aClients[NET_MAX_CLIENTS];
+
+	CNetwork m_Network;
+	CNetBan m_NetBan;
+
+	class CMain *m_pMain;
+
+	bool m_Ready;
+
+	static int NewClientCallback(int ClientID, void *pUser);
+	static int DelClientCallback(int ClientID, const char *pReason, void *pUser);
+
+public:
+	int Init(CMain *pMain, const char *Bind, int Port);
+	void Update();
+	void Send(int ClientID, const char *pLine);
+	void Shutdown();
+
+	CNetwork *Network() { return &m_Network; }
+	CNetBan *NetBan() { return &m_NetBan; }
+	CMain *Main() { return m_pMain; }
+};
+
+#endif
diff --git a/server/src/system.c b/server/src/system.c
new file mode 100644
index 0000000..aac93f9
--- /dev/null
+++ b/server/src/system.c
@@ -0,0 +1,2001 @@
+/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
+/* If you are missing that file, acquire a complete release at teeworlds.com.                */
+#include <stdlib.h>
+#include <stdio.h>
+#include <stdarg.h>
+#include <string.h>
+#include <ctype.h>
+#include <time.h>
+
+#include "system.h"
+
+#if defined(CONF_FAMILY_UNIX)
+	#include <sys/time.h>
+	#include <unistd.h>
+
+	/* unix net includes */
+	#include <sys/stat.h>
+	#include <sys/types.h>
+	#include <sys/socket.h>
+	#include <sys/ioctl.h>
+	#include <errno.h>
+	#include <netdb.h>
+	#include <netinet/in.h>
+	#include <fcntl.h>
+	#include <pthread.h>
+	#include <arpa/inet.h>
+
+	#include <dirent.h>
+
+	#if defined(CONF_PLATFORM_MACOSX)
+		#include <Carbon/Carbon.h>
+	#endif
+
+#elif defined(CONF_FAMILY_WINDOWS)
+	#define WIN32_LEAN_AND_MEAN
+	#define _WIN32_WINNT 0x0501 /* required for mingw to get getaddrinfo to work */
+	#include <windows.h>
+	#include <winsock2.h>
+	#include <ws2tcpip.h>
+	#include <fcntl.h>
+	#include <direct.h>
+	#include <errno.h>
+#else
+	#error NOT IMPLEMENTED
+#endif
+
+#if defined(CONF_PLATFORM_SOLARIS)
+	#include <sys/filio.h>
+#endif
+
+#if defined(__cplusplus)
+extern "C" {
+#endif
+
+IOHANDLE io_stdin() { return (IOHANDLE)stdin; }
+IOHANDLE io_stdout() { return (IOHANDLE)stdout; }
+IOHANDLE io_stderr() { return (IOHANDLE)stderr; }
+
+static DBG_LOGGER loggers[16];
+static int num_loggers = 0;
+
+static NETSTATS network_stats = {0};
+static MEMSTATS memory_stats = {0};
+
+static NETSOCKET invalid_socket = {NETTYPE_INVALID, -1, -1};
+
+void dbg_logger(DBG_LOGGER logger)
+{
+	loggers[num_loggers++] = logger;
+}
+
+void dbg_assert_imp(const char *filename, int line, int test, const char *msg)
+{
+	if(!test)
+	{
+		dbg_msg("assert", "%s(%d): %s", filename, line, msg);
+		dbg_break();
+	}
+}
+
+void dbg_break()
+{
+	*((volatile unsigned*)0) = 0x0;
+}
+
+void dbg_msg(const char *sys, const char *fmt, ...)
+{
+	va_list args;
+	char str[1024*8];
+	char *msg;
+	int i, len;
+
+	str_format(str, sizeof(str), "[%s]: ", sys);
+	len = strlen(str);
+	msg = (char *)str + len;
+
+	va_start(args, fmt);
+#if defined(CONF_FAMILY_WINDOWS)
+	_vsnprintf(msg, sizeof(str)-len, fmt, args);
+#else
+	vsnprintf(msg, sizeof(str)-len, fmt, args);
+#endif
+	va_end(args);
+
+	for(i = 0; i < num_loggers; i++)
+		loggers[i](str);
+}
+
+static void logger_stdout(const char *line)
+{
+	printf("%s\n", line);
+	fflush(stdout);
+}
+
+static void logger_debugger(const char *line)
+{
+#if defined(CONF_FAMILY_WINDOWS)
+	OutputDebugString(line);
+	OutputDebugString("\n");
+#endif
+}
+
+
+static IOHANDLE logfile = 0;
+static void logger_file(const char *line)
+{
+	io_write(logfile, line, strlen(line));
+	io_write_newline(logfile);
+	io_flush(logfile);
+}
+
+void dbg_logger_stdout() { dbg_logger(logger_stdout); }
+void dbg_logger_debugger() { dbg_logger(logger_debugger); }
+void dbg_logger_file(const char *filename)
+{
+	logfile = io_open(filename, IOFLAG_WRITE);
+	if(logfile)
+		dbg_logger(logger_file);
+	else
+		dbg_msg("dbg/logger", "failed to open '%s' for logging", filename);
+
+}
+/* */
+
+typedef struct MEMHEADER
+{
+	const char *filename;
+	int line;
+	int size;
+	struct MEMHEADER *prev;
+	struct MEMHEADER *next;
+} MEMHEADER;
+
+typedef struct MEMTAIL
+{
+	int guard;
+} MEMTAIL;
+
+static struct MEMHEADER *first = 0;
+static const int MEM_GUARD_VAL = 0xbaadc0de;
+
+void *mem_alloc_debug(const char *filename, int line, unsigned size, unsigned alignment)
+{
+	/* TODO: fix alignment */
+	/* TODO: add debugging */
+	MEMTAIL *tail;
+	MEMHEADER *header = (struct MEMHEADER *)malloc(size+sizeof(MEMHEADER)+sizeof(MEMTAIL));
+	dbg_assert(header != 0, "mem_alloc failure");
+	if(!header)
+		return NULL;
+	tail = (struct MEMTAIL *)(((char*)(header+1))+size);
+	header->size = size;
+	header->filename = filename;
+	header->line = line;
+
+	memory_stats.allocated += header->size;
+	memory_stats.total_allocations++;
+	memory_stats.active_allocations++;
+
+	tail->guard = MEM_GUARD_VAL;
+
+	header->prev = (MEMHEADER *)0;
+	header->next = first;
+	if(first)
+		first->prev = header;
+	first = header;
+
+	/*dbg_msg("mem", "++ %p", header+1); */
+	return header+1;
+}
+
+void mem_free(void *p)
+{
+	if(p)
+	{
+		MEMHEADER *header = (MEMHEADER *)p - 1;
+		MEMTAIL *tail = (MEMTAIL *)(((char*)(header+1))+header->size);
+
+		if(tail->guard != MEM_GUARD_VAL)
+			dbg_msg("mem", "!! %p", p);
+		/* dbg_msg("mem", "-- %p", p); */
+		memory_stats.allocated -= header->size;
+		memory_stats.active_allocations--;
+
+		if(header->prev)
+			header->prev->next = header->next;
+		else
+			first = header->next;
+		if(header->next)
+			header->next->prev = header->prev;
+
+		free(header);
+	}
+}
+
+void mem_debug_dump(IOHANDLE file)
+{
+	char buf[1024];
+	MEMHEADER *header = first;
+	if(!file)
+		file = io_open("memory.txt", IOFLAG_WRITE);
+
+	if(file)
+	{
+		while(header)
+		{
+			str_format(buf, sizeof(buf), "%s(%d): %d", header->filename, header->line, header->size);
+			io_write(file, buf, strlen(buf));
+			io_write_newline(file);
+			header = header->next;
+		}
+
+		io_close(file);
+	}
+}
+
+
+void mem_copy(void *dest, const void *source, unsigned size)
+{
+	memcpy(dest, source, size);
+}
+
+void mem_move(void *dest, const void *source, unsigned size)
+{
+	memmove(dest, source, size);
+}
+
+void mem_zero(void *block, unsigned size)
+{
+	memset(block, 0, size);
+}
+
+int mem_check_imp()
+{
+	MEMHEADER *header = first;
+	while(header)
+	{
+		MEMTAIL *tail = (MEMTAIL *)(((char*)(header+1))+header->size);
+		if(tail->guard != MEM_GUARD_VAL)
+		{
+			dbg_msg("mem", "Memory check failed at %s(%d): %d", header->filename, header->line, header->size);
+			return 0;
+		}
+		header = header->next;
+	}
+
+	return 1;
+}
+
+IOHANDLE io_open(const char *filename, int flags)
+{
+	if(flags == IOFLAG_READ)
+	{
+	#if defined(CONF_FAMILY_WINDOWS)
+		// check for filename case sensitive
+		WIN32_FIND_DATA finddata;
+		HANDLE handle;
+		int length;
+
+		length = str_length(filename);
+		if(!filename || !length || filename[length-1] == '\\')
+			return 0x0;
+		handle = FindFirstFile(filename, &finddata);
+		if(handle == INVALID_HANDLE_VALUE)
+			return 0x0;
+		else if(str_comp(filename+length-str_length(finddata.cFileName), finddata.cFileName) != 0)
+		{
+			FindClose(handle);
+			return 0x0;
+		}
+		FindClose(handle);
+	#endif
+		return (IOHANDLE)fopen(filename, "rb");
+	}
+	if(flags == IOFLAG_WRITE)
+		return (IOHANDLE)fopen(filename, "wb");
+	return 0x0;
+}
+
+unsigned io_read(IOHANDLE io, void *buffer, unsigned size)
+{
+	return fread(buffer, 1, size, (FILE*)io);
+}
+
+unsigned io_skip(IOHANDLE io, int size)
+{
+	fseek((FILE*)io, size, SEEK_CUR);
+	return size;
+}
+
+int io_seek(IOHANDLE io, int offset, int origin)
+{
+	int real_origin;
+
+	switch(origin)
+	{
+	case IOSEEK_START:
+		real_origin = SEEK_SET;
+		break;
+	case IOSEEK_CUR:
+		real_origin = SEEK_CUR;
+		break;
+	case IOSEEK_END:
+		real_origin = SEEK_END;
+		break;
+	default:
+		return -1;
+	}
+
+	return fseek((FILE*)io, offset, real_origin);
+}
+
+long int io_tell(IOHANDLE io)
+{
+	return ftell((FILE*)io);
+}
+
+long int io_length(IOHANDLE io)
+{
+	long int length;
+	io_seek(io, 0, IOSEEK_END);
+	length = io_tell(io);
+	io_seek(io, 0, IOSEEK_START);
+	return length;
+}
+
+unsigned io_write(IOHANDLE io, const void *buffer, unsigned size)
+{
+	return fwrite(buffer, 1, size, (FILE*)io);
+}
+
+unsigned io_write_newline(IOHANDLE io)
+{
+#if defined(CONF_FAMILY_WINDOWS)
+	return fwrite("\r\n", 1, 2, (FILE*)io);
+#else
+	return fwrite("\n", 1, 1, (FILE*)io);
+#endif
+}
+
+int io_close(IOHANDLE io)
+{
+	fclose((FILE*)io);
+	return 1;
+}
+
+int io_flush(IOHANDLE io)
+{
+	fflush((FILE*)io);
+	return 0;
+}
+
+void *thread_create(void (*threadfunc)(void *), void *u)
+{
+#if defined(CONF_FAMILY_UNIX)
+	pthread_t id;
+	pthread_create(&id, NULL, (void *(*)(void*))threadfunc, u);
+	return (void*)id;
+#elif defined(CONF_FAMILY_WINDOWS)
+	return CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)threadfunc, u, 0, NULL);
+#else
+	#error not implemented
+#endif
+}
+
+void thread_wait(void *thread)
+{
+#if defined(CONF_FAMILY_UNIX)
+	pthread_join((pthread_t)thread, NULL);
+#elif defined(CONF_FAMILY_WINDOWS)
+	WaitForSingleObject((HANDLE)thread, INFINITE);
+#else
+	#error not implemented
+#endif
+}
+
+void thread_destroy(void *thread)
+{
+#if defined(CONF_FAMILY_UNIX)
+	void *r = 0;
+	pthread_join((pthread_t)thread, &r);
+#else
+	/*#error not implemented*/
+#endif
+}
+
+void thread_yield()
+{
+#if defined(CONF_FAMILY_UNIX)
+	sched_yield();
+#elif defined(CONF_FAMILY_WINDOWS)
+	Sleep(0);
+#else
+	#error not implemented
+#endif
+}
+
+void thread_sleep(int milliseconds)
+{
+#if defined(CONF_FAMILY_UNIX)
+	usleep(milliseconds*1000);
+#elif defined(CONF_FAMILY_WINDOWS)
+	Sleep(milliseconds);
+#else
+	#error not implemented
+#endif
+}
+
+void thread_detach(void *thread)
+{
+#if defined(CONF_FAMILY_UNIX)
+	pthread_detach((pthread_t)(thread));
+#elif defined(CONF_FAMILY_WINDOWS)
+	CloseHandle(thread);
+#else
+	#error not implemented
+#endif
+}
+
+
+
+
+#if defined(CONF_FAMILY_UNIX)
+typedef pthread_mutex_t LOCKINTERNAL;
+#elif defined(CONF_FAMILY_WINDOWS)
+typedef CRITICAL_SECTION LOCKINTERNAL;
+#else
+	#error not implemented on this platform
+#endif
+
+LOCK lock_create()
+{
+	LOCKINTERNAL *lock = (LOCKINTERNAL*)mem_alloc(sizeof(LOCKINTERNAL), 4);
+
+#if defined(CONF_FAMILY_UNIX)
+	pthread_mutex_init(lock, 0x0);
+#elif defined(CONF_FAMILY_WINDOWS)
+	InitializeCriticalSection((LPCRITICAL_SECTION)lock);
+#else
+	#error not implemented on this platform
+#endif
+	return (LOCK)lock;
+}
+
+void lock_destroy(LOCK lock)
+{
+#if defined(CONF_FAMILY_UNIX)
+	pthread_mutex_destroy((LOCKINTERNAL *)lock);
+#elif defined(CONF_FAMILY_WINDOWS)
+	DeleteCriticalSection((LPCRITICAL_SECTION)lock);
+#else
+	#error not implemented on this platform
+#endif
+	mem_free(lock);
+}
+
+int lock_try(LOCK lock)
+{
+#if defined(CONF_FAMILY_UNIX)
+	return pthread_mutex_trylock((LOCKINTERNAL *)lock);
+#elif defined(CONF_FAMILY_WINDOWS)
+	return !TryEnterCriticalSection((LPCRITICAL_SECTION)lock);
+#else
+	#error not implemented on this platform
+#endif
+}
+
+void lock_wait(LOCK lock)
+{
+#if defined(CONF_FAMILY_UNIX)
+	pthread_mutex_lock((LOCKINTERNAL *)lock);
+#elif defined(CONF_FAMILY_WINDOWS)
+	EnterCriticalSection((LPCRITICAL_SECTION)lock);
+#else
+	#error not implemented on this platform
+#endif
+}
+
+void lock_release(LOCK lock)
+{
+#if defined(CONF_FAMILY_UNIX)
+	pthread_mutex_unlock((LOCKINTERNAL *)lock);
+#elif defined(CONF_FAMILY_WINDOWS)
+	LeaveCriticalSection((LPCRITICAL_SECTION)lock);
+#else
+	#error not implemented on this platform
+#endif
+}
+
+#if !defined(CONF_PLATFORM_MACOSX)
+	#if defined(CONF_FAMILY_UNIX)
+	void semaphore_init(SEMAPHORE *sem) { sem_init(sem, 0, 0); }
+	void semaphore_wait(SEMAPHORE *sem) { sem_wait(sem); }
+	void semaphore_signal(SEMAPHORE *sem) { sem_post(sem); }
+	void semaphore_destroy(SEMAPHORE *sem) { sem_destroy(sem); }
+	#elif defined(CONF_FAMILY_WINDOWS)
+	void semaphore_init(SEMAPHORE *sem) { *sem = CreateSemaphore(0, 0, 10000, 0); }
+	void semaphore_wait(SEMAPHORE *sem) { WaitForSingleObject((HANDLE)*sem, INFINITE); }
+	void semaphore_signal(SEMAPHORE *sem) { ReleaseSemaphore((HANDLE)*sem, 1, NULL); }
+	void semaphore_destroy(SEMAPHORE *sem) { CloseHandle((HANDLE)*sem); }
+	#else
+		#error not implemented on this platform
+	#endif
+#endif
+
+
+/* -----  time ----- */
+int64 time_get()
+{
+#if defined(CONF_FAMILY_UNIX)
+	struct timeval val;
+	gettimeofday(&val, NULL);
+	return (int64)val.tv_sec*(int64)1000000+(int64)val.tv_usec;
+#elif defined(CONF_FAMILY_WINDOWS)
+	static int64 last = 0;
+	int64 t;
+	QueryPerformanceCounter((PLARGE_INTEGER)&t);
+	if(t<last) /* for some reason, QPC can return values in the past */
+		return last;
+	last = t;
+	return t;
+#else
+	#error not implemented
+#endif
+}
+
+int64 time_freq()
+{
+#if defined(CONF_FAMILY_UNIX)
+	return 1000000;
+#elif defined(CONF_FAMILY_WINDOWS)
+	int64 t;
+	QueryPerformanceFrequency((PLARGE_INTEGER)&t);
+	return t;
+#else
+	#error not implemented
+#endif
+}
+
+/* -----  network ----- */
+static void netaddr_to_sockaddr_in(const NETADDR *src, struct sockaddr_in *dest)
+{
+	mem_zero(dest, sizeof(struct sockaddr_in));
+	if(src->type != NETTYPE_IPV4)
+	{
+		dbg_msg("system", "couldn't convert NETADDR of type %d to ipv4", src->type);
+		return;
+	}
+
+	dest->sin_family = AF_INET;
+	dest->sin_port = htons(src->port);
+	mem_copy(&dest->sin_addr.s_addr, src->ip, 4);
+}
+
+static void netaddr_to_sockaddr_in6(const NETADDR *src, struct sockaddr_in6 *dest)
+{
+	mem_zero(dest, sizeof(struct sockaddr_in6));
+	if(src->type != NETTYPE_IPV6)
+	{
+		dbg_msg("system", "couldn't not convert NETADDR of type %d to ipv6", src->type);
+		return;
+	}
+
+	dest->sin6_family = AF_INET6;
+	dest->sin6_port = htons(src->port);
+	mem_copy(&dest->sin6_addr.s6_addr, src->ip, 16);
+}
+
+static void sockaddr_to_netaddr(const struct sockaddr *src, NETADDR *dst)
+{
+	if(src->sa_family == AF_INET)
+	{
+		mem_zero(dst, sizeof(NETADDR));
+		dst->type = NETTYPE_IPV4;
+		dst->port = htons(((struct sockaddr_in*)src)->sin_port);
+		mem_copy(dst->ip, &((struct sockaddr_in*)src)->sin_addr.s_addr, 4);
+	}
+	else if(src->sa_family == AF_INET6)
+	{
+		mem_zero(dst, sizeof(NETADDR));
+		dst->type = NETTYPE_IPV6;
+		dst->port = htons(((struct sockaddr_in6*)src)->sin6_port);
+		mem_copy(dst->ip, &((struct sockaddr_in6*)src)->sin6_addr.s6_addr, 16);
+	}
+	else
+	{
+		mem_zero(dst, sizeof(struct sockaddr));
+		dbg_msg("system", "couldn't convert sockaddr of family %d", src->sa_family);
+	}
+}
+
+int net_addr_comp(const NETADDR *a, const NETADDR *b)
+{
+	return mem_comp(a, b, sizeof(NETADDR));
+}
+
+void net_addr_str(const NETADDR *addr, char *string, int max_length, int add_port)
+{
+	if(addr->type == NETTYPE_IPV4)
+	{
+		if(add_port != 0)
+			str_format(string, max_length, "%d.%d.%d.%d:%d", addr->ip[0], addr->ip[1], addr->ip[2], addr->ip[3], addr->port);
+		else
+			str_format(string, max_length, "%d.%d.%d.%d", addr->ip[0], addr->ip[1], addr->ip[2], addr->ip[3]);
+	}
+	else if(addr->type == NETTYPE_IPV6)
+	{
+		if(add_port != 0)
+			str_format(string, max_length, "[%x:%x:%x:%x:%x:%x:%x:%x]:%d",
+				(addr->ip[0]<<8)|addr->ip[1], (addr->ip[2]<<8)|addr->ip[3], (addr->ip[4]<<8)|addr->ip[5], (addr->ip[6]<<8)|addr->ip[7],
+				(addr->ip[8]<<8)|addr->ip[9], (addr->ip[10]<<8)|addr->ip[11], (addr->ip[12]<<8)|addr->ip[13], (addr->ip[14]<<8)|addr->ip[15],
+				addr->port);
+		else
+			str_format(string, max_length, "[%x:%x:%x:%x:%x:%x:%x:%x]",
+				(addr->ip[0]<<8)|addr->ip[1], (addr->ip[2]<<8)|addr->ip[3], (addr->ip[4]<<8)|addr->ip[5], (addr->ip[6]<<8)|addr->ip[7],
+				(addr->ip[8]<<8)|addr->ip[9], (addr->ip[10]<<8)|addr->ip[11], (addr->ip[12]<<8)|addr->ip[13], (addr->ip[14]<<8)|addr->ip[15]);
+	}
+	else
+		str_format(string, max_length, "unknown type %d", addr->type);
+}
+
+static int priv_net_extract(const char *hostname, char *host, int max_host, int *port)
+{
+	int i;
+
+	*port = 0;
+	host[0] = 0;
+
+	if(hostname[0] == '[')
+	{
+		// ipv6 mode
+		for(i = 1; i < max_host && hostname[i] && hostname[i] != ']'; i++)
+			host[i-1] = hostname[i];
+		host[i-1] = 0;
+		if(hostname[i] != ']') // malformatted
+			return -1;
+
+		i++;
+		if(hostname[i] == ':')
+			*port = atol(hostname+i+1);
+	}
+	else
+	{
+		// generic mode (ipv4, hostname etc)
+		for(i = 0; i < max_host-1 && hostname[i] && hostname[i] != ':'; i++)
+			host[i] = hostname[i];
+		host[i] = 0;
+
+		if(hostname[i] == ':')
+			*port = atol(hostname+i+1);
+	}
+
+	return 0;
+}
+
+int net_host_lookup(const char *hostname, NETADDR *addr, int types)
+{
+	struct addrinfo hints;
+	struct addrinfo *result;
+	int e;
+	char host[256];
+	int port = 0;
+
+	if(priv_net_extract(hostname, host, sizeof(host), &port))
+		return -1;
+	/*
+	dbg_msg("host lookup", "host='%s' port=%d %d", host, port, types);
+	*/
+
+	mem_zero(&hints, sizeof(hints));
+
+	hints.ai_family = AF_UNSPEC;
+
+	if(types == NETTYPE_IPV4)
+		hints.ai_family = AF_INET;
+	else if(types == NETTYPE_IPV6)
+		hints.ai_family = AF_INET6;
+
+	e = getaddrinfo(host, NULL, &hints, &result);
+	if(e != 0 || !result)
+		return -1;
+
+	sockaddr_to_netaddr(result->ai_addr, addr);
+	freeaddrinfo(result);
+	addr->port = port;
+	return 0;
+}
+
+static int parse_int(int *out, const char **str)
+{
+	int i = 0;
+	*out = 0;
+	if(**str < '0' || **str > '9')
+		return -1;
+
+	i = **str - '0';
+	(*str)++;
+
+	while(1)
+	{
+		if(**str < '0' || **str > '9')
+		{
+			*out = i;
+			return 0;
+		}
+
+		i = (i*10) + (**str - '0');
+		(*str)++;
+	}
+
+	return 0;
+}
+
+static int parse_char(char c, const char **str)
+{
+	if(**str != c) return -1;
+	(*str)++;
+	return 0;
+}
+
+static int parse_uint8(unsigned char *out, const char **str)
+{
+	int i;
+	if(parse_int(&i, str) != 0) return -1;
+	if(i < 0 || i > 0xff) return -1;
+	*out = i;
+	return 0;
+}
+
+static int parse_uint16(unsigned short *out, const char **str)
+{
+	int i;
+	if(parse_int(&i, str) != 0) return -1;
+	if(i < 0 || i > 0xffff) return -1;
+	*out = i;
+	return 0;
+}
+
+int net_addr_from_str(NETADDR *addr, const char *string)
+{
+	const char *str = string;
+	mem_zero(addr, sizeof(NETADDR));
+
+	if(str[0] == '[')
+	{
+		/* ipv6 */
+		struct sockaddr_in6 sa6;
+		char buf[128];
+		int i;
+		str++;
+		for(i = 0; i < 127 && str[i] && str[i] != ']'; i++)
+			buf[i] = str[i];
+		buf[i] = 0;
+		str += i;
+#if defined(CONF_FAMILY_WINDOWS)
+		{
+			int size;
+			sa6.sin6_family = AF_INET6;
+			size = (int)sizeof(sa6);
+			if(WSAStringToAddress(buf, AF_INET6, NULL, (struct sockaddr *)&sa6, &size) != 0)
+				return -1;
+		}
+#else
+		if(inet_pton(AF_INET6, buf, &sa6) != 1)
+			return -1;
+#endif
+		sockaddr_to_netaddr((struct sockaddr *)&sa6, addr);
+
+		if(*str == ']')
+		{
+			str++;
+			if(*str == ':')
+			{
+				str++;
+				if(parse_uint16(&addr->port, &str))
+					return -1;
+			}
+		}
+		else
+			return -1;
+
+		return 0;
+	}
+	else
+	{
+		/* ipv4 */
+		if(parse_uint8(&addr->ip[0], &str)) return -1;
+		if(parse_char('.', &str)) return -1;
+		if(parse_uint8(&addr->ip[1], &str)) return -1;
+		if(parse_char('.', &str)) return -1;
+		if(parse_uint8(&addr->ip[2], &str)) return -1;
+		if(parse_char('.', &str)) return -1;
+		if(parse_uint8(&addr->ip[3], &str)) return -1;
+		if(*str == ':')
+		{
+			str++;
+			if(parse_uint16(&addr->port, &str)) return -1;
+		}
+
+		addr->type = NETTYPE_IPV4;
+	}
+
+	return 0;
+}
+
+static void priv_net_close_socket(int sock)
+{
+#if defined(CONF_FAMILY_WINDOWS)
+	closesocket(sock);
+#else
+	close(sock);
+#endif
+}
+
+static int priv_net_close_all_sockets(NETSOCKET sock)
+{
+	/* close down ipv4 */
+	if(sock.ipv4sock >= 0)
+	{
+		priv_net_close_socket(sock.ipv4sock);
+		sock.ipv4sock = -1;
+		sock.type &= ~NETTYPE_IPV4;
+	}
+
+	/* close down ipv6 */
+	if(sock.ipv6sock >= 0)
+	{
+		priv_net_close_socket(sock.ipv6sock);
+		sock.ipv6sock = -1;
+		sock.type &= ~NETTYPE_IPV6;
+	}
+	return 0;
+}
+
+static int priv_net_create_socket(int domain, int type, struct sockaddr *addr, int sockaddrlen)
+{
+	int sock, e;
+
+	/* create socket */
+	sock = socket(domain, type, 0);
+	if(sock < 0)
+	{
+#if defined(CONF_FAMILY_WINDOWS)
+		char buf[128];
+		int error = WSAGetLastError();
+		if(FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM|FORMAT_MESSAGE_IGNORE_INSERTS, 0, error, 0, buf, sizeof(buf), 0) == 0)
+			buf[0] = 0;
+		dbg_msg("net", "failed to create socket with domain %d and type %d (%d '%s')", domain, type, error, buf);
+#else
+		dbg_msg("net", "failed to create socket with domain %d and type %d (%d '%s')", domain, type, errno, strerror(errno));
+#endif
+		return -1;
+	}
+
+	/* set to IPv6 only if thats what we are creating */
+#if defined(IPV6_V6ONLY)	/* windows sdk 6.1 and higher */
+	if(domain == AF_INET6)
+	{
+		int ipv6only = 1;
+		setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, (const char*)&ipv6only, sizeof(ipv6only));
+	}
+#endif
+
+	if(type == SOCK_STREAM)
+	{
+		int tmp = 1;
+		setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &tmp, sizeof(tmp));
+	}
+
+	/* bind the socket */
+	e = bind(sock, addr, sockaddrlen);
+	if(e != 0)
+	{
+#if defined(CONF_FAMILY_WINDOWS)
+		char buf[128];
+		int error = WSAGetLastError();
+		if(FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM|FORMAT_MESSAGE_IGNORE_INSERTS, 0, error, 0, buf, sizeof(buf), 0) == 0)
+			buf[0] = 0;
+		dbg_msg("net", "failed to bind socket with domain %d and type %d (%d '%s')", domain, type, error, buf);
+#else
+		dbg_msg("net", "failed to bind socket with domain %d and type %d (%d '%s')", domain, type, errno, strerror(errno));
+#endif
+		priv_net_close_socket(sock);
+		return -1;
+	}
+
+	/* return the newly created socket */
+	return sock;
+}
+
+NETSOCKET net_udp_create(NETADDR bindaddr)
+{
+	NETSOCKET sock = invalid_socket;
+	NETADDR tmpbindaddr = bindaddr;
+	int broadcast = 1;
+	int recvsize = 65536;
+
+	if(bindaddr.type&NETTYPE_IPV4)
+	{
+		struct sockaddr_in addr;
+		int socket = -1;
+
+		/* bind, we should check for error */
+		tmpbindaddr.type = NETTYPE_IPV4;
+		netaddr_to_sockaddr_in(&tmpbindaddr, &addr);
+		socket = priv_net_create_socket(AF_INET, SOCK_DGRAM, (struct sockaddr *)&addr, sizeof(addr));
+		if(socket >= 0)
+		{
+			sock.type |= NETTYPE_IPV4;
+			sock.ipv4sock = socket;
+
+			/* set broadcast */
+			setsockopt(socket, SOL_SOCKET, SO_BROADCAST, (const char*)&broadcast, sizeof(broadcast));
+
+			/* set receive buffer size */
+			setsockopt(socket, SOL_SOCKET, SO_RCVBUF, (char*)&recvsize, sizeof(recvsize));
+		}
+	}
+
+	if(bindaddr.type&NETTYPE_IPV6)
+	{
+		struct sockaddr_in6 addr;
+		int socket = -1;
+
+		/* bind, we should check for error */
+		tmpbindaddr.type = NETTYPE_IPV6;
+		netaddr_to_sockaddr_in6(&tmpbindaddr, &addr);
+		socket = priv_net_create_socket(AF_INET6, SOCK_DGRAM, (struct sockaddr *)&addr, sizeof(addr));
+		if(socket >= 0)
+		{
+			sock.type |= NETTYPE_IPV6;
+			sock.ipv6sock = socket;
+
+			/* set broadcast */
+			setsockopt(socket, SOL_SOCKET, SO_BROADCAST, (const char*)&broadcast, sizeof(broadcast));
+
+			/* set receive buffer size */
+			setsockopt(socket, SOL_SOCKET, SO_RCVBUF, (char*)&recvsize, sizeof(recvsize));
+		}
+	}
+
+	/* set non-blocking */
+	net_set_non_blocking(sock);
+
+	/* return */
+	return sock;
+}
+
+int net_udp_send(NETSOCKET sock, const NETADDR *addr, const void *data, int size)
+{
+	int d = -1;
+
+	if(addr->type&NETTYPE_IPV4)
+	{
+		if(sock.ipv4sock >= 0)
+		{
+			struct sockaddr_in sa;
+			if(addr->type&NETTYPE_LINK_BROADCAST)
+			{
+				mem_zero(&sa, sizeof(sa));
+				sa.sin_port = htons(addr->port);
+				sa.sin_family = AF_INET;
+				sa.sin_addr.s_addr = INADDR_BROADCAST;
+			}
+			else
+				netaddr_to_sockaddr_in(addr, &sa);
+
+			d = sendto((int)sock.ipv4sock, (const char*)data, size, 0, (struct sockaddr *)&sa, sizeof(sa));
+		}
+		else
+			dbg_msg("net", "can't sent ipv4 traffic to this socket");
+	}
+
+	if(addr->type&NETTYPE_IPV6)
+	{
+		if(sock.ipv6sock >= 0)
+		{
+			struct sockaddr_in6 sa;
+			if(addr->type&NETTYPE_LINK_BROADCAST)
+			{
+				mem_zero(&sa, sizeof(sa));
+				sa.sin6_port = htons(addr->port);
+				sa.sin6_family = AF_INET6;
+				sa.sin6_addr.s6_addr[0] = 0xff; /* multicast */
+				sa.sin6_addr.s6_addr[1] = 0x02; /* link local scope */
+				sa.sin6_addr.s6_addr[15] = 1; /* all nodes */
+			}
+			else
+				netaddr_to_sockaddr_in6(addr, &sa);
+
+			d = sendto((int)sock.ipv6sock, (const char*)data, size, 0, (struct sockaddr *)&sa, sizeof(sa));
+		}
+		else
+			dbg_msg("net", "can't sent ipv6 traffic to this socket");
+	}
+	/*
+	else
+		dbg_msg("net", "can't sent to network of type %d", addr->type);
+		*/
+
+	/*if(d < 0)
+	{
+		char addrstr[256];
+		net_addr_str(addr, addrstr, sizeof(addrstr));
+
+		dbg_msg("net", "sendto error (%d '%s')", errno, strerror(errno));
+		dbg_msg("net", "\tsock = %d %x", sock, sock);
+		dbg_msg("net", "\tsize = %d %x", size, size);
+		dbg_msg("net", "\taddr = %s", addrstr);
+
+	}*/
+	network_stats.sent_bytes += size;
+	network_stats.sent_packets++;
+	return d;
+}
+
+int net_udp_recv(NETSOCKET sock, NETADDR *addr, void *data, int maxsize)
+{
+	char sockaddrbuf[128];
+	socklen_t fromlen;// = sizeof(sockaddrbuf);
+	int bytes = 0;
+
+	if(bytes == 0 && sock.ipv4sock >= 0)
+	{
+		fromlen = sizeof(struct sockaddr_in);
+		bytes = recvfrom(sock.ipv4sock, (char*)data, maxsize, 0, (struct sockaddr *)&sockaddrbuf, &fromlen);
+	}
+
+	if(bytes <= 0 && sock.ipv6sock >= 0)
+	{
+		fromlen = sizeof(struct sockaddr_in6);
+		bytes = recvfrom(sock.ipv6sock, (char*)data, maxsize, 0, (struct sockaddr *)&sockaddrbuf, &fromlen);
+	}
+
+	if(bytes > 0)
+	{
+		sockaddr_to_netaddr((struct sockaddr *)&sockaddrbuf, addr);
+		network_stats.recv_bytes += bytes;
+		network_stats.recv_packets++;
+		return bytes;
+	}
+	else if(bytes == 0)
+		return 0;
+	return -1; /* error */
+}
+
+int net_udp_close(NETSOCKET sock)
+{
+	return priv_net_close_all_sockets(sock);
+}
+
+NETSOCKET net_tcp_create(NETADDR bindaddr)
+{
+	NETSOCKET sock = invalid_socket;
+	NETADDR tmpbindaddr = bindaddr;
+
+	if(bindaddr.type&NETTYPE_IPV4)
+	{
+		struct sockaddr_in addr;
+		int socket = -1;
+
+		/* bind, we should check for error */
+		tmpbindaddr.type = NETTYPE_IPV4;
+		netaddr_to_sockaddr_in(&tmpbindaddr, &addr);
+		socket = priv_net_create_socket(AF_INET, SOCK_STREAM, (struct sockaddr *)&addr, sizeof(addr));
+		if(socket >= 0)
+		{
+			sock.type |= NETTYPE_IPV4;
+			sock.ipv4sock = socket;
+		}
+	}
+
+	if(bindaddr.type&NETTYPE_IPV6)
+	{
+		struct sockaddr_in6 addr;
+		int socket = -1;
+
+		/* bind, we should check for error */
+		tmpbindaddr.type = NETTYPE_IPV6;
+		netaddr_to_sockaddr_in6(&tmpbindaddr, &addr);
+		socket = priv_net_create_socket(AF_INET6, SOCK_STREAM, (struct sockaddr *)&addr, sizeof(addr));
+		if(socket >= 0)
+		{
+			sock.type |= NETTYPE_IPV6;
+			sock.ipv6sock = socket;
+		}
+	}
+
+	/* return */
+	return sock;
+}
+
+int net_set_non_blocking(NETSOCKET sock)
+{
+	unsigned long mode = 1;
+	if(sock.ipv4sock >= 0)
+	{
+#if defined(CONF_FAMILY_WINDOWS)
+		ioctlsocket(sock.ipv4sock, FIONBIO, (unsigned long *)&mode);
+#else
+		ioctl(sock.ipv4sock, FIONBIO, (unsigned long *)&mode);
+#endif
+	}
+
+	if(sock.ipv6sock >= 0)
+	{
+#if defined(CONF_FAMILY_WINDOWS)
+		ioctlsocket(sock.ipv6sock, FIONBIO, (unsigned long *)&mode);
+#else
+		ioctl(sock.ipv6sock, FIONBIO, (unsigned long *)&mode);
+#endif
+	}
+
+	return 0;
+}
+
+int net_set_blocking(NETSOCKET sock)
+{
+	unsigned long mode = 0;
+	if(sock.ipv4sock >= 0)
+	{
+#if defined(CONF_FAMILY_WINDOWS)
+		ioctlsocket(sock.ipv4sock, FIONBIO, (unsigned long *)&mode);
+#else
+		ioctl(sock.ipv4sock, FIONBIO, (unsigned long *)&mode);
+#endif
+	}
+
+	if(sock.ipv6sock >= 0)
+	{
+#if defined(CONF_FAMILY_WINDOWS)
+		ioctlsocket(sock.ipv6sock, FIONBIO, (unsigned long *)&mode);
+#else
+		ioctl(sock.ipv6sock, FIONBIO, (unsigned long *)&mode);
+#endif
+	}
+
+	return 0;
+}
+
+int net_tcp_listen(NETSOCKET sock, int backlog)
+{
+	int err = -1;
+	if(sock.ipv4sock >= 0)
+		err = listen(sock.ipv4sock, backlog);
+	if(sock.ipv6sock >= 0)
+		err = listen(sock.ipv6sock, backlog);
+	return err;
+}
+
+int net_tcp_accept(NETSOCKET sock, NETSOCKET *new_sock, NETADDR *a)
+{
+	int s;
+	socklen_t sockaddr_len;
+
+	*new_sock = invalid_socket;
+
+	if(sock.ipv4sock >= 0)
+	{
+		struct sockaddr_in addr;
+		sockaddr_len = sizeof(addr);
+
+		s = accept(sock.ipv4sock, (struct sockaddr *)&addr, &sockaddr_len);
+
+		if (s != -1)
+		{
+			sockaddr_to_netaddr((const struct sockaddr *)&addr, a);
+			new_sock->type = NETTYPE_IPV4;
+			new_sock->ipv4sock = s;
+			return s;
+		}
+	}
+
+	if(sock.ipv6sock >= 0)
+	{
+		struct sockaddr_in6 addr;
+		sockaddr_len = sizeof(addr);
+
+		s = accept(sock.ipv6sock, (struct sockaddr *)&addr, &sockaddr_len);
+
+		if (s != -1)
+		{
+			sockaddr_to_netaddr((const struct sockaddr *)&addr, a);
+			new_sock->type = NETTYPE_IPV6;
+			new_sock->ipv6sock = s;
+			return s;
+		}
+	}
+
+	return -1;
+}
+
+int net_tcp_connect(NETSOCKET sock, const NETADDR *a)
+{
+	if(a->type&NETTYPE_IPV4)
+	{
+		struct sockaddr_in addr;
+		netaddr_to_sockaddr_in(a, &addr);
+		return connect(sock.ipv4sock, (struct sockaddr *)&addr, sizeof(addr));
+	}
+
+	if(a->type&NETTYPE_IPV6)
+	{
+		struct sockaddr_in6 addr;
+		netaddr_to_sockaddr_in6(a, &addr);
+		return connect(sock.ipv6sock, (struct sockaddr *)&addr, sizeof(addr));
+	}
+
+	return -1;
+}
+
+int net_tcp_connect_non_blocking(NETSOCKET sock, NETADDR bindaddr)
+{
+	int res = 0;
+
+	net_set_non_blocking(sock);
+	res = net_tcp_connect(sock, &bindaddr);
+	net_set_blocking(sock);
+
+	return res;
+}
+
+int net_tcp_send(NETSOCKET sock, const void *data, int size)
+{
+	int bytes = -1;
+
+	if(sock.ipv4sock >= 0)
+		bytes = send((int)sock.ipv4sock, (const char*)data, size, 0);
+	if(sock.ipv6sock >= 0)
+		bytes = send((int)sock.ipv6sock, (const char*)data, size, 0);
+
+	return bytes;
+}
+
+int net_tcp_recv(NETSOCKET sock, void *data, int maxsize)
+{
+	int bytes = -1;
+
+	if(sock.ipv4sock >= 0)
+		bytes = recv((int)sock.ipv4sock, (char*)data, maxsize, 0);
+	if(sock.ipv6sock >= 0)
+		bytes = recv((int)sock.ipv6sock, (char*)data, maxsize, 0);
+
+	return bytes;
+}
+
+int net_tcp_close(NETSOCKET sock)
+{
+	return priv_net_close_all_sockets(sock);
+}
+
+int net_errno()
+{
+#if defined(CONF_FAMILY_WINDOWS)
+	return WSAGetLastError();
+#else
+	return errno;
+#endif
+}
+
+int net_would_block()
+{
+#if defined(CONF_FAMILY_WINDOWS)
+	return net_errno() == WSAEWOULDBLOCK;
+#else
+	return net_errno() == EWOULDBLOCK;
+#endif
+}
+
+int net_init()
+{
+#if defined(CONF_FAMILY_WINDOWS)
+	WSADATA wsaData;
+	int err = WSAStartup(MAKEWORD(1, 1), &wsaData);
+	dbg_assert(err == 0, "network initialization failed.");
+	return err==0?0:1;
+#endif
+
+	return 0;
+}
+
+int fs_listdir(const char *dir, FS_LISTDIR_CALLBACK cb, int type, void *user)
+{
+#if defined(CONF_FAMILY_WINDOWS)
+	WIN32_FIND_DATA finddata;
+	HANDLE handle;
+	char buffer[1024*2];
+	int length;
+	str_format(buffer, sizeof(buffer), "%s/*", dir);
+
+	handle = FindFirstFileA(buffer, &finddata);
+
+	if (handle == INVALID_HANDLE_VALUE)
+		return 0;
+
+	str_format(buffer, sizeof(buffer), "%s/", dir);
+	length = str_length(buffer);
+
+	/* add all the entries */
+	do
+	{
+		str_copy(buffer+length, finddata.cFileName, (int)sizeof(buffer)-length);
+		if(cb(finddata.cFileName, fs_is_dir(buffer), type, user))
+			break;
+	}
+	while (FindNextFileA(handle, &finddata));
+
+	FindClose(handle);
+	return 0;
+#else
+	struct dirent *entry;
+	char buffer[1024*2];
+	int length;
+	DIR *d = opendir(dir);
+
+	if(!d)
+		return 0;
+
+	str_format(buffer, sizeof(buffer), "%s/", dir);
+	length = str_length(buffer);
+
+	while((entry = readdir(d)) != NULL)
+	{
+		str_copy(buffer+length, entry->d_name, (int)sizeof(buffer)-length);
+		if(cb(entry->d_name, fs_is_dir(buffer), type, user))
+			break;
+	}
+
+	/* close the directory and return */
+	closedir(d);
+	return 0;
+#endif
+}
+
+int fs_storage_path(const char *appname, char *path, int max)
+{
+#if defined(CONF_FAMILY_WINDOWS)
+	char *home = getenv("APPDATA");
+	if(!home)
+		return -1;
+	_snprintf(path, max, "%s/%s", home, appname);
+	return 0;
+#else
+	char *home = getenv("HOME");
+#if !defined(CONF_PLATFORM_MACOSX)
+	int i;
+#endif
+	if(!home)
+		return -1;
+
+#if defined(CONF_PLATFORM_MACOSX)
+	snprintf(path, max, "%s/Library/Application Support/%s", home, appname);
+#else
+	snprintf(path, max, "%s/.%s", home, appname);
+	for(i = strlen(home)+2; path[i]; i++)
+		path[i] = tolower(path[i]);
+#endif
+
+	return 0;
+#endif
+}
+
+int fs_makedir(const char *path)
+{
+#if defined(CONF_FAMILY_WINDOWS)
+	if(_mkdir(path) == 0)
+			return 0;
+	if(errno == EEXIST)
+		return 0;
+	return -1;
+#else
+	if(mkdir(path, 0755) == 0)
+		return 0;
+	if(errno == EEXIST)
+		return 0;
+	return -1;
+#endif
+}
+
+int fs_is_dir(const char *path)
+{
+#if defined(CONF_FAMILY_WINDOWS)
+	/* TODO: do this smarter */
+	WIN32_FIND_DATA finddata;
+	HANDLE handle;
+	char buffer[1024*2];
+	str_format(buffer, sizeof(buffer), "%s/*", path);
+
+	if ((handle = FindFirstFileA(buffer, &finddata)) == INVALID_HANDLE_VALUE)
+		return 0;
+
+	FindClose(handle);
+	return 1;
+#else
+	struct stat sb;
+	if (stat(path, &sb) == -1)
+		return 0;
+
+	if (S_ISDIR(sb.st_mode))
+		return 1;
+	else
+		return 0;
+#endif
+}
+
+int fs_chdir(const char *path)
+{
+	if(fs_is_dir(path))
+	{
+		if(chdir(path))
+			return 1;
+		else
+			return 0;
+	}
+	else
+		return 1;
+}
+
+char *fs_getcwd(char *buffer, int buffer_size)
+{
+	if(buffer == 0)
+		return 0;
+#if defined(CONF_FAMILY_WINDOWS)
+	return _getcwd(buffer, buffer_size);
+#else
+	return getcwd(buffer, buffer_size);
+#endif
+}
+
+int fs_parent_dir(char *path)
+{
+	char *parent = 0;
+	for(; *path; ++path)
+	{
+		if(*path == '/' || *path == '\\')
+			parent = path;
+	}
+
+	if(parent)
+	{
+		*parent = 0;
+		return 0;
+	}
+	return 1;
+}
+
+int fs_remove(const char *filename)
+{
+	if(remove(filename) != 0)
+		return 1;
+	return 0;
+}
+
+int fs_rename(const char *oldname, const char *newname)
+{
+	if(rename(oldname, newname) != 0)
+		return 1;
+	return 0;
+}
+
+void swap_endian(void *data, unsigned elem_size, unsigned num)
+{
+	char *src = (char*) data;
+	char *dst = src + (elem_size - 1);
+
+	while(num)
+	{
+		unsigned n = elem_size>>1;
+		char tmp;
+		while(n)
+		{
+			tmp = *src;
+			*src = *dst;
+			*dst = tmp;
+
+			src++;
+			dst--;
+			n--;
+		}
+
+		src = src + (elem_size>>1);
+		dst = src + (elem_size - 1);
+		num--;
+	}
+}
+
+int net_socket_read_wait(NETSOCKET sock, int time)
+{
+	struct timeval tv;
+	fd_set readfds;
+	int sockid;
+
+	tv.tv_sec = 0;
+	tv.tv_usec = 1000*time;
+	sockid = 0;
+
+	FD_ZERO(&readfds);
+	if(sock.ipv4sock >= 0)
+	{
+		FD_SET(sock.ipv4sock, &readfds);
+		sockid = sock.ipv4sock;
+	}
+	if(sock.ipv6sock >= 0)
+	{
+		FD_SET(sock.ipv6sock, &readfds);
+		if(sock.ipv6sock > sockid)
+			sockid = sock.ipv6sock;
+	}
+
+	/* don't care about writefds and exceptfds */
+	select(sockid+1, &readfds, NULL, NULL, &tv);
+
+	if(sock.ipv4sock >= 0 && FD_ISSET(sock.ipv4sock, &readfds))
+		return 1;
+
+	if(sock.ipv6sock >= 0 && FD_ISSET(sock.ipv6sock, &readfds))
+		return 1;
+
+	return 0;
+}
+
+int time_timestamp()
+{
+	return time(0);
+}
+
+void str_append(char *dst, const char *src, int dst_size)
+{
+	int s = strlen(dst);
+	int i = 0;
+	while(s < dst_size)
+	{
+		dst[s] = src[i];
+		if(!src[i]) /* check for null termination */
+			break;
+		s++;
+		i++;
+	}
+
+	dst[dst_size-1] = 0; /* assure null termination */
+}
+
+void str_copy(char *dst, const char *src, int dst_size)
+{
+	strncpy(dst, src, dst_size);
+	dst[dst_size-1] = 0; /* assure null termination */
+}
+
+int str_length(const char *str)
+{
+	return (int)strlen(str);
+}
+
+void str_format(char *buffer, int buffer_size, const char *format, ...)
+{
+#if defined(CONF_FAMILY_WINDOWS)
+	va_list ap;
+	va_start(ap, format);
+	_vsnprintf(buffer, buffer_size, format, ap);
+	va_end(ap);
+#else
+	va_list ap;
+	va_start(ap, format);
+	vsnprintf(buffer, buffer_size, format, ap);
+	va_end(ap);
+#endif
+
+	buffer[buffer_size-1] = 0; /* assure null termination */
+}
+
+
+
+/* makes sure that the string only contains the characters between 32 and 127 */
+void str_sanitize_strong(char *str_in)
+{
+	unsigned char *str = (unsigned char *)str_in;
+	while(*str)
+	{
+		*str &= 0x7f;
+		if(*str < 32)
+			*str = 32;
+		str++;
+	}
+}
+
+/* makes sure that the string only contains the characters between 32 and 255 */
+void str_sanitize_cc(char *str_in)
+{
+	unsigned char *str = (unsigned char *)str_in;
+	while(*str)
+	{
+		if(*str < 32)
+			*str = ' ';
+		str++;
+	}
+}
+
+/* makes sure that the string only contains the characters between 32 and 255 + \r\n\t */
+void str_sanitize(char *str_in)
+{
+	unsigned char *str = (unsigned char *)str_in;
+	while(*str)
+	{
+		if(*str < 32 && !(*str == '\r') && !(*str == '\n') && !(*str == '\t'))
+			*str = ' ';
+		str++;
+	}
+}
+
+char *str_skip_to_whitespace(char *str)
+{
+	while(*str && (*str != ' ' && *str != '\t' && *str != '\n'))
+		str++;
+	return str;
+}
+
+char *str_skip_whitespaces(char *str)
+{
+	while(*str && (*str == ' ' || *str == '\t' || *str == '\n' || *str == '\r'))
+		str++;
+	return str;
+}
+
+/* case */
+int str_comp_nocase(const char *a, const char *b)
+{
+#if defined(CONF_FAMILY_WINDOWS)
+	return _stricmp(a,b);
+#else
+	return strcasecmp(a,b);
+#endif
+}
+
+int str_comp_nocase_num(const char *a, const char *b, const int num)
+{
+#if defined(CONF_FAMILY_WINDOWS)
+	return _strnicmp(a, b, num);
+#else
+	return strncasecmp(a, b, num);
+#endif
+}
+
+int str_comp(const char *a, const char *b)
+{
+	return strcmp(a, b);
+}
+
+int str_comp_num(const char *a, const char *b, const int num)
+{
+	return strncmp(a, b, num);
+}
+
+int str_comp_filenames(const char *a, const char *b)
+{
+	int result;
+
+	for(; *a && *b; ++a, ++b)
+	{
+		if(*a >= '0' && *a <= '9' && *b >= '0' && *b <= '9')
+		{
+			result = 0;
+			do
+			{
+				if(!result)
+					result = *a - *b;
+				++a; ++b;
+			}
+			while(*a >= '0' && *a <= '9' && *b >= '0' && *b <= '9');
+
+			if(*a >= '0' && *a <= '9')
+				return 1;
+			else if(*b >= '0' && *b <= '9')
+				return -1;
+			else if(result)
+				return result;
+		}
+
+		if(tolower(*a) != tolower(*b))
+			break;
+	}
+	return tolower(*a) - tolower(*b);
+}
+
+const char *str_find_nocase(const char *haystack, const char *needle)
+{
+	while(*haystack) /* native implementation */
+	{
+		const char *a = haystack;
+		const char *b = needle;
+		while(*a && *b && tolower(*a) == tolower(*b))
+		{
+			a++;
+			b++;
+		}
+		if(!(*b))
+			return haystack;
+		haystack++;
+	}
+
+	return 0;
+}
+
+
+const char *str_find(const char *haystack, const char *needle)
+{
+	while(*haystack) /* native implementation */
+	{
+		const char *a = haystack;
+		const char *b = needle;
+		while(*a && *b && *a == *b)
+		{
+			a++;
+			b++;
+		}
+		if(!(*b))
+			return haystack;
+		haystack++;
+	}
+
+	return 0;
+}
+
+void str_hex(char *dst, int dst_size, const void *data, int data_size)
+{
+	static const char hex[] = "0123456789ABCDEF";
+	int b;
+
+	for(b = 0; b < data_size && b < dst_size/4-4; b++)
+	{
+		dst[b*3] = hex[((const unsigned char *)data)[b]>>4];
+		dst[b*3+1] = hex[((const unsigned char *)data)[b]&0xf];
+		dst[b*3+2] = ' ';
+		dst[b*3+3] = 0;
+	}
+}
+
+void str_timestamp(char *buffer, int buffer_size)
+{
+	time_t time_data;
+	struct tm *time_info;
+
+	time(&time_data);
+	time_info = localtime(&time_data);
+	strftime(buffer, buffer_size, "%Y-%m-%d_%H-%M-%S", time_info);
+	buffer[buffer_size-1] = 0;	/* assure null termination */
+}
+
+int mem_comp(const void *a, const void *b, int size)
+{
+	return memcmp(a,b,size);
+}
+
+const MEMSTATS *mem_stats()
+{
+	return &memory_stats;
+}
+
+void net_stats(NETSTATS *stats_inout)
+{
+	*stats_inout = network_stats;
+}
+
+void gui_messagebox(const char *title, const char *message)
+{
+#if defined(CONF_PLATFORM_MACOSX)
+	DialogRef theItem;
+	DialogItemIndex itemIndex;
+
+	/* FIXME: really needed? can we rely on glfw? */
+	/* HACK - get events without a bundle */
+	ProcessSerialNumber psn;
+	GetCurrentProcess(&psn);
+	TransformProcessType(&psn,kProcessTransformToForegroundApplication);
+	SetFrontProcess(&psn);
+	/* END HACK */
+
+	CreateStandardAlert(kAlertStopAlert,
+			CFStringCreateWithCString(NULL, title, kCFStringEncodingASCII),
+			CFStringCreateWithCString(NULL, message, kCFStringEncodingASCII),
+			NULL,
+			&theItem);
+
+	RunStandardAlert(theItem, NULL, &itemIndex);
+#elif defined(CONF_FAMILY_UNIX)
+	static char cmd[1024];
+	int err;
+	/* use xmessage which is available on nearly every X11 system */
+	snprintf(cmd, sizeof(cmd), "xmessage -center -title '%s' '%s'",
+		title,
+		message);
+
+	err = system(cmd);
+	dbg_msg("gui/msgbox", "result = %i", err);
+#elif defined(CONF_FAMILY_WINDOWS)
+	MessageBox(NULL,
+		message,
+		title,
+		MB_ICONEXCLAMATION | MB_OK);
+#else
+	/* this is not critical */
+	#warning not implemented
+#endif
+}
+
+int str_isspace(char c) { return c == ' ' || c == '\n' || c == '\t'; }
+
+char str_uppercase(char c)
+{
+	if(c >= 'a' && c <= 'z')
+		return 'A' + (c-'a');
+	return c;
+}
+
+int str_toint(const char *str) { return atoi(str); }
+float str_tofloat(const char *str) { return atof(str); }
+
+
+
+static int str_utf8_isstart(char c)
+{
+	if((c&0xC0) == 0x80) /* 10xxxxxx */
+		return 0;
+	return 1;
+}
+
+int str_utf8_rewind(const char *str, int cursor)
+{
+	while(cursor)
+	{
+		cursor--;
+		if(str_utf8_isstart(*(str + cursor)))
+			break;
+	}
+	return cursor;
+}
+
+int str_utf8_forward(const char *str, int cursor)
+{
+	const char *buf = str + cursor;
+	if(!buf[0])
+		return cursor;
+
+	if((*buf&0x80) == 0x0)  /* 0xxxxxxx */
+		return cursor+1;
+	else if((*buf&0xE0) == 0xC0) /* 110xxxxx */
+	{
+		if(!buf[1]) return cursor+1;
+		return cursor+2;
+	}
+	else  if((*buf & 0xF0) == 0xE0)	/* 1110xxxx */
+	{
+		if(!buf[1]) return cursor+1;
+		if(!buf[2]) return cursor+2;
+		return cursor+3;
+	}
+	else if((*buf & 0xF8) == 0xF0)	/* 11110xxx */
+	{
+		if(!buf[1]) return cursor+1;
+		if(!buf[2]) return cursor+2;
+		if(!buf[3]) return cursor+3;
+		return cursor+4;
+	}
+
+	/* invalid */
+	return cursor+1;
+}
+
+int str_utf8_encode(char *ptr, int chr)
+{
+	/* encode */
+	if(chr <= 0x7F)
+	{
+		ptr[0] = (char)chr;
+		return 1;
+	}
+	else if(chr <= 0x7FF)
+	{
+		ptr[0] = 0xC0|((chr>>6)&0x1F);
+		ptr[1] = 0x80|(chr&0x3F);
+		return 2;
+	}
+	else if(chr <= 0xFFFF)
+	{
+		ptr[0] = 0xE0|((chr>>12)&0x0F);
+		ptr[1] = 0x80|((chr>>6)&0x3F);
+		ptr[2] = 0x80|(chr&0x3F);
+		return 3;
+	}
+	else if(chr <= 0x10FFFF)
+	{
+		ptr[0] = 0xF0|((chr>>18)&0x07);
+		ptr[1] = 0x80|((chr>>12)&0x3F);
+		ptr[2] = 0x80|((chr>>6)&0x3F);
+		ptr[3] = 0x80|(chr&0x3F);
+		return 4;
+	}
+
+	return 0;
+}
+
+int str_utf8_decode(const char **ptr)
+{
+	const char *buf = *ptr;
+	int ch = 0;
+
+	do
+	{
+		if((*buf&0x80) == 0x0)  /* 0xxxxxxx */
+		{
+			ch = *buf;
+			buf++;
+		}
+		else if((*buf&0xE0) == 0xC0) /* 110xxxxx */
+		{
+			ch  = (*buf++ & 0x3F) << 6; if(!(*buf)) break;
+			ch += (*buf++ & 0x3F);
+			if(ch == 0) ch = -1;
+		}
+		else  if((*buf & 0xF0) == 0xE0)	/* 1110xxxx */
+		{
+			ch  = (*buf++ & 0x1F) << 12; if(!(*buf)) break;
+			ch += (*buf++ & 0x3F) <<  6; if(!(*buf)) break;
+			ch += (*buf++ & 0x3F);
+			if(ch == 0) ch = -1;
+		}
+		else if((*buf & 0xF8) == 0xF0)	/* 11110xxx */
+		{
+			ch  = (*buf++ & 0x0F) << 18; if(!(*buf)) break;
+			ch += (*buf++ & 0x3F) << 12; if(!(*buf)) break;
+			ch += (*buf++ & 0x3F) <<  6; if(!(*buf)) break;
+			ch += (*buf++ & 0x3F);
+			if(ch == 0) ch = -1;
+		}
+		else
+		{
+			/* invalid */
+			buf++;
+			break;
+		}
+
+		*ptr = buf;
+		return ch;
+	} while(0);
+
+	/* out of bounds */
+	*ptr = buf;
+	return -1;
+
+}
+
+int str_utf8_check(const char *str)
+{
+	while(*str)
+	{
+		if((*str&0x80) == 0x0)
+			str++;
+		else if((*str&0xE0) == 0xC0 && (*(str+1)&0xC0) == 0x80)
+			str += 2;
+		else if((*str&0xF0) == 0xE0 && (*(str+1)&0xC0) == 0x80 && (*(str+2)&0xC0) == 0x80)
+			str += 3;
+		else if((*str&0xF8) == 0xF0 && (*(str+1)&0xC0) == 0x80 && (*(str+2)&0xC0) == 0x80 && (*(str+3)&0xC0) == 0x80)
+			str += 4;
+		else
+			return 0;
+	}
+	return 1;
+}
+
+
+unsigned str_quickhash(const char *str)
+{
+	unsigned hash = 5381;
+	for(; *str; str++)
+		hash = ((hash << 5) + hash) + (*str); /* hash * 33 + c */
+	return hash;
+}
+
+
+#if defined(__cplusplus)
+}
+#endif
diff --git a/web/css/dark.css b/web/css/dark.css
new file mode 100644
index 0000000..bdcf3fa
--- /dev/null
+++ b/web/css/dark.css
@@ -0,0 +1,49 @@
+body { background: #222 url('../img/dark.png'); color: #fff; }
+.navbar { min-height: 40px; }
+.navbar-brand { color: #FFF !important; padding: 10px; font-size: 20px; }
+.dropdown .dropdown-toggle { padding-bottom: 10px; padding-top: 10px; }
+.dropdown-menu > li > a { color: #FFF !important; background-color: #222222 !important; }
+.dropdown-menu > li > a:hover { color: #FFF !important; background: #000 !important; }
+.dropdown-menu { background: #222 !important; background-color: #222222 !important; }
+.navbar-inverse .navbar-inner { background-color:#1B1B1B; background-image:-moz-linear-gradient(top, #222222, #111111); background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#222222), to(#111111)); background-image:-webkit-linear-gradient(top, #222222, #111111); background-image:-o-linear-gradient(top, #222222, #111111); background-image:linear-gradient(to bottom, #222222, #111111); background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222', endColorstr='#ff111111', GradientType=0); border-color: #252525; }
+.content { background: #222; padding: 20px; border-radius: 5px; border: 1px #000 solid; -webkit-box-shadow: 0 1px 10px rgba(0, 0, 0, .1); -moz-box-shadow: 0 1px 10px rgba(0, 0, 0, .1); box-shadow: 0 1px 10px rgba(0, 0, 0, .1); margin-bottom: 20px; }
+.table { background: #000; margin-bottom: 0; border-collapse: collapse; border-radius: 3px; }
+.table th { text-align: center; }
+.table-striped tbody > tr.even > td, .table-striped tbody > tr.even > th { background-color: #2F2F2F; }
+.table-striped tbody > tr.odd > td, .table-striped tbody > tr.odd > th { background-color: #000; }
+.table td { text-align: center; border-color: #2F2F2F; }
+.progress { margin-bottom: 0; background: #222; }
+.table-hover > tbody > tr:hover > td { background: #414141; }
+tr.even.expandRow > :hover { background: #2F2F2F !important; }
+tr.odd.expandRow > :hover { background: #000 !important; }
+.expandRow > td { padding: 0 !important; border-top: 0px !important; }
+#cpu, #ram, #hdd, #network { min-width: 55px; max-width: 100px; }
+
+@media only screen and (max-width: 992px) {
+	#location, tr td:nth-child(5)		{ display:none; visibility:hidden; }
+}
+@media only screen and (max-width: 720px) {
+	#type, tr td:nth-child(4)			{ display:none; visibility:hidden; }
+	#location, tr td:nth-child(5)		{ display:none; visibility:hidden; }
+}
+@media only screen and (max-width: 600px) {
+	#type, tr td:nth-child(4)			{ display:none; visibility:hidden; }
+	#location, tr td:nth-child(5)		{ display:none; visibility:hidden; }
+	#uptime, tr td:nth-child(6)			{ display:none; visibility:hidden; }
+}
+@media only screen and (max-width: 533px) {
+	#type, tr td:nth-child(4)			{ display:none; visibility:hidden; }
+	#location, tr td:nth-child(5)		{ display:none; visibility:hidden; }
+	#uptime, tr td:nth-child(6)			{ display:none; visibility:hidden; }
+	#network, tr td:nth-child(8)		{ display:none; visibility:hidden; }
+}
+@media only screen and (max-width: 450px) {
+	body								{ font-size: 10px; }
+	.content 							{ padding: 0; }
+	#name, tr td:nth-child(3)			{ min-width: 10px; max-width: 25px; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; }
+	#type, tr td:nth-child(4)			{ display:none; visibility:hidden; }
+	#location, tr td:nth-child(5)		{ display:none; visibility:hidden; }
+	#uptime, tr td:nth-child(6)			{ display:none; visibility:hidden; }
+	#network, tr td:nth-child(8)		{ display:none; visibility:hidden; }
+	#cpu, #ram, #hdd { min-width: 25px; max-width: 50px; }
+}
\ No newline at end of file
diff --git a/web/css/light.css b/web/css/light.css
new file mode 100644
index 0000000..5819497
--- /dev/null
+++ b/web/css/light.css
@@ -0,0 +1,46 @@
+body { background: #ebebeb url('../img/light.png'); }
+.navbar { min-height: 40px; }
+.navbar-brand { color: #fff; padding: 10px; font-size: 20px; }
+.dropdown .dropdown-toggle { padding-bottom: 10px; padding-top: 10px; }
+.navbar-inverse .navbar-brand { color: #fff; padding: 10px; font-size: 20px; }
+.content { background: #ffffff; padding: 20px; border-radius: 5px; border: 1px #cecece solid; -webkit-box-shadow: 0 1px 10px rgba(0, 0, 0, .1); -moz-box-shadow: 0 1px 10px rgba(0, 0, 0, .1); box-shadow: 0 1px 10px rgba(0, 0, 0, .1); margin-bottom: 20px; }
+.table { background: #ffffff; margin-bottom: 0; border-collapse: collapse; border-radius: 3px; }
+.table th, .table td { text-align: center; }
+.table-striped tbody > tr.even > td, .table-striped tbody > tr.even > th { background-color: #F9F9F9; }
+.table-striped tbody > tr.odd > td, .table-striped tbody > tr.odd > th { background-color: #FFF; }
+.progress { margin-bottom: 0; }
+.progress-bar { color: #000; }
+.table-hover > tbody > tr:hover > td { background: #E6E6E6; }
+tr.even.expandRow > :hover { background: #F9F9F9 !important; }
+tr.odd.expandRow > :hover { background: #FFF !important; }
+.expandRow > td { padding: 0 !important; border-top: 0px !important; }
+#cpu, #ram, #hdd, #network { min-width: 55px; max-width: 100px; }
+
+@media only screen and (max-width: 992px) {
+	#location, tr td:nth-child(5)		{ display:none; visibility:hidden; }
+}
+@media only screen and (max-width: 720px) {
+	#type, tr td:nth-child(4)			{ display:none; visibility:hidden; }
+	#location, tr td:nth-child(5)		{ display:none; visibility:hidden; }
+}
+@media only screen and (max-width: 600px) {
+	#type, tr td:nth-child(4)			{ display:none; visibility:hidden; }
+	#location, tr td:nth-child(5)		{ display:none; visibility:hidden; }
+	#uptime, tr td:nth-child(6)			{ display:none; visibility:hidden; }
+}
+@media only screen and (max-width: 533px) {
+	#type, tr td:nth-child(4)			{ display:none; visibility:hidden; }
+	#location, tr td:nth-child(5)		{ display:none; visibility:hidden; }
+	#uptime, tr td:nth-child(6)			{ display:none; visibility:hidden; }
+	#network, tr td:nth-child(8)		{ display:none; visibility:hidden; }
+}
+@media only screen and (max-width: 450px) {
+	body								{ font-size: 10px; }
+	.content 							{ padding: 0; }
+	#name, tr td:nth-child(3)			{ min-width: 10px; max-width: 25px; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; }
+	#type, tr td:nth-child(4)			{ display:none; visibility:hidden; }
+	#location, tr td:nth-child(5)		{ display:none; visibility:hidden; }
+	#uptime, tr td:nth-child(6)			{ display:none; visibility:hidden; }
+	#network, tr td:nth-child(8)		{ display:none; visibility:hidden; }
+	#cpu, #ram, #hdd { min-width: 25px; max-width: 50px; }
+}
\ No newline at end of file
diff --git a/web/favicon.ico b/web/favicon.ico
new file mode 100644
index 0000000000000000000000000000000000000000..157d27b8e398bd1c74ace81b0dc2d063c7462098
GIT binary patch
literal 45470
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
bmwp|7vB_^V-)ii)ngOH7ycw`s%knP*N%2&K

literal 0
HcmV?d00001

diff --git a/web/img/dark.png b/web/img/dark.png
new file mode 100644
index 0000000000000000000000000000000000000000..20922820f5ee32b17a52548560388dbc5e2c5c94
GIT binary patch
literal 600
zcmV-e0;m0nP)<h;3K|Lk000e1NJLTq001rk000*V0{{R3LlTW(0000jP)t-s5fKp#
z3=9$y5)>2^5D*X!4h|C&6Aup$7Z(>56%`g177Yyz3kwSh3JM7c38-ifAOHXX=1D|B
zRCwA=lUsr$AqWIfL<^zz{>R-ww`TUoSA}Vkbc?jsbu<&5t1W$|wV}QxLbi{VYj4Y(
z`<!dh?jCQ-GXs7ziWlvrVog+*c+rRmbYESPc{F%09fQ`+JZIEnmqtWnYs=7iXjz=1
zpNW&q?EC4B^+%_0H?qmz(4F)0(-~bemyYS3CokRLVqk{1aoctJ<)_s#uImB#OUDSI
zuv%ifQ}??3qv&mFKRDc>Wq08&8%9Lq<*SnC&26reul>-j;)mzOK*usBd~z4+s{{H3
zD>h_8R#wH+sWAAKse2^!mr1pY=7g9QZ-Rhz*OSi(^SM5HLr070t0mQ;oNx2Jiqsml
z|Cf-?a<Yo=#tcuA1s;Ax=>F&&SgS_)psZ(#@&N&dUXXJ9$VWGMczStPj!hpyHPDA<
zl*~{c(JqZ;n=T7GWgTAh1l@1gYVX@0`RFl`?7lTBs@~GXaAZeLm&shrznR>m^kj<b
zAQ>xt7JFuzI^0K7(u6l$&=SMg64tmFQP(razaoluGO^R+;~))Jg0fxzQ-%K4f?}b`
z?hV(Yr_ib$+*z^5!s##cv)T(StdAMgjy8k%GxG53^2yf<;{FDUu5LEynSYw#AlO}f
mzkdv|erYfIv3Tu9Ab$btOBd6f7(Ds_0000<MNUMnLSTX`E)`P%

literal 0
HcmV?d00001

diff --git a/web/img/light.png b/web/img/light.png
new file mode 100644
index 0000000000000000000000000000000000000000..2f4febcb6297689992386cbd34f8b3cdbcbd52a5
GIT binary patch
literal 19452
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%0AyVqkS<2a$W=)EmN|28yl>Y!Lx$gAkM}
zHw=LW<8m)ZLyT}^%)PnJ4;5$U@v)xoC+hu;Wb1^Lun(`jxx8sTCBiwUuKUk<qMmZ2
z|Ke)>XFXz-`N6u~s*lttr|N_2=LrMXb8yi9xM6#i++WM<vf8Erzv;pV3dEBtOjion
zgN=WZ=!H`t12?|*2Lp$#xjTj{oDPm@gv{W&lCSeax#+rZ&V{!6w<$Oj+&aKcM#mhu
z>EwtUH>Y~Mz_ZVU_(wW`heO8IftO=AZEcv3$XEebaIhKI290L^3_<(q={42hz`jo1
z+v2UI;}OAkaI8O%Ju3rqHLgs{61RRPQmLt1^HUHLqR&%Cc#FSPJy{#yAj8!D6kJ71
zB-sngeN?xDMtQNOR3R^q>@yHUkgb5?KpWN1H?Czmt=`tMPuSm3=xYXJmB(MNmPj9&
zNt`6G5&={M;5_a^yce5Q9&0u>iLN9oU0ET)-g;5caiYQYz=>lpgD_m9=UO>}(eny@
z!czm*?V2n5JVcgJ{Xo5cK-_K%r~Oc4i>+K7r97@XTx*V2k@2Fy7=2xB9iVGlOvz-<
zau|vjSo2R^lA3~<*pUfO{+6UZM=Bkrf9Fx6`~3}u?!|w)Xj<r#B(~N0sEo}ViMJ_x
znSXYDj{%H2$ju?r=v1JHo{#^l?T~HeR`stvwp&=9YRIC$SOtsZ@H$%}T{ySv&*i<Q
zg&_9lS+^6|y5tCTUrOi)vAnkB_iMZa##{Xv8RTBx8J#PTcCe%UM*BDA%;+H7B>h|j
zJe(C1azFw?%?+*dtQ&sjwnd45`c$FrBL~OnjCb<v8NQYsd3cAD?UBSs$V|!;`Ie@<
z-zDOV(wOX<E{t_}OJe!M)J;>0KPr{;vS;wKL+G&O+wzLbx$J&DxUto;0;r(>)+yI}
zUPz5gOdnLUI_-z<+vvR9_mkEE)_J*WcbCcPy8onGyN3M3<k5Wp!hzZ9z#_Y&37p?0
z%=A%}Wqn``w~HGT9TqC0JsPCdpLD#oE-+0ZCRp2*v+dDD4S-UbIN_oLsl*$rn!)b7
z%SLrcbat1N6Ji<-osn{JmHhrJLMw>Lf0-os6Mq!s{ZpnGhg>Z^Qnmh4@m;&zuNF@Z
zhV5cg)$LN_3O|e`Wmv!|-dgKJ+a_7gyEFr<;yTd%F8_2w!UxLs^7Ly2ai;|wIHqlS
zIl)d#wYbFiBy$CMRDVB>t{aY2U0ip^Cyw#E&9g?WB3dU_XNUXG6FQ>}mw@U}#;+5)
z=+ea|YNP{^-1=0F8)kv2`y`A=g?bPWSLL}gHupBa?>*E+@yU60l0UV_ghP@3)8@pm
zXp?F*q`(qbAO<(;47Dp*$-cJDD{WvaRx}V&7>BF;Xm;ypw9_bqf1cxYbwvx5%d}{L
zxOq9ewtpr4gN)~fKb!;}_Tf5Ze@oBgH2W1w02A-{E?wop{NS4ZYy8_@_@lAy&Dc<)
zcgS^@vo|0AKf$FY!pq(thG(B9ZLcK-Bu7TH|3YykWABBy?5Wr+t8MIa5_>K2vVkWZ
zc-gE-8}=n2GP;2wR%~*wwd)mwUQzbg#+I$Q8k8-Nf1pRG<)DsjUXD_fQ$_xEF*WB!
zj|Mg!Cn@2hVF53x=ugvmz3Nt>W*#3er9)Rg*nViK3_H(dS4Ta(1y#ggfF*=h5tu<g
z{{&ERm?-t6k;^$@9ZppCxK}wh8*9g!D(t!6JDbe4*wUmpK%TJN7~CQ={ZqAzKXAQU
zJlYb>Rt~lPoBXR?9#HMJ!hGoM5~QznXD-p|KxE}Q>wgtygcG?-)(DsD={5Zy`t`{^
T36JFk00000NkvXXu0mjfh_8PN

literal 0
HcmV?d00001

diff --git a/web/index.html b/web/index.html
new file mode 100644
index 0000000..6cb0ed6
--- /dev/null
+++ b/web/index.html
@@ -0,0 +1,112 @@
+<!DOCTYPE html>
+<!--
+        json字段保持完整, 后期更新会向下兼容
+        可以自定义前端展示
+        ლ(•̀ _ •́ ლ)
+        ლ(•̀ _ •́ ლ)ლ(•̀ _ •́ ლ)
+        ლ(•̀ _ •́ ლ)ლ(•̀ _ •́ ლ)ლ(•̀ _ •́ ლ)
+        ლ(•̀ _ •́ ლ)ლ(•̀ _ •́ ლ)
+        ლ(•̀ _ •́ ლ)
+-->
+<html>
+	<head>
+		<title>云监控</title>
+		<meta charset="utf-8">
+		<meta http-equiv="X-UA-Compatible" content="IE=edge">
+		<meta name="viewport" content="width=device-width, initial-scale=1.0">
+		<meta name="description" content="云监控">
+		<meta name="author" content="BotoX">
+		<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css">
+		<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap-theme.min.css">
+		<link rel="stylesheet" href="css/dark.css" title="dark">
+		<link rel="stylesheet" href="css/light.css" title="light">
+		<style>
+			body {
+				padding-top: 70px;
+				padding-bottom: 30px;
+			}
+		</style>
+		<!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
+		<!--[if lt IE 9]>
+			<script src="//oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
+			<script src="//oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
+		<![endif]-->
+	</head>
+	<body>
+		<div role="navigation" class="navbar navbar-inverse navbar-fixed-top">
+			<div class="navbar-inner">
+				<div class="container">
+					<div class="navbar-header">
+						<button data-target=".navbar-collapse" data-toggle="collapse" class="navbar-toggle" type="button">
+							<span class="sr-only">Toggle navigation</span>
+							<span class="icon-bar"></span>
+							<span class="icon-bar"></span>
+							<span class="icon-bar"></span>
+						</button>
+						<a href="#" class="navbar-brand">云监控</a>
+					</div>
+					<div class="navbar-collapse collapse">
+						<ul class="nav navbar-nav">
+							<li class="dropdown">
+								<a data-toggle="dropdown" class="dropdown-toggle" href="#">风格<b class="caret"></b></a>
+								<ul class="dropdown-menu">
+									<li><a href="#" onclick="setActiveStyleSheet('dark')">黑夜</a></li>
+									<li><a href="#" onclick="setActiveStyleSheet('light')">白天</a></li>
+								</ul>
+							</li>
+						</ul>
+					</div><!--/.nav-collapse -->
+				</div>
+			</div>
+		</div>
+
+		<div class="container content">
+			<div id="loading-notice">
+				<noscript>
+					<div class="alert alert-danger" style="text-align: center;">
+						<strong>Enable JavaScript</strong> you fucking autist neckbeard, it's not gonna hurt you.
+					</div>
+				</noscript>
+				<div class="progress progress-striped active">
+					<div class="progress-bar progress-bar-warning" style="width: 100%;">加载中...</div>
+				</div>
+				<div style="text-align: center;">
+					如果出现此消息,请确保您已启用Javascript! <br />否则云监控主服务没启动或已关闭.
+				</div>
+				<p></p>
+			</div>
+			<table class="table table-striped table-condensed table-hover">
+				<thead>
+				<tr>
+					<th id="status4" style="text-align: center;">IPv4</th>
+					<th id="ipstatus" style="text-align: center;">Flight</th>
+					<th id="name">节点名</th>
+					<th id="type">虚拟化</th>
+					<th id="location">位置</th>
+					<th id="uptime">在线时间</th>
+					<th id="load">负载</th>
+					<th id="network">网络 ↓|↑</th>
+					<th id="traffic">流量 ↓|↑</th>
+					<th id="cpu">处理器</th>
+					<th id="ram">内存</th>
+					<th id="hdd">硬盘</th>
+				</tr>
+				</thead>
+				<tbody id="servers">
+				<!-- Servers here \o/ -->
+				</tbody>
+			</table>
+			<br />
+			<div id="updated">Updating...</div>
+		</div>
+
+		<div class="container">
+			<p style="text-align: center; font-size: 10px;">
+				<a href="https://github.com/cppla/ServerStatus">ServerStatus中文版</a>
+			</p>
+		</div>
+		<script src="//code.jquery.com/jquery-1.10.2.min.js"></script>
+		<script src="//netdna.bootstrapcdn.com/bootstrap/3.0.3/js/bootstrap.min.js"></script>
+		<script src="js/serverstatus.js"></script>
+	</body>
+</html>
diff --git a/web/js/serverstatus.js b/web/js/serverstatus.js
new file mode 100644
index 0000000..dc962b7
--- /dev/null
+++ b/web/js/serverstatus.js
@@ -0,0 +1,393 @@
+// serverstatus.js
+var error = 0;
+var d = 0;
+var server_status = new Array();
+
+function timeSince(date) {
+	if(date == 0)
+		return "从未.";
+
+	var seconds = Math.floor((new Date() - date) / 1000);
+	var interval = Math.floor(seconds / 31536000);
+
+	if (interval > 1)
+		return interval + " 年前.";
+	interval = Math.floor(seconds / 2592000);
+	if (interval > 1)
+		return interval + " 月前.";
+	interval = Math.floor(seconds / 86400);
+	if (interval > 1)
+		return interval + " 日前.";
+	interval = Math.floor(seconds / 3600);
+	if (interval > 1)
+		return interval + " 小时前.";
+	interval = Math.floor(seconds / 60);
+	if (interval > 1)
+		return interval + " 分钟前.";
+	/*if(Math.floor(seconds) >= 5)
+		return Math.floor(seconds) + " seconds";*/
+	else
+		return "几秒前.";
+}
+
+function bytesToSize(bytes, precision, si)
+{
+	var ret;
+	si = typeof si !== 'undefined' ? si : 0;
+	if(si != 0) {
+		var kilobyte = 1000;
+		var megabyte = kilobyte * 1000;
+		var gigabyte = megabyte * 1000;
+		var terabyte = gigabyte * 1000;
+	} else {
+		var kilobyte = 1024;
+		var megabyte = kilobyte * 1024;
+		var gigabyte = megabyte * 1024;
+		var terabyte = gigabyte * 1024;
+	}
+
+	if ((bytes >= 0) && (bytes < kilobyte)) {
+		return bytes + ' B';
+
+	} else if ((bytes >= kilobyte) && (bytes < megabyte)) {
+		ret = (bytes / kilobyte).toFixed(precision) + ' K';
+
+	} else if ((bytes >= megabyte) && (bytes < gigabyte)) {
+		ret = (bytes / megabyte).toFixed(precision) + ' M';
+
+	} else if ((bytes >= gigabyte) && (bytes < terabyte)) {
+		ret = (bytes / gigabyte).toFixed(precision) + ' G';
+
+	} else if (bytes >= terabyte) {
+		ret = (bytes / terabyte).toFixed(precision) + ' T';
+
+	} else {
+		return bytes + ' B';
+	}
+	if(si != 0) {
+		return ret + 'B';
+	} else {
+		return ret + 'iB';
+	}
+}
+
+function uptime() {
+	$.getJSON("json/stats.json", function(result) {
+		$("#loading-notice").remove();
+		if(result.reload)
+			setTimeout(function() { location.reload(true) }, 1000);
+
+		for (var i = 0; i < result.servers.length; i++) {
+			var TableRow = $("#servers tr#r" + i);
+			var ExpandRow = $("#servers #rt" + i);
+			var hack; // fuck CSS for making me do this
+			if(i%2) hack="odd"; else hack="even";
+			if (!TableRow.length) {
+				$("#servers").append(
+					"<tr id=\"r" + i + "\" data-toggle=\"collapse\" data-target=\"#rt" + i + "\" class=\"accordion-toggle " + hack + "\">" +
+						"<td id=\"online4\"><div class=\"progress\"><div style=\"width: 100%;\" class=\"progress-bar progress-bar-warning\"><small>加载中</small></div></div></td>" +
+						"<td id=\"ip_status\"><div class=\"progress\"><div style=\"width: 100%;\" class=\"progress-bar progress-bar-warning\"><small>加载中</small></div></div></td>" +
+						"<td id=\"name\">加载中</td>" +
+						"<td id=\"type\">加载中</td>" +
+						"<td id=\"location\">加载中</td>" +
+						"<td id=\"uptime\">加载中</td>" +
+						"<td id=\"load\">加载中</td>" +
+						"<td id=\"network\">加载中</td>" +
+						"<td id=\"traffic\">加载中</td>" +
+						"<td id=\"cpu\"><div class=\"progress progress-striped active\"><div style=\"width: 100%;\" class=\"progress-bar progress-bar-warning\"><small>加载中</small></div></div></td>" +
+						"<td id=\"memory\"><div class=\"progress progress-striped active\"><div style=\"width: 100%;\" class=\"progress-bar progress-bar-warning\"><small>加载中</small></div></div></td>" +
+						"<td id=\"hdd\"><div class=\"progress progress-striped active\"><div style=\"width: 100%;\" class=\"progress-bar progress-bar-warning\"><small>加载中</small></div></div></td>" +
+					"</tr>" +
+					"<tr class=\"expandRow " + hack + "\"><td colspan=\"12\"><div class=\"accordian-body collapse\" id=\"rt" + i + "\">" +
+						"<div id=\"expand_mem\">加载中</div>" +
+						"<div id=\"expand_swap\">加载中</div>" +
+						"<div id=\"expand_hdd\">加载中</div>" +
+						"<div id=\"expand_custom\">加载中</div>" +
+					"</div></td></tr>"
+				);
+				TableRow = $("#servers tr#r" + i);
+				ExpandRow = $("#servers #rt" + i);
+				server_status[i] = true;
+			}
+			TableRow = TableRow[0];
+			if(error) {
+				TableRow.setAttribute("data-target", "#rt" + i);
+				server_status[i] = true;
+			}
+
+			// Online4
+			if (result.servers[i].online4) {
+				TableRow.children["online4"].children[0].children[0].className = "progress-bar progress-bar-success";
+				TableRow.children["online4"].children[0].children[0].innerHTML = "<small>开启</small>";
+			} else {
+				TableRow.children["online4"].children[0].children[0].className = "progress-bar progress-bar-danger";
+				TableRow.children["online4"].children[0].children[0].innerHTML = "<small>关闭</small>";
+			}
+
+			// Online6
+			//if (result.servers[i].online6) {
+			//	TableRow.children["online6"].children[0].children[0].className = "progress-bar progress-bar-success";
+			//	TableRow.children["online6"].children[0].children[0].innerHTML = "<small>开启</small>";
+			//} else {
+			//	TableRow.children["online6"].children[0].children[0].className = "progress-bar progress-bar-danger";
+			//	TableRow.children["online6"].children[0].children[0].innerHTML = "<small>关闭</small>";
+			//}
+
+			// Ipstatus
+			if (result.servers[i].ip_status) {
+				TableRow.children["ip_status"].children[0].children[0].className = "progress-bar progress-bar-success";
+				TableRow.children["ip_status"].children[0].children[0].innerHTML = "<small>MH361</small>";
+			} else {
+				TableRow.children["ip_status"].children[0].children[0].className = "progress-bar progress-bar-danger";
+				TableRow.children["ip_status"].children[0].children[0].innerHTML = "<small>MH370</small>";
+			}
+
+			// Name
+			TableRow.children["name"].innerHTML = result.servers[i].name;
+
+			// Type
+			TableRow.children["type"].innerHTML = result.servers[i].type;
+
+			// Location
+			TableRow.children["location"].innerHTML = result.servers[i].location;
+			if (!result.servers[i].online4 && !result.servers[i].online6) {
+				if (server_status[i]) {
+					TableRow.children["uptime"].innerHTML = "–";
+					TableRow.children["load"].innerHTML = "–";
+					TableRow.children["network"].innerHTML = "–";
+					TableRow.children["traffic"].innerHTML = "–";
+					TableRow.children["cpu"].children[0].children[0].className = "progress-bar progress-bar-danger";
+					TableRow.children["cpu"].children[0].children[0].style.width = "100%";
+					TableRow.children["cpu"].children[0].children[0].innerHTML = "<small>关闭</small>";
+					TableRow.children["memory"].children[0].children[0].className = "progress-bar progress-bar-danger";
+					TableRow.children["memory"].children[0].children[0].style.width = "100%";
+					TableRow.children["memory"].children[0].children[0].innerHTML = "<small>关闭</small>";
+					TableRow.children["hdd"].children[0].children[0].className = "progress-bar progress-bar-danger";
+					TableRow.children["hdd"].children[0].children[0].style.width = "100%";
+					TableRow.children["hdd"].children[0].children[0].innerHTML = "<small>关闭</small>";
+					if(ExpandRow.hasClass("in")) {
+						ExpandRow.collapse("hide");
+					}
+					TableRow.setAttribute("data-target", "");
+					server_status[i] = false;
+				}
+			} else {
+				if (!server_status[i]) {
+					TableRow.setAttribute("data-target", "#rt" + i);
+					server_status[i] = true;
+				}
+
+				// Uptime
+				TableRow.children["uptime"].innerHTML = result.servers[i].uptime;
+
+				// Load
+				if(result.servers[i].load == -1) {
+					TableRow.children["load"].innerHTML = "–";
+				} else {
+				    var loadstr = ""
+				    loadstr += result.servers[i].load_1.toFixed(2);
+				    loadstr += " | "
+				    loadstr += result.servers[i].load_5.toFixed(2);
+				    loadstr += " | "
+				    loadstr += result.servers[i].load_15.toFixed(2);
+					TableRow.children["load"].innerHTML = loadstr
+				}
+
+				// Network
+				var netstr = "";
+				if(result.servers[i].network_rx < 1000)
+					netstr += result.servers[i].network_rx.toFixed(0) + "B";
+				else if(result.servers[i].network_rx < 1000*1000)
+					netstr += (result.servers[i].network_rx/1000).toFixed(0) + "K";
+				else
+					netstr += (result.servers[i].network_rx/1000/1000).toFixed(1) + "M";
+				netstr += " | "
+				if(result.servers[i].network_tx < 1000)
+					netstr += result.servers[i].network_tx.toFixed(0) + "B";
+				else if(result.servers[i].network_tx < 1000*1000)
+					netstr += (result.servers[i].network_tx/1000).toFixed(0) + "K";
+				else
+					netstr += (result.servers[i].network_tx/1000/1000).toFixed(1) + "M";
+				TableRow.children["network"].innerHTML = netstr;
+
+				//Traffic
+				var trafficstr = "";
+				if(result.servers[i].network_in < 1024)
+					trafficstr += result.servers[i].network_in.toFixed(0) + "B";
+				else if(result.servers[i].network_in < 1024*1024)
+					trafficstr += (result.servers[i].network_in/1024).toFixed(0) + "K";
+				else if(result.servers[i].network_in < 1024*1024*1024)
+					trafficstr += (result.servers[i].network_in/1024/1024).toFixed(1) + "M";
+				else
+					trafficstr += (result.servers[i].network_in/1024/1024/1024).toFixed(2) + "G";
+				trafficstr += " | "
+				if(result.servers[i].network_out < 1024)
+					trafficstr += result.servers[i].network_out.toFixed(0) + "B";
+				else if(result.servers[i].network_out < 1024*1024)
+					trafficstr += (result.servers[i].network_out/1024).toFixed(0) + "K";
+				else if(result.servers[i].network_out < 1024*1024*1024)
+					trafficstr += (result.servers[i].network_out/1024/1024).toFixed(1) + "M";
+				else
+					trafficstr += (result.servers[i].network_out/1024/1024/1024).toFixed(2) + "G";
+				TableRow.children["traffic"].innerHTML = trafficstr;
+
+				// CPU
+				if (result.servers[i].cpu >= 90)
+					TableRow.children["cpu"].children[0].children[0].className = "progress-bar progress-bar-danger";
+				else if (result.servers[i].cpu >= 80)
+					TableRow.children["cpu"].children[0].children[0].className = "progress-bar progress-bar-warning";
+				else
+					TableRow.children["cpu"].children[0].children[0].className = "progress-bar progress-bar-success";
+				TableRow.children["cpu"].children[0].children[0].style.width = result.servers[i].cpu + "%";
+				TableRow.children["cpu"].children[0].children[0].innerHTML = result.servers[i].cpu + "%";
+
+				// Memory
+				var Mem = ((result.servers[i].memory_used/result.servers[i].memory_total)*100.0).toFixed(0);
+				if (Mem >= 90)
+					TableRow.children["memory"].children[0].children[0].className = "progress-bar progress-bar-danger";
+				else if (Mem >= 80)
+					TableRow.children["memory"].children[0].children[0].className = "progress-bar progress-bar-warning";
+				else
+					TableRow.children["memory"].children[0].children[0].className = "progress-bar progress-bar-success";
+				TableRow.children["memory"].children[0].children[0].style.width = Mem + "%";
+				TableRow.children["memory"].children[0].children[0].innerHTML = Mem + "%";
+				ExpandRow[0].children["expand_mem"].innerHTML = "内存: " + bytesToSize(result.servers[i].memory_used*1024, 2) + " / " + bytesToSize(result.servers[i].memory_total*1024, 2);
+				// Swap
+				ExpandRow[0].children["expand_swap"].innerHTML = "交换分区: " + bytesToSize(result.servers[i].swap_used*1024, 2) + " / " + bytesToSize(result.servers[i].swap_total*1024, 2);
+
+				// HDD
+				var HDD = ((result.servers[i].hdd_used/result.servers[i].hdd_total)*100.0).toFixed(0);
+				if (HDD >= 90)
+					TableRow.children["hdd"].children[0].children[0].className = "progress-bar progress-bar-danger";
+				else if (HDD >= 80)
+					TableRow.children["hdd"].children[0].children[0].className = "progress-bar progress-bar-warning";
+				else
+					TableRow.children["hdd"].children[0].children[0].className = "progress-bar progress-bar-success";
+				TableRow.children["hdd"].children[0].children[0].style.width = HDD + "%";
+				TableRow.children["hdd"].children[0].children[0].innerHTML = HDD + "%";
+				ExpandRow[0].children["expand_hdd"].innerHTML = "硬盘: " + bytesToSize(result.servers[i].hdd_used*1024*1024, 2) + " / " + bytesToSize(result.servers[i].hdd_total*1024*1024, 2);
+
+				// Custom
+				if (result.servers[i].custom) {
+					ExpandRow[0].children["expand_custom"].innerHTML = result.servers[i].custom
+				} else {
+					ExpandRow[0].children["expand_custom"].innerHTML = ""
+				}
+			}
+		};
+
+		d = new Date(result.updated*1000);
+		error = 0;
+	}).fail(function(update_error) {
+		if (!error) {
+			$("#servers > tr.accordion-toggle").each(function(i) {
+				var TableRow = $("#servers tr#r" + i)[0];
+				var ExpandRow = $("#servers #rt" + i);
+				TableRow.children["online4"].children[0].children[0].className = "progress-bar progress-bar-error";
+				TableRow.children["online4"].children[0].children[0].innerHTML = "<small>错误</small>";
+				//TableRow.children["online6"].children[0].children[0].className = "progress-bar progress-bar-error";
+				//TableRow.children["online6"].children[0].children[0].innerHTML = "<small>错误</small>";
+				TableRow.children["ip_status"].children[0].children[0].className = "progress-bar progress-bar-error";
+				TableRow.children["ip_status"].children[0].children[0].innerHTML = "<small>错误</small>";
+				TableRow.children["uptime"].innerHTML = "<div class=\"progress progress-striped active\"><div style=\"width: 100%;\" class=\"progress-bar progress-bar-error\"><small>错误</small></div></div>";
+				TableRow.children["load"].innerHTML = "<div class=\"progress progress-striped active\"><div style=\"width: 100%;\" class=\"progress-bar progress-bar-error\"><small>错误</small></div></div>";
+				TableRow.children["network"].innerHTML = "<div class=\"progress progress-striped active\"><div style=\"width: 100%;\" class=\"progress-bar progress-bar-error\"><small>错误</small></div></div>";
+				TableRow.children["traffic"].innerHTML = "<div class=\"progress progress-striped active\"><div style=\"width: 100%;\" class=\"progress-bar progress-bar-error\"><small>错误</small></div></div>";
+				TableRow.children["cpu"].children[0].children[0].className = "progress-bar progress-bar-error";
+				TableRow.children["cpu"].children[0].children[0].style.width = "100%";
+				TableRow.children["cpu"].children[0].children[0].innerHTML = "<small>错误</small>";
+				TableRow.children["memory"].children[0].children[0].className = "progress-bar progress-bar-error";
+				TableRow.children["memory"].children[0].children[0].style.width = "100%";
+				TableRow.children["memory"].children[0].children[0].innerHTML = "<small>错误</small>";
+				TableRow.children["hdd"].children[0].children[0].className = "progress-bar progress-bar-error";
+				TableRow.children["hdd"].children[0].children[0].style.width = "100%";
+				TableRow.children["hdd"].children[0].children[0].innerHTML = "<small>错误</small>";
+				if(ExpandRow.hasClass("in")) {
+					ExpandRow.collapse("hide");
+				}
+				TableRow.setAttribute("data-target", "");
+				server_status[i] = false;
+			});
+		}
+		error = 1;
+		$("#updated").html("更新错误.");
+	});
+}
+
+function updateTime() {
+	if (!error)
+		$("#updated").html("最后更新: " + timeSince(d));
+}
+
+uptime();
+updateTime();
+setInterval(uptime, 2000);
+setInterval(updateTime, 500);
+
+
+// styleswitcher.js
+function setActiveStyleSheet(title) {
+	var i, a, main;
+	for(i=0; (a = document.getElementsByTagName("link")[i]); i++) {
+		if(a.getAttribute("rel").indexOf("style") != -1 && a.getAttribute("title")) {
+			a.disabled = true;
+			if(a.getAttribute("title") == title) a.disabled = false;
+		}
+	}
+}
+
+function getActiveStyleSheet() {
+	var i, a;
+	for(i=0; (a = document.getElementsByTagName("link")[i]); i++) {
+		if(a.getAttribute("rel").indexOf("style") != -1 && a.getAttribute("title") && !a.disabled)
+			return a.getAttribute("title");
+	}
+	return null;
+}
+
+function getPreferredStyleSheet() {
+	var i, a;
+	for(i=0; (a = document.getElementsByTagName("link")[i]); i++) {
+		if(a.getAttribute("rel").indexOf("style") != -1	&& a.getAttribute("rel").indexOf("alt") == -1 && a.getAttribute("title"))
+			return a.getAttribute("title");
+	}
+return null;
+}
+
+function createCookie(name,value,days) {
+	if (days) {
+		var date = new Date();
+		date.setTime(date.getTime()+(days*24*60*60*1000));
+		var expires = "; expires="+date.toGMTString();
+	}
+	else expires = "";
+	document.cookie = name+"="+value+expires+"; path=/";
+}
+
+function readCookie(name) {
+	var nameEQ = name + "=";
+	var ca = document.cookie.split(';');
+	for(var i=0;i < ca.length;i++) {
+		var c = ca[i];
+		while (c.charAt(0)==' ')
+			c = c.substring(1,c.length);
+		if (c.indexOf(nameEQ) == 0)
+			return c.substring(nameEQ.length,c.length);
+	}
+	return null;
+}
+
+window.onload = function(e) {
+	var cookie = readCookie("style");
+	var title = cookie ? cookie : getPreferredStyleSheet();
+	setActiveStyleSheet(title);
+}
+
+window.onunload = function(e) {
+	var title = getActiveStyleSheet();
+	createCookie("style", title, 365);
+}
+
+var cookie = readCookie("style");
+var title = cookie ? cookie : getPreferredStyleSheet();
+setActiveStyleSheet(title);
diff --git a/web/json/.gitignore b/web/json/.gitignore
new file mode 100644
index 0000000..54c8f20
--- /dev/null
+++ b/web/json/.gitignore
@@ -0,0 +1,2 @@
+stats.json
+stats.json~
\ No newline at end of file