2010年12月29日星期三

RGD - libgd binding for Ruby

前些日子在硬盘里乱逛,看到以前写的那一堆半成品,突然觉得自己还蛮不负责的。想说挑一些别人可能用得上的稍微整理一下吧,也算是给自己一个交待吧…

Ruby图片处理一直以来似乎都是用ImageMagick,可这东西即使没有内存泄漏的问题也显得过于庞大了,而且Win32下gem装上的包会把Ruby目录搞得非常恶心,所以一年多以前我根据只支持1.8的Ruby/GD改了一个出来用。

因为Ruby API在IO的部分变化大了点,再加上我对图片处理以及GD其实不熟,后来改着改着就完全重写了一遍。但因为代码并不是集中在一个时间段内完成的,所以还是会有些乱。好在就是这次整理没太偷懒,至少把libgd官方的文档都搬了过来,好歹算有点样子了吧。

代码托管在github:https://github.com/oTnTh/rgd

安装可以直接用gem:gem install rgd

用Windows的同学可以抓precompiled的版本,1.8和1.9同时支持:gem install rgd --platform x86-mingw32

文档的话,http://rubydoc.info/gems/rgd似乎还没更新?反正rdoc是可用的,暂时先看本地的吧。

有bug和问题欢迎找我。

以上。

2010年11月17日星期三

微软NLS文件格式

最近一直在纠缠Ruby的字符串编码问题,其中就涉及到了CP936、CP950和CP951等代码页的码表。想说与其去翻不知道靠谱与否的资料,不如直接从系统里的NLS文件中提取数据,这又牵涉到了NLS的文件格式问题。

网上能找到的NLS文件格式信息很少,Konstantin Kazarnovsky童鞋在2002年写的一篇是其中最详细的了。不过比对一下c_936.nls等双字节编码发现,那篇东西错处还是不少,表格也很不知所云。于是打开WinHex猜了老半天,算是有了一点成果吧。

注:NT和非NT系统的NLS文件格式有所不同,下面的内容只适用于NT系统内的NLS文件。

以下是文件头信息,还蛮简单的:

地址 字节长 备注
0x00 2 文件标志,NT内的NLS应为0x000D(注一)
0x02 2 代码页,如936
0x04 2 1表示单字节编码,2表示双字节编码
0x06 8 四个0x003F,疑似为转换表中的无效值(注二)
0x0E 12 前导字节(注三)

注一:Win9X内的NLS文件中,该字段为0表示SBCS编码,为1表示DBCS编码。

注二:根据ANSI/OEM代码页以及SBCS/DBCS的不同,NLS文件中最多会有四张转换表,此处的四个DWORD很可能表示四张表中无对应字符时的值,不过貌似所有NLS文件此处都是四个0x003F就是。另外Win32中CPINFOEX结构体内有一个UnicodeDefaultChar成员似乎也都返回0x003F,或许就是这么来的。

注三:针对DBCS编码,此处保存第一字节可能的取值,SBCS编码该部分全为0,DBCS如CP936就是0x81和0xFE。取这12个字节的最小值并左移8位,如CP936就是0x8100,下面提到的CP2UC表DBCS部分,就是从这个值开始保存的。呃,至少CP936和CP950是这样。

接下来是Codepage To Unicode(下称CP2UC)和Unicode To Codepage(下称UC2CP)转换表。根据ANSI/OEM代码页以及SBCS/DBCS的不同,转换表一共可能有二到四张。

地址 字节长 备注
0x1A 2 CP2UC转换表长度(@t_len),类型和单位都是DWORD(注一)
0x1C 512 CP2UC转换表,0x00到0xFF部分,每字符2字节
0x021C 6 / 2 疑似分隔符,ANSI SBCS代码页此处6字节长,否则2字节长(注二)
 0x021E 512 CP2UC转换表,OEM部分,每字符2字节,ANSI SBCS代码页无此部分
 0x041E 4 / 2 疑似分隔符,OEM SBCS代码页此处4字节长,否则2字节长
  0x0420 (@t_len - 515)*2 CP2UC转换表,双字节部分,每字符2字节,非DBCS代码表无此部分
  -(注三) 2 疑似分隔符,非DBCS代码表无此部分
0x0222 / 0x0422 / - 65536 / 131072 UC2CP转换表,SBCS代码页每字符1字节,否则每字符2字节

注一:该值其实就三种,ANSI SBCS为0x103,OEM SBCS为0x203,其他则为DBCS。

注二:间隔符共6字节。CP2UC可能有1到3张表,每两张表之间间隔2字节,末尾补齐6字节。间隔符并不总是0,是否有啥含义未知。

注三:嗯,随便注一下,0x420 + (@t_len - 515)*2,谁不会算呢?

上表貌似画得挺复杂,可我已经尽力了……

从文件格式来看,NLS最多只能处理二字节编码,而GB18030最长需要4个字节,难怪CP54936不可能成为系统代码页。而且NLS格式的Unicode部分只覆盖了BMP,但较新版本的Big5-HKSCS有相当一部分却是在SIP(0x2XXXX),恐怕这也是CP951只能支持到Big5-HKSCS:2001的原因。

再次感叹一下,Ruby 1.9居然还在跟ANSI纠缠,实在是馊到不能再馊的馊主意,真不知道那几枚大神究竟是怎么想的。

最后是代码,随便参考一下吧:

require 'stringio'

