PHP安装redis扩展模块

每次要用redis都要引入外部库,突然想到为什么不能用php扩展模块的方式来调用redis,毕竟PHP的extension是很强大的。
于是找了一下还真有。
说一下安装过程
https://github.com/phpredis/phpredis
在这里获取phpredis的最新安装包
然后unzip或者tar解压。
先使用phpize得到configure文件,然后执行./configure --with-php-config=/usr/local/php/bin/php-config
然后make && make install
这样就在extension目录里产生了一个redis.so文件
我们要在php.ini里面载入它
在php.ini中加入

[redis]
extension=redis.so

重启一下php-fpm。然后进phpinfo()看一下。有redis就成功了。调用方法稍后再写。

DHL用自己的check digits规范把我坑了一下

这几天一直在搞条形码生成。因为有一个和DHL的合作项目要自动生成条形码打印出来。
而DHL给的条形码要求是interleaved 2of5.

查了一圈资料得知interleaved 2of5是没有check digits的
但是生成出来的条码很明显不对

后来在wikipedia发现A checksum can be added as last digit, which is calculated in the same way as UPC checksums.
于是用标准的checksums来计算check digit即

In the UPC-A system, the check digit is calculated as follows:

Add the digits in the odd-numbered positions (first, third, fifth,
etc.) together and multiply by three. Add the digits in the
even-numbered positions (second, fourth, sixth, etc.) to the result.
Find the result modulo 10 (i.e. the remainder when divided by 10… 10
goes into 58 5 times with 8 leftover). If the result is not zero,
subtract the result from ten.

但是生成出来的条码依旧和DHL给的范例不一样。反馈给DHL他们也说他们的机器扫描不成功。
后来以为是条码宽度问题。根据DHL给的要求把每条宽度设置为0.5mm到0.33mm之间。总宽度42mm
再次提交,DHL反馈说扫描出来了但是数字不对。首位多了0,末尾多了8.
万能的wikipedia又告诉我

Before the actual pairs there is a start code consisting of nnnn
(narrow bar-narrow space- narrow bar-narrow space), and after all
symbols there is the stop code consisting of Wnn (Wide bar-narrow
space-narrow bar).

Because digits are encoded by pairs, only an even number of digits can
be encoded. Typically an odd number of digits is encoded by adding a
“0” as first digit, but sometimes an odd number of digits is encoded
by using five narrow spaces in the last digit.

瞬间想到最后以为是不是barcode生成器自动给我添加的校验位。去查看了生成类的源代码发现

static public function compute($code, $crc, $type){
        if (! $crc) {
            if (strlen($code) % 2) $code = '0' . $code;
        } else {
            if ( ($type == 'int25') && (strlen($code) % 2 == 0) ) $code = '0' . $code;
            $odd = true;
            $sum = 0;
            for($i=strlen($code)-1; $i>-1; $i--){
                $v = intval($code[$i]);
                $sum += $odd ? 3 * $v : $v;
                $odd = ! $odd;
            }
            $code .= (string) ((10 - $sum % 10) % 10);
        }
        return($code);
    }

然而不传递checkdigit进去生成出来的依旧不对。
这时候想到DHL一开始发的Check digit calculation , Factor 4 and 9 see Specification。
wikipedia了一下。终于发现了

Identcode and Leitcode are variants of interleaved 2 of 5 with check
digits used by Deutsche Post.

一开始还不知道Deutsche Post是什么。查了一下 ,不就是DHL么。。。。
好坑啊。DHL还用自己的checkdigit规范。

果断去把生成类里的代码改成

$sum += $odd ? 4 * $v : 9 * $v;

又生成了一张。发现和DHL给的范例终于一模一样了。。

大功告成。
然而不知道为啥DHL用自己的checkdigit规范不用国际通用标准。真是好坑好坑。
另外,wikipedia真是个好东西。然而国内百度一下并没有任何资料。全都是卖东西的。

