0%

whmcs模块的shadowsocks插件

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

效果图

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

shadowsocks.php解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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,再填写下面的UsernamePassword,分别设置和数据库服务器的登录名和密码,在代码中可以看出是通过$params['serverusername']$params['serverpassword']传递。然后通过PDO来跟数据库服务器建立连接,如果连接成功没有触发异常就显示成功,触发异常就显示false。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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空了才用新端口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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个参数读一下这样写的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
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、数据库名、serverusernameserverpassword后,就开始连接数据库,由于whmcs在开通每个服务时会分配一个serviceid,就先用数据库查找是否存在相同的serviceid,这一部分使用ALREADY_EXISTS来检索。如果没有,就继续执行。然后再读取params['configoption3']为设定的最大带宽。然后就开始在回收站表中看是否有回收的port,优先使用,没有就检索user表,在现有port的值上再加1,如果user表为空,就设定params['configoption5']的端口为初始端口。然后就使用CREATE_ACCOUNT来插入一个user表。成功创建后,还要回头看看是否用了回收站表的端口,用了的话就要从回收站中删除相应的port。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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来做前端显示,这里可以看出传递了两个关键的结构体,usagenodes结构体。 然后还有其他shadowsocks_ResetBandwidth重置流量、shadowsocks_ChangePassword修改密码、shadowsocks_ChangePackage修改套餐、shadowsocks_TerminateAccount删除账号、shadowsocks_SuspendAccount停用账号等,都是一些对数据库表的操作,大同小异。

details.tpl解析

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

shadowsocks安装

默认是使用breakwa11的shadowsocks-rss,详细配置说明在此,其实由于rss的混淆和协议均能选择兼容版本,所以能一次提供shadowsocks原版和shadowsocks-rss两种服务,如果实在只想用原版,就是线路配置的地方“xxx|服务器地址|加密方式|协议|混淆”变成“xxx|服务器地址|加密方式||”即可。
这里就简单说一下自己遇到的坑:

  1. 服务器的防火墙一定要看是否关闭,要不死活连不上;
  2. 协议插件可以设为:auth_sha1_v4_compatible,混淆插件可以设为tls1.2_ticket_auth_compatible,有compatible字样就能兼容原版。

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
以下分割线为更新内容,2017年12月20日更新


本博客已不再维护Github上的项目,而且该项目也进行了多次的版本迭代,如有疑问,请在Github上提交Issues。