2011年10月26日

rewrite模块和connector的URIEncoding对url解析的影响

如果url中包含非ASCII字符(比如中文),W3C建议对url使用UTF-8编码并把编码后的每个byte用%HH的形式表示:比如"啊&啊""就被编码成"%E5%95%8A%26%E5%95%8A",使用java的话,就是调用URLEncoder.encode("啊&啊", "UTF-8")。

如果url是/?tag=%E5%95%8A%26%E5%95%8A,那么request.getQueryString()返回tag=%E5%95%8A%26%E5%95%8A,request.getParameter("tag")返回å•Š&å(在iso-8859-1环境下就是显示???&???)

根据api文档,getQueryString返回的是 String containing the query string or null if the URL contains no query string. The value is not decoded by the container.这是对的。

getParameter返回的是乱码,这也是对的,在上一篇日志中写过,"啊&啊"被tomcat用iso-8859-1编码了。

但是当url应用重写规则并给connector配置URIEncoding的时候,又会出现什么情况呢?

重写规则为了能匹配规则,会对url进行decode,connector的URIEncoding也会影响url的解析。

假设有这么个重写规则:RewriteRule ^/tag/(.)$ /tag.do?tag=$1 [PT,L]*
我在win7+apache2.2+tomcat6.0.29下,加上jboss的rewrite模块,针对 /tag/%E5%95%8A%26%E5%95%8A(需要被rewrite)和/?tag=%E5%95%8A%26%E5%95%8A(无需rewrite)

测试了各种条件下getQueryString和getParameter的返回值:

观察以上数据,可以看出:

  1. 在需要rewrite的时候:使用apache和jboss的rewrite模块,没有本质区别,经过rewrite后,getParameter得到的值都是一样的。唯一的不同是ajp的URIEncoding为"UTF-8"时,getQueryString返回的值不同。这是因为当使用jboss的rewrite模块时,"/tag/%E5%95%8A%26%E5%95%8A"被转为"/tag/啊&啊"后再rewrite,getQueryString就返回"tag=啊&啊了"。
  2. 在不需要rewrite的时候:apache通过ajp传给tomcat是"tag=%E5%95%8A%26%E5%95%8A",只要AJP Connector的URIEncoding为"UTF-8",getParameter就可以得到正确的值
  3. 由于url都是apache通过ajp传给tomcat,因此这里http connector的配置不起作用

再来看一下乱码,"啊"变成"???":字节信息还是正确的,通过new String(value.getBytes("ISO-8859-1"), "UTF-8")就能获取原来的值

在实践中可以遵循以下规则:

  1. 如果url由我们控制,就把url调用URLEncoder.encode(url, "UTF-8"),然后把对应的connector的URIEncoding设置成"UTF-8"(apache+tomcat就设置ajp,仅tomcat就设置http)。如果url经过rewrite,需要我们自己处理QueryString,否则直接getParameter即可。
  2. 如果url是根据用户输入产生,浏览器可能对url采用非UTF-8编码,此时最好不要对url做rewrite,并自己处理QueryString。

2011年9月16日

Sublime Text 2的Tortoise插件中文路径问题

今天在使用Sublime Text 2的Tortoise插件的时候,由于svn的路径中有中文,导致Tortoise一直报错:

error: 'ascii' codec can't encode character in position 25-28: ordinal not in range(128)

调试发现是Tortoise.py中如下代码抛出的错误:

proc = subprocess.Popen(self.args, stdin=subprocess.PIPE,stdout=subprocess.PIPE, stderr=subprocess.STDOUT,startupinfo=startupinfo, cwd=self.cwd)

经过一番google,发现这应该是python的bug,具体分析请看subprocess.call fails with unicode strings in command line.
这个bug一直到python2.7都没有fix,而Sublime Text 2用的还是python2.6...

幸好这个页面有人提供了两个解决方案:

  1. 给subprocess.py打patch
  2. 给_subprocess.c打patch

我尝试了第一个patch,原本的修改如下:

--- Lib/subprocess.py.orig    2008-10-01 11:37:34.034500000 -0700  
 +++ Lib/subprocess.py    2008-10-01 12:35:10.018875000 -0700  
 @@ -766,11 +766,15 @@  
               startupinfo, creationflags, shell,  
               p2cread, p2cwrite,  
               c2pread, c2pwrite,  
 -              errread, errwrite):  
 +              errread, errwrite, sys=sys):  
        """Execute program (MS Windows version)"""  
        if not isinstance(args, types.StringTypes):  
          args = list2cmdline(args)  
 +      if isinstance(args, unicode):  
 +        args = args.encode(sys.getfilesystemencoding())  
 +      if isinstance(executable, unicode):  
 +        executable = executable.encode(sys.getfilesystemencoding())  
        # Process startup details  
        if startupinfo is None:  

但是使用这个patch还是报错,这是因为除了args和executable有可能是unicode的,cwd也有可能是unicode。
在这里,Tortoise插件传给subprocess的cwd就是中文路径,所以还要对cwd进行encode,因此还需要加上下面这段:

if isinstance(cwd, unicode):
    cwd = cwd.encode(sys.getfilesystemencoding())

Sublime Text 2是通过python26.zip中的subprocess.pyo来加载subprocess的,把修改过的subprocess.py编译成subprocess.pyo再替换就可以了.
当然直接删掉subprocess.pyo,再添加修改过的subprocess.py也没啥问题.
重启Sublime Text 2就可以顺利使用Tortoise插件了.

2011年7月23日

tumblr自定义风格系统的实现

要实现类似tumblr的自定义风格系统,首先需要一个模板引擎。 该模板引擎需要满足以下需求:

  1. 语法足够简单,最好只支持variable、block元素,不要支持循环。由于要对所有用户开放,支持循环的模板存在安全问题,可能遭到攻击,因为用户可以构造一个死循环的模板。

  2. 数据不通过${对象.属性}的方式来访问,这可能会造成普通用户理解困难,数据是单层的。比如日志标题就是${Title},而不是${post.title}

这样的话,freemarker、velocity虽然功能强大,却不符合上面两点,就不适合在这里使用了。

不过还是有很多轻量级的模板引擎可供选择的,比如MiniTemplator、Mustache,这两个引擎都有多种语言的实现。

其中MiniTemplator的语法更接近tumblr,稍加修改就能改成和tumblr一样的语法,正好符合我们的要求。MiniTemplatorde的解析部分基本可以直接使用,无需做太大修改。

由于tumblr模板的数据是单层的,因此同一个变量在不同的块级元素下可能需要填充不同的数据,比如${Title}在post的block中就是日志的标题,而在顶级的block中就是博客的标题。

MiniTemplator只提供了最基本的数据填充方法,因此需要自己实现一套处理上下文相关的数据填充框架。对这种数据的填充逻辑必然是和具体的数据定义耦合在一起的:

/**  
  * 数据填充接口  
  *  
  */  
 public interface Data {  
     /**  
     * 该block是否需要处理  
     * @param blockName  
     * @return  
     */  
     public boolean needProcessBlock(String blockName);  
     /**  
     * 填充该block数据  
     * @param xpath  
     * @param blockName  
     * @param blockNo  
     * @param miniTemplator  
     */  
     public void process(String xpath, String blockName, int blockNo, MiniTemplator miniTemplator);  
     /**  
     * 获取该变量的值  
     * @param key  
     * @return  
     */  
     public Object getValue(String key);  
 }  

process是个递归的过程,调用process("","",0,templator)就完成对模板的数据填充:

public void process(String xpath, String curBlockName, int curBlockNo, MiniTemplator miniTemplator) {  
     List blockNos = miniTemplator.getSubBlocks(curBlockNo);  
     for (int blockNo : blockNos) {  
         String blockName = miniTemplator.getBlockName(blockNo);  
         if (needProcessBlock(blockName)) {  
             process(xpath, blockName, blockNo, miniTemplator);  
         }  
     }  
     List variables = miniTemplator.getSubVariables(curBlockNo);  
     for (String variable : variables) {  
         miniTemplator.setVariable(variable, String.valueOf(getValue(variable)));  
     }  
     miniTemplator.addBlockByNo(curBlockNo);  
 }  

