本文是mod_rewrite
参考文档的补充材料。阐述在实际应用中如何解决网管所面临的基于URL的典型问题,并详细描述了如何配置URL重写规则集以解决这些问题。
mod_alias
和mod_userdir
的情况下要增加[PT]
标志,或者为了适应目录级(.htaccess
)的配置而将针对服务器级的规则集进行重写。对一个特定的规则集应该先透彻理解然后再考虑应用,这样才能避免出现问题。user1 server_of_user1 user2 server_of_user2 : :
map.xxx-to-host
文件。其次,如果URL在一个服务器上无效,我们还需要引导所有的服务器将
/u/user/anypath /g/group/anypath /e/entity/anypath
http://physical-host/u/user/anypath http://physical-host/g/group/anypath http://physical-host/e/entity/anypath
RewriteEngine on RewriteMap user-to-host txt:/path/to/map.user-to-host RewriteMap group-to-host txt:/path/to/map.group-to-host RewriteMap entity-to-host txt:/path/to/map.entity-to-host RewriteRule ^/u/([^/]+)/?(.*) http://${user-to-host:$1|server0}/u/$1/$2 RewriteRule ^/g/([^/]+)/?(.*) http://${group-to-host:$1|server0}/g/$1/$2 RewriteRule ^/e/([^/]+)/?(.*) http://${entity-to-host:$1|server0}/e/$1/$2 RewriteRule ^/([uge])/([^/]+)/?$ /$1/$2/.www/ RewriteRule ^/([uge])/([^/]+)/([^.]+.+) /$1/$2/.www/$3\
/~foo/anypath
代表/home/f/foo/.www/anypath
,而/~bar/anypath
代表/home/b/bar/.www/anypath
~
'以达到上述目的。
RewriteEngine on RewriteRule ^/~(([a-z])[a-z0-9]+)(.*) /home/$2/$1/.www$3
drwxrwxr-x 2 netsw users 512 Aug 3 18:39 Audio/ drwxrwxr-x 2 netsw users 512 Jul 9 14:37 Benchmark/ drwxrwxr-x 12 netsw users 512 Jul 9 00:34 Crypto/ drwxrwxr-x 5 netsw users 512 Jul 9 00:41 Database/ drwxrwxr-x 4 netsw users 512 Jul 30 19:25 Dicts/ drwxrwxr-x 10 netsw users 512 Jul 9 01:54 Graphic/ drwxrwxr-x 5 netsw users 512 Jul 9 01:58 Hackers/ drwxrwxr-x 8 netsw users 512 Jul 9 03:19 InfoSys/ drwxrwxr-x 3 netsw users 512 Jul 9 03:21 Math/ drwxrwxr-x 3 netsw users 512 Jul 9 03:24 Misc/ drwxrwxr-x 9 netsw users 512 Aug 1 16:33 Network/ drwxrwxr-x 2 netsw users 512 Jul 9 05:53 Office/ drwxrwxr-x 7 netsw users 512 Jul 9 09:24 SoftEng/ drwxrwxr-x 7 netsw users 512 Jul 9 12:17 System/ drwxrwxr-x 12 netsw users 512 Aug 3 20:15 Typesetting/ drwxrwxr-x 10 netsw users 512 Jul 9 14:08 X11/
/e/netsw/.www/
中:
-rw-r--r-- 1 netsw users 1318 Aug 1 18:10 .wwwacl drwxr-xr-x 18 netsw users 512 Aug 5 15:51 DATA/ -rw-rw-rw- 1 netsw users 372982 Aug 5 16:35 LOGFILE -rw-r--r-- 1 netsw users 659 Aug 4 09:27 TODO -rw-r--r-- 1 netsw users 5697 Aug 1 18:01 netsw-about.html -rwxr-xr-x 1 netsw users 579 Aug 2 10:33 netsw-access.pl -rwxr-xr-x 1 netsw users 1532 Aug 1 17:35 netsw-changes.cgi -rwxr-xr-x 1 netsw users 2866 Aug 5 14:49 netsw-home.cgi drwxr-xr-x 2 netsw users 512 Jul 8 23:47 netsw-img/ -rwxr-xr-x 1 netsw users 24050 Aug 5 15:49 netsw-lsdir.cgi -rwxr-xr-x 1 netsw users 1589 Aug 3 18:43 netsw-search.cgi -rwxr-xr-x 1 netsw users 1885 Aug 1 17:41 netsw-tree.cgi -rw-r--r-- 1 netsw users 234 Jul 30 16:35 netsw-unlimit.lst
DATA/
子目录包含了上述目录结构,即实际的net.sw原始内容,由rdist
在需要的时候自动更新。第二个部分的问题是:如何将这两个结构连接为一个观感平滑的URL树?我希望在为各种URL运行对应的CGI脚本的时候,用户感觉不到DATA/
目录的存在。方案如下:首先,我把下列配置放在针对每个目录的配置文件里,将公布的URL"/net.sw/
"重写为内部路径"/e/netsw
":
RewriteRule ^net.sw$ net.sw/ [R] RewriteRule ^net.sw/(.*)$ e/netsw/$1
/e/netsw/.www/.wwwacl
中的杀手级的配置了:
Options ExecCGI FollowSymLinks Includes MultiViews RewriteEngine on # 通过"/net.sw/"前缀到达 RewriteBase /net.sw/ # 首先将根目录重写到cgi处理脚本 RewriteRule ^$ netsw-home.cgi [L] RewriteRule ^index\.html$ netsw-home.cgi [L] # 当浏览器请求perdir页面时剥去子目录 RewriteRule ^.+/(netsw-[^/]+/.+)$ $1 [L] # 现在打断对本地文件的重写 RewriteRule ^netsw-home\.cgi.* - [L] RewriteRule ^netsw-changes\.cgi.* - [L] RewriteRule ^netsw-search\.cgi.* - [L] RewriteRule ^netsw-tree\.cgi$ - [L] RewriteRule ^netsw-about\.html$ - [L] RewriteRule ^netsw-img/.*$ - [L] # 任何别的东西都是一个由另一个cgi脚本处理的子目录 RewriteRule !^netsw-lsdir\.cgi.* - [C] RewriteRule (.*) netsw-lsdir.cgi/$1
阅读提示:
L
(最后)标志和非替换部分('-
')!
(非)符号和C
(链)标志ErrorDocument
的CGI脚本来解决,此外,还有使用mod_rewrite
的方案。但是须注意,这种方法的执行效率不如使用ErrorDocument
的CGI脚本!RewriteEngine on RewriteCond /your/docroot/%{REQUEST_FILENAME} !-f RewriteRule ^(.+) http://webserverB.dom/$1
DocumentRoot
中的页面有效。虽然可以增加更多的条件(比如同时还处理用户主目录,等等),但是还有一个更好的方法:
RewriteEngine on RewriteCond %{REQUEST_URI} !-U RewriteRule ^(.+) http://webserverB.dom/$1
mod_rewrite
提供的"前瞻"(look-ahead)的功能,是一种对所有URL类型都有效而且安全的方法,但是对web服务器的性能有不利影响。如果web服务器有一个强大的CPU,那就用这个方法。而在慢速机器上,可以用第一种方法,或者用性能更好的CGI脚本。mod_rewrite
如何实现呢?mod_rewrite
从3.0.0版本开始可以重写"ftp:
"类型。其次,可以用RewriteMap
取得对客户端顶级域名的最短路径。利用链式规则集,并用顶级域名作为查找多路复用映射表的键,可以这样做:
RewriteEngine on RewriteMap multiplex txt:/path/to/map.cxan RewriteRule ^/CxAN/(.*) %{REMOTE_HOST}::$1 [C] RewriteRule ^.+\.([a-zA-Z]+)::(.*)$ ${multiplex:$1|ftp.default.dom}$2 [R,L]
## ## map.cxan -- CxAN多路映射表 ## de ftp://ftp.cxan.de/CxAN/ uk ftp://ftp.cxan.uk/CxAN/ com ftp://ftp.cxan.com/CxAN/ : ##EOF##
foo.html
重写为foo.NS.html
,并终止重写操作;如果是"Lynx"或者版本号为1和2的"Mozilla",则重写为foo.20.html
;而对其他所有浏览器则是foo.32.html
。
RewriteCond %{HTTP_USER_AGENT} ^Mozilla/3.* RewriteRule ^foo\.html$ foo.NS.html [L] RewriteCond %{HTTP_USER_AGENT} ^Lynx/.* [OR] RewriteCond %{HTTP_USER_AGENT} ^Mozilla/[12].* RewriteRule ^foo\.html$ foo.20.html [L] RewriteRule ^foo\.html$ foo.32.html [L]
mirror
程序在本地机器上维持一个对远程数据的最新的拷贝;对HTTP服务器,可以使用webcopy
程序。但这两种技术都有一个主要的缺点:本地拷贝必须通过这个程序来更新。所以,比较好的方法是不采用静态镜像,而采用动态镜像,即在有数据请求时自动更新(远程主机上更新的数据)。[P]
标志),将远程页面甚至整个远程网络区域映射到我们的域名空间:
RewriteEngine on RewriteBase /~quux/ RewriteRule ^hotsheet/(.*)$ http://www.tstimpreso.com/hotsheet/$1 [P]
RewriteEngine on RewriteBase /~quux/ RewriteRule ^usa-news\.html$ http://www.quux-corp.com/news/index.html [P]
RewriteEngine on RewriteCond /mirror/of/remotesite/$1 -U RewriteRule ^http://www\.remotesite\.com/(.*)$ /mirror/of/remotesite/$1
www2.quux-corp.dom
)上保存和维护实际数据,而在Internet上虚拟地运行web服务器(www.quux-corp.dom
)。方法是外部服务器在空闲时间从内部服务器取得被请求的数据。ALLOW Host www.quux-corp.dom Port >1024 --> Host www2.quux-corp.dom Port 80 DENY Host * Port * --> Host www2.quux-corp.dom Port 80
RewriteRule ^/~([^/]+)/?(.*) /home/$1/.www/$2 RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^/home/([^/]+)/.www/?(.*) http://www2.quux-corp.dom/~$1/pub/$2 [P]
www.foo.com
的负载均衡到www[0-5].foo.com
(一共是6个服务器)?mod_rewrite
的方案
最简单的方法是用BIND
的DNS循环特性,只要按惯例设置www[0-9].foo.com
的DNS的A(地址)记录,如:
www0 IN A 1.2.3.1 www1 IN A 1.2.3.2 www2 IN A 1.2.3.3 www3 IN A 1.2.3.4 www4 IN A 1.2.3.5 www5 IN A 1.2.3.6
www IN CNAME www0.foo.com. IN CNAME www1.foo.com. IN CNAME www2.foo.com. IN CNAME www3.foo.com. IN CNAME www4.foo.com. IN CNAME www5.foo.com. IN CNAME www6.foo.com.
BIND
中一个特意的特性,而且也可以这样用。在解析www.foo.com
时,BIND
将给出www0-www6
的结果(虽然每次在次序上会有轻微的置换/循环),客户端的请求可以被分散到各个服务器。但这并不是一个优秀的负载均衡方案,因为DNS解析信息可以被网络中其他域名服务器缓冲,一旦www.foo.com
被解析为wwwN.foo.com
,该请求的所有后继请求都将被送往wwwN.foo.com
。但是最终结果是正确的,因为请求的总量的确被分散到各个服务器了。
一种成熟的基于DNS的负载均衡方法是使用lbnamed
程序,它是一个Perl程序,并带有若干辅助工具,实现了真正的基于DNS的负载均衡。
这是一个使用mod_rewrite
以及代理吞吐特性的方法。首先,在DNS记录中将www0.foo.com
固定为www.foo.com
,如下:
www IN CNAME www0.foo.com.
其次,将www0.foo.com
转换为一个专职代理服务器,即由这个机器把所有到来的URL通过内部代理分散到另外5个服务器(www1-www5
)。为此,必须建立一个规则集,对所有URL调用一个负载均衡脚本lb.pl
。
RewriteEngine on RewriteMap lb prg:/path/to/lb.pl RewriteRule ^/(.+)$ ${lb:$1} [P,L]
lb.pl
的内容:
#!/path/to/perl ## lb.pl -- 负载平衡脚本 $| = 1; $name = "www"; # the hostname base $first = 1; # the first server (not 0 here, because 0 is myself) $last = 5; # the last server in the round-robin $domain = "foo.dom"; # the domainname $cnt = 0; while (<STDIN>) { $cnt = (($cnt+1) % ($last+1-$first)); $server = sprintf("%s%d.%s", $name, $cnt+$first, $domain); print "http://$server/$_"; } ##EOF##
www0.foo.com
也会超载呀?答案是:没错,它的确会超载,但是它超载的仅仅是简单的代理吞吐请求!所有诸如SSI、CGI、ePerl等的处理完全是由其他机器完成的,这个才是重点。还有一个硬件解决方案。Cisco有一个叫LocalDirector的东西,实现了TCP/IP层的负载均衡,事实上,它是一个位于网站集群前端的电路级网关。如果你有足够资金而且的确需要高性能的解决方案,那么可以用这个。
PATH_INFO
和QUERY_STRINGS
时才很好用。首先,配置一种新的后缀为.scgi
的(安全CGI)文件类型,其处理器是很常见的cgiwrap
程序。问题是:如果使用同类URL规划(见上述),而用户宿主目录中的一个文件的URL是/u/user/foo/bar.scgi
,可是cgiwrap
要求的URL的格式是/~user/foo/bar.scgi/
,以下重写规则解决了这个问题:
RewriteRule ^/[uge]/([^/]+)/\.www/(.+)\.scgi(.*) ... ... /internal/cgi/user/cgiwrap/~$1/$2.scgi$3 [NS,T=application/x-http-cgi]
wwwlog
(显示access.log
中的一个URL子树)和wwwidx
(对一个URL子树运行Glimpse),则必须为两个程序提供URL区域作为其操作对象。比如,对/u/user/foo/
执行swwidx
程序的超链是这样的:
/internal/cgi/user/swwidx?i=/u/user/foo/
RewriteRule ^/([uge])/([^/]+)(/?.*)/\* /internal/cgi/user/wwwidx?i=/$1/$2$3/ RewriteRule ^/([uge])/([^/]+)(/?.*):log /internal/cgi/user/wwwlog?f=/$1/$2$3
/u/user/foo/
的超链简化成了:
HREF="*"
/internal/cgi/user/wwwidx?i=/u/user/foo/
:log
"的超链拼装出调用CGI程序的参数。
RewriteCond %{REQUEST_FILENAME} !-s RewriteRule ^page\.html$ page.cgi [T=application/x-httpd-cgi,L]
page.html
不存在或者文件大小为null ,则对page.html
的请求会导致page.cgi
的运行。其中奥妙在于page.cgi
是一个将输出写入到page.html
的(同时也写入STDOUT
)的CGI脚本。执行完毕,服务器则将page.html
的内容发送出去。如果网管需要强制更新其内容,只须删除page.html
即可(通常由一个计划任务完成)。mod_rewrite
的URL操控特性。首先,建立一个新的URL特性:对在文件系统中需要刷新的所有URL加上":refresh
" 。
RewriteRule ^(/[uge]/[^/]+/?.*):refresh /internal/cgi/apache/nph-refresh?f=$1
/u/foo/bar/page.html:refresh
/internal/cgi/apache/nph-refresh?f=/u/foo/bar/page.html
#!/sw/bin/perl ## ## nph-refresh -- 用于自动刷新页面的 NPH/CGI 脚本 ## Copyright (c) 1997 Ralf S. Engelschall, All Rights Reserved. ## $| = 1; # 分解 QUERY_STRING 变量 @pairs = split(/&/, $ENV{'QUERY_STRING'}); foreach $pair (@pairs) { ($name, $value) = split(/=/, $pair); $name =~ tr/A-Z/a-z/; $name = 'QS_' . $name; $value =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg; eval "\$$name = \"$value\""; } $QS_s = 1 if ($QS_s eq "); $QS_n = 3600 if ($QS_n eq "); if ($QS_f eq ") { print "HTTP/1.0 200 OK\n"; print "Content-type: text/html\n\n"; print "<b>ERROR</b>: No file given\n"; exit(0); } if (! -f $QS_f) { print "HTTP/1.0 200 OK\n"; print "Content-type: text/html\n\n"; print "<b>ERROR</b>: File $QS_f not found\n"; exit(0); } sub print_http_headers_multipart_begin { print "HTTP/1.0 200 OK\n"; $bound = "ThisRandomString12345"; print "Content-type: multipart/x-mixed-replace;boundary=$bound\n"; &print_http_headers_multipart_next; } sub print_http_headers_multipart_next { print "\n--$bound\n"; } sub print_http_headers_multipart_end { print "\n--$bound--\n"; } sub displayhtml { local($buffer) = @_; $len = length($buffer); print "Content-type: text/html\n"; print "Content-length: $len\n\n"; print $buffer; } sub readfile { local($file) = @_; local(*FP, $size, $buffer, $bytes); ($x, $x, $x, $x, $x, $x, $x, $size) = stat($file); $size = sprintf("%d", $size); open(FP, "<$file"); $bytes = sysread(FP, $buffer, $size); close(FP); return $buffer; } $buffer = &readfile($QS_f); &print_http_headers_multipart_begin; &displayhtml($buffer); sub mystat { local($file) = $_[0]; local($time); ($x, $x, $x, $x, $x, $x, $x, $x, $x, $mtime) = stat($file); return $mtime; } $mtimeL = &mystat($QS_f); $mtime = $mtime; for ($n = 0; $n < $QS_n; $n++) { while (1) { $mtime = &mystat($QS_f); if ($mtime ne $mtimeL) { $mtimeL = $mtime; sleep(2); $buffer = &readfile($QS_f); &print_http_headers_multipart_next; &displayhtml($buffer); sleep(5); $mtimeL = &mystat($QS_f); last; } sleep($QS_s); } } &print_http_headers_multipart_end; exit(0); ##EOF##
<VirtualHost>
功能很强,在有几十个虚拟主机的情况下运行得仍然很好,但是如果你是ISP,需要提供成百上千个虚拟主机,那么这就不是最佳选择了。[P]
标志)映射远程页面甚至整个远程网络区域到自己的域名空间:
## ## vhost.map ## www.vhost1.dom:80 /path/to/docroot/vhost1 www.vhost2.dom:80 /path/to/docroot/vhost2 : www.vhostN.dom:80 /path/to/docroot/vhostN
## ## httpd.conf ## : # 在重定向时使用规范化的主机名等等 UseCanonicalName on : # 在CLF-format之前添加虚拟主机 CustomLog /path/to/access_log "%{VHOST}e %h %l %u %t \"%r\" %>s %b" : # 为主服务器启用重写引擎 RewriteEngine on # define two maps: one for fixing the URL and one which defines # the available virtual hosts with their corresponding # DocumentRoot. RewriteMap lowercase int:tolower RewriteMap vhost txt:/path/to/vhost.map # Now do the actual virtual host mapping # via a huge and complicated single rule: # # 1. make sure we don't map for common locations RewriteCond %{REQUEST_URI} !^/commonurl1/.* RewriteCond %{REQUEST_URI} !^/commonurl2/.* : RewriteCond %{REQUEST_URI} !^/commonurlN/.* # # 2. make sure we have a Host header, because # currently our approach only supports # virtual hosting through this header RewriteCond %{HTTP_HOST} !^$ # # 3. lowercase the hostname RewriteCond ${lowercase:%{HTTP_HOST}|NONE} ^(.+)$ # # 4. lookup this hostname in vhost.map and # remember it only when it is a path # (and not "NONE" from above) RewriteCond ${vhost:%1} ^(/.*)$ # # 5. finally we can map the URL to its docroot location # and remember the virtual host for logging puposes RewriteRule ^/(.*)$ %1/$1 [E=VHOST:${lowercase:%{HTTP_HOST}}] :
RewriteEngine on RewriteMap hosts-deny txt:/path/to/hosts.deny RewriteCond ${hosts-deny:%{REMOTE_HOST}|NOT-FOUND} !=NOT-FOUND [OR] RewriteCond ${hosts-deny:%{REMOTE_ADDR}|NOT-FOUND} !=NOT-FOUND RewriteRule ^/.* - [F]
## ## hosts.deny ## ## 注意! 这是一个映射而不是列表(即使我们这样看待它)。 ## mod_rewrite会将它作为 键/值 对进行解析。 ## 所以每一项至少要存在一个伪值:"-" ## 193.102.180.41 - bsdti1.sdm.de - 192.76.162.40 -
mod_rewrite
在mod_proxy
的下面!使它在mod_proxy
之前被调用。然后使用如下方法拒绝某个主机:
RewriteCond %{REMOTE_HOST} ^badhost\.mydomain\.com$ RewriteRule !^http://[^/.]\.mydomain.com.* - [F]
RewriteCond %{REMOTE_IDENT}@%{REMOTE_HOST} ^badguy@badhost\.mydomain\.com$ RewriteRule !^http://[^/.]\.mydomain.com.* - [F]
mod_auth_basic
的基本认证方法时可能会出现的)任何提示。RewriteCond %{REMOTE_IDENT}@%{REMOTE_HOST} !^friend1@client1.quux-corp\.com$ RewriteCond %{REMOTE_IDENT}@%{REMOTE_HOST} !^friend2@client2.quux-corp\.com$ RewriteCond %{REMOTE_IDENT}@%{REMOTE_HOST} !^friend3@client3.quux-corp\.com$ RewriteRule ^/~quux/only-for-friends/ - [F]
RewriteMap deflector txt:/path/to/deflector.map RewriteCond %{HTTP_REFERER} !="" RewriteCond ${deflector:%{HTTP_REFERER}} ^-$ RewriteRule ^.* %{HTTP_REFERER} [R,L] RewriteCond %{HTTP_REFERER} !="" RewriteCond ${deflector:%{HTTP_REFERER}|NOT-FOUND} !=NOT-FOUND RewriteRule ^.* ${deflector:%{HTTP_REFERER}} [R,L]
## ## deflector.map ## http://www.badguys.com/bad/index.html - http://www.badguys.com/bad/index2.html - http://www.badguys.com/bad/index3.html http://somewhere.com/
-
"值的时候)重定向回其引用页(referring page),或者(在映射表中URL有第二个参数时)重定向到一个特定的URL。