本地机器上运行远程X程序、X环境

November 6th, 2009

X11Forwarding
X11转发,在本地机器(Linux/Windows)上运行远程服务器上的X程序

***Linux***

方法1、2是在本地Linux机器上的当前窗口上显示远程服务器上的X程序(单独窗口的形式);
方法2、3使用SSH的X11转发,因此确保远程服务器上的/etc/ssh/sshd_config的X11Forwarding为yes;
方法3、4是在本地Linux上新的控制台下的新的X server上运行远程服务器的完整X环境;
方法1、4需要设置DISPLAY环境变量;
* 所有方法在两端都不需要root权限
* 192.168.1.xx 为本地Linux的IP

方法一:
本地Linux:
$ xhost +
$ ssh user@remotehost
远程服务器:
$ export DISPLAY=192.168.1.xx:0[.0]
$ xeyes& ; nautilus&

* xhost + 允许远程X程序在当前X server上允许,详细man

方法二:
本地Linux:
$ xhost +
$ ssh -X user@remotehost
远程服务器:
$ xeyes&; konqueror&

方法三:
本地Linux:
$ X :12.0 vt12 2>&1 > /dev/null &
$ xterm -display :12.0 -e ssh -X user@remotehost &
切到Ctrl+Alt+F12,在里面的xterm里运行 xeyes 等X程序,或者gnome-session、
startkde等运行完成的X环境

方法四:
本地Linux:
$ X :11.0 vt11 2>&1 > /dev/null &
$ xterm -display :11.0 &

切到Ctrl+Alt+F11在xterm里运行:
$ xhost +

本地Linux:
$ ssh user@remotehost
$ export DISPLAY=192.168.1.xx
$ xeyes& or gnome-session& or startkde&

一般来讲用ssh -X比较方便,方法1、4只是在探讨原理,实际使用相对麻烦。
根据你的不同需求选择方法2、3,在本地只单独运行某些X程序,还是需要完整的X环境。

***Windows***

Windows下需要如下软件:
X server – 本文使用Xming
ssh client – 本本使用Putty

Putty设置:
Connect -> SSH -> X11,勾选”允许 X11 映射”,填入localhost:0.0,MIT-Magic-Cookie-1协议

Xming设置:
如果你只运行单独的X程序,选择哪种窗口模式无所谓;
如果你需要完整的X环境,请选择单窗口或者全屏模式;
一定要勾选上”No Access Control”

最后用Putty登录到远程服务器,运行xeyes等独立X程序,或者gnome-session、startkde完整X环境。

References:
[1] Linux下DISPLAY环境变量的作用
[2] 用SSH实现X11转发

Opera里域名跳转

October 31st, 2009

用twitter多了就会发现有很多短网址,而且一些常见的bit.ly、j.mp都被墙了,这非常的杯具。今天在推特上看到@feelingluck兄写的一个短域名跳转的实现,方法很好,比起我原先想的好。东西在此:http://lab.gracecode.com/free-jump.php

不过这个方法在Opera里遇到了问题。Opera对于指向localhost(127.0.0.1)的域名会直接给你空白页面,而不是像firefox那样Unable to connect。你得不到任何关于此域名页面的信息,比如上面提到方法里的location.href,也就无法通过location.href把短网址传递到@feelinglucky的网页实现跳转了。

但Opera就是Opera,她依然有方法达到此目的,只需在你的menu.ini文件的[Link Popup Menu]段加一行:

Item, free-dump=New page & Go to page, "javascript:(function(){c=document.createElement('script');c.src='http://lab.gracecode.com/free-jump.php?url='+encodeURIComponent('%l');document.body.appendChild(c);})()"

使用的时候对着短网址右击就可以了。效果如下图,而且不需要修改hosts:
free-jump-opera

shortname以及自动挂载

August 18th, 2009

一切问题来自我前些天在优盘上搞的移动版的wiki(采用DokuWiki),插上优盘后,直接编辑我的wiki。但是发现有些页面是空白,功能不正常。后来发现是因为某些文件文件名在Linux下挂载后发生了变化,比如:”JSON.php”变成了”json.php”,造成了wiki无法正常运行 -______-!!

