当前位置

技术

技术

翻墙代理的加密部分

就是怎么在 PHP/mcrypt 和 PyCrypto 之间 DES(或其他加密算法,比如3DES/RSA/..) 通信的问题,我这里还额外考察了下 .NET 平台的算法

网上询问相关问题的还挺多,尤其是 PHP 和 .NET 之间的 DES 转换。主要是 PHP/mcrypt 隐藏了 padding 的细节,且只保留了 ZERO_PADDING 模式,所以不明白cyrpto原理的不太容易找到症结所在。我的传输方案统一用 pkcs#7 padding.

首先是给服务器端增加的 PKCS#7 PADDING 函数,来自PHP官方函数手册上某人的注释

  1. function padding_pkcs7($crypto, $mode, $dat)
  2. {
  3.         $block = mcrypt_get_block_size($crypto, $mode);
  4.         $len = strlen($dat);
  5.         $padding = $block - ($len % $block);
  6.         $dat .= str_repeat(chr($padding),$padding);
  7.         return $dat;
  8. }
  9.  
  10. function strip_pkcs7($crypto, $mode, $text)
  11. {
  12.         $block = mcrypt_get_block_size($crypto, $mode);
  13.         $packing = ord($text{strlen($text) - 1});
  14.         if($packing and ($packing < $block)){
  15.                 for($P = strlen($text) - 1; $P >= strlen($text) - $packing; $P--){
  16.                         if(ord($text{$P}) != $packing){
  17.                                 $packing = 0;
  18.                         }
  19.                 }
  20.         }
  21.         return substr($text,0,strlen($text) - $packing);
  22. }

本地端 .NET 平台的 descrypto 封装

  1. # -*- coding: utf-8 -*-
  2. # ipcrypto.py
  3. from System import Array, Byte
  4. from System.Security.Cryptography import DESCryptoServiceProvider, CryptoStream, CryptoStreamMode, PaddingMode
  5. from System.IO import MemoryStream, StreamWriter, StreamReader
  6.  
  7. from hashlib import md5
  8.  
  9. def s2ab(s):
  10.     return Array[Byte](tuple(Byte(ord(c)) for c in s))
  11.  
  12. def ab2s(ab, len=0):
  13.     if len == 0: len = ab.Length
  14.     return ''.join([chr(ab[i]) for i in range(len)])
  15.  
  16. class descrypto():
  17.     def __init__(self, password):
  18.         pwmd5 = md5(password).digest()
  19.         self.key = s2ab(pwmd5[:8])
  20.         self.iv = s2ab(pwmd5[8:])
  21.         self.des = DESCryptoServiceProvider() # 缺省 des.Mode = CipherMode.CBC
  22.  
  23.     def enc(self, input):
  24.         ms = MemoryStream()
  25.         encStream = CryptoStream(ms, self.des.CreateEncryptor(self.key, self.iv), CryptoStreamMode.Write)
  26.         sw = StreamWriter(encStream)
  27.         sw.Write(input)
  28.         sw.Flush()
  29.         encStream.FlushFinalBlock()
  30.         return ab2s(ms.GetBuffer(), ms.Length)
  31.  
  32.     def dec(self, input):
  33.         ms = MemoryStream(s2ab(input))
  34.         length = len(input)
  35.         decStream = CryptoStream(ms, self.des.CreateDecryptor(self.key, self.iv), CryptoStreamMode.Read)
  36.         byteArray = Array.CreateInstance(Byte, length)
  37.         length = decStream.Read(byteArray, 0, length)
  38.         return ab2s(byteArray, length)