PHP升级

刚刚把PHP升级到最新的5.6.6,本想升级到PHP7,但是由于没get到源码作罢。
升级完毕后部分文件显示access denied。
在stackoverflow上发现了几个解决方案,

In your php-fpm www.conf set security.limit_extensions to .php or
.php5 or whatever suits your environment. For some users, completely
removing all values or setting it to FALSE was the only way to get it
working.

In your nginx config file set fastcgi_pass to your socket address
(e.g. unix:/var/run/php-fpm/php-fpm.sock;) instead of your server
address and port.

Check your SCRIPT_FILENAME fastcgi param and set it according to the
location of your files.

In your nginx config file include fastcgi_split_path_info
^(.+.php)(/.+)$; in the location block where all the other fastcgi
params are defined.

都无效,最后发现是php.ini里cgi.fix_pathinfo的值为0,造成pathinfo的部分文件没法用,修改为1重启php后解决问题。

从如何获取可信赖的IP地址聊起

作者: joyqi 时间: October 11, 2014 分类: 编程开发,PHP
起因

写这篇文章缘起SF的一个问题 http://segmentfault.com/q/1010000000686700/a-1020000000687155。由此我想到了很多,就和大家随便聊聊吧
在PHP中获取ip地址有一段网上流传甚广的代码,还有它的各种变种

function real_ip()
{
    static $realip = NULL;

    if ($realip !== NULL)
    {
        return $realip;
    }

    if (isset($_SERVER))
    {
        if (isset($_SERVER['HTTP_X_FORWARDED_FOR']))
        {
            $arr = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);

            /* 取X-Forwarded-For中第一个非unknown的有效IP字符串 */
            foreach ($arr AS $ip)
            {
                $ip = trim($ip);

                if ($ip != 'unknown')
                {
                    $realip = $ip;

                    break;
                }
            }
        }
        elseif (isset($_SERVER['HTTP_CLIENT_IP']))
        {
            $realip = $_SERVER['HTTP_CLIENT_IP'];
        }
        else
        {
            if (isset($_SERVER['REMOTE_ADDR']))
            {
                $realip = $_SERVER['REMOTE_ADDR'];
            }
            else
            {
                $realip = '0.0.0.0';
            }
        }
    }
    else
    {
        if (getenv('HTTP_X_FORWARDED_FOR'))
        {
            $realip = getenv('HTTP_X_FORWARDED_FOR');
        }
        elseif (getenv('HTTP_CLIENT_IP'))
        {
            $realip = getenv('HTTP_CLIENT_IP');
        }
        else
        {
            $realip = getenv('REMOTE_ADDR');
        }
    }

    preg_match("/[\d\.]{7,15}/", $realip, $onlineip);
    $realip = !empty($onlineip[0]) ? $onlineip[0] : '0.0.0.0';

    return $realip;
}

这段代码的原理是从HTTP_X_FORWARDED_FOR以及HTTP_CLIENT_IP还有REMOTE_ADDR中获取ip地址。

这段代码有问题吗?如果从功能角度来看,它是没有问题的,因为它保证了你可以尽可能地获取到一个ip地址,如果你地程序不需要验证这个ip地址是否正确,只是为了显示它的来源,那么你可以这样做没问题。

但是很多情况下我们需要确切地知道当前跟服务器连接的是哪个ip,那这段代码就不妥了。

在PHP的$_SERVER变量中,凡是以HTTP_开头的键值都是直接从客户端的HTTP请求头中直接解析出来的。

这句话啥意思,意思就是客户端告诉你ip是多少就是多少,伪造它的成本可以说非常低。除了这两个以HTTP_开头的地址外,还有一个REMOTE_ADDR地址,这个地址是与你的服务器进行实际tcp连接的地址,它是一个可信赖的地址。要伪造这个地址成本很高,这是由于TCP三次握手协议的原理决定的,除非你能骗过服务器端的路由,那这样的成本就太高了,还不如直接黑进你的服务器

