之前想弄一个shadowsocks的插件,发现网上要不是frankwei98开发的这种需要架设API的,就是soft-wiki这种功能极其简陋,UI不友好的。然后就又在网上找了一些传说中shadowsocks 3.x版本的安装,发现里面的代码被改得乱七八糟的,明显是不想给人用的,然后自己能看懂一点代码,就这几天把这些代码改好,然后放出来共享一下。

1.效果图

image0
shadowsocks插件其实关键的文件就是shadowsocks.php和templates/details.tpl两个文件,其实php文件是负责处理后台逻辑,tpl文件是负责前端显示。而其他css、js等文件就是UI显示锦上添花而已。

2.shadowsocks.php解析

function shadowsocks_TestConnection(array $params)
{
	try {
		$dbhost = $params['serverip'];
		$dbuser = $params['serverusername'];
		$dbpass = $params['serverpassword'];
		$db = new PDO('mysql:host=' . $dbhost, $dbuser, $dbpass);
		$success = true;
		$errorMsg = '';
	}
	catch (Exception $e) {
		logModuleCall('shadowsocks', 'shadowsocks_TestConnection', $params, $e->getMessage(), $e->getTraceAsString());
		$success = false;
		$errorMsg = $e->getMessage();
	}
	return array('success' => $success, 'error' => $errorMsg);
}

上面这是一个测试数据库服务器通断的函数,这里需要在whmcs后台配置Setup->Products/Services->Servers->Add New Server页面,其中页面中的IP Address其实就是通过$params['serverip']来传递数据库IP的参数,Server Details中选中Shadowsocks for whmcs,再填写下面的Username和Password,分别设置和数据库服务器的登录名和密码,在代码中可以看出是通过$params['serverusername']和$params['serverpassword']传递。然后通过PDO来跟数据库服务器建立连接,如果连接成功没有触发异常就显示成功,触发异常就显示false。

function initialize(array $params)
{
	$query['RECYCLE'] = 'SELECT `port` FROM `recycle_bin` ORDER BY `created_at` DESC LIMIT 1';
	$query['DELETE_RECYCLE'] = 'DELETE FROM `recycle_bin` WHERE `port` = :port';
	$query['ADD_RECYCLE'] = 'INSERT INTO `recycle_bin`(`port`,`created_at`) VALUES (:port,UNIX_TIMESTAMP())';
	$query['LATEST_USER'] = 'SELECT `port` FROM `user` ORDER BY `port` DESC LIMIT 1';
	$query['CREATE_ACCOUNT'] = 'INSERT INTO `user`(`passwd`,`transfer_enable`,`port`,`created_at`,`need_reset`,`sid`) VALUES (:passwd,:transfer_enable,:port,UNIX_TIMESTAMP(),:need_reset,:sid)';
	$query['ALREADY_EXISTS'] = 'SELECT `port` FROM `user` WHERE `sid` = :sid';
	$query['ENABLE'] = 'UPDATE `user` SET `enable` = :enable WHERE `sid` = :sid';
	$query['DELETE_ACCOUNT'] = 'DELETE FROM `user` WHERE `sid` = :sid';
	$query['CHANGE_PASSWORD'] = 'UPDATE `user` SET passwd = :passwd WHERE `sid` = :sid';
	$query['USERINFO'] = 'SELECT `id`,`passwd`,`port`,`t`,`u`,`d`,`transfer_enable`,`enable`,`created_at`,`updated_at`,`need_reset`,`sid` FROM `user` WHERE `sid` = :sid';
	$query['RESET'] = 'UPDATE `user` SET `u`=0,`d`=0 WHERE `sid` = :sid';
	$query['CHANGE_PACKAGE'] = 'UPDATE `user` SET `transfer_enable` = :transfer_enable WHERE `sid` = :sid';
	return $query;
}

上面这个初始化函数就是生成一大串数据库检索要用到的函数,就是各种插入,更新的函数。还有,这个shadowsocks数据库中有两个表,一个recycle_bin,一个是user,其中recycle_bin就是为了回收端口,user表中删除的端口先放到recycle_bin中,然后新增用户优先从recycle_bin中检索,recycle_bin空了才用新端口。

function shadowsocks_ConfigOptions()
{
	return array(
	'数据库名' => array('Type' => 'text', 'Size' => '25'),
	'重置流量' => array(
		'Type'        => 'dropdown',
		'Options'     => array('1' => '需要重置', '0' => '不需要重置'),
		'Description' => '是否需要重置流量'
		),
	'流量限制' => array('Type' => 'text', 'Size' => '25', 'Description' => '单位MB'),
	'授权密钥' => array('Type' => 'text', 'Size' => '32', 'Description' => '请输入您从购买者手中获取的密钥'),
	'起始端口' => array('Type' => 'text', 'Size' => '25', 'Description' => '如果数据库有记录此项无效'),
	'线路列表' => array('Type' => 'textarea', 'Rows' => '3', 'Cols' => '50', 'Description' => '格式 xxx|服务器地址|加密方式|协议|混淆| 一行一个')
	);
}

上面的代码是shadowsocks插件的配置页面代码,就是Setup->Products/Services->Products/Services->Create a New Product->Module Settings配置的信息,这里自定义了6个参数,到时候就是按顺序$params['configoption1']到$params['configoption6']这样来定位,其中第一个参数就是shadowsocks数据库名,第二个参数是重置流量,就是在数据库中多了一个reset参数需要配置,其实到时候就看你的重置逻辑怎么写而已,第三个是最大流量,第四个是授权密钥,感觉这个插件之前是收费的,在initialize函数中有一个认证密钥的过程,那认证过程已经删掉,留空不填就可以了,第五个是起始端口,就是shadowsocks的端口从哪个开始分配,第六个是线路列表,格式就是“xxx|服务器地址|加密方式|协议|混淆”,这个参数的读取代码写得有点问题,添加多个服务器时,混淆后面要加|,最后一行才不用加|,因为我是用|来分割字符串,然后5个参数读一下这样写的。

function shadowsocks_CreateAccount(array $params)
{
	$query = initialize($params);
	try {
		$dbhost = $params['serverip'];
		$dbname = $params['configoption1'];
		$dbuser = $params['serverusername'];
		$dbpass = $params['serverpassword'];
		$db = new PDO('mysql:host=' . $dbhost . ';dbname=' . $dbname, $dbuser, $dbpass);
		$already = $db->prepare($query['ALREADY_EXISTS']);
		$already->bindValue(':sid', $params['serviceid']);
		$already->execute();
		if ($already->fetchColumn()) {
			return 'User already exists.';
		}
		$bandwidth = (!empty($params['configoption3']) ? convert($params['configoption3'], 'mb', 'bytes') : (!empty($params['configoptions']['traffic']) ? convert($params['configoptions']['traffic'], 'gb', 'bytes') : '1099511627776'));

		$recycle = $db->prepare($query['RECYCLE']);
		$recycle->execute();
		$recycle = $recycle->fetch();
		if ($recycle) {
			$port = $recycle['port'];
			define('RECYCLE', true);
		}
		else {
			$port = $db->prepare($query['LATEST_USER']);
			$port->execute();
			$port = $port->fetch();
			if ($port) {
				$port = $port['port'] + 1;
			}
			else {
				$port = (!empty($params['configoption5']) ? $params['configoption5'] : '10000');
			}
		}
		$create = $db->prepare($query['CREATE_ACCOUNT']);
		$create->bindValue(':passwd', $params['customfields']['password']);
		$create->bindValue(':transfer_enable', $bandwidth);
		$create->bindValue(':port', $port);
		$create->bindValue(':need_reset', $params['configoption2']);
		$create->bindValue(':sid', $params['serviceid']);
		$create = $create->execute();
		
		if ($create) {
			if (defined('RECYCLE') && RECYCLE) {
				$recycle = $db->prepare($query['DELETE_RECYCLE']);
				$recycle->bindParam(':port', $port);
				$recycle->execute();
			}
			return 'success';
		}
		else {
			$error = $db->errorInfo();
			return $error;
		}
	}
	catch (Exception $e) {
		logModuleCall('shadowsocks', 'shadowsocks_CreateAccount', $params, $e->getMessage(), $e->getTraceAsString());
		return $e->getMessage();
	}
}

上面就是开通账号的函数,逻辑如下:拿到serverip、数据库名、serverusername和serverpassword后,就开始连接数据库,由于whmcs在开通每个服务时会分配一个serviceid,就先用数据库查找是否存在相同的serviceid,这一部分使用ALREADY_EXISTS来检索。如果没有,就继续执行。然后再读取params['configoption3']为设定的最大带宽。然后就开始在回收站表中看是否有回收的port,优先使用,没有就检索user表,在现有port的值上再加1,如果user表为空,就设定params['configoption5']的端口为初始端口。然后就使用CREATE_ACCOUNT来插入一个user表。成功创建后,还要回头看看是否用了回收站表的端口,用了的话就要从回收站中删除相应的port。