module OtNtH;module FileFormat
  class MS_NLS
    attr_reader :codepage, :bytes_pre_char, :leadbytes
    attr_reader :oem_to_uc, :cp_to_uc, :uc_to_cp
    
    def initialize(fpath)
      s = StringIO.new(File.open(fpath, 'rb') { |f|f.read }, 'rb')
      def s.read_i2
        self.readbyte + (self.readbyte << 8)
      end

      # 文件头标志,0x0d表示NT格式的NLS
      buf = s.read_i2
      if buf == 0 then
        raise 'Win9x single-byte (SBCS) NLS Format!'
      elsif buf == 1 then
        raise 'Win9x double-byte (DBCS) NLS Format!'
      elsif buf != 0x0d
        raise 'Unknow File Format!'
      end
      
      @codepage = s.read_i2
      @bytes_pre_char = s.read_i2 # 为1则是SBCS单字节编码,为2则是DBCS
      
      s.pos += 8 # 0x003F * 4,可能是CPINFOEX里边的UnicodeDefaultChar
      
      buf = s.read(12) # 前导字节,如cp936是81和fe
      @leadbytes = []
      buf.strip.each_byte { |b| @leadbytes.push(b.to_s(16)) }
      
      @t_len = s.read_i2 # 转换表长度,cp936为32771
      # 0x0103表示单字节ANSI代码页,0x0203表示单字节OEM代码页
      # 为其他值时应该都是DBCS代码页
      raise 'Broken NLS File?' if s.size != s.pos + @t_len*2 + 0x10000*@bytes_pre_char
      
      @oem_to_uc = {}
      @cp_to_uc = {}
      @uc_to_cp = {}
      
      # 所有代码页都有的一张表
      0x100.times { |n| @cp_to_uc[sprintf('0x%.4X', n)] = sprintf('0x%.4X', s.read_i2) }
      
      while true do
        if @t_len == 0x103 then
          s.pos += 6
          break
        end
        
        s.pos += 2 # 未知内容,437为1,936和950都是0
        0x100.times { |n| @oem_to_uc[sprintf('0x%.4X', n)] = sprintf('0x%.4X', s.read_i2) }
        if @t_len == 0x203 then
          s.pos += 4
          break
        end
        
        s.pos += 2 # 未知内容,cp936是0
        b = (@leadbytes.min + '00').to_i(16)
        # 256 + 1 + 256 + 1 + .... + 1
        (@t_len-515).times { |n| @cp_to_uc[sprintf('0x%.4X', b+n)] = sprintf('0x%.4X', s.read_i2) }
        
        s.pos += 2 # 未知内容,936和950为4
        break
      end
      read_unit = @bytes_pre_char == 1 ? :readbyte : :read_i2
      0x10000.times { |n| @uc_to_cp[sprintf('0x%.4X', n)] = sprintf('0x%.4X', s.send(read_unit))}
      
    end
  end
end;end

nls = OtNtH::FileFormat::MS_NLS.new(ARGV[0])
ms = {}
nls.cp_to_uc.each do |cp, uc|
  n = uc.hex
  next if uc == '0x003F' || cp.hex == n || n == 0
  ms[cp] = uc.to_i(16)
end

n = 0
ms.each do |cp, uc|
  n += 1 if cp.hex != nls.uc_to_cp['0x%.4X' % uc].hex
end
puts n

2010年11月14日星期日

Encoding,还是Encoding

话说,上次发现台湾用big5-uao很多,于是和某台湾人士讨论了一下,而下面要说的问题就是在讨论过程中遇到的。

嗯,该问题虽然还是跟Encoding有关,不过这次倒不能说是Ruby的错。

以下是代码:

require 'mechanize'

agent = Mechanize.new
# 请自备梯子
agent.set_proxy('127.0.0.1', '8118')
# 该页面有用big5-uao编码的字符串
url = 'http://www.ptt.cc/man/Japanese-B95/index.html'
page = agent.get(url)
t = page.root.css('#finds > p')[0]

puts t.text.encoding
p t.text
p t.text.encode('gbk')
puts t.to_s.encoding
p t.to_s
p t.to_s.encode('gbk')

# 以下为程序输出:

#UTF-8
#"\u60A8\u73FE\u5728\u7684\u4F4D\u7F6E\u662F Japanese-B95 -          "
#"您現在的位置是 Japanese-B95 -          "
#Big5
#"<p>\x{B17A}\x{B27B}\x{A662}\x{AABA}\x{A6EC}\x{B86D}\x{AC4F} Japanese-B95 -          </p>"
#"<p>您現在的位置是 Japanese-B95 -          </p>"
#[#<Nokogiri::XML::SyntaxError: input conversion failed due to input error, bytes 0x93 0xAB 0xC7 0x66>, #<Nokogiri::XML::SyntaxError: input conversion failed due to input error, bytes 0x93 0xAB 0xC7 0x66>, #<Nokogiri::XML::SyntaxError: htmlCheckEncoding: encoder error>, #<Nokogiri::XML::SyntaxError: input conversion failed due to input error, bytes 0x93 0xAB 0xC7 0x66>, #<Nokogiri::XML::SyntaxError: encoder error>]

以我对Mechanize和Nokogiri的理解,该脚本的输出很正常。因为Nokogiri在其内部使用libxml解析HTML,而libxml使用libiconv处理编码问题。libiconv用big5去解码big5-uao字符串并尝试转换为utf-8,遇到不认识的部分时中断处理并丢弃剩余的部分,libxml则补完HTML的关闭标签并返回给Nokogiri。

(注:用Nokogiri去抓有编码问题的HTML页面时并不会主动报错,因此必须去检查page.root.errors,这很重要。)

然而这么一个符合我认知的脚本在某人的Mac上却跑出了不一样的结果。以下是稍微修改了一点的代码:

puts t.text.encoding
p t.text.lines.first.force_encoding('utf-8')
p t.text.lines.first.force_encoding('utf-8').encode('gbk', :undef=>:replace)
puts t.to_s.encoding
p t.to_s.lines.first
p t.to_s.lines.first.force_encoding('big5-uao').encode('gbk')

