偷梁换柱:在Bambook中阅读自有epub电子书

本文内容已过时,云梯0.98b版本开始,直接支持向Bambook中传送原生EPUB格式文件,不再需要用本文的方法去折腾了。

先写一句话指南,免得浪费有很强动手能力的技术青年们的时间:通过假DNS把res.gk.sdo.com劫持到本地,对于非.epub文件下载请求,反向代理到真正的res.gk.sdo.com上;对于.epub文件请求,你想把什么书传到Bambook上,就在本地把这个.epub通过HTTP返回出去吧。在Linux上,可以用nginx做Web服务器、反向代理和Rewrite,用dnsmasq或我下面提供的Python脚本做假DNS。

我也算是Bambook的老用户了,从它上市第一天起就开始使用,当年写的体验心得算是很给面子、非常委婉指出了它的一些致命缺点(对于我自己而言的),其中最重要的两条是:只有Windows-Only的客户端程序和不支持除了它自有的SNB以外的任何电子书格式。第一个问题,我通过盛大后来开放的SDK给calibre写了一些插件基本解决了(支持Windows/Linux/Mac OS X,使用方法在这里),第二个问题盛大后来自己开始支持PDF格式了,算是解决了一半,但如果再能支持一下epub格式就美好了。

在盛大去年发布的最后一版支持第三方书库(Feedbooks和古登堡计划)的固件中,某些人敏锐的注意到,从第三方书库下载的电子书明显有比SNB格式更丰富的排版,而且下载页面上也以很小的字体提示了说,这是epub格式的。由此可知,Bambook的固件已经可以支持epub的直读,剩下的问题就是怎么把epub书放进去了。

尝试过一些方法,比如把epub文件跟pdf文件一样,用SNB格式包裹起来再通过SDK传到Bambook上,总之都是失败。最后实在没有办法,决定用Wireshark抓包分析Bambook从Feedbooks下载图书的交互过程,从中做点坏事。

分析过程其实一点也不复杂,会用Wireshark的人应该都能搞定。我选了对Feedbooks的交互过程进行分析,分析的结果是,Bambook从第三方书库下载图书时,仍然会与盛大的服务器交互,并且最终是从盛大的服务器的URL上下载实际的文件(最新的分析结果是:可能从盛大的服务器,也可能从原始feedbooks的服务器,取决于盛大的服务器有没有缓存过这个文件。该分析未完全确定),所以简单的劫持对Feedbooks.com的访问是没用的。反复尝试,下面描述的方法是可行的(但很可能不是最简单的,懒得再深入研究了)。为照顾大部分Windows用户,下面以Windows环境来介绍。Linux用户请参考本文第一段自己折腾,应该更简单一些。

前提条件:有一台正常上网的电脑A,Bambook用无线上网并在上网过程中可以访问到电脑A。或者,有两台电脑(可以是虚拟机)A和B,A用来做服务器,B上安装云梯给Bambook共享网络,AB之间也可以相互访问。

1. 在电脑A上下载安装nginx for Windows

http://nginx.org/download/nginx-1.1.15.zip

下载后,解压缩到C:\,得到C:\nginx-1.1.15

2. 配置nginx,实现Web服务和反向代理

用记事本打开C:\nginx-1.1.15\conf\nginx.conf,把它修改成以下内容,其中的192.168.8.102为A电脑的IP地址,请自行替换

worker_processes  1;
events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;
    server {
        listen          80;
        server_name     res.gk.sdo.com;
        location / {
            proxy_pass        http://res.gk.cdn.sdo.com/;
            proxy_redirect    off;
            proxy_set_header  Host            $host;
            proxy_set_header  X-Real-IP       $remote_addr;
            proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
            rewrite ^.*/.*\.epub$ http://192.168.8.102:81/1.epub redirect;
        }
    }
    server {
        listen       81;
        location / {
            root   html;
            index  index.html index.htm;
        }
    }           
    sendfile        on;
    keepalive_timeout  65;
}

3. 启动nginx

双击运行C:\nginx-1.1.15\nginx.exe,用任务管理查看nginx进程,确认启动成功。如果用netstat -na检查,应该在80和81端口上有LISTENING。

4. 安装Python 2.7

http://www.python.org/getit下载Python Windows Installer并安装到C:\Python27目录。

5. 写Python脚本,建假DNS。把下面的内容存为C:\FakeDNS.py。同样,其中的192.168.8.102为A电脑的IP地址,请自行替换

#! /usr/bin/env python
# This code comes from
# http://code.activestate.com/recipes/491264-mini-fake-dns-server/
# with some modifications
import socket