衍生

上图是一个经过简化的TCP握手协议,握手就是指在你正式传输信息之前,客户端和服务端确认双方是否可信的过程,这三步可以通俗化成下面的过程

小C (Client) 向小S (Server) 发了一封信(SYN),告诉它我要寄一个包裹 (数据) 给你,但是为了保证这个包裹你能收到,我先发一封信确认下你的地址 (Server IP) 是不是正确的,怎么确认呢?收到这封信后请按信上的地址 (Client IP) 回信就行了。
小S收到了这封信以后一想,我不仅要回信确认我的地址是存在的(ACK),还要问问你的地址是不是正确的,要不然我咋知道你是不是靠谱的人。于是小S给小C发了一封回信,不仅在信里确认自己的地址是正确的(ACK),也要求(SYN)小C在收到信件后给自己发一封回信,以确认他的地址也是真实的。要不然我会拒绝你的连接
小C在收到小S的确认后,马上发了一封回信(ACK)告诉我已经收到信了
从此它们就可以愉快的通信了。

那么问题来了

如果小C没有收到小S的确认信,但他也发了一封回信骗小S说自己已经收到了,怎么破?

这个问题实际上在TCP协议中已经解决了,每个SYN请求都会包含一个随机数字,发送回信ACK的时候,必须一并将这个数字加一再发回去。这样我们收到ACK的时候只要确认这个值是否正确就行了。

如果邮政局被收买了怎么?

这确实是一个风险,这就是我刚才提到的,路由器被黑掉的可能。这也就是很多人为什么说互联网的基础是非常脆弱的,只要公网上的其中一个路由器说了假话,所有经过这个路由器的数据都会变成不可信的。

但同时这个成本非常高,因为路由器的安全级别通常是很高的,而且一般的路由器管理端口也不会向普通访问者开放。所以能做到这一点的黑客,通常会选择直接去黑你的服务器。

这就好比我为了寄点东西噁心你一下,还要去贿赂一整个邮政局,显然不划算。还不如我直接买张火车票,赶到你的住处,把你门撬了,放你桌上。

再发散下

SYN FLOOD攻击

也就是SYN洪水攻击,看了上面的解释你应该大概知道SYN是啥意思了,就是一个询问包。所谓SYN洪水攻击就是指Client发送第一个SYN包的时候,告诉Server的源地址是一个假的,当Server发送回执时会进入到一个half-open(半开)状态。

为啥叫半开,因为这个端口只开了一半,握手过程已经开始,但还有一半没完成。这个半开连接大家应该都听过,早先玩迅雷下载或者bt的时候都需要打一个windows半开连接数补丁,就是因为桌面操作系统的计算资源是优先倾向于GUI的,当系统处于半开连接时因为处于等待状态,是会消耗内存和计算资源的,所以操作系统会默认把这个值调低,限制你能接受的连接。而我们p2p下载又需要比较高的连接量,所以就有了这个补丁,把它的限制打开。

我花这段篇幅解释半开连接数并不是没事找事,这其实就是SYN FLOOD攻击的原理。因为当系统处于半开状态时要消耗资源,而服务器往往半开连接数限制都比较大(或者干脆没限制),因此接第一段的话,因为服务器得到的源地址是个假的,发送回执后肯定会收不到确认,因此就会进入到漫长的等待过程(相对于响应时间)。攻击者通过伪造大量的这种无效请求,使服务器端等待大量连接,从而耗尽服务器资源,以达到使其瘫痪的目的。

想看看你是不是受到了此种攻击,只需要在netstat时看看是不是有大量的SYN_RCVD连接即可。SYN_RCVD表示服务器处于握手的第二步。

随机数的漏洞

我先将刚刚讲过的一个问题引用下