/etc/mtab显示有一个选项是shortname=lower,于是顺藤摸瓜google,发现通过手工挂载用参数shortname=mixed就正常。Linux下mount对fat/vfat文件系统(通常就是优盘的文件系统格式)默认挂载参数shortname的值就是lower。其实这个不太好,所有有人给了patch建议改成默认为shortname=mixed,可以看如下代码,更多见LKML里的讨论

diff --git a/fs/fat/inode.c b/fs/fat/inode.c
index 8970d8c..f9af501 100644
--- a/fs/fat/inode.c
+++ b/fs/fat/inode.c
@@ -971,7 +971,7 @@  static int parse_options(char *options, int is_vfat, int silent, int *debug,
 	opts->codepage = fat_default_codepage;
 	opts->iocharset = fat_default_iocharset;
 	if (is_vfat) {
-		opts->shortname = VFAT_SFN_DISPLAY_LOWER|VFAT_SFN_CREATE_WIN95;
+		opts->shortname = VFAT_SFN_DISPLAY_WINNT|VFAT_SFN_CREATE_WIN95;
 		opts->rodir = 0;
 	} else {
 		opts->shortname = 0;

这个shortname参数都是因为有8.3文件名这种东西的存在而存在的,可以参考Wikipedia上的这篇文章
还没完,每次手工挂载太蠢了。所以我想到了HAL,当时觉得可以改变默认的mount挂载参数(但事实不是)。还找到了如下的HAL policy:

<?xml version="1.0" encoding="UTF-8"?>
 
<deviceinfo version="0.2">
  <device>
    <match key="block.is_volume" bool="true">
      <match key="volume.fsusage" string="filesystem">
        <match key="volume.fstype" string="vfat">
          <merge key="volume.policy.mount_option.shortname="
type="string">shortname=mixed</merge>
        </match>
      </match>
    </match>
  </device>
</deviceinfo>

但是没有成功,lshal里看到了 volume.policy.mount_option.shortname= = ‘mixed’ (string) 这样的信息,可是我在Konqueror里打开仍然是以lower挂载的。在cppgx大牛的提醒和google到的这篇文章中,找到了两个原因:

  1. KDE这类DE会自己管理mount参数
  2. volume.policy.mount_option.shortname 这种写法已废弃

对于第一点,你可以在插上优盘后,打开Konqueror,到Storage Media里右击你的优盘盘符,属性里有一个挂载的标签,里面就有一些自定义的参数,其中就有shortname,而且默认就是lower。因此,如果你使用KDE,那么你可以在这个标签里将shortname改成mixed就可以正常显示优盘的文件名了。不过这不是全局的,你换了个优盘后,还得改一下。这些参数保存在~/.kde/share/config/mediamanagerrc

对于第二点,难怪在最新的hal-spec里打死找不到类似的方法。

到这里我才渐渐明白HAL是无法改变mount的挂载参数的,它可以发现硬件,然后提供给上层应用关于硬盘设备的很多信息,比如什么文件系统,可用的挂载参数等。像KDE这种能否自动挂载的,一定是从HAL那儿获得信息,然后有自己的程序去最终挂载设备,然后显示给用户。我很想看看KDE到底是怎么处理这些挂载参数的,所以经过一番长时间的搜索终于找到了相关代码(从没看过KDE代码,也不懂C++,而且觉得KDE网站搜索做的也不太好):
kdebase-3.5.10/kioslave/media/mediamanager/halbackend.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
QStringList HALBackend::mountoptions(const QString &name)
{
    const Medium* medium = m_mediaList.findById(name);
    if (medium && !isInFstab(medium).isNull())
        return QStringList(); // not handled by HAL - fstab entry
 
    KConfig config("mediamanagerrc");
    config.setGroup(name);
 
    char ** array = libhal_device_get_property_strlist(m_halContext, name.latin1(), "volume.mount.valid_options", NULL);
    QMap<QString,bool> valids;
 
    for (int index = 0; array && array[index]; ++index) {
        QString t = array[index];
        if (t.endsWith("="))
            t = t.left(t.length() - 1);
        valids[t] = true;
        kdDebug() << "valid " << t << endl;
    }
    libhal_free_string_array(array);
    QStringList result;
    QString tmp;
 
    QString fstype = libhal_device_get_property_QString(m_halContext, name.latin1(), "volume.fstype");
    if (fstype.isNull())
        fstype = libhal_device_get_property_QString(m_halContext, name.latin1(), "volume.policy.mount_filesystem");
 
    QString drive_udi = libhal_device_get_property_QString(m_halContext, name.latin1(), "block.storage_device");
 
    bool removable = false;
    if ( !drive_udi.isNull() )
        removable = libhal_device_get_property_bool(m_halContext, drive_udi.latin1(), "storage.removable", NULL)
                     || libhal_device_get_property_bool(m_halContext, drive_udi.latin1(), "storage.hotpluggable", NULL);
 
    config.setGroup(drive_udi);
    bool value = config.readBoolEntry("automount", false);
    config.setGroup(name);
 
    if (libhal_device_get_property_bool(m_halContext, name.latin1(), "volume.disc.is_blank", NULL)
        || libhal_device_get_property_bool(m_halContext, name.latin1(), "volume.disc.is_vcd", NULL)
        || libhal_device_get_property_bool(m_halContext, name.latin1(), "volume.disc.is_svcd", NULL)
        || libhal_device_get_property_bool(m_halContext, name.latin1(), "volume.disc.is_videodvd", NULL)
        || libhal_device_get_property_bool(m_halContext, name.latin1(), "volume.disc.has_audio", NULL))
        value = false;
 
    result << QString("automount=%1").arg(value ? "true" : "false");
 
    if (valids.contains("ro"))
    {
        value = config.readBoolEntry("ro", false);
        tmp = QString("ro=%1").arg(value ? "true" : "false");
        if (fstype != "iso9660") // makes no sense
            result << tmp;
    }
 
    if (valids.contains("quiet"))
    {
        value = config.readBoolEntry("quiet", false);
        tmp = QString("quiet=%1").arg(value ? "true" : "false");
        if (fstype != "iso9660") // makes no sense
            result << tmp;
    }
 
    if (valids.contains("flush"))
    {
        value = config.readBoolEntry("flush", fstype.endsWith("fat"));
        tmp = QString("flush=%1").arg(value ? "true" : "false");
        result << tmp;
    }
 
    if (valids.contains("uid"))
    {
        value = config.readBoolEntry("uid", true);
        tmp = QString("uid=%1").arg(value ? "true" : "false");
        result << tmp;
    }
 
	if ( valids.contains("locale") )
	{
		value = config.readBoolEntry( "locale", true );
		tmp = QString( "locale=%1" ).arg( value ? "true" : "false" );
		result << tmp;
	}
 
    if (valids.contains("utf8"))
    {
        value = config.readBoolEntry("utf8", true);
        tmp = QString("utf8=%1").arg(value ? "true" : "false");
        result << tmp;
    }
 
    if (valids.contains("shortname"))
    {
        QString svalue = config.readEntry("shortname", "lower").lower();
        if (svalue == "winnt")
            result << "shortname=winnt";
        else if (svalue == "win95")
            result << "shortname=win95";
        else if (svalue == "mixed")
            result << "shortname=mixed";
        else
            result << "shortname=lower";
    }
 
    if (valids.contains("sync"))
    {
        value = config.readBoolEntry("sync", ( valids.contains("flush") && !fstype.endsWith("fat") ) && removable);
        tmp = QString("sync=%1").arg(value ? "true" : "false");
        if (fstype != "iso9660") // makes no sense
            result << tmp;
    }
 
    if (valids.contains("noatime"))
    {
        value = config.readBoolEntry("atime", !fstype.endsWith("fat"));
        tmp = QString("atime=%1").arg(value ? "true" : "false");
        if (fstype != "iso9660") // makes no sense
            result << tmp;
    }
 
    QString mount_point = libhal_device_get_property_QString(m_halContext, name.latin1(), "volume.mount_point");
    if (mount_point.isEmpty())
        mount_point = libhal_device_get_property_QString(m_halContext, name.latin1(), "volume.policy.desired_mount_point");
 
    mount_point = config.readEntry("mountpoint", mount_point);
 
    if (!mount_point.startsWith("/"))
        mount_point = "/media/" + mount_point;
 
    result << QString("mountpoint=%1").arg(mount_point);
    result << QString("filesystem=%1").arg(fstype);
 
    if (valids.contains("data"))
    {
        QString svalue = config.readEntry("journaling").lower();
        if (svalue == "ordered")
            result << "journaling=ordered";
        else if (svalue == "writeback")
            result << "journaling=writeback";
        else if (svalue == "data")
            result << "journaling=data";
        else
            result << "journaling=ordered";
    }
 
    return result;
}

第10行就是获取HAL提供的有效参数(lshal | grep valid_options);
第13到19行的for循环将各个参数放到valids数组中;
第21行的QStringList result;是所有参数/值对列表;
第92行就是和shortname有关的了。它先是读取配置文件~/.kde/share/config/mediamanagerrc中某个设备的shortname参数值,如果没有默认就是lower。看来KDE里处理shortname也是如此。

不过从上述代码来看,提供给KDE配置mount参数仍然不是很多,比如dmask、fmask等都没有。通过HAL还是可以添加所有的参数到valid_options。

* BTW,上述代码是KDE 3.5.10环境下的,KDE 4默认的shortname已经是mixed了。不过不再是mediamanager了,相关代码还没找到去看。

最后提供个方法修改mount任意挂载参数,以挂载vfat文件系统为例:

$ cat /sbin/mount.vfat
#!/bin/sh
/sbin/mount -i -t vfat -o umask=0000,fmask=0111,dmask=0000,shortname=mixed "$@"

注意,这是全局修改vfat的挂载参数。KDE最后还是调用mount挂载的设备,因此有了这个文件你就可以自定义KDE等不支持的参数了。

EOF

Test

August 6th, 2009

2009/08/06 13:44

修改Twitter Opera Widget的API地址

July 30th, 2009

看到@yegle兄把TwitterFox的API改成了第三方的twitter api proxy,想到Opera的那个Twitter Widget估计也可以改。

找到Twitter Opera Widget的wgt文件,发现它其实就是个zip,解压后把所有文件浏览了遍,基本确定是要对script/twitter-api.js这个文件动手。它原来是直接调用Twitter官方的API,现在我通通把http[s]://twitter.com的地方改成第三方API,比如http://nest.onedd.net/api。嗯,应该是这样。

可是,它竟然提示”Could not contact Twitter.”。我没想通为啥不行,直接调用nest.onedd.net的API是可以的。后来google、抓包都没啥结果。源代码也没看出啥不对劲的。然后跑到论坛的Widget,发现竟然已有前人做了此事[1],主要方法就是上面说的。不过,关键得在config.xml里加一句“nest.onedd.net”。OMG,终于可以了!

host标签是限定widget可访问的主机名。文档[2]中明确注明了这点,可我懒得去翻,结果就是为了这个弄了一个上午 =.=!! 不看文档的下场。。。

我对原来的Twitter Opera widget_7206_3.22.wgt做了修改,重新打包,想直接用的朋友点这里下载。
下面贴个diff就能明白对原来的Twitter Opera Widget做了些啥:

diff -u -r Twitter Opera widget_7206_3.22/config.xml Twitter Opera widget_7206_3.22-nest.onedd.net/config.xml
--- Twitter Opera widget_7206_3.22/config.xml	2009-06-05 11:00:34.000000000 +0800
+++ Twitter Opera widget_7206_3.22-nest.onedd.net/config.xml	2009-07-30 10:46:22.000000000 +0800
@@ -17,9 +17,10 @@
       <protocol>http</protocol>
       <host>api.twitter.com</host>
       <host>twitter.com</host>
+      <host>nest.onedd.net</host>
       <host>maps.google.com</host>
       <host>widgets.opera.com</host>
       <host>search.twitter.com</host>
     </access>
   </security>
-</widget>
\ No newline at end of file
+</widget>
diff -u -r Twitter Opera widget_7206_3.22/script/twitter-api.js Twitter Opera widget_7206_3.22-nest.onedd.net/script/twitter-api.js
--- Twitter Opera widget_7206_3.22/script/twitter-api.js	2009-06-05 11:00:34.000000000 +0800
+++ Twitter Opera widget_7206_3.22-nest.onedd.net/script/twitter-api.js	2009-07-30 10:46:50.000000000 +0800
@@ -57,7 +57,7 @@
     publicTimeline: function(callback, errorCallback) {
       makeRequest({
         method: 'GET',
-        url: 'http://twitter.com/statuses/public_timeline' + format,
+        url: 'http://nest.onedd.net/api/statuses/public_timeline' + format,
         callback: callback,
         errorCallback: errorCallback
       }, true);
@@ -69,7 +69,7 @@
 
       makeRequest({
         method: 'GET',
-        url: 'https://twitter.com/statuses/friends_timeline' + format + query,
+        url: 'http://nest.onedd.net/api/statuses/friends_timeline' + format + query,
         headers: {
           'Cache-Control': 'no-cache'
         },
@@ -86,7 +86,7 @@
 
       makeRequest({
         method: 'GET',
-        url: 'https://twitter.com/statuses/user_timeline' + format + query,
+        url: 'http://nest.onedd.net/api/statuses/user_timeline' + format + query,
         headers: {
           'Cache-Control': 'no-cache'
         },
@@ -101,7 +101,7 @@
     show: function(id, callback, errorCallback) {
       makeRequest({
         method: 'GET',
-        url: 'https://twitter.com/statuses/show/' + id + format,
+        url: 'http://nest.onedd.net/api/statuses/show/' + id + format,
         username: this.username,
         password: this.password,
         callback: callback,
@@ -115,7 +115,7 @@
 
       makeRequest({
         method: 'POST',
-        url: 'https://twitter.com/statuses/update' + format + query,
+        url: 'http://nest.onedd.net/api/statuses/update' + format + query,
         headers: {
           'Content-Type': 'application/x-www-form-urlencoded'
         },
@@ -134,7 +134,7 @@
 
       makeRequest({
         method: 'GET',
-        url: 'https://twitter.com/statuses/mentions' + format + query,
+        url: 'http://nest.onedd.net/api/statuses/mentions' + format + query,
         username: this.username,
         password: this.password,
         callback: callback,
@@ -146,7 +146,7 @@
     destroy: function(id, callback, errorCallback) {
       makeRequest({
         method: 'DELETE',
-        url: 'https://twitter.com/statuses/destroy/' + id + format,
+        url: 'http://nest.onedd.net/api/statuses/destroy/' + id + format,
         username: this.username,
         password: this.password,
         callback: callback,
@@ -160,7 +160,7 @@
     friends: function(callback, errorCallback) {
       makeRequest({
         method: 'GET',
-        url: 'https://twitter.com/statuses/followers' + format,
+        url: 'http://nest.onedd.net/api/statuses/followers' + format,
         username: this.username,
         password: this.password,
         callback: callback,
@@ -174,7 +174,7 @@
     directTimeline: function(callback, errorCallback) {
       makeRequest({
         method: 'GET',
-        url: 'https://twitter.com/direct_messages' + format,
+        url: 'http://nest.onedd.net/api/direct_messages' + format,
         username: this.username,
         password: this.password,
         callback: callback,
@@ -188,7 +188,7 @@
     verifyCredentials: function(username, password, callback, errorCallback) {
       makeRequest({
         method: 'HEAD',
-        url: 'https://twitter.com/account/verify_credentials' + format,
+        url: 'http://nest.onedd.net/api/account/verify_credentials' + format,
         username: username,
         password: password,
         callback: callback,
@@ -200,7 +200,7 @@
     endSession: function(callback) {
       makeRequest({
         method: 'POST',
-        url: 'http://twitter.com/account/end_session',
+        url: 'http://nest.onedd.net/api/account/end_session',
         callback: callback,
         errorCallback: callback
       });
@@ -212,7 +212,7 @@
     createFriendship: function(id, callback, errorCallback) {
       makeRequest({
         method: 'POST',
-        url: 'https://twitter.com/friendships/create/' + id + format,
+        url: 'http://nest.onedd.net/api/friendships/create/' + id + format,
         username: this.username,
         password: this.password,
         callback: callback,
@@ -224,7 +224,7 @@
     destroyFriendship: function(id, callback, errorCallback) {
       makeRequest({
         method: 'DELETE',
-        url: 'https://twitter.com/friendships/destroy/' + id + format,
+        url: 'http://nest.onedd.net/api/friendships/destroy/' + id + format,
         username: this.username,
         password: this.password,
         callback: callback,
@@ -236,7 +236,7 @@
     friendshipExists: function(id, callback, errorCallback) {
       makeRequest({
         method: 'POST',
-        url: 'https://twitter.com/friendships/exists/' + id + format,
+        url: 'http://nest.onedd.net/api/friendships/exists/' + id + format,
         username: this.username,
         password: this.password,
         callback: callback,

[1] Opera Twitter Widget 复活手记 – http://bbs.operachina.com/viewtopic.php?f=38&t=49093
[2] Opera Widget 规范 1.0 第三版 – http://kb.operachina.com/node/98#xml_security

我的常用Twitter应用列表

July 28th, 2009

伟大的国家都是相似的,流氓的政府各有各的无耻行径。

下面列出了我常用的Twitter应用服务,做个备份,也方便路人 ;-)

浏览器网页类:

桌面客户端:

手机端:

  • Opera Mini – Opera公司荣誉产品,直接访问twitter.com即可
  • Twibble – 速度很快,功能强大,不过看不了@自己和D自己的推

Email方式:

  • TwitterMail – 非常赞!可以发推、图片到推特,@你自己邮件提醒(非即时),获得朋友的timelines,发送大于140字的推,定时发送,推特挂了也不要紧,会帮你暂存,等好了再发上去。

另外,还有SOCIALSCOPE,Gravity等据说异常强大的应用,可惜我还没用过。

Twitter API Proxy:

BTW,有了推特wiki我更懒的更新这个博客了 :(

乌龙

May 14th, 2009

上周末发了篇博文,希望有朋友能看到并帮我测试一下。结果至今无一个留言 :( 今天越发不对劲了,从别的超链接到我这个网站统统显示403,被禁止访问!而通过手工在浏览器的地址栏输入一摸一样的地址却能正常访问。。。我把这个疑问发到了twitter上,很快 @rainux 提醒我是不是限制了HTTP_REFERER。我立马登录空间,查看.htaccess的内容,确实有两条HTTP_REFERER的规则在里面。不过我对这个HTTP_REFERER很陌生,为了确定是否是它引起的,我先后把我自己添加的规则和HTTP_REFERER注释掉。结果正如 @rainux 所说。

RewriteCond %{HTTP_REFERER} !^$
RewriteCond %{HTTP_REFERER} !^https?://(www\.)?vvoody.org/?.*$ [NC]
RewriteRule \.*$ - [F,NC]

G了一下,明白了HTTP_REFERER干嘛的。上面的两条规则意思是HTTP_REFERER不为空或者不是从vvoody.org链接来的统统禁止访问。因为从浏览器输入地址时HTTP_REFERER为空,因此不会被禁止访问。

唉,这个乌龙好啊!

参考:
http://ted.is-programmer.com/posts/4460.html
http://zh.wikipedia.org/wiki/HTTP_referrer
http://www.fwolf.com/blog/post/320

简单实现tinyurl.com类短网址服务

May 10th, 2009

常用Twitter或者饭否之类服务的朋友都应该知道tinyurl的大名吧~ 简言之就是将长长的网址缩短,变成这个样子:http://tinyurl.com/qn8v7y

如果在自个儿的域名上实现一下类似的服务,应该很好玩~ 现在我已经做好了,只需4个文件配合即可工作,有兴趣的朋友可以继续往下看。测试方法,在浏览器中输入 http://vvoody.org/cmprzurl.py?longurl=http://vvoody.org/blog/?p=265 ,在longurl=后添上你想缩短的地址即可得到一个短网址。这篇文章缩短后的地址就是:http://vvoody.org/baJjPy 哈哈真巧,Py 结尾!

cmprzurl-example

原理看下图:
structure_of_cmprzurl
点击看大图

关键在于.htaccess规则,它负责重写地址。比如我的域名 http://vvoody.org/ 下面有 blog 等目录和文件,得保证这些能正常访问,也得把 3pHj9A 这样的字符串交给后台脚本处理。这个.htaccess规则之前我写了一堆正则,却都无法满足上述要求,后来在这篇文章中找到了方法,就两条,Apache 很好很强大!

curl -I http://vvoody.org/baJjPy 可以看到 Location 头部已经是转向网址了。不过返回的http头信息不是转向网站的,虽然不影响转向,但总觉得不太好。这方面我实在不懂,希望有朋友能帮我解决这个问题 ;-)

服务器端要求:
* Apache 2.0 以上并支持.htaccess和python cgi; [2.0.63]
* SQLite3; [3.2.1]
* Python; [2.3.5]
* pysqlite2 或 sqlite3 Python模块; [??]

方括号里是我的服务器端情况。

总结:
由于后端程序采用CGI的方式(非mod_python),使用 SQLite 这种非C/S数据库,以及我对web编程和数据库操作方面知识很浅,因此在性能、并发处理上不会达到很高的要求。

这个东西仅仅是给自个儿玩啦 ;-) 路过大牛轻拍,也欢迎指点任何程序设计、安全、配置等上的问题。

如果你也有自己的域名和空间,那么只要把上图中的 cmprzurl.py, redirect.py, cmprzurl.db, .htaccess 复制到你域名的根目录下,赋上可执行权限,就可以了。文件请从下面的地址检出。

PS:
Google docs 里的 drawing 真好用啊~~

源代码在这里:
http://github.com/vvoody/cmprzurl/tree/master

February 14th, 2009

有这样一位女子:

      余与芸联句以遣闷怀,而两韵之后,逾联逾纵,想入非夷,随口乱道。芸已漱涎涕泪,笑倒余怀,不能成声矣。觉其鬓边茉莉浓香扑鼻,因拍其背以他词解之曰:“想古人以茉莉形色如珠,故供助妆压鬓,不知此花必沾油头粉面之气,其香更可爱,所供佛手当退三舍矣。”芸乃止笑曰:“佛手乃香中君子,只在有意无意间; 茉莉是香中小人,故须借人之势,其香也如胁肩谄笑。”余曰:“卿何远君子而近小人?”芸曰:“我笑君子爱小人耳。”

心而向之。

1234567890

February 11th, 2009

这个数字最先是从王聪大哥的饭否那儿得知。后来陆续的就有相关报道,不知道这个代表啥的请看这篇文章。图新鲜,想亲手把这数转成具体时间看看。当时不知道 date 命令能很方便的转换,就用了 Python,于是问题随之而来。

vvoody@slackware:~$ python
Python 2.5.2 (r252:60911, Sep 11 2008, 13:43:31)
[GCC 4.2.4] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from time import strftime, gmtime, localtime
>>> strftime("%a, %d %b %Y %H:%M:%S", gmtime(1234567890))
'Fri, 13 Feb 2009 23:31:06'
>>> strftime("%a, %d %b %Y %H:%M:%S", localtime(1234567890))
'Sat, 14 Feb 2009 07:31:30' 
>>> strftime("%a, %d %b %Y %H:%M:%S", gmtime(915148821))
'Thu, 31 Dec 1998 23:59:60'

1234567890 对应的时间应该是 23:31:30 ,而我的 Python 里的 gmtime 运行出来是 23:31:06,少了 24 秒。问了很多人,平台相同的都是正确的,就我的不对。后来经讨论得知那 24 秒和闰秒有关,Python 没有加上这 24 秒。但 gmtime(915148821) 这个数确是有闰秒的操作,因此结果就有了第 60 秒。

后来,索性直接看看调用 C 库函数和 date 命令的结果。如下代码:

vvoody@slackware:~/lab$ cat gmtime.c
#include
#include 
 
int main(void)
{
  struct tm *foo;
  time_t bar = 1234567890;
  foo = gmtime(&bar);
  printf("tm_sec: %dn", foo->tm_sec);
 
  char egg[100];
  strftime(egg, 100, "%a, %b %d %Y %H:%M:%S", foo);
  printf("1234567890 is at:n%sn", egg);
  return 0;
}
vvoody@slackware:~/lab$ gcc -Wall gmtime.c
vvoody@slackware:~/lab$ ./a.out
tm_sec: 6
1234567890 is at:
Fri, Feb 13 2009 23:31:06
vvoody@slackware:~/lab$ date -ud @1234567890
Fri Feb 13 23:31:06 UTC 2009

最后经 cppgx@newsmth 大牛的指点,重新运行了下 timeconfig,就好了。据他说可能是时区文件被胡乱编辑了,就引起了上面遇到的问题。timeconfig 会把你指定的系统自带的某个时区文件复制到/etc去。

呼,终于好了。不过还是不清楚到底是哪个程序动了我的配置文件。

相关讨论:
http://www.newsmth.net/bbscon.php?bid=284&id=51926
http://groups.google.com/group/python-cn/browse_thread/thread/2d0e9ce534d2d93c?hl=en