本地端 PyCrypto 的封装

  1. # -*- coding: utf-8 -*-
  2. # pycrypto.py
  3. from hashlib import md5
  4. from Crypto.Cipher import DES
  5.  
  6. class descrypto():
  7.     def __init__(self, password):
  8.         pwmd5 = md5(password).digest()
  9.         self.key = pwmd5[:8]
  10.         self.iv = pwmd5[8:]
  11.  
  12.     def enc(self, input):
  13.         des = DES.new(self.key, DES.MODE_CBC, self.iv)
  14.         lastblock = len(input) % 8
  15.         if lastblock > 0:
  16.             padding = 8 - lastblock
  17.             input += padding * chr(padding)
  18.  
  19.         return des.encrypt(input)
  20.  
  21.     def dec(self, input):
  22.         des = DES.new(self.key, DES.MODE_CBC, self.iv)
  23.         ret = des.decrypt(input)
  24.         padding = ord(ret[-1])
  25.         for i in range(padding):
  26.             if ord(ret[-1 - i]) != padding:
  27.                 padding = 0
  28.                 break
  29.         if padding > 0:
  30.             ret = ret[:-padding]
  31.         return ret
Topic: 

翻墙代理的远程部分

既然要翻墙,肯定要有一台墙外主机。为了配合加密,以及 HTTP/HTTPS 协议代理,需要编译有 mcrypt 和 curl 的 PHP;在如今我估计这应该都属于web主机标配环境.

如果是文本数据,就加密后返回;如果非文本数据,就不加密了。返回给本地代理以第一个字符是"0" or "1"来指示接下来的数据是否经过加密。

配合其运行的代码见
翻墙代理的本地部分
翻墙代理的加密部分

  1. <?PHP
  2. $PASSWORD = "yourpasswordhere";
  3. $pw_md5 = md5($PASSWORD, true);
  4. $key = substr($pw_md5, 0, 8);
  5. $iv = substr($pw_md5, 8, 8);
  6.  
  7. $input = file_get_contents("php://input");
  8.  
  9. $td = mcrypt_module_open('des', '', 'cbc', '');
  10. mcrypt_generic_init($td, $key, $iv);
  11. if (strlen($input) > 0 && $input % 8 == 0) {
  12.         $input = strip_pkcs7("des", "cbc", mdecrypt_generic($td, $input));
  13.  
  14.         $req = explode("\r\n\r\n", $input, 3);
  15.  
  16.         $rawreqline = explode(" ", $req[0]);
  17.         $url = parse_url($rawreqline[1]);
  18.  
  19.         $_headers = explode("\r\n", trim($req[1]));
  20.         //$_headers[count($_headers)] = "X-Forwarded-For: ".$_SERVER['REMOTE_ADDR'];
  21.         if ($url["scheme"] == "http" || $url["scheme"] == "https") {
  22.                 $ch = curl_init($rawreqline[1]);
  23.                 /*  avoid HTTP/1.1 Transfer-Encoding: chunked */
  24.                 curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0);
  25.                 curl_setopt($ch, CURLOPT_HTTPHEADER, $_headers);
  26.                 curl_setopt($ch, CURLOPT_HEADER, 1);
  27.                 if ($url["scheme"] == "https") {
  28.                         curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
  29.                 }
  30.                 curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
  31.                 if ($rawreqline[0] == "POST" && count($req) == 3) {
  32.                         curl_setopt($ch, CURLOPT_POST, 1);
  33.                         curl_setopt($ch, CURLOPT_POSTFIELDS, $req[2]);
  34.                 }
  35.                 $data = curl_exec($ch);
  36.  
  37.                 curl_close($ch);
  38.         }
  39.         $text_mode = "0";
  40.  
  41.         $res = explode("\r\n\r\n", $data, 2);
  42.         $header = explode("\r\n", $res[0], 2); // STATUS HEADER
  43.         $headers = explode("\r\n", $header[1]);
  44.         foreach ($headers as $hline) {
  45.                 $h = explode(":", $hline, 2);
  46.                 $k = strtolower(trim($h[0]));
  47.                 if ($k == "content-type" && strpos(strtolower(trim($h[1])), "text/") === 0) {
  48.                         $text_mode = "1";
  49.                         break;
  50.                 }
  51.         }
  52.         if ($text_mode == "1") {
  53.                 mcrypt_generic_deinit($td);
  54.                 mcrypt_generic_init($td, $key, $iv);
  55.                 $data = mcrypt_generic($td, padding_pkcs7("des", "cbc", $data));
  56.         }
  57.         $data = $text_mode . $data;
  58. }
  59. ?><?=$data?>