如果小C没有收到小S的确认信,但他也发了一封回信骗小S说自己已经收到了,怎么破?
这个问题实际上在TCP协议中已经解决了,每个SYN请求都会包含一个随机数字,发送回信ACK的时候,必须一并将这个数字加一再发回去。
这段话段的关键在于“随机数”这三个字,因为我们这个验证过程的一大基础是Server生成的随机数是不可能被Client端提前知道的,这就好比打扑克时你不可能知道其他人的牌是什么,一个道理。

但如果这个Client是个赌神呢?

http://www.securityfocus.com/bid/25348/discuss
根据安全机构的研究,Linux内核中包含了一个可导致Dos和权限提升的漏洞,可以被黑客利用来运行攻击代码。这是一个核心内存的堆栈溢出问题,可以导致系统的崩溃,再特定的环境下,还可以提升权限。这个漏洞影响到了Linux内核2.6.22.3前的版本。
这是一个被爆出来的随机数漏洞,如果我知道了服务端的随机数漏洞,那么完全可以利用这一点来伪造出一个完美的假ip。

最后弱弱地总结下

那我们到底该相信哪个ip呢?

首选REMOTE_ADDR,因为虽然有如此多地伪造方法,但在语言层面你只能选一个最可靠地。
如果你地服务隐藏在负载均衡或者缓存系统后面,它通常会给你发一个Client-Ip或者X-Forwarded-For的HTTP头,告诉你跟它连接的客户端ip,这时的HTTP_CLIENT_IP和HTTP_X_FORWARDED_FOR是可信的,因为它是从前端服务器上的直接传递过来的,当然你必须在程序中指定只信任这一个来源,而不要像最开始的代码那样每个都检测一遍

利用Redis作为前端静态缓存加速网站访问

本想做Mysql的缓存来加速读取,但是为了练手,先用Redis做HTML页面缓存好了。

先介绍一个Credis的PHP库,官方介绍为Credis_Client, a lightweight Redis PHP standalone client and phpredis wrapper。基本操作函数都有,用起来也是很顺手的,再介绍一个名为RedisDesktopManager的跨平台redis桌面管理软件,类似navicat吧。看redis的内容和修改也是挺好用的。说起redis,前几天装了redis来练手nosql,安装过程略简单,就不写文了。
用Redis作为页面缓存无非是把不太变化的PHP页面静态化然后写入redis,然后时常检测页面更新或者设置TTL来自动更新,也可以手动更新,比如这个首页就经常采用手动更新的方式。
主要用到的就是
ob_start();
require 'index.php';
$html = ob_get_contents();
ob_end_clean();
来把本来index.php的内容静态化写入redis,这里采用Hash的数据方式用hSet(string $key, string $field, string $value)函数来写入数据库,用域名做key,用网址做field参数,value就是页面内容的html代码。虽然是一种比较简单粗暴的办法,但也是极为好用的。

当页面被访问时首先会用$redis->hexists(域名, 网址)来判断Redis内是否存在页面缓存,如果存在则直接用$redis->hget(域名, 网址);来调用内容显示出来。

如果页面不存在会做一次是否需要把该页面缓存的判断,这里会把不需要缓存的页面,例如:搜索页,404页之类的来进行一次判断。
如果该页面是需要缓存的页面,则调用上面的函数进行缓存,然后用hSet写入redis,再用$redis->expire(域名, 86400);来控制整体的刷新时间。

同时为了可以手动清除缓存,这里我进行了一次$_get['clean'],如果该值为all,则将会执行$redis->del(域名);把整个hash数据清空,即清除整个网站的页面缓存,如果该值为page,则执行$redis->hdel(域名,网址);来清空本页面的缓存。

至此,利用Redis来缓存页面内容已经全部搞定了,测试结果显示缓存之前每个页面的执行时间是0.05403 seconds,缓存之后的页面执行时间(仅相当于一次redis读取)为0.00068 seconds,为原来的百分之一,可见效果还是很明显的。

注:以上采用域名和网址作为key和field要进行md5之类的hash转换。