function shadowsocks_ClientArea(array $params)
{
	$query = initialize($params);
	try {
		$dbhost = $params['serverip'];
		$dbname = $params['configoption1'];
		$dbuser = $params['serverusername'];
		$dbpass = $params['serverpassword'];
		$db = new PDO('mysql:host=' . $dbhost . ';dbname=' . $dbname, $dbuser, $dbpass);
		$usage = $db->prepare($query['USERINFO']);
		$usage->bindValue(':sid', $params['serviceid']);
		$usage->execute();
		$usage = $usage->fetch();
		$nodes = $params['configoption6'];
		$results = array();
		$node = explode('|', $nodes);
		$x=0;$count=count($node)-1;
		while($x <= $count){
			$results[$x/5][$x%5] = $node[$x];
			$x++;
		}
		$user = array('passwd' => $usage['passwd'], 'port' => $usage['port'], 'u' => $usage['u'], 'd' => $usage['d'], 't' => $usage['t'], 'sum' => $usage['u'] + $usage['d'], 'transfer_enable' => $usage['transfer_enable'], 'created_at' => $usage['created_at'], 'updated_at' => $usage['updated_at']);
		if ($usage && $usage['enable']) {
			return array(
	'tabOverviewReplacementTemplate' => 'details.tpl',
	'templateVariables'              => array('usage' => $user, 'params' => $params, 'nodes' => $results)
	);
		}
		return array(
	'tabOverviewReplacementTemplate' => 'error.tpl',
	'templateVariables'              => array('usefulErrorHelper' => '出现了一些问题,可能您的服务还未开通,请稍后再来试试。')
	);
	}
	catch (Exception $e) {
		logModuleCall('shadowsocks', 'shadowsocks_ClientArea', $params, $e->getMessage(), $e->getTraceAsString());
		return array(
	'tabOverviewReplacementTemplate' => 'error.tpl',
	'templateVariables'              => array('usefulErrorHelper' => $e->getMessage())
	);
	}
}

shadowsocks_ClientArea函数就是用户登录后,点击shadowsocks服务会触发的一个请求,这个函数会从数据库中检索出数据,然后传递给details.tpl来做前端显示,这里可以看出传递了两个关键的结构体,usage和nodes结构体。
然后还有其他shadowsocks_ResetBandwidth重置流量、shadowsocks_ChangePassword修改密码、shadowsocks_ChangePackage修改套餐、shadowsocks_TerminateAccount删除账号、shadowsocks_SuspendAccount停用账号等,都是一些对数据库表的操作,大同小异。

3.details.tpl解析

这个前端显示的文件就更简单了,就是大部分是html、css、js代码,中间夹杂着几个数据,就是shadowsocks_ClientArea函数传递过来的usage和nodes结构体。

4.shadowsocks安装

默认是使用breakwa11的shadowsocks-rss,详细配置说明在此,其实由于rss的混淆和协议均能选择兼容版本,所以能一次提供shadowsocks原版和shadowsocks-rss两种服务,如果实在只想用原版,就是线路配置的地方“xxx|服务器地址|加密方式|协议|混淆”变成“xxx|服务器地址|加密方式||”即可。
这里就简单说一下自己遇到的坑:
1.服务器的防火墙一定要看是否关闭,要不死活连不上;
2.协议插件可以设为:auth_sha1_v4_compatible,混淆插件可以设为tls1.2_ticket_auth_compatible,有compatible字样就能兼容原版。

5.shadowsocks插件的使用说明

插件放到Modules/servers/shadowsocks目录下
1.服务器配置
image0
新建服务器
image0
IP Address此处填写服务器的IP地址,Username和Password处填写服务器的MySQL用户密码(要求有远程连接权限,并且至少拥有Shadowsocks表认证表的增删该查权限)
2.配置产品
image0
新建产品
image0
选择Module Settings,填写配置信息
image0
在Custom Fields添加自定义密码,如图一字不差的填写。

大功告成,收工,shadowsocks插件代码https://github.com/kesuki/whmcs-shadowsocks-plugin/

以下分割线为更新内容,2017年4月18日更新
--------------------------------------------------------------

重置流量

已更新github代码,新增cron.php重置流量脚本,重置流量的代码使用php语言编写,可以直接放在搭建whmcs服务的vps上,建议不要放在web站点目录上,建议修改cron.php文件权限为644。
在vps上新建计划任务

crontab -e

每月1日使用php运行该脚本

0 0 1 * * /usr/bin/php -f 目录路径/cron.php &> /dev/null
更多