Topic: 

翻墙代理的本地部分

我的这个 proxy 代码参考自 SUZUKI Hisao 的 Tiny HTTP Proxy。主要修改的有两点:

  1. 原版的 do_CONNECT 是两个套接字直接互相转发数据,我改成了 SSL 中间人代理.. 而且依赖到 python 2.6 才支持的 server-side ssl wrap
  2. 另外就是自己封装了 descrypto 类,完成和远程 PHP 的加密

配合其运行的代码见
翻墙代理的远程部分
翻墙代理的加密部分

  1. # -*- coding: utf-8 -*-
  2. # 建议 Python 2.6 环境,以支持 https proxy
  3. # Win32 下需安装
  4. # <a href="http://www.python.org/ftp/python/2.6.5/python-2.6.5.msi
  5. #">http://www.python.org/ftp/python/2.6.5/python-2.6.5.msi
  6. #</a> <a href="http://www.voidspace.org.uk/downloads/pycrypto-2.0.1.win32-py2.6.exe
  7. #">http://www.voidspace.org.uk/downloads/pycrypto-2.0.1.win32-py2.6.exe
  8. #</a> 至于 IronPython 目前还没有 server-side ssl 支持,据说 IP 2.6.1 将会有...
  9. try:
  10.     from ipcrypto import descrypto
  11.     #IronPython 里的 socket 不支持 bind 到 '0.0.0.0'
  12.     import platform
  13.     bind_address = (platform.node(), 8000)
  14. except:
  15.     from pycrypto import descrypto
  16.     bind_address = ('0.0.0.0', 8000)
  17.  
  18. import urllib2
  19. import BaseHTTPServer, SocketServer
  20.  
  21. #REMOTEURL/PASSWORD 和国外主机配合
  22. REMOTEURL = '<a href="http://www.dup2.org/blarblarblar.php'
  23. PASSWORD">http://www.dup2.org/blarblarblar.php'
  24. PASSWORD</a> = 'yourpasswordhere'
  25.  
  26. # KEY/CERT 的生成参考 <a href="http://docs.python.org/library/ssl.html
  27. KEYFILE">http://docs.python.org/library/ssl.html
  28. KEYFILE</a> = 'cert.pem'
  29. CERTFILE = 'cert.pem'
  30.  
  31. #自定义允许的IP列表, 给每个IP起个名字帮助记忆
  32. allow_clients = {'127.0.0.1': 'myself'}
  33.  
  34. desobj = descrypto(PASSWORD)
  35.  
  36. skip_headers = ["keep-alive", "proxy-connection", "connection", "accept-encoding"]
  37.  
  38. class pseudofile():
  39.     ''' SSL Pseudo File Object'''
  40.     def __init__(self, sslobj):
  41.         self.sslobj = sslobj
  42.         self.closed = 0
  43.  
  44.     def read(self, size):
  45.         chunks = []
  46.         read = 0
  47.         while read < size:
  48.             data = self.sslobj.read(size-read)
  49.             read += len(data)
  50.             chunks.append(data)
  51.         return ''.join(chunks)
  52.  
  53.     def readline(self):
  54.         line = []
  55.         while 1:
  56.             char = self.sslobj.read(1)
  57.             line.append(char)
  58.             if char == "\n": return ''.join(line)
  59.  
  60.     def write(self, data):
  61.         bytes = len(data)
  62.         while bytes > 0:
  63.             sent = self.sslobj.write(data)
  64.             if sent == bytes:
  65.                 break    # avoid copy
  66.             data = data[sent:]
  67.             bytes = bytes - sent
  68.  
  69.     # 下面两个方法是 BaseHTTPServer 里会调用到的
  70.     def flush(self):
  71.         pass
  72.     close = flush
  73.  
  74. def checkip(f):
  75.     def new_f(_self):
  76.         (ip, port) = _self.client_address
  77.         if ip in allow_clients:
  78.             f(_self)
  79.         else:
  80.             _self.send_error(403)
  81.     return new_f
  82.  
  83. class ProxyHandler(BaseHTTPServer.BaseHTTPRequestHandler):
  84.  
  85.     @checkip
  86.     def do_GET(self):
  87.         content_length = 0
  88.         if hasattr(self, "sslhost"): self.raw_requestline = "%s https://%s%s %s\r\n" % (self.command, self.sslhost, self.path
  89. , self.request_version)
  90.         h = [self.raw_requestline]
  91.         for kv in self.headers.items():
  92.             if kv[0] == 'content-length':
  93.                 content_length = int(kv[1])
  94.             if kv[0] in skip_headers: continue
  95.             h.append("%s: %s" % kv)
  96.         h.append("connection: close")
  97.         req = "\r\n".join(h) + "\r\n\r\n"
  98.        
  99.         if content_length:
  100.             req += self.rfile.read(content_length)
  101.  
  102.         encreq = desobj.enc(req)
  103.         req = urllib2.Request(REMOTEURL, encreq)
  104.         f = urllib2.urlopen(req)
  105.        
  106.         text_mode = f.read(1)
  107.         response = f.read()
  108.         if text_mode == "1":
  109.             response = desobj.dec(response)
  110.         self.wfile.write(response)
  111.         print 'REQUEST:', self.raw_requestline.strip()
  112.         #有时候一些看起来是 text/* 的请求也是 binary mode,通常是 304 Not Modified
  113.         print 'RESPONSE: %s, %d Bytes' % ('crypted mode' if text_mode == "1" else 'raw mode', len(response))
  114.        
  115.         self.close_connection = 1
  116.  
  117.     @checkip
  118.     def do_CONNECT(self):
  119.         # print self.raw_requestline
  120.         # "CONNECT twitter.com:443 HTTP/1.1"
  121.         self.sslhost = self.raw_requestline.split()[1]
  122.         self.wfile.write(self.protocol_version + " 200 Connection established\r\n")
  123.         self.wfile.write("Proxy-agent: QYB\r\n\r\n")
  124.         # TODO 浏览器端会看到一个警告,但是没有办法;避免警告是不对的,必须让使用者认识到现在是中间人模式
  125.         try:
  126.             import ssl
  127.             self.rfile = pseudofile(ssl.wrap_socket(self.connection, KEYFILE, CERTFILE, True))
  128.             self.wfile = self.rfile
  129.             self.handle_one_request()
  130.         except:
  131.             print 'ssl error:', self.raw_requestline
  132.             self.close_connection = 1
  133.        
  134.     do_PUT = do_GET
  135.     do_POST = do_GET
  136.     do_HEAD = do_GET
  137.     do_DELETE = do_GET
  138.  
  139. class ThreadingHTTPServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer): pass
  140. httpd = ThreadingHTTPServer(bind_address, ProxyHandler)
  141. httpd.serve_forever()
