查看文章 |
[UCenter] 地狱之旅
2011-01-13 18:17
UCENTER包含表: | uc_admins | | uc_applications | | uc_badwords | | uc_domains | | uc_failedlogins | | uc_feeds | | uc_friends | | uc_mailqueue | | uc_memberfields | | uc_members | | uc_mergemembers | | uc_newpm | | uc_notelist | | uc_pms | | uc_protectedmembers | | uc_settings | | uc_sqlcache | | uc_tags | | uc_vars | +———————+ 下面我们一一来解释每一个表: 1:uc_admins // UCenter 管理员列表,可以为管理员分配权限 +——————-+———————–+ | Field | Type | +——————-+———————–+ | uid | mediumint(8) unsigned | | username | char(15) | | allowadminsetting | tinyint(1) | | allowadminapp | tinyint(1) | | allowadminuser | tinyint(1) | | allowadminbadword | tinyint(1) | | allowadmintag | tinyint(1) | | allowadminpm | tinyint(1) | | allowadmincredits | tinyint(1) | | allowadmindomain | tinyint(1) | | allowadmindb | tinyint(1) | | allowadminnote | tinyint(1) | | allowadmincache | tinyint(1) | | allowadminlog | tinyint(1) | +——————-+———————–+ 2:uc_applications //应用列表 +————–+———————-+ | Field | Type | +————–+———————-+ | appid | smallint(6) unsigned | | type | char(16) | | name | char(20) | | url | char(255) | | authkey | char(255) | | ip | char(15) | | viewprourl | char(255) | | apifilename | char(30) | | charset | char(8) | | dbcharset | char(8) | | synlogin | tinyint(1) | | recvnote | tinyint(1) | | extra | mediumtext | | tagtemplates | mediumtext | +————–+———————-+ 3:uc_badwords //词语过滤 +————-+———————-+ | Field | Type | +————-+———————-+ | id | smallint(6) unsigned | | admin | varchar(15) | | find | varchar(255) | | replacement | varchar(255) | | findpattern | varchar(255) | +————-+———————-+ 4:uc_domains //域名解析 +——–+——————+ | Field | Type | +——–+——————+ | id | int(10) unsigned | | domain | char(40) | | ip | char(15) | +——–+——————+ 5:uc_failedlogins //登录失败日志 +————+———————+ | Field | Type | +————+———————+ | ip | char(15) | | count | tinyint(1) unsigned | | lastupdate | int(10) unsigned | +————+———————+ 6:uc_feeds +—————-+———————–+ | Field | Type | +—————-+———————–+ | feedid | mediumint(8) unsigned | | appid | varchar(30) | | icon | varchar(30) | | uid | mediumint(8) unsigned | | username | varchar(15) | | dateline | int(10) unsigned | | hash_template | varchar(32) | | hash_data | varchar(32) | | title_template | text | | title_data | text | | body_template | text | | body_data | text | | body_general | text | | image_1 | varchar(255) | | image_1_link | varchar(255) | | image_2 | varchar(255) | | image_2_link | varchar(255) | | image_3 | varchar(255) | | image_3_link | varchar(255) | | image_4 | varchar(255) | | image_4_link | varchar(255) | | target_ids | varchar(255) | +—————-+———————–+ 7:uc_friends //好友 +———–+———————–+ | Field | Type | +———–+———————–+ | uid | mediumint(8) unsigned | | friendid | mediumint(8) unsigned | | direction | tinyint(1) | | version | int(10) unsigned | | delstatus | tinyint(1) | | comment | char(255) | +———–+———————–+ 8:uc_mailqueue //邮件队列 +———-+———————–+ | Field | Type | +———-+———————–+ | mailid | int(10) unsigned | | touid | mediumint(8) unsigned | | tomail | varchar(32) | | frommail | varchar(100) | | subject | varchar(255) | | message | text | | charset | varchar(15) | | htmlon | tinyint(1) | | level | tinyint(1) | | dateline | int(10) unsigned | | failures | tinyint(3) unsigned | | appid | smallint(6) unsigned | +———-+———————–+ 9:uc_memberfields //黑名单 +———–+———————–+ | Field | Type | +———–+———————–+ | uid | mediumint(8) unsigned | | blacklist | text | +———–+———————–+ 10:uc_members//用户列表 +—————+———————–+ | Field | Type | +—————+———————–+ | uid | mediumint(8) unsigned | | username | char(15) | | password | char(32) | | email | char(32) | | myid | char(30) | | myidkey | char(16) | | regip | char(15) | | regdate | int(10) unsigned | | lastloginip | int(10) | | lastlogintime | int(10) unsigned | | salt | char(6) | | secques | char(8) | +—————+———————–+ 11:uc_mergemembers//重名的用户名保存到该表中 +———-+———————-+ | Field | Type | +———-+———————-+ | appid | smallint(6) unsigned | | username | char(15) | +———-+———————-+ 12:uc_newpm +——-+———————–+ | Field | Type | +——-+———————–+ | uid | mediumint(8) unsigned | +——-+———————–+ 13:uc_notelist//积分对换,词语过滤,域名解析,应用管理,日志 +————+———————-+ | Field | Type | +————+———————-+ | noteid | int(10) unsigned | | operation | char(32) | | closed | tinyint(4) | | totalnum | smallint(6) unsigned | | succeednum | smallint(6) unsigned | | getdata | mediumtext | | postdata | mediumtext | | dateline | int(10) unsigned | | pri | tinyint(3) | +————+———————-+ 14:uc_pms //UCHOME的短信息,公共消息 序号,发送消息[私信]的会员名(公共消息为空),发消息的会员ID(公共消息0),接受消息的会员ID,x(inbox),x,主题,是否标记灵删除,是否回复(?) +———–+————————+ | Field | Type | +———–+————————+ | pmid | int(10) unsigned | | msgfrom | varchar(15) | | msgfromid | mediumint(8) unsigned | | msgtoid | mediumint(8) unsigned | | folder | enum(‘inbox’,'outbox’) | | new | tinyint(1) | | subject | varchar(75) | | dateline | int(10) unsigned | | message | text | | delstatus | tinyint(1) unsigned | | related | int(10) unsigned | | fromappid | smallint(6) unsigned | +———–+————————+ 15:uc_protectedmembers//受保护的用户 +———-+———————–+ | Field | Type | +———-+———————–+ | uid | mediumint(8) unsigned | | username | char(15) | | appid | tinyint(1) unsigned | | dateline | int(10) unsigned | | admin | char(15) | +———-+———————–+ 16:uc_settings//基本设置 +——-+————-+ | Field | Type | +——-+————-+ | k | varchar(32) | | v | text | +——-+————-+ 17:uc_sqlcache +——–+——————+ | Field | Type | +——–+——————+ | sqlid | char(6) | | data | char(100) | | expiry | int(10) unsigned | +——–+——————+ 18:uc_tags +————+———————-+ | Field | Type | +————+———————-+ | tagname | char(20) | | appid | smallint(6) unsigned | | data | mediumtext | | expiration | int(10) unsigned | +————+———————-+ 19:uc_vars +——-+———–+ | Field | Type | +——-+———–+ | name | char(32) | | value | char(255) | +——-+———–+ UCenter包括两个部分Server端与Client端。 Server端主要功能是提供服务,和管理应用。 Client端主要功能是与服务端通信,及把更新及时传递到Server,并在需要时从Server端取数据给自己用。 UCenter如何通讯? 用到的网络应用层协议基于Http1.1协议、p3p协议。 UCenter以Http1.1协议做为通信底层协议,同时以p3p协议实现JavaScript对Cookie的跨域调用。 对于Http1.1协议与P3P协议,不多做解释了,不是一两天就能理解的东西,大家自己看看吧。 UCenter的程序结构是什么样的? UCenter分为Server端与Client端。 Client端相对比较简单,有api目录和uc_client目录。 api目录保存是与Server通信相关的文件,也是直接与Server进行通讯的第一道关口,来自于服务器的Request直接被Dispatch到api/uc.php文件里。 uc.php就是位于该目录下,uc.php也是这个目录里最主要的文件,主要负责对传来的code进行解码,而解码的唯一依据是UC_KEY. 这也就解释了,为什么当Server与Client的UC_KEY不一致,会导致所有的接口调用都会失败(因为解码失败,所有从S传来的数据都乱了,没办法继续走下去)。 uc.php文件里定义了uc_note类,用这个类实现了S端请求C端去进行一些操作比如test,deleteuser……等等,这类主要是S请求C 同步的操作。应用如果需要在请求到来之际,操作自己数据库或者Cookie,那么在这里定义这些操作,是最好的方式。 uc_client目录里有4个子目录,control、data、lib、model。另外,最主要的文件是client.php文件,并判断安装的应 用是以“数据库”模式,或者是以“消息”模式来与UCenter通讯的。以此为依据,来调用相应的函数与UCenter的S端进行通讯。 client.php文件里定义了很多的函数。如:uc_serialize,uc_unserialize,uc_authcode,uc_api_post……等等。这个函数基本都是类的成员函数。 client.php文件以include_once的形式,分别从4个子目录里引用需要的类定义。来实现以上这些函数。 这些函数的主要实现的都是比较核心的功能。比如序列化、反序列化、加密、解密……等等。 总之,uc_client里面的文件主要实现的功能是与S端进行通讯。 Server端相对比较复杂,依然采用的是MVC结构。 S端程序对各种UCenter所管理的资源都提供了相应管理的界面。 并为与之相连接的每一个应用请求这些资源而做出了相应的接口,提供与应用的通讯。 C端,我已经不可能用几篇简单的贴子来描述完。S端更不可能啦。 我们从简单的例子来看一下,S端大概的结构,以及S端是如何与C端进行通讯的吧。 我们就从最简单的功能看起。 在S的后台,“应用管理”界面。会列表出所有已经注册过的应用。 同时会在这些列表的最后一列出现“通讯情况”。如果通讯成功,则返回绿色的“通讯成功”,不成功则返回红色的“通讯失败”。 这里,可以肯定的说,S与其所有注册过的应用,都发生了一次通讯。这里,就是我们的切入点。我们将从这里切进来,理清整个UCenter的通讯流程。 注:S与C的通讯,和C与S的通讯,是类似的。也就是说,只要理清了S到C的流程,C到S的也很容易看懂了。 那“应用管理”的界面在哪里? 以DisuczX为例。在uc_server\control\admin\app.php文件里。 这个是算是C层,这里定义了class control extends adminbase {……}类。 这个类的函数onadd,onls,onping……分别实现了添加新应用、应用列表、测试应用连接……功能。 我们要说的就是从这个onping函数开始。 那么M层在哪里呢? 在uc_server\model\app.php文件里。 定义了如:delete_apps($appids)之类的方法。 其实UCenter的MVC并没有完全的分开,不是很规范。如上面的C层里的onadd()应该是不会去直接操作数据库的。但是仍然直接访问了数据库,而且还在应用数据表里添加了一条应用记录。 这个在这里就不多说了。我们继续…… 我们来跟进uc_server\control\admin\app.php里的onping()函数。 function onping() { $ip = getgpc('ip'); $url = getgpc('url'); $appid = intval(getgpc('appid')); $app = $_ENV['app']->get_app_by_appid($appid); $status = ''; if($app['extra']['apppath'] && @include $app['extra']['apppath'].'./api/'.$app['apifilename']) { $uc_note = new uc_note(); $status = $uc_note->test($note['getdata'], $note['postdata']); //WriteToLog("ping.log",'\$uc_note='.$uc_note,''); } else { $this->load('note'); $url = $_ENV['note']->get_url_code('test', '', $appid); $status = $_ENV['app']->test_api($url, $ip); //WriteToLog("ping.log",'URL='.$url,''); file_put_contents("ping.log",strtotime("now").' '.'URL='.$url."\r",FILE_APPEND); } if($status == '1') { echo 'document.getElementById(\'status_'.$appid.'\').innerHTML = "<img src=\'images/correct.gif\' border=\'0\' class=\'statimg\' \/><span class=\'green\'>'.$this->lang['app_connent_ok'].'</span>";testlink();'; WriteToLog("ping.log",$appid.' '.$this->lang['app_connent_ok'],''); } else { echo 'document.getElementById(\'status_'.$appid.'\').innerHTML = "<img src=\'images/error.gif\' border=\'0\' class=\'statimg\' \/><span class=\'red\'>'.$this->lang['app_connent_false'].'</span>";testlink();'; WriteToLog("ping.log",$appid.' '.$this->lang['app_connent_false'],''); } } 注意以上代码红色处,$status 的值是由函数$_ENV['app']->test_api($url, $ip);返回的。 这表示调用uc_server\model\app.php里的test_api函数。 我们来看一下test_api函数。 function test_api($url, $ip = '') { $this->base->load('misc'); if(!$ip) { $ip = $_ENV['misc']->get_host_by_url($url);//在这里调用了misc模块的get_host_by_url($url)来获得正确的应用的IP地址以实现通讯。 } if($ip < 0) { return FALSE; } file_put_contents("test_api.log",strtotime("now").' '.'ip>=0 and url='.$url.'ip='.$ip."\r",FILE_APPEND); $myreturn=$_ENV['misc']->dfopen($url, 0, '', '', 1, $ip);//在这里调用了misc模块的dfopen()来向应用发送调用。 file_put_contents("test_api.log",strtotime("now").' '.'\$_ENV[\'misc\']->dfopen'.$myreturn."\r",FILE_APPEND); return $myreturn; } 那么我们跟进\uc_client\model\'misc'模块的dfopen函数。 function dfopen($url, $limit = 0, $post = '', $cookie = '', $bysocket = FALSE , $ip = '', $timeout = 15, $block = TRUE, $encodetype = 'URLENCODE') { //error_log("[uc_client]\r\nurl: $url\r\npost: $post\r\n\r\n", 3, 'c:/log/php_fopen.txt'); $return = ''; $matches = parse_url($url); $host = $matches['host']; $path = $matches['path'] ? $matches['path'].($matches['query'] ? '?'.$matches['query'] : '') : '/'; $port = !empty($matches['port']) ? $matches['port'] : 80; if($post) { $out = "POST $path HTTP/1.0\r\n"; $out .= "Accept: */*\r\n"; //$out .= "Referer: $boardurl\r\n"; $out .= "Accept-Language: zh-cn\r\n"; $boundary = $encodetype == 'URLENCODE' ? '' : ';'.substr($post, 0, trim(strpos($post, "\n"))); $out .= $encodetype == 'URLENCODE' ? "Content-Type: application/x-www-form-urlencoded\r\n" : "Content-Type: multipart/form-data$boundary\r\n"; $out .= "User-Agent: $_SERVER[HTTP_USER_AGENT]\r\n"; $out .= "Host: $host\r\n"; $out .= 'Content-Length: '.strlen($post)."\r\n"; $out .= "Connection: Close\r\n"; $out .= "Cache-Control: no-cache\r\n"; $out .= "Cookie: $cookie\r\n\r\n"; $out .= $post; } else { $out = "GET $path HTTP/1.0\r\n"; $out .= "Accept: */*\r\n"; //$out .= "Referer: $boardurl\r\n"; $out .= "Accept-Language: zh-cn\r\n"; $out .= "User-Agent: $_SERVER[HTTP_USER_AGENT]\r\n"; $out .= "Host: $host\r\n"; $out .= "Connection: Close\r\n"; $out .= "Cookie: $cookie\r\n\r\n"; } $fp = @fsockopen(($ip ? $ip : $host), $port, $errno, $errstr, $timeout); if(!$fp) { return ''; } else { stream_set_blocking($fp, $block); stream_set_timeout($fp, $timeout); @fwrite($fp, $out); $status = stream_get_meta_data($fp); if(!$status['timed_out']) { while (!feof($fp)) { if(($header = @fgets($fp)) && ($header == "\r\n" || $header == "\n")) { break; } } $stop = false; while(!feof($fp) && !$stop) { $data = fread($fp, ($limit == 0 || $limit > 8192 ? 8192 : $limit)); $return .= $data; if($limit) { $limit -= strlen($data); $stop = $limit <= 0; } } } @fclose($fp); return $return; } } 我们可以看到,dfopen其实在最直接的一个向应用端(S端)发送http1.1 Request的函数。所有的请求,都会由它先编码,然后发送出去。 我们截获由dfopen发送的字串,发现是这样的: GET /test/discuzx/api/uc.php?code=04e27HZ%2FyCLXqy%2BfWDKM57uRzvFbIdA0Oky2sVXCrbdxH%2FOQc9xGGz0ZQklzbrFFycAQxwDzsDw HTTP/1.0 Accept: */* Accept-Language: zh-cn User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; GTB6.5; .NET CLR 2.0.50727; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; 360SE) Host: localhost:80 Connection: Close Cookie: 也就是说,所有的请求,都被发送到了应用的 ./api/uc.php文件里。 S的请求,全部交由应用的uc.php来处理。 由此证明,服务端(S端)发送到应用端(C端)与的通信,第一个关口就是S端的uc.php文件。 那么那段uc.php?code=04e27HZ%2FyCLXqy%2BfWDKM57uRzvFbIdA0Oky2sVXCrbdxH%2FOQc9xGGz0ZQklzbrFFycAQxwDzsDw 中的code=.....到底包含了哪些数据,到底是什么东西呢? 让我们返回到:uc_server\control\admin\app.php里的onping()函数。 function onping() { $ip = getgpc('ip'); $url = getgpc('url'); $appid = intval(getgpc('appid')); $app = $_ENV['app']->get_app_by_appid($appid); $status = ''; if($app['extra']['apppath'] && @include $app['extra']['apppath'].'./api/'.$app['apifilename']) { $uc_note = new uc_note(); $status = $uc_note->test($note['getdata'], $note['postdata']); //WriteToLog("ping.log",'\$uc_note='.$uc_note,''); } else { $this->load('note'); $url = $_ENV['note']->get_url_code('test', '', $appid); $status = $_ENV['app']->test_api($url, $ip); )很明显,那段code就是$url,而这段$url来自于'note'模型的get_url_code的函数。 我们跟进\uc_client\model\'note'模型的get_url_code函数。 function get_url_code($operation, $getdata, $appid) { $app = $this->apps[$appid]; $authkey = UC_KEY; $url = $app['url']; $apifilename = isset($app['apifilename']) && $app['apifilename'] ? $app['apifilename'] : 'uc.php'; $action = $this->operations[$operation][1]; $code = urlencode($this->base->authcode("$action&".($getdata ? "$getdata&" : '')."time=".$this->base->time, 'ENCODE', $authkey)); return $url."/api/$apifilename?code=$code"; } 注意以上两段红色的代码,说明get_url_code函数。实际上是在用S与C约定好的UC_KEY,利用authcode函数,来实现对$action,time......等参数的组合加密。 这同样也说明了,为什么当S与C的UC_KEY不一至时,所有的S发送到C的请求,都会失败的原因。因为C没有办法对传过来的code正确的解密、解析。所以根本不知道,S端传过来的是什么。 S端向C端发送请求的流程到此已经结束。 下面我们来分析一下,C端是如何去处理S端发送过来的请求的。 '因为所有的S端的请求,是发往uc.php的。我们来分析一下uc.php文件。 uc.php文件分为几个主要段落。 第一是define段。用来定义UCenter版本号、发行号;哪些同步方法的是否打开,哪些关闭;文件主目录; define('UC_CLIENT_VERSION', '1.5.1'); define('UC_CLIENT_RELEASE', '20100501'); define('API_DELETEUSER', 1); define('API_RENAMEUSER', 1); define('API_GETTAG', 1); define('API_SYNLOGIN', 1); define('API_SYNLOGOUT', 1); define('API_UPDATEPW', 1); define('API_UPDATEBADWORDS', 1); define('API_UPDATEHOSTS', 1); define('API_UPDATEAPPS', 1); define('API_UPDATECLIENT', 1); define('API_UPDATECREDIT', 1); define('API_GETCREDIT', 1); define('API_GETCREDITSETTINGS', 1); define('API_UPDATECREDITSETTINGS', 1); define('API_ADDFEED', 1); define('API_RETURN_SUCCEED', '1'); define('API_RETURN_FAILED', '-1'); define('API_RETURN_FORBIDDEN', '1'); define('IN_DISCUZ', true); define('IN_UC',true); define('DISCUZ_ROOT', dirname(dirname(__FILE__)).'/'); define('CURSCRIPT', 'api'); 这里的define一般不会出错,都会顺利执行下去,如果有问题,那么无非是定义赋值错误,或者遗漏了某些常量的定义。 接下来是require段。用来引入一些类、函数的定义文件。 require_once DISCUZ_ROOT.'./config/config_global.php'; require_once DISCUZ_ROOT.'./config/config_ucenter.php'; require_once DISCUZ_ROOT.'./source/function/function_core.php'; require_once DISCUZ_ROOT.'./source/class/class_core.php'; $discuz = & discuz_core::instance(); $discuz->init(); require DISCUZ_ROOT.'./config/config_ucenter.php'; require_once DISCUZ_ROOT.'./data/config.inc.php'; require段,要注意,虽然require一个不存在的文件,不会影响程序往下执行,但是:如果文件没有被正确引入,那么段内的红色的两行语句,会因为找不到对应的类文件而无法生成实例。这是很严重的错误,因为这会导致程序直接在此处停止。不同的应用,大部分是因为这里出错而导致C端停止解析S端的请求。 继续往下看。 $get = $post = array();//定义数组。 $code = @$_GET['code'];//得到S端传来的code=92fjd892fhidf2fop2fl22f 继续往下看。 //以下这句很重要,调用了uc_authcode,来解密code字串。同时使用应用端的UC_KEY做为解密的参数。因为传来的code字串,正是用UC_KEY来加密的。并将解密的结果(是一个数组),放到$get变量里。 parse_str(uc_authcode($code, 'DECODE', UC_KEY), $get); //那么放入$get变量里的数组是什么形式的呢? //我们已经捕获了。 //这里给大家看一下$get变量的大概内容包括哪些。如: //action=synlogin&username=admin&uid=1&password=f03a9e498589d7b882786b5f70e49a75&time=1283314514 //action=test&time=1283314644 //现在理解了吧。至少包含action,time这两个数组元素。 //此处看看请求是否超时 if(time() - $get['time'] > 3600) { exit('Authracation has expiried'); file_put_contents("uc.log",strtotime("now").' '.'请求错误,已经超时'."\r",FILE_APPEND); } //此处看看是否解析code成功,如果解析code字串成功,那么empty($get)就不会为null。 if(empty($get)) { exit('Invalid Request'); file_put_contents("uc.log",strtotime("now").' '.'错误的请求'.$code."\r",FILE_APPEND); } 继续往下看。 include_once DISCUZ_ROOT.'./uc_client/lib/xml.class.php';//引入XML序列化类准备对$post反序列化 file_put_contents("uc.log",strtotime("now").' '.'开始反序列化......'.$code."\r",FILE_APPEND); $post = xml_unserialize(file_get_contents('php://input'));//反序列化$post file_put_contents("uc.log",strtotime("now").' '.'反序列化成功!'.$code."\r",FILE_APPEND); //以下判断一下,解析出来的$get['action']是否是uc.php里列出的几种函数名之后,如果不是直接抛出一个API_RETURN_FAILED给S端。 //如果是uc.php列出的函数之后,那么生成一个uc_note()实例,然后调用uc_note->$get['action']($get,$post);来把请求交给uc_note实例来处理。 if(in_array($get['action'], array('test', 'deleteuser', 'renameuser', 'gettag', 'synlogin', 'synlogout', 'updatepw', 'updatebadwords', 'updatehosts', 'updateapps', 'updateclient', 'updatecredit', 'getcredit', 'getcreditsettings', 'updatecreditsettings', 'addfeed'))) { $uc_note = new uc_note(); echo $uc_note->$get['action']($get, $post); exit(); } else { exit(API_RETURN_FAILED); } OK,以上uc.php主要完成了对S端传来的code进行解析、检测的过程。并生成了一个uc_note类来处理这些请求。 继续往下,就是class uc_note()的定义了。相信有一点php基础的人都可以看懂了。这里就列出个大概了,想了解的,请自己去看uc.php。 S端发往C端,以及C端处理S端的流程。就介绍到这里。告破。 class uc_note { var $dbconfig = ''; var $db = ''; var $tablepre = ''; var $appdir = ''; function _serialize($arr, $htmlon = 0) { if(!function_exists('xml_serialize')) { include_once DISCUZ_ROOT.'./uc_client/lib/xml.class.php'; } return xml_serialize($arr, $htmlon); } function uc_note() { } function test($get, $post) { return API_RETURN_SUCCEED; } ………… } |
最近读者:

