php模版引擎 模版继承{extends}的简单实现

套模版的时候最烦的就是重复内容了,虽然ecshop cls_template.php支持{include file=”}, 但是有些时候还是麻烦了点, 所以就打算在此扩展一下 增加模版继承的支持。

下面是实现代码

这里只是简单实现了{extend}{block}标签 ,支持多重继承。

/*
 * 把结果中的 block标签过滤掉,只要里面的内容
 */
function trim_block($source){
     return preg_replace('/{block name=[^}]*}(.*){\\/block}/Us', '\1', $source);
}
function extend_file($source){
    global $files;
    $p="/{extends file='(.*)'}/";
    $count=preg_match_all($p,$source,$out);
    if($count==0){
        return $source;
    }
    #print_r($out);
    $filename=$out[1][0];//获取继承的模版路径路径
    $p_contents=extend_file($files[$filename]);

    return extend_block($source,$p_contents);
}
/**
 * 继承模版内容
 * @param $s_contents 子模板中的内容
 * @param $p_contents 父模板中的内容
 * return string 继承后的模版内容
 */
function extend_block($s_contents,$p_contents){
    $pblock=get_block($p_contents);
    $sblock=get_block($s_contents);

    foreach($pblock as $key=>$v){
        if(isset($sblock[$key])){
            $p_contents=str_replace($v['b'],$sblock[$key]['b'],$p_contents);
        }

    }
    return $p_contents;

}
/**
 * 提取模版中的block标签信息 返回一个数组
 * @param $str 模版内容
 * return array 
 返回格式如下(简化了表示一下为json格式):
 {  "title" :{"val":"默认页面标题","b":"{block name='title'}默认页面标题{/block}"},
    "head"  :{"val":"head 内容","b" : "{block name='head'}head 内容{/block}"}
  }
 */
function get_block($str){
    $pattern ="/{block name=['|\"](\w*)['|\"]}(.*){\\/block}/Us";
    preg_match_all($pattern ,$str,$matches ,PREG_SET_ORDER);
    $l=array();
    foreach($matches as $v){
        $l[$v[1]]=array('val'=>$v[2],'b'=>$v[0]);
    }
    #print_r($l);
    return $l;
}

下面是测试代码

 //此处为了简化代码将文件内容放到了数组中
$files['test1.html']="
<html>
    <head>
      <title>{block name='title'}默认页面标题{/block}</title>
    {block name='head'}{/block}
    </head>
    <body>
    {block name='body'}{/block}
    </body>

</html>
";

$files['test2.html']="
{extends file='test1.html'}
{block name='head'}
    <!--子模版中的head覆盖了test1的title  但是也会被下面test3覆盖掉-->
    <link href=\"/css/style.css\" type=\"text/css\"/>
{/block}

";
$files['test3']="

{extends file='test2.html'}
{block name='title'}我的页面标题 我把test1 中的title覆盖了{/block}
{block name='head'}
    <link href=\"/css/style.css\" type=\"text/css\"/>
    <script src=\"/js/common.js\"></script>
{/block}
{block name=\"body\"}我的HTML页面内容在这里{/block}

 ";
echo "result:\n";
print $out=extend_file($files['test3']);

echo "最终结果:\n";
print trim_block($out);

输出结果

result:

<html>
    <head>
      <title>{block name='title'}我的页面标题 我把test1 中的title覆盖了{/block}</title>
    {block name='head'}
    <link href="/css/style.css" type="text/css"/>
    <script src="/js/common.js"></script>
{/block}
    </head>
    <body>
    {block name="body"}我的HTML页面内容在这里{/block}
    </body>

</html>
最终结果:

<html>
    <head>
      <title>我的页面标题 我把test1 中的title覆盖了</title>

    <link href="/css/style.css" type="text/css"/>
    <script src="/js/common.js"></script>

    </head>
    <body>
    我的HTML页面内容在这里
    </body>

</html>

参考

smarty模板继承

商品SKU选择实现

在客户购买商品的时候,若这个商品存在多种”规格”(SKU),就需要客户手动选择自己想要的哪款。当时并不是每种我们都有库存, 所有就需要提供提供一个筛选功能,当客户选中一个条件的时候,需要设置其他条件中的一些值不可选。 这样就能保证不论客户怎么选择,到最后的选择都是有库存的”规格”(SKU)

开始之前先定义几种术语

属性集: 一个商品全部属性的集合

属性: 商品中的 尺码 颜色 就是两个属性

属性值: 尺码、颜色可有多个值 比如 衣服的尺码:S M XL 衣服的颜色:黑色 白色 蓝色 其中 XL 黑色就是属性值

SKU: Stock Keeping Uint(库存量单位) 由属性值组合而成(这些属性值属于不通的属性)如 一件衣服 (XL,白色) 它能确定商品的 唯一性 ,同一款式的衣服 可能有不通的颜色和大小 ,把颜色大小限制住,就能确定这件‘商品’了。

为了简化 我们虚拟出一个的商品, 下面是它的属性集和SKU:

var keys = {
    'attr1':['10','11'],
    'attr2':['20','21','22','23'],
    'attr3':['30','31','32'],
    'attr4':['40','41']
};
/*num是库存*/
var sku_list=[
    {'attrs':'10|20|30|40','num':120},
    {'attrs':'10|21|30|40','num':10},
    {'attrs':'10|22|30|40','num':28},
    {'attrs':'10|22|31|41','num':220},
    {'attrs':'10|22|32|40','num':130},
    {'attrs':'11|23|32|41','num':120},
    ];    

为选择之前是这种显示状态

attr1  10    11
attr2  20    21   22   23 
attr3  30    31   32 
attr4  40    41 

attr1-attr4中有 2x4x3x2=42种组合,但是有库存的只有上面6种组合

我们要做的就是,在选择某一个属性的时候,把其他为选择的属性中的一些值设置为block(锁定状态) 因为它们和已选属性的组合是没有库存的(有库存的只有上面6种)

比如:
选择 10 和 21后, 有库存的SKU(组合) 只有

{'attrs':'10|21|30|40','num':10},

attr3中的31 32 以及attr4中的 41 就要设置为block

attr1 *10*   11 
attr2  20   *21*  22   23 
attr3  30   -31- -32-
attr4  40   -41-

总结1:在所有库存组合(sku_list)中筛选出包含选中属性值(10,21)组合(10|21|30|40)未选的属性(attr3、attr4)中的所有属性值若不在这个组合中就设置为block

还有一个问题,在已选属性(attr1,attr2)中 , 我可以把21换成2022; 因为 10|21 10|22 也是一种可行组合(有库存),10|23不可行,所以需要2022也可选,23设置为block

或者21不变,修改attr1中的属性值 11|21 不可行,11设置block。

attr1 *10*  -11-
attr2  20   *21*  22  -23-
attr3  30   -31- -32-
attr4  40   -41-  

总结2: 已选属性 (attr1,attr2) 中任意一个属性(attr1)的属性值(10,11), 若不能和其他属性(attr2)中的选中属性值(21) 组合成有效(有库存)组合, 则设置该属性(11)为block

好了,接下来就是根据上面总结的两条,来进行程序的实现 点击 商品SKU选择DEMO 可查看demo

<!DOCTYPE HTML>
<html lang="en-US">
<head>
	<meta charset="UTF-8">
	<title>商品SKU选择DEMO</title>
</head>
<body>
	<style type="text/css">
ul,li{ padding:0px; margin:0px;}    
#panel{ width:500px; margin:30px auto;}

.goods_attr{ overflow:hidden;}
.goods_attr .label {font: 12px/30px '宋体';color: #777;width: 50px;;padding-right: 10px;float: left; display:block;}
.goods_attr ul {float:left;width:300px;}

.goods_attr li{color:#333;overflow:hidden;position:relative;float:left;text-align:center; vertical-align:middle; border:1px solid #999;text-indent:0; cursor:pointer}
.goods_attr li.b{border:1px dotted #CCC;color:#DDD; pointer:none;}
.goods_attr li.b img {opacity:0.4;}
.goods_attr li.sel{ border:1px solid #c80a28;color:#333;}

.goods_attr li.text{margin:5px 10px 5px 0; height:23px;line-height:23px;text-indent:0;padding:0 23px;font-style:normal;}
.goods_attr li.img{ margin-right:10px;width:35px;height:35px; line-height:35px;text-align:center;}

    </style>

<div id="panel">
    <div id="panel_sku_list"><pre></pre></div>
    <div id="panel_sel">

    </div>

</div>      

<script src="http://libs.baidu.com/jquery/1.9.0/jquery.min.js"></script>    
<script type="text/javascript">
/*
属性集
下面一共有4个属性
属性item1 下面有 2个属性值 分别是 10,11
(举个常见的例子 属性尺码 下有 S M L XL 4个属性值 )
*/
var keys = {
    'attr1':['10','11'],
    'attr2':['20','21','22','23'],
    'attr3':['30','31','32'],
    'attr4':['40','41']
    };
//SKU,Stock Keeping Uint(库存量单位)    
var sku_list=[
        {'attrs':'10|20|30|40','num':120},
        {'attrs':'10|21|30|40','num':10},
        {'attrs':'10|22|30|40','num':28},
        {'attrs':'10|22|31|41','num':220},
        {'attrs':'10|22|32|40','num':130},
        {'attrs':'11|23|32|41','num':120},
        ];    

/**init start */

//显示html结构
function show_attr_item(){        
    var html='';
    for(k in keys){
        html+='<div class="goods_attr" > <span class="label">'+k+'</span>';
            html+='<ul>'
            for(k2 in keys[k]){
                _attr_id=keys[k][k2];
                html+='<li class="text" val="'+_attr_id+'" >';
                html+='<span>'+_attr_id+'</span>';
                html+='<s></s>';
                html+='</li>'
            }
            html+='</ul>';
        html+='</div>';
    }
    $('#panel_sel').html(html);
}
//显示数据
function show_data(sku_list){
    var str="";
    for( k in sku_list){
        str+=sku_list[k]['attrs']+"\t"+sku_list[k]['num']+"\n";
    }
    $('#panel_sku_list pre').html(str);
}

show_data(sku_list);
show_attr_item()

/**init end */

//获取所有包含指定节点的路线
function filterProduct(ids){
    var result=[];
    $(sku_list).each(function(k,v){
        _attr='|'+v['attrs']+'|';
        _all_ids_in=true;
        for( k in ids){
            if(_attr.indexOf('|'+ids[k]+'|')==-1){
                _all_ids_in=false;
                break;
            }
        }
        if(_all_ids_in){
            result.push(v);    
        }

    });
    return result;
}

//获取 经过已选节点 所有线路上的全部节点
// 根据已经选择得属性值,得到余下还能选择的属性值
function filterAttrs(ids){
    var products=filterProduct(ids);
    //console.log(products);
    var result=[];
    $(products).each(function(k,v){
        result=result.concat(v['attrs'].split('|'));

    });
    return result;
}

//已选择的节点数组
function _getSelAttrId(){

     var list=[];
     $('.goods_attr li.sel').each(function(){
        list.push($(this).attr('val'));
     });
     return list;
}

$('.goods_attr li').click(function(){
    if($(this).hasClass('b')){
        return ;//被锁定了
    }
    if($(this).hasClass('sel')){
        $(this).removeClass('sel');
    }else{
        $(this).siblings().removeClass('sel');
        $(this).addClass('sel');

    }
    var select_ids=_getSelAttrId();

    //已经选择了的规格
    var $_sel_goods_attr=$('li.sel').parents('.goods_attr');

    // step 1
    var all_ids=filterAttrs(select_ids);     

    //获取未选择的
    var $other_notsel_attr=$('.goods_attr').not($_sel_goods_attr);

    //设置为选择属性中的不可选节点
    $other_notsel_attr.each(function(){
       set_block($(this),all_ids);

    });

    //step 2
    //设置已选节点的同级节点是否可选
    $_sel_goods_attr.each(function(){ 
        update_2($(this));  
    });

});

function update_2($goods_attr){
    // 若该属性值 $li 是未选中状态的话,设置同级的其他属性是否可选
    var select_ids=_getSelAttrId();
    var $li=$goods_attr.find('li.sel');

    var select_ids2=del_array_val(select_ids,$li.attr('val'));

    var all_ids=filterAttrs(select_ids2);

    set_block($goods_attr,all_ids);
}

function set_block($goods_attr,all_ids){

//根据 $goods_attr下的所有节点是否在可选节点中(all_ids) 来设置可选状态
    $goods_attr.find('li').each(function(k2,li2){

        if($.inArray($(li2).attr('val'),all_ids)==-1){
            $(li2).addClass('b');
        }else{
            $(li2).removeClass('b');
        }

    });

}
function del_array_val(arr,val){
//去除 数组 arr中的 val ,返回一个新数组
    var a=[];
    for(k in arr){
        if(arr[k]!=val){
            a.push(arr[k]);
        }
    }
    return a;
}

</script>

</body>
</html>

 

 

会说话的树莓派,将文字转换为语音

树莓派带了个音频输出接口,可外接小音箱。要是能实现文本到语音转换(text2speech)的话。 它的可玩性大大的提高了,比如 起床的时候语音播报天气、朗读电子邮件…更多的发挥你的想象力吧

这里介绍两种方式:1.通过本地程序转换 2使用在线api接口,这个需要联网

本地通过软件转换

我使用的是festival 一个免费的文字转语音软件,不需要联网。

在树莓派上可直接安装

$ apt-get install festival
$ festival -v
festival: Festival Speech Synthesis System: 2.1:release November 2010

安装好后 测试下

直接输入festival可进入交互方式

$ festival
festival> (SayText "hello boy")

或者直接通过命令行

$ echo 'hello boy' |festival --tts

festival 默认带的声音kal_diphone效果并不好(呃,有点模糊,电音效果?)

官方也有其他的声音,效果也不错online demo 遗憾的是这些没有开放下载

在官方FAQ http://www.cstr.ed.ac.uk/projects/festival/demofaq.html#voices

Q:I really like your demo, where can I download voice XYZ?

A: ….

The good news, though, is that we’re currently putting together a portfolio of dozens and dozens (really!) of voices which will be available to download shortly. Some voices will be available for commercial use only, some for non-commercial use only, and indeed some for both.

不知道shortly是什么时候

但是我们可以选择安装一些第三方的语音库,下面介绍两个

mbroal

官方mbroal安装说明

我按照说明做了一下,路径稍微有点不同,上面说要解压 festvox_us1到 /usr/share/festival/lib/voices, 而实际目录是/usr/share/festival/voices/ 不然会报错,可能跟festival的版本有关系

1.Install the festival voice wrapper. 安装 festvox_us1

root@coolpi:/tmp# 
$ wget http://www.cstr.ed.ac.uk/downloads/festival/1.95/festvox_us1.tar.gz
$ tar -xzvf festvox_us1.tar.gz
festival/lib/voices/english/us1_mbrola/festvox/us1_mbrola.scm
festival/lib/voices/english/us1_mbrola/festvox/usdurtreeZ.scm
festival/lib/voices/english/us1_mbrola/usradio
$ cp festival/lib/voices/english/us1_mbrola /usr/share/festival/voices/english/ -R 

2.Get the MBROLA voice and binary. 到 http://tcts.fpms.ac.be/synthesis/mbrola.html

下载 MBROLA binary 和 MBROLA Voices

分别选择

Raspberri_pi 注意binary要选择适用于树莓派的版本

us2: American English Male (6.3Mb) Babel Technology 也可以选择其它声音

压缩包中的文件 $ wget http://tcts.fpms.ac.be/synthesis/mbrola/bin/raspberri_pi/mbrola.tgz #里面只有一个二进制文件mbrola,把它解压到 /usr/local/bin下 $ tar -xzvf mbrola.tgz -C /usr/local/bin/

#
$ wget http://tcts.fpms.ac.be/synthesis/mbrola/dba/us1/us1-980512.zip
$ unzip us1_mbrola.zip -d /usr/share/festival/voices/english/us1_mbrola/

最后目录结构看起来是这样的

/usr/share/festival/voices/english/us1_mbrola/
├── festvox
│   ├── us1_mbrola.scm
│   └── usdurtreeZ.scm
├── us1
│   ├── license.txt
│   ├── TEST
│   │   ├── alice.pho
│   │   ├── mbrola.pho
│   │   ├── mbroli.ini
│   │   ├── push.pho
│   │   └── xmas.pho
│   ├── us1
│   ├── us1mrpa
│   └── us1.txt
└── usradio

查看 已安装的语音

festival> (voice.list)
(us1_mbrola kal_diphone)

选择声音,测试下

festival> (voice_us1_mbrola)
us1_mbrola
festival> (SayText "hello boy")

嗯 比自带的那个声音好点…

cmu(更接近真人声音的)

1.安装 festlex-cmu

$ apt-get install festlex-cmu

2.下载 cmu

cd /usr/share/festival/voices/english/
#这个压缩包100多M
sudo wget -c http://www.speech.cs.cmu.edu/cmu_arctic/packed/cmu_us_clb_arctic-0.95-release.tar.bz2
sudo tar jxf cmu_us_clb_arctic-0.95-release.tar.bz2 
sudo ln -s cmu_us_clb_arctic cmu_us_clb_arctic_clunits

选择声音,测试下

festival> (voice_cmu_us_clb_arctic_clunits)
cmu_us_clb_arctic_clunits
festival> (SayText "hello boy")   

声音又上了一个档次,但是解析的时候能明显感觉出来有延迟。估计放到配置高点的电脑上会好些。

可以在配置文件中设置默认的声音

sudo cp /etc/festival.scm /etc/festival.scm.backup
sudo echo "(set! voice_default 'voice_cmu_us_clb_arctic_clunits)" >> /etc/festival.scm      

在线tts api接口

找到两个

1.tts-api这个接口是免费的,效果还行,起码比festival好

http://tts-api.com/tts.mp3?q=hello+world.

2.google翻译的接口

这个接口并不是官方正式提供的,不过从速度,声音效果来说是最好的(非常好!),网速好的话基本上感觉不出来延迟

点这里测试下

http://translate.google.com/translate_tts?ie=UTF-8&tl=en&q=hello+world

三个参数 q=要转换的文字; tl=文字的语言 中文选择zh-cn; ie 要转换的文字q的编码方式;

上面接口返回音频格式是mp3的,可使用mpg123 直接播放

$ apt-get install mpg123
$ mpg123 'http://translate.google.com/translate_tts?ie=UTF-8&tl=en&q=today+is+sunday'

上面两种方式可以结合着使用,比如在为联网的时候使用festival,联网使用效果更好的在线接口

参考

给树莓派添加重启关机按钮

最近一段时间开始捣鼓树莓派了,弄的都是试验性质的。经常会出现些问题 导致连不上树莓派可能是卡死或者断网什么的,有没有显示器也不知道到底是什么情况。 没办法只有拔掉电源重启了。

后来想想长期这样弄也不是办法SD卡禁不起折腾啊,于是就想办法给它添加了个“关机和重启按钮”。

这里实现上用了GPIO接口,通过读取一个接口的高低状态然后调用 重启以及关机命令。

我的pi是B版v1。GPIO接口和v2稍微不一样。具体看下图 把代码的接口定义替换成对应的就行了

rasberrypi GPIO

下面这个两幅图更容易看

树莓派v1 GPIO接口定义 树莓派v2 GPIO接口定义

另外使用pin的编号有两种方式

  1. Board Pin 这种是自然排序的,重p1到p26
  2. BCM GPIO Broadcom的编号方法 (上图中的绿色方块部分GPIO*)

我在代码中使用的是 *BCM *。

这里用到了两个GPIO接口,7和17 你也可以自己修改下。 一个用来接收按键信号,另外一个驱动led显示状态

led有三个状态 长亮:正在重启, 闪动:正在关机, 不亮:等待状态。

面包板连接 树莓派重启面包板连接图
若你有杜邦线的话也可以不通过面包板连接,按钮那个GND的线也不是必须的,用手直接触摸 GPIO7 也能起到把这个电压拉低的效果

实现代码

#!/usr/bin/env python
# coding=utf-8
# author:ksc

import RPi.GPIO as GPIO
import time
import os,sys
import signal

GPIO.setmode(GPIO.BCM)

#define GPIO pin
pin_btn=7
pin_led=17

GPIO.setup(pin_btn, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(pin_led, GPIO.OUT, initial=GPIO.LOW)

press_time=0
count_down=10
led_on=1

def cleanup():
    '''释放资源,不然下次运行是可能会收到警告
    '''
    print('clean up')
    GPIO.cleanup()

def handleSIGTERM(signum, frame):
    #cleanup()
    sys.exit()#raise an exception of type SystemExit

def onPress(channel):
    global press_time,count_down
    print('pressed')
    press_time+=1
    if press_time >3:
        press_time=1
    if press_time==1:
        GPIO.output(pin_led, 1)
        print('system will restart in %s'%(count_down))
    elif press_time==2:
        print('system will halt in %s'%(count_down))
    elif press_time==3:
        GPIO.output(pin_led, 0)
        print 'cancel '
        count_down=10

GPIO.add_event_detect(pin_btn, GPIO.FALLING, callback= onPress,bouncetime=500)

#signal.signal(signal.SIGTERM, handleSIGTERM)
try:
    while True:
        if press_time==1:
            if count_down==0:
                print "start restart"
                os.system("shutdown -r -t 5 now")
                sys.exit()
            led_on=not led_on
            GPIO.output(pin_led, led_on)# blink led
        if press_time==2 and count_down==0:
            print "start shutdown"
            os.system("shutdown  -t 5 now")
            sys.exit()

        if press_time==1 or press_time==2:
            count_down-=1
            print "%s second"%(count_down)
        time.sleep(1)
except KeyboardInterrupt:
    print('User press Ctrl+c ,exit;')
finally:
    cleanup()

使用方法

#创建程序,把代码粘贴进去保存
root@mypi:~# vi reboot.py
#修改可执行
root@mypi:~# chomod 775 reboot.py
#测试下
root@mypi:~# ./reboot.py

按一下按钮 系统进入重启状态倒计时10秒,在这段时间内你可以接着按一下切换到关机状态。 按第三下取消关机进入等待状态。这三种状态可通过按按钮不断切换。

#若没问题就可以让它后台运行了,
root@mypi:~# nohup ./reboot.py &
#想结束后台运行?
ps auxf #查找PID
kill PID

参考

上拉电阻和下拉电阻

使用 RPI.GPIO 模块的输入(INPUT)功能

RPI.GPIO WIKI

使用python向服务器POST大文件

使用python向服务器POST大文件

python 对http操作有几个库 urllib 、 urllib2 还有httplib

httplib比较偏底层 一般情况下使用urllib和urllib2就行了

NOTICE

在python3中urllib与urllib2被分割合并为了 urllib.request, urllib.parse, and urllib.error

httplib重命名为 http.client

分析http协议

python的这几个库中并没有提供直接上传文件的接口 我们先看下普通浏览器是怎么上传文件的 这里我在本地创建简单php程序 若有提交文件,则打印出文件相关的信息。否则显示一个上传表单表单

<?php
header('Content-Type: text/html; charset=utf-8');
if(!empty($_FILES)){
    var_dump($_FILES);
    exit;
}

?>

<!DOCTYPE HTML>
<html lang="en-US">
<head>
    <meta charset="UTF-8">
    <title></title>
</head>
<body style="text-align:center;">
    <form action="" method="POST" enctype="multipart/form-data" >
        <input type="text" name="username" value="ksc"/> <br/>
        <input type="file" name="file" />
        <input type="submit" value="submit"/>
    </form>
</body>
</html>

这里推荐一个抓包工具fildder 可以很方便的抓取htttp数据,并且直观的显示

下面就是抓取到的内容

POST http://localhost/test/postfile.php HTTP/1.1
Host: localhost
Connection: keep-alive
Content-Length: 295
Cache-Control: max-age=0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Origin: http://localhost
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.107 Safari/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarynHbCm1TeYE1AHFXv
DNT: 1
Referer: http://localhost/test/postfile.php
Accept-Encoding: gzip,deflate,sdch
Accept-Language: zh-CN,zh;q=0.8,en;q=0.6

------WebKitFormBoundarynHbCm1TeYE1AHFXv
Content-Disposition: form-data; name="username"

ksc
------WebKitFormBoundarynHbCm1TeYE1AHFXv
Content-Disposition: form-data; name="file"; filename="b.txt"
Content-Type: text/plain

我是文件内容
------WebKitFormBoundarynHbCm1TeYE1AHFXv--

从第1-13行就是请求消息http头部的内容,然后下面有个空行 比较重要的几行是

POST http://localhost/test/postfile.php HTTP/1.1
Host: localhost
Content-Length: 295
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarynHbCm1TeYE1AHFXv

内容数据

请求消息(Request message)有以下几部分组成

  • 请求行(request line), 例如 GET /images/logo.png HTTP/1.1
  • 请求头(Request Headers) 比如上面的 Host: localhost、 Content-Length: 295
  • 空行
  • 消息主体 message body

NOTICE

请求行与请求头必须以结尾,空行只能有不能有空格什么的 在HTTP/1.1协议中,除了Host 所有的请求头都是可选的(当然若上传文件的话,就必须设置了Content-Length和Content-Type了, 不然服务器收不到数据的,虽然也能成功响应)

这里message body的类型是multipart/form-data;

boundary 是随机的内容每次请求都不一样, Content-Type为 multipart/form-data; 可同时传输多项数据,而这些数据就是通过 boundary分割开来的

每一项的数据都是’–‘+boundary+换行开始 ,然后是Content-Disposition: form-data;name=”表单项名”

若是文件的话 还有个filename 以及Content-Type,接下来一个空行

最后’–‘+boundary+’–‘+换行结束

到这里整个http请求就结束了

模拟提交数据

其实http协议就是字符串按照约定规则拼接到一起 然后服务器再来解析得到数据 所以我们自己直接使用socket也能发起了一个http请求

但是有了urllib2我们可以省很多事 只需“拼接”内容部分就行了

urllib2.urlopen(url[, data][, timeout])

url 可以是一个链接或者Request对象 data 就是我们要“拼接的内容”

# coding=utf-8

import urllib2
import mimetypes
import os
import uuid

mimetypes.init()

url='http://localhost/test/postfile.php'
fileFieldName='myfile'
file_path='/a.txt'

boundary='--------ksc'+uuid.uuid4().hex;
print('boundary:'+boundary)
req = urllib2.Request(url)
req.add_header("User-Agent", 'ksc')
req.add_header("Content-Type", "multipart/form-data, boundary="+boundary)
def getdata(boundary,file_path):
    global fileFieldName
    file_name=os.path.basename(file_path)
    file_name=file_name.encode('utf-8')
    file_type= mimetypes.guess_type(file_name)[0] #根据文件名匹配文件的mime类型若没有匹配到会返回None
    if file_type==None:
        file_type = "text/plain; charset=utf-8"

    print file_type
    print file_name
    fileData=open(file_path,'rb').read()
    CRLF='\r\n'
    body = '';     
    body += '--' + boundary + CRLF;
    body += 'Content-Disposition: form-data; name="'+fileFieldName+'"; filename="' + file_name + '"'+CRLF;     
    body += "Content-Type: "+file_type+CRLF
    body += CRLF;     
    body += fileData + CRLF;     
    body += "--" + boundary + "--"+CRLF;  
    print 'body size:{0}'.format(len(body))
    return body

#req.add_header('Content-Length',len(body) ) #urllib2会自动添加
res = urllib2.urlopen(req,getdata(boundary,file_path))
print res.read().decode('utf-8');

下面是运行后的输出结果

boundary:--------ksc114eaa38291f43fdaca15ab3b4265a82
text/plain
a.txt
body size:388
array(1) {
  ["myfile"]=>
  array(5) {
    ["name"]=>
    string(5) "a.txt"
    ["type"]=>
    string(10) "text/plain"
    ["tmp_name"]=>
    string(24) "D:\xampp\tmp\php151B.tmp"
    ["error"]=>
    int(0)
    ["size"]=>
    int(197)
  }
}

php能正常接收传输的文件了 但是这里有个问题 ,我们拼接数据的时候使用的是

fileData=open(file_path,'rb').read()

直接把文件内容读到内存中去了,小文件还好,要是几百M上G的文件还用这种方法就不行了,这样会把你的内存吃干。

所以我们就需要弄个变通的方法,让它一块一块的发送数据,读一点发送一点。 这几个库并没有提供类似的方法实现这一个功能,所以就需要自己动手了。

首先介绍一个python的关键字 yield 关于介绍python yield的文章可以查看Python yield 使用浅析 可以通过它来实现一个生成器(generator) 来不断的读取文件数据,而且是只在我们需要的时候读取

def read_file(fpath): 
    BLOCK_SIZE = 1024 
    with open(fpath, 'rb') as f: 
        while True: 
            block = f.read(BLOCK_SIZE) 
            if block: 
                yield block 
            else: 
                return

然后我重写了 httplib 中 HTTPConnection.send(data)使它可以接收一个generator,从里面取数据然后发送 下面是原版HTTPConnection.send的源码

def send(self, data):
    """Send data to the server."""
    if self.sock is None:
        if self.auto_open:
            self.connect()
        else:
            raise NotConnected()

    if self.debuglevel > 0:
        print "send:", repr(data)
    blocksize = 8192
    if hasattr(data,'read') and not isinstance(data, array):
        if self.debuglevel > 0: print "sendIng a read()able"
        datablock = data.read(blocksize)
        while datablock:
            self.sock.sendall(datablock)
            datablock = data.read(blocksize)
    else:
        self.sock.sendall(data)

它支持文件类型File Object 或者是一个字符串

下面是个重新版本

def send(self, data):
    """Send data to the server."""
    if self.sock is None:
        if self.auto_open:
            self.connect()
        else:
            raise NotConnected()

    if self.debuglevel > 0:
        print "send:", repr(data)
    blocksize = 8192
    if hasattr(data,'next'):#支持迭代
        for datablock in data:
            self.sock.sendall(datablock)
    elif hasattr(data,'read') and not isinstance(data, array):
        if self.debuglevel > 0: print "sendIng a read()able"
        datablock = data.read(blocksize)
        while datablock:
            self.sock.sendall(datablock)
            datablock = data.read(blocksize)
    else:
        self.sock.sendall(data)

当然若想实现完美的文件提交,还需要做个封装,下面是一个简单的实现,还有写不完善的地方,post数据只能传文件,打算做个python lib封装一下

# coding=utf-8

import httplib
import mimetypes
import os
import uuid
import socket

mimetypes.init()

url='http://localhost/test/postfile.php'
fileFieldName='myfile'
file_path='/temp/a.txt'

content_length=0   
class builder:

    def __init__(self):

        self.boundary='--------ksc'+uuid.uuid4().hex;
        self.boundary='--------kscKKJFkfo93jmjfd0ismf'
        self.ItemHeaders=[];
        self.CRLF='\n'

    def getBoundary(self):
        return self.boundary

    def getHeaders(self):    
        '''return Request Headers'''
        headers={"Content-Type":"multipart/form-data, boundary="+self.boundary
                ,"Content-Length":self.getContentLength()
                }
        return headers        
    def putItemHeader(self,fieldName,file_path):

        file_name=os.path.basename(file_path)
        print file_name
        file_name=file_name.encode('utf-8')
        file_type= mimetypes.guess_type(file_name)[0] #guess file's mimetype
        if file_type==None:
            file_type = "text/plain; charset=utf-8"

        print file_type

        CRLF=self.CRLF
        head = '';     
        head += '--' + self.boundary + CRLF;
        head += 'Content-Disposition: form-data; name="'+fieldName+'"; filename="' + file_name + '"'+CRLF;     
        head += "Content-Type: "+file_type+CRLF
        head += CRLF;
        self.ItemHeaders.append({'head':head,'file_path':file_path})

    def getContentLength(self):    
        length=0
        for item in self.ItemHeaders:
            length+=len(item['head'])+os.path.getsize(item['file_path'])+len(self.CRLF)

        return length+len("--" + self.boundary + "--"+self.CRLF)

    def getdata(self):

        blocksize=4096
        for item in self.ItemHeaders:
            yield item['head']

            fileobj=open(item['file_path'],'rb')
            while True:
                block = fileobj.read(blocksize)
                if block:
                    yield block
                else:
                    yield self.CRLF
                    break

        body = "--" +self.boundary + "--"+self.CRLF;  

        yield body

class MyHTTPConnection(httplib.HTTPConnection):
    def send(self, value):
        if self.sock is None:
            if self.auto_open:
                self.connect()
                print '-----------------ksc: reconnect'
            else:
                raise NotConnected()

        try:
            if hasattr(value,'next'):
                print "--sendIng an iterable"
                for data2 in value:
                    self.sock.sendall(data2)    
            else:
                print '\n--send normal str'
                self.sock.sendall(value) 
        except socket.error, v: 
            if v[0] == 32:      # Broken pipe
                print(v)
                self.close()
            raise
        print '--send end' 

bl=builder()    

bl.putItemHeader(fileFieldName, file_path) 
bl.putItemHeader('bb', u'/temp/贪吃蛇.zip') 
#bl.putItemHeader('zipfile', u'/temp/a.zip') 

#注意web端post数据大小限制  
#这里 php.ini post_max_size = 30M

content_length=bl.getContentLength()
print "content_length:"+str(content_length)

url='/test/postfile.php'
#这里使用fiddler捕捉数据包,设置了fiddler为代理
fiddler_monitor=True
fiddler_monitor=False

if fiddler_monitor:
    httpconnect=MyHTTPConnection('127.0.0.1',8888)
    url='http://localhost'+url
else:
    httpconnect=MyHTTPConnection('localhost')

httpconnect.set_debuglevel(1)

headers = bl.getHeaders()
data    = bl.getdata()

httpconnect.request('POST', url, data ,headers)

httpres=httpconnect.getresponse()
print httpres.read().decode('utf-8')

现在有很多现成的第三方库比如 poster Requests 可以实现这些功能了,但是由于是第三方库需要用户手动安装,在一个我需要上传大文件时有个回调功能,这样就能显示进度了。 等运行稳定了会添加到kuaipan cli 里面,就可以摆脱poster依赖

参考资料

HTTP

去除http头部的Referer信息,防止重要网址被浏览器“偷偷的泄漏”

有时候我们需要在点击链接时候去除http头部的referer属性,
比如在网站后台有链接到外部网站的地方,若不去除的话
很容易暴漏我们的后台地址
浏览器 我只测试了IE8以及chrome33

通常下面的一些方式,$_SERVER[‘HTTP_REFERER’] 会无效:
1.直接输入网址访问该网页。
–!肯定不能让用户复制链接然后在粘贴打开吧

2.Javascript 打开的网址。

<a href="javascript:void(0)" onclick="window.open('/test/index.php?a');">window.open()</a>

IE8没有问题,但是chrome还是携带referer

3.Javascript 重定向(window.location)网址。

<a href="javascript:void(0)" onclick="location='/test/index.php?a'">location</a><br/>

 

表现与使用open()一样
看样子chrome只要js代码在a标签的点击事件中就会携带referer信息

4.使用html5中noreferrer

<a href="/test/index.php?noreferer" rel="noreferrer" target="_blank">noreferrer</a>

很明显IE8以及以下肯定是不支持的
chrome正常点击的话没问题,但是假如是右击链接然后重新窗口或标签打开,同样会传递referer
操作性:4 稳定性:3 兼容性:3

5.使用 meta refresh 重定向的网址。
这个会多出来个跳转页面,
IE8支持,chrome同样携带referer
6.flash打开 中的链接。

7.浏览器未加设置或被用户修改。
有些浏览器可以修改在https网址中是否传递referer信息,但是我没在浏览器中找到这个选项
或者是安装插件referer信息进行屏蔽

最方便可靠的方法还是使用flash,你不能要求所有人都是用最新的浏览器,并且不是右击新窗口打开,或者安装个插件什么的

用flash想到有两种实现方式
1.使用flash按钮替换a标签 然后把链接当作参数传递给flash程序
假如链接是动态的需要计算处理话,可以通过flash调用js函数,然后由这个js函数返回链接地址
2.还有就是js调用flash中的方法
这种最方便了,对原有代码改动最小。
但是弄了几次在chrome总是调用失败,改天在研究下吧

下面是第一种实现方式的示例代码

<!--flash引用代码-->
<object id="open_url" width="25" height="20" classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=7,0,19,0" >
    <param name="movie" value="js/fla_open_url.swf?callback=get_url" />
    <param name="quality" value="high" />
    <param name="wmode" value="transparent" />
    <embed width="25" height="20"  src="js/fla_open_url.swf?callback=get_url" quality="high" pluginspage="http://www.macromedia.com/go/getflashplayer" type="application/x-shockwave-flash" wmode="transparent"/>
</object>
<script type="text/javascript">
    function get_url(){
        return 'http://www.baidu.com';
    }

</script>

新建个flash,添加个button 在动作中粘贴如下代码

    on (release) { 
        import flash.external.ExternalInterface;
        loadVariablesNum('','callback');//获取js回调函数名称,存放在callback变量中

        if(callback!=''){
            open_url=ExternalInterface.call(callback);//调用js程序,获取url

            if(open_url!=undefined){
                getURL(open_url,'_blank');//在新窗口打开链接 
            }

        }
    }

 notice:

利用ExternalInterface.call调用html里的js函数,参照了flash帮助文档里的做法.可以执行js的函数.但就是无法获取return的值.
网上一大堆教程也只是抄flash帮助文档的东西.也试了allowScriptAccess=always.结果无效,
最后我去搜了N次后,答案居然是只要给<object>加上一个id或是name就解决了.

python argparse模块bug:将版本信息输出到标准错误

今天打算把 kuaipan cli 编译成 二进制版本

kuaipan cli 使用的官方模块argparse解析命令行参数

下面这种方式,已经被官方废弃了,应该是为了解决 -v冲突的问题

parser=argparse.ArgumentParser(version='0.2')

后来修改为

    parser.add_argument('-v','--version', action='version', version='0.2', help="show program's version number and exit")

于是写了个脚本自动编译,并且获取版本号

version=`kuaipan.py -v`
echo $version

但是 $version 始终为空 ,单独执行 kuaipan.py -v是没问题的

运行环境 debian下是Python 2.7.3 ,window下是Python 2.7.6 都不行

然后怀疑是 argparse的问题

查看了下argparse模块的源码 版本号 是 1.1

class _VersionAction(Action):
....
    def __call__(self, parser, namespace, values, option_string=None):
        version = self.version
        if version is None:
            version = parser.version
        formatter = parser._get_formatter()
        formatter.add_text(version)
        parser.exit(message=formatter.format_help())
....
def _print_message(self, message, file=None):
    if message:
        if file is None:
            file = _sys.stderr
        file.write(message)

# ===============
# Exiting methods
# ===============
def exit(self, status=0, message=None):
    if message:
        self._print_message(message, _sys.stderr)
    _sys.exit(status)

问题出来知道了,这里默认的version action 把信息输出到了 stderr标准错误

所以在命令行中获取不到

已经有人提交了issue18920 以及path 不知道什么时候能发布出来

目前临时解决方法是:将标准错误stderr输出到stdout

version=`kuaipan.py -v 2>&1`
echo $version#0.2

使ecshop 模板中可引用 常量

这篇很早前在博客园写的,现在稍微修改下转过来

据说ecshop的模板类是修改的smarty,不过个人感觉不是修改是完全重写了。它和smarty上只是模板标签上有相同的地方,同时阉割了很多功能。

比如$smarty.const.’常量’,这个就不能用。(其实在模版的有些地方可以直接使用常量,比如在判断语句中,但是你若想输出常量值就不是很方便了)

其实模板引擎原理上并不复杂,只是把一些模板标签替换为php中的函数,变量,语法结构罢了。

这次要在ecshop模板中加入引用常量的功能,只需在函数 cls_template.php中的 make_var()中加入两行代码

 

function make_var($val)
    {
        if (strrpos($val, '.') === false)
        {
            if (isset($this->_var[$val]) && isset($this->_patchstack[$val]))
            {
                $val = $this->_patchstack[$val];
            }
            $p = '$this->_var[\'' . $val . '\']';
        }
        else
        {
            $t = explode('.', $val);
            $_var_name = array_shift($t);
            if (isset($this->_var[$_var_name]) && isset($this->_patchstack[$_var_name]))
            {
                $_var_name = $this->_patchstack[$_var_name];
            }
            if ($_var_name == 'smarty')
            {    
                if($t[0] == 'const'){#此处为新添加
                    return strtoupper($t[1]);
                }
                 $p = $this->_compile_smarty_ref($t);
            }
            else
            {
                $p = '$this->_var[\'' . $_var_name . '\']';
            }
            foreach ($t AS $val)
            {
                $p.= '[\'' . $val . '\']';
            }
        }

        return $p;
    }

其中21-23行是新加的,这让就可在模板文件中通过 {$smarty.const.常量}来引用php中定义的常量了

linux 用户通过ssh登录后邮件提醒

我一般没事的话,不长登录服务器,长时间没有登录的话,就会担心服务器会不会被入侵了,没安全感啊!–

所以写了一个脚本,只要有用户通过ssh登录到服务器上,自动把ip地址所在地图时间发送到特定邮箱中

所需软件

1.nali 获取ip的所在地区

wget --no-check-certificate  https://qqwry.googlecode.com/files/nali-0.2.tar.gz
tar zxvf nali-0.2.tar.gz 
cd nali-0.2/
./configure 
make
make install

2. mutt 以及 msmtp

Ubuntu 下使用 mutt 和 msmtp 发送 Gmail 邮件

配置成功了,发个邮件测试一下

echo "邮件内容"| mutt -s "邮件主体" 接收邮件地址

假如能收到的话,可以进行下一步了

新建一个脚本 放到 /opt下其他目录也行比如 /usr/local/bin/ 只要是在环境变量path中就行了

vi /opt/user_login_notice.sh
#输入下面的内容
#! /bin/sh
echo  login from `nali ${SSH_CLIENT%% *}` at `date +'%H:%M:%S %m/%d'` | mutt -s "[ssh notice]$USER@`hostname` " ksc@qq.com

也可以不通过mutt,直接通过 msmtp发送

#!/bin/sh
content="login from `nali ${SSH_CLIENT%% *}` at `date +'%H:%M:%S %m/%d'`"

head="MIME-Version: 1.0\r\nContent-type: text/html; charset=utf-8\r\n"
head=$head"To: ${to}\r\nFrom: geekli notice \r\nSubject: [ssh notice]$USER@`hostname`\r\n\r\n"

echo $head$content | msmtp -a notice ksc@qq.com
# msmtp -a 指定发送邮件的账户 若设置了默认的 可不填写
#邮件头中要设置 字符编码格式 不然可能会出现乱码的情况
# From:中的 发送地址要跟msmtp中配置的一致
# 希望所有用户都能发送,需要msmtp的全局配置文件 /etc/msmtprc

 

chmod 775 /opt/user_login_notice.sh #添加执行权限
vi /etc/ssh/sshrc #可能会没有这个文件
#然后输入以下内容

#!/bin/sh
/opt/user_login_notice.sh

这样每次登录后会调用邮件发送脚本 我发送接受都是用的QQ邮箱,基本上3秒之内就能收到通知

把树莓派打造成智能无线路由器(可对国外网站自动选择线路)

本文的目的是教大家访问特定网站时走加密线路(vpn)时更加方便不用来回切换网络

前提

能有正常上网的环境 即pi插上网线后就能上网(如何上网不在此文讨论范围)

  1. 树莓派一个 (B型的带有线网卡)
  2. USB无线网卡一个( 我的是在京东上买的 二十块钱左右)
  3. vpn账户一个,并且能正常连通上网

我这里用的是openvpn,如果你有国外vps 如何配置请查看  OpenVPN安装配置教程

在这里我只简单说下客户端的配置

开始配置

ifconfig 
eth0      Link encap:Ethernet  HWaddr b8:27:eb:3c:2b:11
          inet addr:192.168.2.11  Bcast:255.255.255.255  Mask:255.255.255.0
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:47072 errors:0 dropped:0 overruns:0 frame:0
          TX packets:48828 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:29137358 (27.7 MiB)  TX bytes:9871271 (9.4 MiB)

lo        Link encap:Local Loopback
          inet addr:127.0.0.1  Mask:255.0.0.0
          UP LOOPBACK RUNNING  MTU:16436  Metric:1
          RX packets:693 errors:0 dropped:0 overruns:0 frame:0
          TX packets:693 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:121328 (118.4 KiB)  TX bytes:121328 (118.4 KiB)

wlan0     Link encap:Ethernet  HWaddr c8:3a:35:c1:29:ab
          inet addr:192.168.10.1  Bcast:192.168.10.255  Mask:255.255.255.0
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:29999 errors:0 dropped:0 overruns:0 frame:0
          TX packets:27439 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:3228721 (3.0 MiB)  TX bytes:27597707 (26.3 MiB)

eth0是我的有线网卡 已经获自动获取了ip(我现在就是通过有线ssh控制pi的)

wlan0就是那个无线网卡了(一般情况下是wlan0,若没有的话,你可以输入lsusb,看一下有没有显示出 你的无线网卡设备 。没有的就是驱动问题。)

修改 网络配置文件 给wlan0设置固定ip 在这里我设置的是192.168.10.1

注意一定不要和eth0在同一个网段
另外若你启用了wicd等网络管理服务请关闭

root@coolpi:~# cat /etc/network/interfaces
auto lo

iface lo inet loopback
iface eth0 inet dhcp

#allow-hotplug wlan0
#iface wlan0 inet manual
#wpa-roam /etc/wpa_supplicant/wpa_supplicant.conf
#iface default inet dhcp

iface wlan0 inet static
address 192.168.10.1
netmask 255.255.255.0

1 vpn客户端配置

安装openvpn,这里我们只是用它的客户端功能(如何配置,看上面的连接)

apt-get install openvpn

下面是我的配置文件

client
dev tun       #要与前面server.conf中的配置一致。
proto udp              #要与前面server.conf中的配置一致。
remote chicago.vps 8080    #将服务器IP替换为你的服务器IP,端口与前面的server.conf中配置一致。

resolv-retry infinite
nobind
persist-key
persist-tun

ca ca.crt
cert client.crt
key client.key
ns-cert-type server

#是否作为网关出口,会影响路由表的默认出口
;redirect-gateway

keepalive 20 60
#tls-auth ta.key 1
comp-lzo
verb 3
mute 20
route-method exe
route-delay 2

需要注意的是

ca ca.crt
cert client.crt
key client.key

这几个证书文件最好写成得路径,否则执行命令

openvpn /etc/openvpn/client.conf

会提示找不到这几个文件
只有cd /etc/openvpn/ 然后 openvpn client.conf 才行
若配置的没有有问题的话 输入ifconfig 会多出来一个虚拟网络设备tun0

tun0      Link encap:UNSPEC  HWaddr 00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00
          inet addr:10.2.2.10  P-t-P:10.2.2.9  Mask:255.255.255.255
          UP POINTOPOINT RUNNING NOARP MULTICAST  MTU:1500  Metric:1
          RX packets:22212 errors:0 dropped:0 overruns:0 frame:0
          TX packets:18882 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:100
          RX bytes:26477654 (25.2 MiB)  TX bytes:1607636 (1.5 MiB)

记住这两个地址 inet addr:10.2.2.10 P-t-P:10.2.2.9 待会要用到

2 安装路由软件

下一步就是安装两个软件hostapd isc-dhcp-server

1) AP热点 hostapd 服务

#安装
apt-get install hostapd
#编辑默认配置文件路径
vi /etc/default/hostapd
将
#DAEMON_CONF= ""
这一行修改为
DAEMON_CONF="/etc/hostapd/hostapd.conf"

#解压默认配置文件
zcat /usr/share/doc/hostapd/examples/hostapd.conf.gz >/etc/hostapd/hostapd.conf
#修改默认配置文件 
vi /etc/hostapd/hostapd.conf

刚开始的时候可以不加密直接使用默认配置就行 只需要修改下 ssid 和channel

注意: 默认是不加密的连接,建议等你完全测试成功了再修改为wpa加密的(配置文件中有各种模版)

ssid= test_ap #搜索无线信号时看到的那个
channel = 11 #此值最好不要与其他无线路由器重复。但是我试了一下别的信道 13、12等 hostapd 提示错误 貌似是网卡不支持

2) ip地址自动分配 dhcpd服务

#安装
apt-get install isc-dhcp-server
#将一下配置添加进  /etc/dhcp/dhcpd.conf 

subnet 192.168.10.0 netmask 255.255.255.0 {
  range 192.168.10.10 192.168.10.100; #分配给客户端的ip范围
  option routers 192.168.10.1;#网关地址 
  option broadcast-address 192.168.10.255; #广播地址 注意和上面的子网掩码255.255.255.0 对应
  option domain-name-servers 8.8.8.8,8.8.4.4; #DNS服务器地址
  default-lease-time 600; #租约时间 到期后客户端需要重新发起请求获取ip地址(可以续约原来的ip)
  max-lease-time 7200;
}

注意:

  1. 配置文件中的网段,子网掩码,以及网关要和给wlan0设置的静态ip【相对应】wlan0就是这个网段的网关)
  2. dns目前最好是配置成google的dns。因为国内的在根上都被污染了, google的dns走vpn的话没问题不会被污染,相应的解析速度也会变慢

(还有一种解决办法就是修改hosts,但是这个太麻烦,国外大网站不止一个域名,ip地址也是多个,收集这些信息也挺麻烦,相对来说收集国外网站ip段来说还是容易的)

3) 全部配置完毕后 ,启动服务

ifup wlan0 #启用网卡
service hostapd  start

or

hostapd -ddK /etc/hostapd.conf #方便查看调试信息

service isc-dhcp-server  start #注意在这一步的时候一定要确保wlan0已经启用并配置好了ip地址 不然会dhcpd会报错

4. 连接测试

现在你拿出手机搜索一下无线信号 有没有 [test_ap] 然后连接上试试。

若能成功连接并且获取ip,则表明你的pi无线路由器了

但是现在你得到手机还不能上网
需要设置一下NAT转发,即把wlan0上的网络连接转发到eth0上面通过有线来上网

3. 路由转发

———-分割线下面这些 觉得麻烦的可以不看,我在后面提供了一个脚本 不过最好还是看一下,有助于理解————-

  • 1)
    将192.168.10.0/24 这个网段的流量转发到 eth0上
    注意:192.168.2.11 是eth0的ip

    iptables -t nat -A POSTROUTING -s 192.168.10.0/24 -j SNAT --to-source 192.168.2.11

    目前位置,手机连上无线后应该就能上网了,但访问国外网站还是不行的

  • 2)
     iptables -t nat -F #先把上面那条规则删除了

    下面事例中我用8.8.8.8 代指国外ip ,你可以任意修改成“任意国外ip地址”

    将192.168.10.0/24 并且是去往8.8.8.8(任意国外ip地址) 的流量通过 10.2.2.10转发出去

    iptables -t nat -A POSTROUTING -s 192.168.10.0/24 --dst 1.1.1.1 -j SNAT  --to-source 10.2.2.10
  • 3)
    注意:目前默认的网关仍然是eth0 192.168.2.1
    看一下下面的路由表 默认是从 192.168.2.1出去的

    root@coolpi:~/raspi_router# route
    Kernel IP routing table
    Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
    default         192.168.2.1     0.0.0.0         UG    0      0        0 eth0
    10.2.2.0        10.2.2.9        255.255.255.0   UG    0      0        0 tun0
    10.2.2.9        *               255.255.255.255 UH    0      0        0 tun0
    192.168.2.0     *               255.255.255.0   U     0      0        0 eth0
    192.168.10.0    *               255.255.255.0   U     0      0        0 wlan0

    所以我们要添加一个路由项 (10.2.2.9 就是上面tun0 p-t-p 那一项,我的理解就是vpn的虚拟网关)。
    把去往8.8.8.8的流量路由到vpn上面去,不走原来的路线了

    route add -net 8.8.8.8/32 gw 10.2.2.9 dev tun0
    root@coolpi:~/raspi_route# traceroute 8.8.8.8
    traceroute to 8.8.8.8 (8.8.8.8), 30 hops max, 60 byte packets
     1  10.2.2.1 (10.2.2.1)  306.001 ms  308.014 ms  308.195 ms
     2  省略……

    用traceroute命令可以看到 现在走的是vpn

    若你只设置8.8.8.8的路由而不设置该ip的NAT转发,在pi上没有什么问题,会按照你的设置自动选择线路

    但是无线网络下面的终端(手机等)还是只能访问国内的ip地址 你设置的“国外ip”将不能访问(假如原来能访问的话hehe)

    总的来说 路由规则,NAT转发缺一不可

    route add -net 8.8.8.8/32 gw 10.2.2.9 dev tun0
    iptables -t nat -A POSTROUTING -s 192.168.10.0/24 --dst 8.8.8.8 -j SNAT  --to-source 10.2.2.10

    vpn

  • 4)
    现在国外的网站能访问了,我们再把第一步在重复一遍 设置默认转发规则
    *注意:这一步一定要放到最后执行 *iptables规则就像一个筛子 第一层被挡住以后就不会往下走了

    iptables -t nat -A POSTROUTING -s 192.168.10.0/24 -j SNAT --to-source 192.168.2.11

—————————–结束——————————–

我写了个脚本,python写的(shell不是很熟悉)

check.py
build_sh.py #根据route.txt 生成 添加路由表和 iptables NAT转发命令
route.txt #国外的ip网段,不是很全

可以把一些网段自动转换称shell脚本 已经传到了 git@osc 上面

你可以 用git检出一份

apt-get install git
git clone http://git.oschina.net/ksc/raspi_router

或者直接下载zip压缩包

wget http://git.oschina.net/ksc/raspi_router/repository/archive?ref=master -O t.zip
unzip t.zip
cd raspi_router/

check.py 检查你前几步的设置是否有效
若你用的是openvpn 一般不需要修改,直接运行即可

root@coolpi:~/raspi_route# ./check.py
eth0 ok
eth0_ip=192.168.2.11
wlan0 ok
wlan0_ip=192.168.10.1/24
tun0 ok
tun0_ip=10.2.2.10
p-t-p:10.2.2.9
检查通过 请运行一下命令生成 路由及转发脚本
./build_sh.py

安照提示运行上述命令

root@coolpi:~/raspi_route# ./build_sh.py   
/root/raspi_route/route.txt
linux_route.sh 及 linux_nat.sh 已经生成 请运行

然后执行

./linux_route.sh && ./linux_nat.sh

以后若ip信息都没有变化的话 ,直接运行如下命令即可

ifup wlan0 #若wlan0已经启用并获取ip地址了、则不用执行此命令
service hostapd start
service isc-dhcp-server start
openvpn client.conf #具体看你的配置文件而定
./linux_route.sh 
./linux_nat.sh

最后

  1. route表是我从网上搜集的,只有几个常用的网段,不是很全。
  2. 上述过程我检查了几遍,但难免有疏漏的地方。若您发现请指正

原文地址