getValue是个类似回溯的过程,根据数据填充逻辑的复杂度,可以实现多个Data接口,比如GlobalData表示全局数据,PostData表示日志数据,PostData会保存对GlobalData的引用,调用PostData的getValue时,如果postData无法处理该值,就调用GlobalData的getValue。

Appearance Options是tumblr模板的一个特色,可以给模板的使用者提供一些选项,模板根据用户的设置会显示成不同的效果,比如就是一个条件值。

虽然minitemplator本身提供条件判断,但是如果把这些条件值在初始化minitemplator的时候传给TemplateSpecification,就会导致模板的结构和用户的数据交织在一起,这样每个用户都会有一个minitemplator的实例,给缓存模板带来麻烦。

因此还是使用和tumblt一样的语法,把这些参数都作为数据,和templator的解析区分开:
如果if:Show Likes的选项会真,就设置数据IfShowLikes为true,IfNotShowLikes为false,{block:IfShowLikes}{/block:IfShowLikes}中内容会被渲染,{block:IfNotShowLikes}{/block:IfNotShowLikes}中内容不会被渲染。

Appearance Options的定义也单独解析:

(?i)<meta\s+name=\"(color|font|if|image|text|customcss):([^"]+)"\s+content="([^"]*)"

只要再对MiniTemplator做一些扩展(主要是添加一些接口),这样就基本上实现tumblr的自定义风格模板系统,只要针对自己的需求添加一些处理各种块数据的逻辑就可以了。

使用这样的自定义风格系统,所有的数据都是在服务器端填充的,因此数据的加载速度是很重要的。
需要用一些方法来加速页面的加载:

  1. 服务器端对于相互间没有依赖的数据的获取,应该使用并行的方式来查询数据,这样可以成倍的减少加载时间。

  2. 对某些特殊的数据块,服务器不直接填充数据,而是填充js代码,当页面加载完成后,再异步加载这些数据并显示。

  3. 对某些用户数据,可是适当的使用本地缓存。

由于tumblr允许用户上传自己的html模板,因此防范恶意代码也是相当重要的。tumblr的cookie是放在www.tumblr.com下的,因此可以防止个人主页窃取cookie。

但是在个人主页加入xss代码似乎还是比较容易的。安全方面不熟悉,就不多说了。

2011年7月6日

切换无线网络连接脚本

由于公司网络使用DHCP,而自己家里是用静态IP的,每天手动切换很是麻烦,所以写了个批处理来切换网络连接。

公司的无线网络连上后,还要做web认证;家里网络连上后,还要连vpn,脚本顺便把这些都做了,直接开机启动即可。

wlan_config.bat:

@ECHO OFF
 @rem 家庭无线名称
 set home=xxx
 @rem 公司无线名称
 set office=yyy
 echo detecting current wlan config...
 setlocal enabledelayedexpansion
 netsh wlan show networks | findstr /C:"%home%" > nul
 IF !errorlevel! equ 0 (
     ipconfig /all | findstr /C:"192.168.0.100" > nul
     IF !errorlevel! equ 0 (
         echo already static ip, exit...
     ) ELSE (
         echo use static ip...
         call %~dp0home.bat
         echo connect %home%...
         netsh wlan connect %home%
     )
     echo connect vpn...
     call %~dp0vpn.bat
     echo done
 ) ELSE (
     netsh wlan show networks | findstr /C:"%office%" > nul
     IF !errorlevel! equ 0 (
         echo use dhcp...
         call %~dp0dhcp.bat
         echo connect %office%...
         netsh wlan connect %office%
         call %~dp0auth.bat
         echo done
     ) ELSE (
         echo unknow wlan, use dhcp default...
         call %~dp0dhcp.bat
         echo done
         pause
     )
 )  

其中用到了home.bat,dhcp.bat,vpn.bat,auth.bat,如果不需要执行某个bat,去掉调用的那句就可以了。
这些bat都要放在相同目录下。

home.bat(设置静态IP):

netsh interface ip set address "无线网络连接"  static 192.168.0.100 255.255.255.0 192.168.0.1 1
netsh interface ip set dns "无线网络连接"  static 202.101.172.35
netsh interface ip add dns "无线网络连接"  202.101.172.47

dhcp.bat(设置为DHCP):

netsh interface ip set address name="无线网络连接" source=dhcp
netsh interface ip set dns name="无线网络连接" source=dhcp register=PRIMARY

vpn.bat(vpn拨号):

rasdial vpn username pwd

auth.bat(web认证,这个脚本需要cygwin才能运行,因为用了curl):

@echo off
d:
chdir d:\cygwin\bin
curl -d "username=xxx&password=yyy" http://wireless-gateway.netease.com/login.html >nul

最后在启动里创建一个快捷方式指向wlan_config.bat就可以了。

2011年6月9日

tumblr和blogger的自定义风格模板

tumblr和blogger都具有高度可定制化的,可扩展的风格模板系统,允许用户制作十分精美的风格模板。

下面就简单介绍一下这两套模板系统。

tumblr的自定义风格模板的语法非常简单,只有两种类型:

  1. Variables是需要被填充的动态数据
  2. Blocks表示块级的结构,在不同的情况下会有不同的语义

    • 大部分时候都是条件判断(隐式或显式的)。
      • {Block:Text},{block:PermalinkPage}就是隐式的判断,{Block:Text}表示如果日志是Text类型的,就渲染{Block:Text}里的内容,{block:PermalinkPage}表示如果当前是单日志页面,就渲染{block:PermalinkPage}里的内容
      • {block:IfFlickrUsername}就是显式的判断,Flickr Username是模板作者自定义的变量,允许使用模板的用户自定义。
    • 对某些特殊的block,起了循环的作用,{block:Posts}就会循环渲染多篇日志

对于下面这个模板片段

{block:Posts}  
     {block:Text}  
         ${Content}  
     {/block:Text}  
     {block:Photo}  
         ${LinkURL}  
     {/block:Photo}  
 {/block:Posts}  

用freemarker来写就是这样:

<#list post as posts>  
     <#if post.type=='text'>  
         ${post.content}  
     <#elseif post.type=='photo'>  
         ${post.linkURL}  
     </#if>  
 </#list> 

对普通用户来说,应该会觉得tumblr的模板更容易上手,因为从模板上看不到条件判断、循环什么的,都被隐含在block中了。

tumblr还允许模板作者提供扩展点,比如上面提到的Flickr Username,只要在head中定义,这个值就会出现在Appeareance菜单中,使用该风格的用户就可以提供必要的值,或者覆盖默认值。 比如定义:

<meta name="image:Background" content="">
<meta name="text:Disqus Shortname" content="" />
<meta name="if:Enable endless scrolling" content="1">
就会在apearance菜单中出现以下选项:

tumblr的模板系统对国际化的支持也相当的好,{lang:Follow me}就可以在使用不同语言的用户博客显示相应的文本。

但是tumblr文档有个缺点,某些部分的描述不清晰:
比如在示例中定义了<meta name="image:Header" content=""/> 使用{block:IfHeaderImage}判断是否有值

定义<meta name="text:Flickr Username" content=""/> 却使用{block:IfFlickrUsername}判断是否有值。

不够统一,也没找到文档在哪里有说明。也有可能是历史原因造成的兼容性问题。

而blogger的风格模板的语法相对比较复杂,样式使用<b:skin>,html结构使用<b:section>,<b:widget>,这里的“b”应该就是block的缩写。

<b:skin>块中可以定义Variable,多个Variable可以属于一个group。这里的Variable就相当于tumblr中在meta中定义的扩展点。

blogger提供了更加高级的所见即所得的模板编辑器,这些定义的Variable就会出现在模板编辑器中给风格的使用者修改。

<Group description="Backgrounds" selector=".body-fauxcolumns-outer">  
    <Variable name="body.background.color" description="Outer Background" type="color" default="#296695" value="#296695"/>  
    <Variable name="header.background.color" description="Header Background" type="color" default="transparent" value="transparent"/>  
    <Variable name="post.background.color" description="Post Background" type="color" default="#ffffff" value="#ffffff"/>  
 </Group>

就会在模板编辑器中出现这些选项:

<b:section>,<b:widget>则用来定义页面元素,widget需要包含在section中,section是widget的容器。

页面语法元素有以下几种:

  • Includes:定义widget里的具体结构
  • Data:需要填充的数据,相当于tumblr中的变量,使用obj.properties的方式来访问,比如<data:photo.url/>
  • Loops:循环
  • If/Else:条件判断

用blogger的模板来写tumblr的这个片段:

{block:Posts}
    ${Content}
    {block:IndexPage}
        {block:NoteCount}${NoteCount}{/block:NoteCount}
    {/block:IndexPage}
{/block:Posts}

就是:

<b:section>  
     <b:widget type='Blog'>  
         <b:loop var='post' values='posts'>  
             <b:includable id='post' var='post'>  
                 <data:post.body/>  
                 <b:if cond='data:blog.pageType == "index"'>  
                     <b:if cond='data:post.numComments >0'><data:post.numComments/></b:if>  
                 </b:if>  
             </b:includable>  
         </b:loop>  
     </b:widget>  
 </b:section>

写法上显然是tumblr的比较简洁。

但是可读性是blogger的较好,如果完全不看各自的文档,还是大概能明白blogger模板的逻辑,但tumblr的模板就很难理解了。

比较一下模板的数量,tumblr到现在差不多有900套风格,数量每天都在增加,而blogger只有区区十几套风格,tumblr的风格系统显然更加活跃,这和他模板语法的简单性,降低了普通用户的学习门槛有很大关系的。打个比方的话,tumblr的模板系统就是jquery,而blogger的模板系统就是YUI。

由于两者都允许用户上传javascript,因此安全性是必须要考虑的。

tumblr的做法是把认证cookie设到www.tumblr.com下,而自定义的风格都是只在个性化域名下才起作用,比如xxx.tumblr.com,从而阻止xss。

在tumblr上传一套原创风格貌似是需要人工审核的,这可以保证public的风格不会存在恶意代码。

另外技术上tumblr也会限制iframe的使用,但是用户还是很容易的通过修改自己主页的代码给自己的主页加上恶意代码。

blooger的安全机制不清楚。

2011年5月13日

SSLHandshakeException问题

这两天发现服务器上使用httpclient访问https的网站时会抛出下面这个异常:

javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target  
    at com.sun.net.ssl.internal.ssl.Alerts.getSSLException(Alerts.java:174)

查了一下,都说是证书没有导入导致的,可是以前使用是好的,只是最近某一天开始才出现这个问题。

其实这时候就应该想到是证书过期了,但是以前没接触过这类问题,所以还是继续查看日志。发现一批旧的机器才有问题,新机器却是好的。猜测是caserts的问题,在旧机器上指定 -Djavax.net.ssl.trustStore=cacerts-new 果然好了。

解决证书过期,直接的办法就是升级jre,如果条件不允许的话,直接使用最新的cacerts替换旧的就可以了。

cacerts文件在java安装目录的security目录里。 当然如果是证书不存在,就是另外一回事了,需要手工导入。

2011年3月22日

cygwin编译python3

因为目前用Cygwin在线安装的python还是2.6的,想要在Cygwin里使用python3的话,就需要自己编译python3了。

以下就是编译的过程,主要是make会报一个错,解决掉就ok了:

  1. 下载源代码后解压
  2. 运行./configure --enable-shared --with-wide-unicode
  3. 运行make,make会报一个错误退出:

    make: *** No rule to make target `libpython3.2mu.dll.a', needed by `python.exe'. Stop.

    此时make已经在当前目录下生成了一个libpython3.2mu.a,只要复制这个文件并重名为libpython3.2mu.dll.a,然后重新执行make即可。

  4. 运行make install就安装好了

  5. 最后在.bash_profile加上
    alias python3=/usr/local/bin/python3.2mu export python3

打开Cygwin后运行python3就进入交互模式了。

2011年1月31日

xhtmlrenderer死循环问题

今天使用xhtmlrenderer时发现它在css背景图片不存在时存在bug,程序在渲染这种html时会死循环。

调试发现问题出在org.xhtmlrenderer.render.AbstractOutputDevicepaintTiles方法(paintVerticalBand和paintHorizontalBand 也有一样问题):

private void paintTiles(FSImage image, int left, int top, int right, int bottom) {  
     int width = image.getWidth();  
     int height = image.getHeight();  
     for (int x = left; x < right; x+= width) {  
         for (int y = top; y < bottom; y+= height) {  
             drawImage(image, x, y);  
         }  
     }  
 }  

当图片不存在时(比如404),此时会提供一个默认的Image对象org.xhtmlrenderer.swing.AWTFSImage.NULL_FS_IMAGE, 这个对象的width和height都是0.当把它作为参数传给paintTiles时,就死循环了。

找到原因就很好解决了,只要在paint前判断一下,如果是NULL_FS_IMAGE,直接return即可。

core-renderer-repack.jar是我重新打的包。

今天还在The Perils of Image.getScaledInstance()找到一个提高图片压缩质量的方法,用着还不错:

/**  
  * Convenience method that returns a scaled instance of the  
  * provided {@code BufferedImage}.  
  *  
  * @param img the original image to be scaled  
  * @param targetWidth the desired width of the scaled instance,  
  *  in pixels  
  * @param targetHeight the desired height of the scaled instance,  
  *  in pixels  
  * @param hint one of the rendering hints that corresponds to  
  *  {@code RenderingHints.KEY_INTERPOLATION} (e.g.  
  *  {@code RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR},  
  *  {@code RenderingHints.VALUE_INTERPOLATION_BILINEAR},  
  *  {@code RenderingHints.VALUE_INTERPOLATION_BICUBIC})  
  * @param higherQuality if true, this method will use a multi-step  
  *  scaling technique that provides higher quality than the usual  
  *  one-step technique (only useful in downscaling cases, where  
  *  {@code targetWidth} or {@code targetHeight} is  
  *  smaller than the original dimensions, and generally only when  
  *  the {@code BILINEAR} hint is specified)  
  * @return a scaled version of the original {@code BufferedImage}  
  */  
 public BufferedImage getScaledInstance(BufferedImage img,  
                     int targetWidth,  
                     int targetHeight,  
                     Object hint,  
                     boolean higherQuality)  
 {  
   int type = (img.getTransparency() == Transparency.OPAQUE) ?  
     BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_INT_ARGB;  
   BufferedImage ret = (BufferedImage)img;  
   int w, h;  
   if (higherQuality) {  
     // Use multi-step technique: start with original size, then  
     // scale down in multiple passes with drawImage()  
     // until the target size is reached  
     w = img.getWidth();  
     h = img.getHeight();  
   } else {  
     // Use one-step technique: scale directly from original  
     // size to target size with a single drawImage() call  
     w = targetWidth;  
     h = targetHeight;  
   }  
   do {  
     if (higherQuality && w > targetWidth) {  
       w /= 2;  
       if (w < targetWidth) {  
         w = targetWidth;  
       }  
     }  
     if (higherQuality && h > targetHeight) {  
       h /= 2;  
       if (h < targetHeight) {  
         h = targetHeight;  
       }  
     }  
     BufferedImage tmp = new BufferedImage(w, h, type);  
     Graphics2D g2 = tmp.createGraphics();  
     g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, hint);  
     g2.drawImage(ret, 0, 0, w, h, null);  
     g2.dispose();  
     ret = tmp;  
   } while (w != targetWidth || h != targetHeight);  
   return ret;  
 }