Topic: 

最近为翻墙作的一些技术准备

本来自从换到国外主机后,也就有了一个 ssh 帐号,用来翻墙是通行无阻。但是在某些情况下无法访问外面的 22 端口(比如搜狐的WIFI),那就只能想法通过80端口来翻了.

大概有三种方案:

  1. 弄一个代理页面,输入URL,得到HTML。更强一点的还可以把内容变化一下,在浏览器端重新组装,以避开深度包检测。
  2. 直接在URL上生成一个HTTP Proxy,在浏览器里配置通过它访问。这个可能会被RESET
  3. 我尝试的方向是类似phpproxy,在本地启动Proxy服务。这样的好处是可以同时帮助其他人翻墙

我这个重复造的轮子比 phpproxy 增加的地方有两处,一个是在墙内外传输之间弄了一个DES加密,避开内容检查;另外一个是很凑合的实现了HTTPS代理(就是会有浏览器警告,很像中间人证书欺骗)。本地的 Proxy 在 Python 和 IronPython 下都能运行,如果哪天想用手机翻墙的话再试试 PyS60 能不能跑起来

感觉对付防火墙还是得靠 P2P 的模式,部分人贡献出口通道,静态内容在墙内大量缓存。打算要是有时间的话研究研究 .NET Framework 里的 P2P 支持,做出来的产品能在 Windows 上运行才是王道。