class DNSQuery:
  def __init__(self, data):
    self.data=data
    self.domain=''
    tipo = (ord(data[2]) >> 3) & 15
    if tipo == 0:
      ini=12
      lon=ord(data[ini])
      while lon != 0:
        self.domain+=data[ini+1:ini+lon+1]+'.'
        ini+=lon+1
        lon=ord(data[ini])

  def respuesta(self, ip):
    packet=''
    if self.domain:
      packet+=self.data[:2] + "\x81\x80"
      packet+=self.data[4:6] + self.data[4:6] + '\x00\x00\x00\x00'
      packet+=self.data[12:]
      packet+='\xc0\x0c'
      packet+='\x00\x01\x00\x01\x00\x00\x00\x3c\x00\x04'
      packet+=str.join('',map(lambda x: chr(int(x)), ip.split('.')))
    return packet

if __name__ == '__main__':
  udps = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  udps.bind(('',53))
  try:
    while 1:
      data, addr = udps.recvfrom(1024)
      p = DNSQuery(data)
      if p.domain == 'res.gk.sdo.com.':
        ip = '192.168.8.102'
      else:
        ip = socket.gethostbyname(p.domain)
      udps.sendto(p.respuesta(ip), addr)
      print p.domain + "=>" + ip
  except KeyboardInterrupt:
    udps.close()

6. 启动假DNS

在命行提示符中运行:

C:\Python27\python.exe C:\FakeDNS.py

启动后应该会停在那里,有请求时会打印出解析结果。可以再开一个窗口用nslookup确认它的工作状态:对于除了res.gk.sdo.com以外的域名,都解析为正常IP地址;对于res.gk.sdo.com,解析为电脑A的IP地址。

7. Bambook设置

设置Bambook的无线连接,其中DNS服务器地址设为假DNS所在电脑A的IP地址。

如果采用两台电脑的方案,就把电脑B的网卡配置中的DNS指向电脑A的IP地址。然后连接Bambook与电脑B,让Bambook通过电脑B上网。

8. 把打算要传到Bambook上的EPUB电子书放到C:\nginx-1.1.15\html中,命名为1.epub。

9. Bambook上选择Feedbooks.com上任意一本Public Domain的图书,下载阅读。

找书->1.访问云中书城->*.切换到第三方书库->3.feedbooks->3.Public Domain Books->2.Recently Added->任选一本以前没有下载过的书->1. 免费试读本书。

搞定!返回书架后就可以看到你自己的epub电子书已经放进了Bambook,可以尽情的阅读了。

重要更新:如果有时候发现下载下来的还是第三方书库的书,不是被自己替换过的。也许是因为盛大服务器上还没缓存过这本书,所以我们劫持盛大服务器就没用了。暂时的解决方案是换一本排在比较前面的书(这些书有缓存的可能性比较大),或者到书架上把刚刚下载的书删掉,重新再去下同一本,那时这书可能已经被缓存在盛大的服务器上了,这样我们的劫持就有效了。

其它说明:

1. 不能只用一台电脑或不用Bambook无线上网搞定的原因是:Bambook USB共享上网中的NAT服务中带了DNS服务,会与我们的假DNS抢53端口。

2. 制作在Bambook上阅读的EPUB电子书时,内部单个文件不要太大,不然翻页速度会让你抓狂。如果用calibre转换格式,可以在EPUB格式输出插件的设置中设定页面文件分割大小。

3. 虽然我们偷换掉了EPUB文件,但似乎Bambook还是认为我们是下载了我们所选的书籍并会记住这个状态,再次选择同一本书时,就不会再下载了。所以每次传书都得从Feedbooks中找一本以前没有用过书才行。

4. 这个方法只在Bambook SD928经典白色版上验证有效,未在全键盘版上验证过。且盛大只要服务器那里稍微动动手脚就可以让它失效,所以不保证有效期。并欢迎大家寻求更简单完美的解决方案。

5. 我曾经有过另一个在Bambook上阅读富格式书籍的想法,就是把HTML(或EPUB、MOBI等,反正本质都是HTML)页面封装成Bambook的Widget,用JavaScript在HTML中实现阅读器应该有的功能。现在是有SDK可以把Widget传入Bambook的,所以用这个方法完全可以写软件(或者给calibre写个插件,非常简单)自动完成常见的电子书格式转成Widget和传入Bambook的全过程。这个方法我已经做过POC,是完全可行的。但我没有能力用Java Script写出一个让我自己满意的阅读程序,所以最终放弃了。

6. 本文描述的所有内容仅供技术宅折腾和探讨,不愿折腾的人、Kindle或其它功能强大的阅读器拥护者请勿喷。谢谢。

GNU.org的本地化框架

前几天Junwen同学翻译了Android设计指南的网页,由于他用的SourceForge的Web空间访问限额有限,我在自己的服务器上给他做了一个镜象站点。于是就想到了多镜象站点同步以及本地化语言版本与上游英文版网页之间的同步问题,正好我是GNU.org中文翻译组的一员,在这里跟大家简单分享一下GNU网站的本地化框架,也算是补交了去年在NJLUG活动中曾经承诺过的一份作业。

以前,GNU网站的本地化是由各本地化翻译小组手工完成维护的,翻译组协调人确定要翻译的页面,翻译组成员自己从GNU网站或CVS上下载html网页,直接在网页上进行翻译,然后把html网页提交到翻译组的CVS中,经过审阅后,由具有GNU网站提交权限的人直接把html文件提交到GNU网站上。

这种简单的做法带来的问题主要有两个:

  • 本地化版本的维护问题。由于全部是手工操作,很容易出现本地化版本与英文原始版本之间不同步的问题,而且这样的不同步很难被发现和跟踪。
  • 网站改版的问题。当英文版网站进行大规模网页页面结构化调整时,即使页面上文字内容没有变,所有的翻译版本都需要全部手工进行更新。

大约是2008年开始,GNU开发了GNUnited Nation,简称GNUN,作为GNU网站本地化框架。GNUN实质上是一些脚本、DTD和Makefile,通过这些脚本,对GNU的网页自动进行各种必要的字符串提取、替换和合法性验证。

GNUN的实现基于在自由软件本地化中广为使用的gettext,GNUN从原始的英文HTML网页中提取需要翻译的字符串,生成gettext的.pot文件,各语言翻译组基于.pot文件翻译形成对应各自的语言的.po文件,最后GNUN系统再依据这些po文件生成相应语言版本的本地化网页。

     .---<--- * Original ARTICLE.html
     |
     |   .---> ARTICLE.pot ---> * ARTICLE.LANG.po --->---.
     `---+                                               |
         `--->---.   .------<----------------------------'
                 |   |
                 |   `---.
                 |       +---> Translated ARTICLE.LANG.html
                 `-------'

这是GNUN的工作流程图,其中需要人工干预的只有图中打星号的两个步骤,其它都可以自动化完成。

  • 编写原始ARTICLE.html:由GNU的Web维护小组完成。
  • 翻译ARTICLE.LANG.po文件:由GNU各语言翻译组完成。

对于翻译组,GNUN框架中也提供了很轻量级的脚本用于从合并更新.po文件和自动检查.po文件的翻译进度,用起来很方便。

其实,从现在的眼光看,用gettext做网页的本地化也并不是什么新鲜的事儿,比如大家现在在用的WordPress就是这样处理的。不过,你有没有注意到GNUN跟WordPress中gettext的用法的不同呢?

在WordPress(或其它大部分用gettext的软件项目)中,要翻译的字符串资源常常会用类似于_(“”)这样的形式来标记,这样xgettext工具才能从中正确的提取出需要的翻译的资源,当然也是为了程序编译后运行起来时真正能把字符串完成翻译。而GNU的那些静态HTML网页上并没有也不方便用这样的标记去标记所有的字符串,GNUN实际上是一个特别针对GNU网站做的框架,它直接通过一些预定义的固定规则去处理GNU网页上的内容,在已知网页内容架构的情况下,跟据HTML页面中各标签本身的语意,去提取字符串内容。.po文件翻译好后,也是由固定的脚本直接把字符串替换回原始的网页,从而实现页面翻译。所以,暂时GNUN其实并不能成为一个通用的网站本地化框架,目前它只能用在GNU的网站上。但是这样的一套处理思路,也许在有些特定的情况下还是值得借鉴的。

用GNU screen管理多台Linux主机

需要在Windows下管理一小堆Linux机器,开一堆PuTTY窗口的方式显然是会让人无法忍受的,怎么办?

否决,能不用商业软件就不用商业软件,尤其是在没有人愿意为License买单时。虽然xshell个人和教育使用是免费的,功能也足够强大,但我花了N个小时改它和Emacs的配置,还是没有办法在它的终端下完美的使用Emacs,Emacs有那么多变态的快捷键,怎么配总有那么几个快捷键不能正常工作。虽然知道配不好是我自己的错,但能力精力有限,放弃。

  • 用第三方PuTTY的外挂或窗口管理器

你真的确信有一个很美好的PuTTY多窗口管理器么?我把PuTTY官网上相关话题中给出的链接中的第三方程序都试了一遍,真没有靠谱的。

想来想去,最终还是决定采用开一个PuTTY窗口,先连上一台Linux主机,然后在上面启动GNU screen再连接到多台主机。

以前对screen的认识,是用来在下载机上运行rtorrent,所以screen的第一个用处是把程序放到后台去运行,不受SSH连接断开的限制,随时连上去都可以恢复到断开连接时的状态,这对于维持一个稳定持久的工作环境很有用。