# 以下为程序输出:

#ASCII-8BIT
#"\u60A8\u73FE\u5728\u7684\u4F4D\u7F6E\u662F Japanese-B95 -          \uE66B\uF735\uE6C3\uF735\u4E2D\n"
#"您現在的位置是 Japanese-B95 -          ????中\n"
#Big5
#"<p>\x{B17A}\x{B27B}\x{A662}\x{AABA}\x{A6EC}\x{B86D}\x{AC4F} Japanese-B95 -          \x93\x{ABC7}f\x94D\x{C766}\x{A4A4}\n"
#"<p>您現在的位置是 Japanese-B95 -          読み込み中\n"
#[]

t.text.encoding不知为何变成了ascii-8bit,内容倒仍然是utf-8,可是却包含无效字符。而且libxml解析该页面没有发现错误(page.root.errors为空),t.to_s还居然能拿到正确的内容。

伊妹来伊妹去,最后我们俩在gtalk上兜了半天圈子,终于找到了问题所在。原来,他的Nokogiri用的并不是libiconv,而是ICU(IBM的那个,OSX里边默认安装),就这样而已。

(插花,t.text.encoding变成ascii-8bit的问题应该是Nokogiri的bug,这个还没去追的说。)

big5-uao只是一个big5的扩展,也就是在big5的private use area里安排了很多本不存在于big5里的内容。libiconv用严格符合big5的码表去解码big5-uao,于是就出错了。

而ICU的处理策略跟libiconv不同。在ICU的big5码表中,将big5的PUA与Unicode的PUA一一对应(第二个脚本输出的“\uE66B\uF735\uE6C3\uF735”这四个字符就在PUA里)。这样做的好处是即使字符串内包含非法字符,big5到utf-8再到big5也能还原到原始字符串而不会丢失信息。坏处么,ICU和别的编码转换工具混用会变成一件非常危险的事情。

虽然非常偏门,不过对处理big5-uao有需要的同学,可以试试这个:http://www.boxcn.net/shared/iu0uo12l70

压缩包里有我弄的一个为libiconv增加big5-uao的补丁(该补丁非常初步,不过正确性应该不成问题)以及编译好的dll,还有libxml以及相关dll。如果是动态链接的库,换上libiconv就行,只是Windows里gem安装的Nokogiri是静态编译,要处理big5-uao还需要继续改。

打开Nokogiri里的lib/nokogiri/ffi/libxml.rb,开头的部分改成这样:

# :stopdoc:
module Nokogiri
  module LibXML
    extend FFI::Library
    if RbConfig::CONFIG['host_os'] =~ /(mswin|mingw)/i
      raise(RuntimeError, "Nokogiri requires JRuby 1.4.0 or later on Windows") if RUBY_PLATFORM =~ /java/ && JRUBY_VERSION < "1.4.0"

再把压缩包里的所有dll文件放到lib/ext/nokogiri下,gem装上ffi,就可以这么用了:

ENV['NOKOGIRI_FFI'] = '1'
require 'mechanize'

agent = Mechanize.new
agent.set_proxy('127.0.0.1', '8118')

url = 'http://www.ptt.cc/man/Japanese-B95/index.html'
page = agent.get(url)
page.encoding = 'big5-uao'

t = page.root.css('#finds > p')[0]
puts t.text.encoding # => ASCII-8BIT,这里还是不对,仍然需要force一下
puts t.to_s.encoding # => Big5-UAO

貌似我应该去找找text的问题,然后给Nokogiri提交两个补丁…

git代码库里的Nokogiri已经将ffi的部分移除,以后如果要处理big5-uao,看来只能自己编译Nokogiri了。

2010年11月7日星期日

Encoding,又是Encoding

话说,前几天在用Ruby的Mechanize抓一个台湾网站的时候,又遇到了一系列令人无语的事情。详细的过程我就不复述了,以下只是精简版。

假设我们用如下脚本:

agent = Mechanize.new
page = agent.get(url)
puts page.root.inner_html.encode('gbk') if page.root.css('#ln2').length == 0

去抓这么一个HTML页面:

<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=big5" />
<title>TT</title>
</head>

<body>
<p id="ln1">豬八戒照鏡子——裏外不是人</p>
<p id="ln2">/你看不見我/</p>
</body>
</html>

程序输出的结果是:

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=big5">
<title>TT</title>
</head>
<body>
<p id="ln1">豬八戒照鏡子——</p>
</body>
</html>

大陆的童鞋们可能不熟,不过对岸的童鞋肯定一眼就能看出问题在哪里。最早的Big5是非常不完善的一种编码,缺字缺字符,譬如“裏”字就没有。而根据我从Linux里学来的经验,CP950应该靠谱。因为CP950就是Big5外加一些扩充字符,而且大部分HTML页面都是在Windows里写的。于是代码和输出变成这样:

agent = Mechanize.new
page = agent.get(url)
page.encoding = 'cp950'
node = page.root.css('#ln2')
puts node.text.encoding # => UTF-8
puts node.text.encode('gbk') # => /你看不見我/
puts puts node.inner_html # => 乱码,不过内容是对的
######### 以下是诡异的部分 ###########
puts node.inner_html.encoding # => Big5
puts node.inner_html.encode('gbk') # => Encoding::UndefinedConversionError: "\xA1\xFE" to UTF-8 in conversion from Big5 to UTF-8 to GBK
puts node.inner_html.force_encoding('cp950').encoding # => Big5

首先,node.text的值是对的,证明page.encoding的设置生效了,可node.inner_html.encoding为什么会是Big5?然后,force_encoding无效?

第一个问题,难道是先get再设置encoding所导致,也就是Mechanize的错?可mechanize/page.rb里的代码显示,Mechanize是无辜的:

def encoding=(encoding)
  @encoding = encoding

  if @parser
    parser_encoding = @parser.encoding
    if (parser_encoding && parser_encoding.downcase) != (encoding && encoding.downcase)
      # lazy reinitialize the parser with the new encoding
      @parser = nil
    end
  end

  encoding
end

既然Mechanize没问题,那就去翻Nokogiri吧。一层又一层追下去之后,在nokogiri/xml/node.rb里找到了这么一段:

def serialize *args, &block
  options = args.first.is_a?(Hash) ? args.shift : {
    :encoding   => args[0],
    :save_with  => args[1] || SaveOptions::FORMAT
  }

  encoding = options[:encoding] || document.encoding

  outstring = ""
  if encoding && outstring.respond_to?(:force_encoding)
    outstring.force_encoding(Encoding.find(encoding))
  end
  io = StringIO.new(outstring)
  write_to io, options, &block
  io.string
end

包括上面提到的force_encoding无效的问题,答案呼之欲出了:

puts Encoding.find('CP950') # => Big5
puts Encoding.aliases['CP950'] # => Big5
puts Encoding.aliases['CP936'] # => GBK

总结下来,问题有两个:

  1. CP936和GBK很相似,但还是有不一样的地方,CP950和Big5差得就更多了,Ruby却混为一谈;
  2. Nokogiri内部使用libxml解析HTML,但同样的编码名称在libxml和Ruby眼里不是一样的东西。

第一个问题我觉得应该能算作Bug。不过从Ruby 1.9 Feature #1784,还有这里这里看来,Big5的复杂和纠结远超我过去的了解。假如Big5-UAO的码表能兼容CP950的话,或许将Big5-UAO当成CP950来用也不错。

第二个问题再次说明了ANSI是多么万恶的一种东西。直接用Nokogiri的话还可以在parse前先用Iconv将HTML转换为utf-8躲过去,而Mechanize就只能打补丁了。

(Mechanize其实也应该打补丁,很多简体的HTML把Charset声明为GB2312,可想而知会有什么毛病。只是大陆UTF-8推广还不错,问题不彰而已。)

好吧,反正我对Ruby 1.9使用ANSI表示不理解也不是第一次了。

2010年10月30日星期六

Let's SSH

28号的时候SSHChina又断了整整一天,无法,只好在网上找个临时的。翻了一下,发现Paying.org.ru还不错,只是会半个小时就修改密码并且断线一次。Bitvise Tunnelier蛮好用,但是每半个小时就打开浏览器再复制粘贴,也太折磨人了。

网上现成的脚本大多不是Windows里可以用的,而且也处理不了密码会变的问题,所以就自己写了个。这个脚本比较适用于频繁修改密码的SSH帐户,登陆信息固定那种还是用Bitvise Tunnelier吧。

话说,每次为了方便给别人用而逼不得已写VBScript或者JavaScript的时候我都在想,Windows要是内置一套Ruby该多好……

将以下内容保存为letsSSH.js,再去下一个Plink放到同一个目录,双击letsSSH.js即可。

function lets_ssh(ask_for_account) {
    var fso = new ActiveXObject('Scripting.FileSystemObject');
    var plink = fso.GetParentFolderName(WScript.ScriptFullName) + '\\plink.exe';

    var sh = new ActiveXObject('WScript.Shell');
    var ios;
    var s, cmd, buf;
    while (true) {
        s = ask_for_account();
        cmd = plink + ' -N -D 127.0.0.1:7070 -l ' + s[2] + ' -pw ' + s[3] + ' -P ' + s[1] + ' ' + s[0];
        if (_debug) WScript.Echo(cmd);

        ios = sh.exec(cmd);
        buf = '';
        while (true) {
            try {
                buf += ios.StdErr.read(1); // 写大了不成,会等读满N个字符才返回
                if (/Store key in cache\? \(y\/n\)/.test(buf)) {
                    ios.StdIn.write("n\n");
                }
            } catch(e) {
                break;
            }
        }
    }
}

function bytes_to_str(data, charset) {
    var stm = new ActiveXObject('ADODB.Stream');
    stm.Type = 1;
    stm.Open();
    stm.Write(data);
    
    stm.Position = 0;
    stm.Type = 2;
    stm.Charset = charset;
    var s = stm.ReadText();
    stm.close();
    return s;
}

function get_account(url, args) {
    var xhr = new ActiveXObject('Msxml2.XMLHTTP');
    var s, html, r, re = / charset=([^"']+)[^"']?/i;
    var n = 0;
    while (n < 3) {
        try {
            xhr.open('GET', url, false);
            xhr.send();
            if (xhr.getResponseHeader('Content-Type').match(re)) {
                html = xhr.responseText;
            } else {
                r = xhr.responseText.match(re);
                if (r) {
                    html = bytes_to_str(xhr.responseBody, r[1]);
                } else {
                    throw 'Unknow Content Charset!!';
                }
            }
            
            s = [];
            for (var i=0;i<args.length;i++) {
                if (typeof(args[i]) == 'string') {
                    s.push(args[i]);
                } else {
                    r = html.match(args[i]);
                    s.push(r[1])
                }
            }
            
            break;
        } catch(e) {
            WScript.Echo(e.description);
        }

        n++;
    }

    if (s.length != 4) {
        WScript.Echo("Cannot Get SSH Account Info!\n" + url);
        WScript.Quit(-1);
    }
    return s;
}

function paying_org_ru() {
    return get_account(
        'http://blog.paying.org.ru',
        [
            /服务器地址:.*?value="(\d+\.\d+\.\d+\.\d+)"/,
            '22',
            /服务器用户:.*?value="([^"]+)"/,
            /服务器密码:.*?value="([^"]+)"/
        ]
    );
}

function sshdlw_com() {
    return get_account(
        'http://www.sshdlw.com/ssh/gd/46.html',
        [
            /ssh服务器 :<font color="#000000">([^<]+)<\/font>/,
            /ssh端口:<\/strong><\/span>(\d+)/,
            /帐号:<\/strong><a href="[^"]+"><strong>([^<]+)<\/strong><\/a>/,
            /<strong>密码:([^<]+)<\/strong>/
        ]
    );
}

var _debug = false;
if (/cscript.exe$/i.test(WScript.FullName)) {
    _debug = true;
    lets_ssh(paying_org_ru);
} else {
    // Shell.Exec无法隐藏plink窗口,所以只能这么干
    var sh = new ActiveXObject('WScript.Shell');
    var r = sh.Run('cscript.exe "' + WScript.ScriptFullName + '"', 0, true);
    if (r != 0) {
        WScript.Echo('Something Wrong...');
    }
}


2010年10月18日星期一

SSL证书制作

证书签名方案

使用OpenSSL制作证书其实还蛮简单的,只是证书签名的部分有一点复杂,方法有三:

  1. 交由受浏览器信任的第三方证书颁发机构签名;
  2. 自签名;
  3. 自制CA证书并用其签名。

对于上线运营的网站来说,第一个方案是首选,因为只有这样浏览器才不会报警。过去买证书很贵,现在倒是有免费的了,比如IE和Firefox都内置的StartSSL

不同的证书颁发机构对于证书生成多少都会有自己的要求,所以本文主要讨论后两种方案。

自签名证书

注:下面提到的很多命令都需要一个openssl配置文件,该文件一般名为openssl.cnf。Linux里一般不用管,Windows可使用参数config指定路径,使用环境变量OPENSSL_CONF也可以。

使用如下命令即可生成自签名证书:

openssl req -x509 -newkey rsa:1024 -nodes -days 365 -out self.pem -keyout self.key

req命令表示创建证书,newkey参数表示创建私钥而不是从已存在的文件中读取,nodes参数表示不加密私钥。如果不添加nodes参数,以后每次使用私钥时都必须输入密码(如Apache每次重启)。

输入证书信息时,Common Name需要写域名,支持通配符,如*.domain.tld。

程序运行完成后,self.pem即为完成签名的证书,self.key则为私钥。

使用上述命令创建的证书只能写一个域名匹配字符串,如写了*.domain.tld之后,访问https://domain.tld仍然会报错。如果想在一个证书中指定多个域名匹配字符串,则需要打开openssl.cnf文件,在末尾处添加如下内容:

[ ca_x509v3_more_cn_ext ]
nsComment="OpenSSL Generated Certificate"
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid:always,issuer:always
basicConstraints=CA:true
subjectAltName = DNS:domain.tld,DNS:*.domain.tld

将subjectAltName行修改为需要的内容后保存退出,使用如下命令重新生成自签名证书:

openssl req -x509 -newkey rsa:1024 -nodes -days 365 -out self.pem -keyout self.key -extensions ca_x509v3_more_cn_ext

自制CA证书并用其签名

如需要使用多个证书,每一个都自签名则意味着每一个都需要导入浏览器,这样就很麻烦。自制CA证书并用其签名后,无论制作了多少个证书,都只需要将自制的CA证书导入浏览器即可,相对来说方便些。

上例中用一条命令直接生成了私钥和证书,这次换个方法,分解为两步。

首先,生成自己的个人私钥:

openssl genrsa -des3 -out my.key 2048

des3参数表示使用该算法加密生成的私钥,以后每次使用私钥时都必须输入密码,去掉这个参数则不进行加密。

根据个人私钥生成自签名CA证书:

openssl req -x509 -key my.key -days 911 -new -out my.pem

输入证书信息时,Common Name可以写自己的名字。将该CA证书导入浏览器后,该名字会显示在证书管理器中。

接下来生成服务器的私钥和证书:

openssl req -newkey rsa:1024 -nodes -days 365 -out server_unsigned.pem -keyout server.key

再之后,就可以用个人证书签名服务器证书了:

openssl ca -days 365 -md sha1 -in server_unsigned.pem -out server.pem -cert my.pem -keyfile my.key

如该命令报错找不到index.txt之类的,视openssl.cnf中的配置,则需要做点准备工作:

mkdir demoCA
mkdir demoCA\newcerts
touch demoCA\index.txt
echo 11 > demoCA\serial

index.txt为空,serial随便写一个两位的数字进去。

多个域名的支持问题,跟上面说的一样,添加extensions参数:

openssl ca -days 365 -md sha1 -in server_unsigned.pem -out server.pem -cert my.pem -keyfile my.key -extensions ca_x509v3_more_cn_ext

完成后,server.key和server.pem即为已签名的证书及私钥。如果用在Lighttpd中,将两个文件合并即可。

最后,将my.pem导入至浏览器中“受信任的根证书颁发机构”即可。

其他

如果要去掉私钥的密码保护,可以这样做:

openssl rsa -in my.key -out my_plain.key

如果需要根据私钥生成公钥,可以这样做:

openssl rsa -in my.key -pubout -out my_public.key

证书生成后,可以使用如下命令查看详细信息:

openssl x509 -fingerprint -text -in my.pem

使用证书加密信息:

openssl smime -encrypt -in ptest.txt -out etest.txt my.pem

使用私钥解密:

openssl smime -decrypt -in etest.txt -out dtest.txt -inkey my.key

若需要DER格式的证书,可使用如下命令进行转换:

openssl x509 -in my.pem -outform DER -out my.der

2010年10月16日星期六

《植物大战僵尸》之我圆满了…

首先是有钱就能办到的,两个方向各11种颜色的金盏花。

若说是彩虹金字塔吧,其实也挺像张开双臂躺下的人……