Topic: 

闪电邮箱-梦工场,以及第三方应用

其实 Labs 这个东西,本来我最初是想起名为"作坊"的,一方面是俺们行业总是被称呼为作坊,另一方面我确实认为这是项手艺活,该名字非常贴切。但是这个主意肯定会被产品同学痛批,所以还是用"工场"来代替"作坊"了

如果不是来自理工专业的研究生,只能凭空想象 Labs 是什么地方吧。我把它称之为工程师梦开始的地方,希望不会遭到误解。

在我的计划里,梦工场的出产未必是用户所迫切要求,未必是产品/运营所欣赏;而完全是工程师主导的,充满了不确定,以及那么一点点小bug的项目;梦工场的另外一个考虑就是优化框架,让各项应用可以很方便的以 plugins 形式嵌入——但现在还没这么作,需要长时间的磨合。

之所以有上述这样的打算,是因为我认为未来可能有样激动人心的事情就是把WebMail作为一个平台,鼓励第三方加入开发。2月份我在twitter上说,"关于gmail,实验室就是它的appstore...另外,google apps潜在的appstore商业机会才叫一个大"。仿佛为了回应我这段话,上个星期,GoogleCode Blog连续发表两条消息,一条针对企业,"Integrate, Publish, Sell - The Google Apps Marketplace",一条针对个人应用,"Coming soon: Gmail contextual gadgets available for trusted testers"

回到现实,梦工场目前也是产品经理们的实验地,看看产品经理驱动和工程师驱动最后谁会被用户接受,也是让人期待的事情。

Topic: 

关于 RPM 的一些事实

不喜欢有人妖魔化 rpm,尤其是如果从来没有用过 fedora/opensuse 也出来乱喷

1. 这只是一个应用的打包规范,本质上和 deb 没有什么差别。
2. 它是 LSB(Linux Standards Base) 的标准格式
3. 它支持 LZMA 压缩,比传统的 zlib 压缩比更高
4. 有一种叫 DeltaRPM 的格式,可以极大缩小每次 Update 的带宽开销。这非常酷

另外和 apt 对应的是 yum/zypper 等高级命令行工具
1. yum 的 fastestmirror 插件自动选择最快的 mirror
2. 我使用 yum 和 zypper 的时候,没有感到什么 rpm 依赖关系的麻烦

我最近两年使用的都是 rpm 的系统,确实不了解最近 deb 的进展。但就我搜索来看,LZMA,Delta Patch 这两个特性 deb 系应该还差一点.

Topic: 

IronPython 里某些类型的显式转换

  1. import clr, System
  2. x = clr.Reference[System.UInt64](0)
  3. #创建一个到System.UInt64对象的引用,C的行话是指针
  4.  
  5. x.Value = 1 * 1

上述代码在最后 x.Value = 1*1 的时候抛了一个类型异常出来..

我就直接在 ipy console 里执行 x.Value = 1 测试

我靠,没有任何错误啊。为啥后面赋值把整数替换成表达式就错呢?我想当然的以为这个是 IronPython 的 bug,差点就去开 Issue 了。后来转念一想,这个 2.0.3 的 ipy 出错,或许 2.6 就 fix 了...

然后发现在 2.6 里更悲剧,x.Value = 1 也直接抛异常..我这才意识到这种赋值操作是有问题的,改成 x.Value = System.UInt64(1*1) 搞定

Topic: 

关于动态gif的帧速

做了一个小实验,结果预期的目的失败了,倒是额外发现了关于动态gif帧速的一个链接:http://humpy77.deviantart.com/journal/12374968/