要让screen可以做为一个多连接管理器,还需要花点精力,这可以通过配置~/.screenrc来实现。

  • 让我知道我正在screen环境下工作,并知道我开了哪些连接

hardstatus alwayslastline "%{=b}%{G} Screen(s): %{b}%w %=%{kG}%C%A %D, %M/%d/%Y "

显示状态栏,并列出当前打开的窗口(创建的连接)

  • 把screen的默认快捷键由Ctrl-A换成Ctrl-Z

escape ^Zz

在我的Emacs配置中最用不到一个快捷键是Ctrl-Z,所以把screen的快捷键前缀由非常常用的Ctrl-A换成Ctrl-Z,真的需要Ctrl-Z时,只要按Ctrl-Z z就可以了。

  • 启动时就打开多个窗口并连接到各远程主机上,并为每个连接所在窗口起个名字

screen ssh user@host1
title host1
screen ssh user@host2
title host2

这样启动screen时就会分别连上host1和host2,把设置好对应的窗口名字,在状态栏上可见。

  • 远程连接关闭(或意外断开)时,不要关闭窗口

zombie "^["
默认情况下,一个连接断开后,screen会关闭这个连接所在的窗口,如果要再连就得重新打开新窗口手工创建连接。用了zombie配置后,某个远程连接断开后对应的窗口仍会保留,切换到对应窗口后可以按Ctrl-[手工关闭窗口,也以Ctrl-@重新激活窗口(连接到远程主机)。

  • screen启动完成后默认切换回第1个窗口

select 0
完成这一堆配置后,用起来就已经很得心应手了。按Ctrl-Z 0到Ctrl-Z 9可以在各窗口间切换,按Ctrl-Z Ctrl-Z可以在最近两个窗口间跳转。Ctrl-Z d把screen环境放到后台,返回shell。Ctrl-Z c新建一个本地shell窗口。如果需要在多个连接间复制文本,可以用Ctrl-Z <ESC>进入复制/滚屏模式,用回车键设定复制范围,然后用C-z ]贴粘到目标位置。

完成一切设置后,就可以在PuTTY连上终端后执行screen -d -R来创建或连接到screen环境了,用完了可以随手把PuTTY窗口关掉,再连上去时就恢复到上次工作的状态,非常方便。

在我完成了screen的学习和配置后,有人为我推荐了另一个终端复用程序tmux,它比screen的功能更为强大,鉴于screen已经满足了我的需求的前提下,我没有再去折腾tmux,如果你是第一次尝试使用终端复用程序,也不妨试一下tmux。

五周年-我的独立博客

五年前的今天,因为“自由软件综合症”发作,在朋友给的fx.jiuzhe.com域名上用Emacs+Muse创建了我的独立博客,通过几次改进,最后就形成了今天大家所看到的FreeMindWorld.com。

为了折腾,我一直在享受和忍受着Emacs+Muse所建的Blog的各种方便和不方便之处。这次,终于是痛定思痛,放弃了个性,把整个Blog系统切换到了WordPress上面,不过还是花了不少的时间去定制WordPress,让整个网站的风格与原来大体上保持一致,所有页面的链接也尽量保持跟原来的一样,免得造成太多的死链。

这次迁移半自动半手动的迁移了原来FreeMindWorld.com上和我以前CSDN博客和Live Space上的文章169篇和465条评论,非常艰辛。现在我这个博客上包含了以下的文章:

  • 2002年2月至2003年7月在CSDN文档中心发表的文章。
  • 2004年6月至2007年2月在CSDN博客和Live Space上写的文章。
  • 2007年2月以后FreeMindWorld.com上的文章。

所以,如果把当年给CSDN文档中心写文章也算上的话,这次也可以算是个10周年纪念。在转移这些文章时,不可避免的把它们都回顾了一遍,很有感触,人生的十年、互联网的十年,变化都很大。

五年,见证了微博从兴起到到兴旺。十年,见证了博客从兴起到衰落。谁会知道下一个五年又会发生什么。

上个月关停了我的另一台运行了5年的ASP虚拟主机,上面原本运行着一些从1999年就开始做的网站,但是这些网站早就没有了任何的访问量,只有发Spam的机器人坚持不懈的在留言板上活跃着。那台主机上还存放着的我早年做的一些静态个人主页,这次也一并迁移了过来,放在这里怀个旧吧。点击图片就可以访问相关的网页。三个版本的个人主页除了长的不一样,内容几乎是一样的。那年头,做个人主页就是做页面设计,内容有什么并不重要,现在可大不一样了。

个人主页第一版

个人主页第一版 1999年2月19日-1999年5月

个人主页第二版

个人主页第二版 1999年5月-2000年5月

个人主页第三版

个人主页第三版 2000年5月-?(不记得什么时候它就不存在了)