水生植物就3种。呃,十字固定?

夜生植物8种。

队形怎么排纠结了一下,最后就这个了。

朵朵红花向太阳……

最后,也是最花时间的,日常植物27种。

向左看齐向右看齐的貌似都有人收集过,我就弄了一个向左看向右看……

2010年9月15日星期三

SImages

协助用户保存当前页面图片的Firefox扩展。用处的话,你们懂的。

好吧,其实就是Imagez在Firefox上的实现。嗯,初步的。

至于写这个东西的理由,马桶2愈加老旧,遨游却把少得可怜的开发力量都扔到了马桶3上面。而Chrome的API是相当有限的,SImages的功能在Chrome内无法实现。

我之前其实根本没怎么用过Firefox,主要是对Firefox当初的恐吓式营销(安全,安全,像不像如今360的措辞?)相当不感冒,这自然也是我第一次写Firefox扩展。

感觉嘛,Firefox的扩展系统的确蛮强大,不过莫名其妙的小毛病也不少。(打开代码看看我写的那一大堆注释就知道了。)

有问题欢迎留言。

http://www.boxcn.net/shared/tpizij8ea2

v0.1 - 2010-9-15

  • 实现基本功能
  • 不会JS的话,设置内的东西就表乱改了

v0.2 - 2010-9-16

  • 修正工具栏图标相关问题
  • 添加右键菜单项
  • 修改设置项内的函数签名

2010年7月6日星期二

手贱

前两天试了试Blogger的导入和导出功能,今天才发现就这么一试导致链接地址全变了。PR什么的我倒是不在乎,可看着也挺不舒服的。Blogger后端没有修改POST URL的功能,API里也没有。等空了找找看有没有什么办法吧,不过暂时只能这么放着了。

要是有人链到我这儿,对不起先。

Update:

折腾了一下,总算是解决了这个问题,部分解决。

首先,打开导出的XML文件,检查每一个entry元素的link子元素,确保href是你想要的。接着写一个脚本,将entry的title子元素改成链接的文件名部分。这里跟直接发帖一样,不改的话Blogger会根据entry的title给你自动生成一个

# coding: UTF-8
require 'rexml/document'

doc = REXML::Document.new(File.open(ARGV[0], 'r'))

posts = {}
doc.root.elements.each('/feed/entry') do |entry|
  # 有5个link的元素才是blog文章
  next if entry.get_elements('link').length != 5
  
  link = entry.elements['link[@rel="alternate"]']
  url = link.attribute('href').value
  posts[url] = entry.elements['title'].text
  url = url[url.rindex('/')+1..-6]
  entry.elements['title'].text = url
end

doc.write(File.open(ARGV[1], 'w'))
File.open(ARGV[2], 'w') { |f| f.write(posts.to_s) }

其次,在Blogger控制台新建一个blog,名字照旧,域名换一个如otnthnew。选择好模板之后,切忌按照向导的提示立刻导入旧帖子。先去设置中,将所有设置改成跟原blog一样。尤其需要注意的是“格式设置”中的“转换断行”选择为“否”,还有“基本”中的“选择帖子编辑器”选择为“旧编辑器”。

最后,导入备份的XML。把原blog的域名改成otnthold,再把otnthnew改成otnth。再然后还是写一个脚本,把帖子标题给改回来。

# coding: UTF-8
require 'gdata'

GMAIL = ''
PWD = ''
BLOGID = 'xxxxxxxxxxxxxxxxxxx'

=begin
因为墙的问题,gdata需要一点小hack。打开lib/gdata/http/default_service.rb,将:

http = Net::HTTP.new(url.host, url.port)

修改为:

http = Net::HTTP::Proxy(ENV['PROXY_ADD'], ENV['PROXY_PORT']).new(url.host, url.port)
=end
ENV['PROXY_ADD'] = '127.0.0.1'
ENV['PROXY_PORT'] = '8118'


posts = eval(File.open(ARGV[0], 'r:utf-8') { |f| f.read })

client = GData::Client::Blogger.new
client.clientlogin(GMAIL, PWD)

url = 'http://www.blogger.com/feeds/#{BLOGID}/posts/default?max-results=10'
idx = 1
while true
  feed = client.get(url + '&start-index=' + idx.to_s).to_xml
  feed.elements.each('entry') do |entry|
    entry.elements.each('link') do |link|
      link = entry.elements['link[@rel="alternate"]']
      entry.elements['title'].text = posts[link.attribute('href').value]

      entry.add_namespace('http://www.w3.org/2005/Atom')
      entry.add_namespace('gd','http://schemas.google.com/g/2005')
      entry.delete_element('thr:total')

      edit_uri = entry.elements['link[@rel="edit"]'].attributes['href']
      response = client.put(edit_uri, entry.to_s)
    end
  end
  
  idx += 10
  break if idx >= feed.elements['openSearch:totalResults'].text.to_i
end

因为blogid已经变了,对于Blogger来说这就是一个全新的blog,会不会有什么严重的后遗症还有待观察。

2010年3月18日星期四

Opera Mini翻墙笔记

一直在用的某一个Opera Mini 4.2修改版突然不能爬墙了。本不想折腾,可实在是被各种“优化”过的皮肤刺激得不轻,于是花了点时间,有了这一篇笔记。

自从Opera Mini国际版服务器封掉所有来自大陆IP的请求后,要用OPM翻墙就需要解决两个问题:首先是找一个,或者自己搭一个中继服务器;其次是将OPM程序中原来的服务器地址改掉。