如果没耐心读英文,我中文介绍一下:

动态GIF的定义,是弄一组GIF图片,然后指定渲染图片序列的时候,处理下一帧之前 delay 多长时间。这个时间是一个整数n,表示延时 n * 1/100 秒。

理论上我们可以得到非常棒的动画图像,要知道电影也不过每秒24帧而已,但我们从来没有看到过效果很好的动态gif,不是么?

原因在浏览器对这个规范的实现上。所有的浏览器,都不支持 n=0,1的延迟,而且如果把值设成0或1,会得到更糟糕的动画效果!

  1. IE,所有n<6的延迟,统统提高到10 (0.1秒 or 10fps)
  2. Firefox,所有n<2的延迟,统统提高到 10。就是说 n=2 确实可以在firefox上看到很漂亮的动画,可惜IE上会慢得一塌糊涂
  3. Opera,最糟糕,最小是0.1秒
  4. Safari,和Firefox各有利弊,所有n<3的延迟,会提高到3。换句话说你不小心做了一个n=1的图,在其它浏览器下都悲剧了,但在Safari上还凑合
  5. 最后,上面都是2007年的测试,我自己又重新用各个浏览器最新的版本重新测了一下,看n=2以及n=6动画的表现:
    • 对IE 8/Firefox3.6来说,上面表现一样
    • 新版的Opera表现很好,n=2的时候,效果和Firefox一致,赞!没有测是否支持n=1
    • 新版Safari,至少是Windows上的Safari 4吧,比老版本退步了。n小于某个值的时候,会提高到一个更高的值(应该是10)。在我测试里n=6的图片,效果比n=2要好;但不知道现在它支持的最小值是多少,也许仍然是3?n<3的时候n=10?
    • Chrome表现和Safari一样。我猜所有webkit浏览器,应该都一样的

等下周有时间了生成n=1,2,3,4,5,6,10, 再全面测试看看

结论,千万不要用 0/1 来指定 n,这就彻底茶几了;用2/3/4/5?在Firefox和Opera下效果会如你预期;但为了兼容所有浏览器,还是用6吧

Topic: 

IronPython 的 .NET 线程

IronPython 除了实现 Python 的 threading 接口,也包括了 .NET 线程接口。如果不考虑跨平台移植的话,.NET 的接口应该是更为好用,也更强大些.

IronPython 线程开发最权威同时也是最简洁的介绍,就是 Michael Foord 大神的 http://www.voidspace.org.uk/ironpython/threading.shtml。剩下的就去研究 MSDN 上的 System.Threading 吧

我模拟了一个 start_new_thread 实现如下:

  1. from System.Threading import Thread, ParameterizedThreadStart
  2.  
  3. def start_new_thread(func, args, kwargs=None):
  4.     def thread_func(obj):
  5.         f = obj[0]
  6.         a = obj[1]
  7.         k = obj[2]
  8.         if not k:
  9.             return f(*a)
  10.         else:
  11.             return f(*a, **k)
  12.     t = Thread(ParameterizedThreadStart(thread_func))
  13.     t.Start((func, args, kwargs))
  14.     return t

 
使用 .NET 线程要注意,Thread.Abort() 方法无法中断非托管代码,比如一个阻塞的 socket 调用。线程会一直停留在 AbortRequested 状态下,而不去抛 ThreadAbortException 异常.

Topic: 

搜狐邮箱最近几个后台更新

1. 个人邮箱的 lookup db 全部升级完毕...基本上也意味着这个平台完成度达到90%.. 接下来就是升级企业邮箱了

2. 不怕大家笑我们老土,邮件应用服务器——或者说桶服务器,开始进行64位OS测试。前两年纠结的是向 kernel 2.6/epoll,以及应用程序,存储格式的转换;今年主要是头疼计算环境了,除了32->64外,还包括虚拟化技术的应用。

3. imap.sohu.com 貌似可以连接了,但据说除了内部测试帐号外,也就只能认证玩玩,呵呵

Topic: 
订阅 RSS - 技术