网上别人搭好的中继服务器很多,几乎全用的是opm-server-mirror。验证一个中继服务器是否可用很简单,只要在服务器地址最后加上“/?test=1”(例如:http://xx.oo.com/opm/?test=1)并用浏览器访问,如显示“Hello Opera Mini Server! Fuck GFW!” 则表示中继服务器工作正常。

要求不高的话,用别人搭好的中继服务器也没啥,可这样会存在一个严重的安全隐患。因为OPM所有的请求和回应都要从中继服务器绕一圈,假若搭建服务器的人心怀不轨,获取用户的各种账号及密码是非常容易的事情。可能的话还是自己弄一个吧,随便找一个国外的支持PHP和cURL的免费空间就成,程序及教程opm-server-mirror都有。

搞定服务器的问题后,接下来则是修改OPM的服务器地址。如果更喜欢官方原版,opm-server-mirror提供了一个OPM服务器地址修改器。这种修改方式的问题在于,一旦中继服务器不再可用,需要用计算机再次修改并重新安装,未免有些麻烦,所以我个人更喜欢使用可自定义中继服务器的修改版。

可自定义服务器的修改版很多,个人推荐如下三个版本:

某些版本如安装出现问题,可尝试寻找一个可用的Socket服务器。比如用JD-GUI反编译这个版本

如需要测试某个修改版或某个服务器,可以用PC上的J2ME模拟器,如MicroEmulator

2010年1月21日星期四

Ruby版SendKey

VBScript中的SendKey是个很有用的方法,可以用代码实现一些简单的自动化操作。实际上,通过SendMessage发送WMKEYDOWN和WMKEYUP消息就可以实现同样的功能。而且这个办法可以向任意窗口发送消息,目标窗口并不需要具有焦点。

MSDN可以查到,WM_KEYDOWN和WM_KEYUP消息除了virtual-key code之外,还需要一个scan code。文档只说这个值根据不同的键盘会有所不同,但如何获得这个值却没有提。

好吧,这篇东西值得看的其实也就这一点点了,那就是:MapVirtualKey

以下程序是我用No$gba玩《恶魔城·苍月的十字架》时写来刷25号怪的,该作的魂系统实在太变态,不想点办法我会有一种被游戏玩了的感觉。还别说,有一种当年玩MUD的感觉。

require "Win32API"

module Win32
  module_function

  WM_KEYDOWN = 0x100
  WM_KEYUP = 0x101
  
  VK_SHIFT = 0x10
  VK_NUMPAD4 = 0x64
  VK_A = 0x41
  VK_D = 0x44
  VK_W = 0x57

  def sendMessage(hWnd, nMsg, wParam, lParam)
    f = Win32API.new('user32', 'SendMessage', 'LILL', 'L')
    f.call(hWnd, nMsg, wParam, lParam)
  end
  
  def findWindow(sClass, sTitle)
    f = Win32API.new('user32', 'FindWindow', 'PP', 'L')
    f.call(sClass, sTitle)
  end
  
  def mapVirtualKey(uCode, uMapType)
    f = Win32API.new('user32', 'MapVirtualKey', 'II', 'I')
    f.call(uCode, uMapType)
  end
  
  def sendKey(hWnd, aKeys)
    aKeys.each do |key_code|
      sendMessage(hWnd, WM_KEYDOWN, key_code, 1 + (mapVirtualKey(key_code, 0) << 16))
      sleep(0.1)
    end
    
    aKeys.reverse.each do |key_code|
      sendMessage(hWnd, WM_KEYUP, key_code, 0xc000_0001 + (mapVirtualKey(key_code, 0) << 16))
      sleep(0.1)
    end
  end
end


s = 'No$gba Emulator '
h = Win32.findWindow(nil, s)
while true
  4.times do |n|
    Win32.sendKey(h, [Win32::VK_SHIFT])
    sleep(0.5)
  end
  Win32.sendKey(h, [Win32::VK_A])
  sleep(0.1)
  3.times do |n|
    Win32.sendKey(h, [Win32::VK_SHIFT])
    sleep(0.5)
  end
  Win32.sendKey(h, [Win32::VK_D])
  sleep(0.1)
  Win32.sendKey(h, [Win32::VK_D])
  sleep(0.1)
  5.times do |n|
    Win32.sendKey(h, [Win32::VK_W, Win32::VK_NUMPAD4])
    sleep(0.5)
  end
end

2010年1月8日星期五

Windows 7 IPv6不完全折腾

按照网上很多人的说法,Windows 7下只需要开一个有管理员权限的控制台,执行类似如下两条命令即可:

netsh interface ipv6 isatap set router isatap.tsinghua.edu.cn
netsh interface ipv6 isatap set state enabled

然而很奇怪的是,ipconfig显示我已经有了一个2001开头的IPv6地址,ipv6.google.com却打不开。试了很多个isatap服务器都不行,正像只没头苍蝇一样在网上乱逛的时候,鬼使神差地敲了一句ping -6 ipv6.google.com。呃,居然有回复?!然后试着在浏览器里访问http://[2001:4860:c004::68],还真的可以打开……

C:\>nslookup www.kame.net
服务器:  google-public-dns-a.google.com
Address:  8.8.8.8

非权威应答:
名称:    www.kame.net
Addresses:  2001:200:0:8002:203:47ff:fea5:3085
          203.178.141.194


C:\>ping www.kame.net

正在 Ping www.kame.net [203.178.141.194] 具有 32 字节的数据:
来自 203.178.141.194 的回复: 字节=32 时间=111ms TTL=45
来自 203.178.141.194 的回复: 字节=32 时间=131ms TTL=45
来自 203.178.141.194 的回复: 字节=32 时间=99ms TTL=45
来自 203.178.141.194 的回复: 字节=32 时间=137ms TTL=45

203.178.141.194 的 Ping 统计信息:
    数据包: 已发送 = 4,已接收 = 4,丢失 = 0 (0% 丢失),
往返行程的估计时间(以毫秒为单位):
    最短 = 99ms,最长 = 137ms,平均 = 119ms

C:\>nslookup ipv6.google.com
服务器:  google-public-dns-a.google.com
Address:  8.8.8.8

非权威应答:
名称:    ipv6.l.google.com
Address:  2001:4860:c004::68
Aliases:  ipv6.google.com


C:\>ping ipv6.google.com
Ping 请求找不到主机 ipv6.google.com。请检查该名称,然后重试。

C:\>ping -6 ipv6.google.com

正在 Ping ipv6.l.google.com [2001:4860:c004::68] 具有 32 字节的数据:
来自 2001:4860:c004::68 的回复: 时间=401ms
来自 2001:4860:c004::68 的回复: 时间=400ms
来自 2001:4860:c004::68 的回复: 时间=401ms
来自 2001:4860:c004::68 的回复: 时间=401ms

2001:4860:c004::68 的 Ping 统计信息:
    数据包: 已发送 = 4,已接收 = 4,丢失 = 0 (0% 丢失),
往返行程的估计时间(以毫秒为单位):
    最短 = 400ms,最长 = 401ms,平均 = 400ms

nslookup能够获得IPv6地址,而且排在IPv4地址的前面,似乎DNS并没有什么问题。然而无论是ping还是浏览器,却都对IPv6地址视若无睹,就好像IPv6根本没有启用一样。

接下来抱着试一试的心情,我又往hosts文件里添加了几个IPv6地址,比如:2001:4860:c004::68 www.google.com

居然,好了?嗯,ping没问题了,浏览器也能正常打开。看起来还是DNS解析的问题,网络应用程序默认情况下无法取得IPv6地址。

可问题究竟出在哪?在网上翻了很久,仍然不明所以。我甚至开始怀疑,是不是自己网络环境的原因(我用没有独立IP的小区宽带,家里还有一个路由器,相当于是内网中的内网),但试着在Ubuntu里装上miredo,同样用Google DNS,同一个ISATAP服务器,一切正常。

看来,只能是Windows 7的设置有问题了。虽然解决办法到现在也没找到,不过至少hosts可用,先将就吧……

用上IPv6之后,我常用的绝大部分网络程序都没出什么问题,只有Privoxy。跑到官网上看了看,最新版的3.0.15是可以支持IPv6的,只是mingw32编译的Windows版还不行,需要修改代码。Socket编程我碰都没碰过,这可不是我擅长的领域,所以试着用Cygwin编译了一个Privoxy,能用。虽然没有GUI,不过那个窗口也没大用,继续将就……

2010年1月4日星期一

初尝黑苹果

最近手上多出来一块160G的硬盘,拿来干什么都太小,突然想起来还有黑苹果这种东西,正好对传说中Windows的GUI+Linux的Console很有兴趣,于是装上试了试。

手上没有刻录机,安装过程参考了远景论坛的这张帖子,基本没有遇到太大的问题,不过一些小地方还是耗费了不少精神。

Boot Think 2.3.18在OS X中安装至MBR无法引导,Chameleon 2.0 RC4虽然可以安装,但是有部分KEXT又会无法加载。最后索性多分了一个区装上Windows XP,把Boot Think放在Windows分区上。反正现在用的Windows 7也有部分软件表现不良,就当备用吧。

驱动方面方面,我用了如下这些KEXT:AppleNForceATA、ElliottForceLegacyRTC、fakesmc、NullCPUPowerManagement、OpenHaltRestart、PlatformUUID、VoodooHDA。

DSDT.aml虽然可以解决很多问题,可是需要针对不同主板不同BIOS版本进行修改,而网上的文档很杂乱,那个ACPI Patcher又很不好用,再加上找到ElliottForceLegacyRTC这个KEXT解决了BIOS Checksum Error的问题,所以我还是放弃了。

显卡驱动一开始用的是NVEnabler,可是总觉得中文小字体的表现很糟糕,颜色很淡又很模糊。找了一个EFI Studio生成显卡的EFI String,写入Boot Think的Darwin目录下的com.apple.Boot.plist(如果是com.apple.Boot__.plist,则改名之),感觉似乎稍好了些。

Update:关于中文字体颜色太淡的问题我已经放弃了。喜欢折腾的人去看看这个文件:/System/Library/Frameworks/ApplicationServices.framework/Frameworks/CoreText.framework/Resources/DefaultFontFallbacks.plist,这是设置默认字体的地方,跟Linux的fontconfig差不多。另外还有:defaults -currentHost write -globalDomain AppleFontSmoothing -int 1,取值1、2、3分别代表渲染度由淡至浓。

AMD的CPU还需要用Marvin's AMD Utility生成CPUID的补丁,引导至单用户模式(参数-s)打上补丁才能用iTunes和QuickTime。点菜单栏上的“关于本机”强制注销的问题,装上AppleSMBIOS和AppleSMBIOSEFI两个KEXT可以解决。

XCode 3.1.4在10.6下貌似有问题,装完以后/usr/bin下并没有gcc等东西,重下一个XCode 3.2之后问题解决。不过System Tools在AMD CPU的机器上不能装,会五国。(装上了也没关系,-x引导安全模式,运行/Developer/Applications/Performance Tools/CHUD下的CHUD Remover即可。)

安装部分大致就是这样,虽然启动有些慢(怀疑是AppleNForceATA的问题,可找不到可以替换的KEXT),不过已经完全可用了。感觉黑苹果的破解程度还是颇高,即使当成日常使用的系统,也完全没有问题。

至于使用感受,下次有精神再说吧……

Update:关于Boot Think无法引导Grub2(Ubuntu等)的问题,用任意一个十六进制编辑器打开Darwin/rc/grubloader2,搜索“menu.lst”,全部替换为“grub.cfg”即可。