This page looks plain and unstyled because you're using a non-standard compliant browser. To see it in its best form, please visit upgrade to a browser that supports web standards. It's free and painless.

Fillano's Learning Notes 會員登入 會員註冊

最近在思考如何控管httpd伺服器,來減少spam機器人灌水的效率,結果找到了一個叫做mod_limitipconn的模組。這個模組支援apache httpd-2.0以及httpd-2.2,透過修改一下httpd的原始碼,還可以讓他偵測X-Forwarded-For檔頭中的ip資訊,以分辨透過同一個ip當作proxy過來的不同連線。

套件的網址:http://dominia.org/djao/limitipconn2.html,裡面有簡單的說明、與下載的連結等,也有預先編譯好的win32模組。

透過他內建的diff檔案來patch過httpd以後重新編譯,很順利。接著只要依照mod_limitipconn中的說明,編譯安裝,修改httpd.conf設定,就可以使用。

根據說明,mod_limitipconn支援幾個設定:

  1. MaxConnPerIP這是每個ip最大可同時連接的數量
  2. NoIPLimit指定要排除限制的內容種類(mime type)
  3. OnlyIPLimit指定只要限制的內容種類(mime type)

要注意的是,他只能以per location的方式做設定,所以至少要有:

<Location />
     MaxConnPerIP 1
</Location>

類似的設定才會發生作用。

寫了一個小程式測試一下:

<html>
<body>
<script>
var errlink=0;
var successlink=0;
var starttime = 0;
function test() {
	try {
		errlink=0;
		successlink=0;
		var count = document.getElementById('testn').value;
		starttime = (new Date()).getTime();
		for (var i=0; i<count; i++) {
			(new ajax('test.php')).run();
		}
	}catch(e){alert(e);}
}
function xmlhttp() {
	try{return new XMLHttpRequest();} catch(e){}
	try{return new ActiveXObject("Msxml2.XMLHTTP");} catch(e){}
	try{return new ActiveXObject("Microsoft.XMLHTTP");} catch(e){}
	alert("XMLHttpRequest Object not existed!!");
	return null;
}
function ajax (url) {
	this.url = url;
	this.xmlhttp = xmlhttp();
	var ajaxinst = this;
	this.xmlhttp.onreadystatechange = function () {
		try {
			if (ajaxinst.xmlhttp.readyState == 4) {
				if (ajaxinst.xmlhttp.status == 200) {
					successlink += 1;
				} else {
					errlink += 1;
				}
			}
			var obj = document.getElementById("panel");
			obj.innerHTML = "successlink: " + successlink + "
errlink: " + errlink + "
time: " + ((new Date()).getTime() - starttime)/1000; }catch(e){alert(e);} }; this.run = function () { this.xmlhttp.open("POST",this.url,true); this.xmlhttp.send(""); } } </script> <input type="text" id="testn" name="testn"><input type="button" value="linking" onclick="test();"> <div id="panel"></div> </body> </html>

果然,在沒啟用mod_limitipconn模組前,都可以順利連線,但是開啟限制後,就會有錯誤出現。(ie7為了提昇速度,如果用GET方法,預設會直接取cache的資料,所以測不出結果。後來改成POST就可以看到效果了。Firefox沒有這個問題。另外,ie7也支援了原生的XMLHttpRequest,不再依賴msxml了。)

考慮到一般的spam comment,應該會用post的方式把資料送到伺服器(伺服器的php應該也會這樣設計),所以接著想在mod_limitipconn加上只攔阻POST而不攔阻GET的功能試試看。

在mod_limitipconn.c的程式原始碼裡面,有用到很多request_rec這個資料結構,找了一下httpd的原始碼,在include/httpd.h裡面找到了定義。在這個資料結構裡面,有一個叫做method_number的成員(型別為int)。嗯嗯,就拿來用用看。

接著在mod_limitipconn.c裡面找到

static int limitipconn_handler(request_rec *r)
這個函數,在裡面找到
    if (cfg->limit == 0) {
	return OK;
    }
在這下面加幾行判斷:
    if (r->method_number == M_GET) {
        return OK;
    }
(其實對於這些原始碼還不熟悉啦,但是這些資訊剛好夠用。嘿嘿)

重新編譯模組後,再用剛剛的程式測試一下。果然可以:)。用GET方法就不會發生錯誤。如果用POST方法,他依舊會限制連線數。不過說限制,也只是減緩他的速度,並不是把他的spam comment真的擋掉:(

mod_limitipconn這個模組有一個問題,就是只能偵測位於proxy(而且必須帶有X-Forwarded-For檔頭)之後的機器,在NAT之後的機器沒辦法,都會被當成同一個ip擋下來。也許透過mod_usertrack模組,利用cookie的方式可以做限制?有機會再來試試。

方法及程式發表在[酷!學園 -> PHP程式設計]論壇中,這個網址:http://www.ez2.us/~ricky/RobotAway/則有範例與程式下載,也可以線上檢視原始碼。使用起來十分簡單。

他的原理是用一些javascript的混淆技術,包括隨機變數名稱、隨機註解、程式內容編碼等方法,讓機器人難以即時破解,來達到防止灌水的目的。(這些也是病毒上常見的變形、混淆技術)

最大的好處是,對於一般的網頁用戶來說,不會感覺到這個程式的存在,不像一般的captcha需要額外輸入文數字來驗證。對於程式設計師來說,只要用到三個方法,配合網頁程式,就可以做出來,非常簡單。

在上述的連結,Ricky兄有提供使用的範例。不過因為使用簡單,所以也不太需要繁雜的說明。另外,程式是寫成php5的類別,對於php4的用戶來說,只要拿掉private關鍵字,把constructor函數從__construct改名為RobotAway就可以了。

使用步驟大致如下:

  1. 在php程式中引用"rw.inc.php"
  2. $ra = new RobotAway("SITEKEY","INPUTNAME");
  3. 在form中加入名為"INPUTNAME"的隱藏欄位,記得要把id也設為"INPUTNAME"
  4. 用$ra->GenerateJS()方法產生混淆過的javascript函數
  5. 用$ra->CheckFunction()方法產生呼叫上述函數的javascript
  6. 在submit之前攔截submit,插入步驟5的php來產生呼叫步驟4的javascript函數,動態賦值給INPUTNAME隱藏欄位,然後才submit
  7. 在伺服器端,利用$ra->Verify()方法來檢查$_POST['INPUTNAME'],(在之前要用步驟2的同樣參數生成$ra),就知道傳進來的值是否正確。

應用一些方法,還可以讓這個程式更難捉摸。就是在要輸入表單的地方,用隨機產生的SITEKEY與INPUTNAME來產生$ra物件,然後把SITEKEY與INPUTNAME存入session,要做Verify時,再從session中取出SITEKEY與INPUTNAME來產生$ra物件,因為每次欄位名稱與sitekey都不一樣,會更增加機器人灌水的難度。

昨天檢視了一下以前寫的程式,想要改良一下程式的效率,並且讓程式清晰易懂。這個時候發現一個問題,就是依照條件建構SQL的過程太複雜,需要許多if/else判斷,使得程式容易不小心出錯。

仔細觀察了一下,問題發生的主要原因在於兩個地方:

  1. SQL子句有固定的順序,但是程式邏輯不一定符合這個順序
  2. 查詢的條件必須集中在Where子句中,但是形成這些條件的過程不一定是集中的

結果為了依照SQL的規則來產生SQL語句,就會讓程式流程變得很複雜。

想到的方法是,將sql語法的邏輯與產生sql的邏輯分開,寫一個類別,任意將各種條件傳入,等到需要用到SQL語句時,再依照SQL語法來將各種條件組合成SQL語句。依照這個想法,寫了一個簡單的SQL Builder類別:

class QueryBuilder {
	var $table;
	var $columns;
	var $active;
	var $order;
	var $where;
	var $limit;
	function QueryBuilder ($table, $columns) {
		$this->__construct ($table, $columns);
	}
	function __construct ($table, $columns) {
		$this->table = $table;
		if (strpos($columns,':')===false) {
			$this->columns = array($columns);
		} else {
			$this->columns = explode(':',$columns);
		}
		$this->where = array();
		$this->order = "";
		$this->limit = "";
	}
	function setActive ($active) {
		$this->where[] = " $active=1";
	}
	function setOrder ($order, $sort="ASC") {
		$this->order = sprintf(" ORDER BY %s %s", $order, $sort);
	}
	function setWhereInt ($col, $val) {
		$this->where[] = sprintf(" %s=%u", $col, intval($val));
	}
	function setWhereStr ($col, $val) {
		$this->where[] = sprintf("%s='%s'", $col, $val);
	}
	function setSearch ($col, $val) {
		$this->where[] = sprintf("%s LIKE '%%s%'", $col, $val);
	}
	function setSearchLt ($col, $val) {
		$this->where[] = sprintf("%s < %u", $col, intval($val));
	}
	function setSearchGt ($col, $val) {
		$this->where[] = sprintf("%s > %u", $col, intval($val));
	}
	function setLimit ($offset,$rowcount) {
		$this->limit = " LIMIT $offset, $rowcount";
	}
	function compose () {
		$ret = sprintf("SELECT %s FROM %s", implode(',',$this->columns), $this->table);
		if ($this->where) {
			$ret .= sprintf(" WHERE %s", implode(' AND ', $this->where));
		}
		$ret .= $this->order;
		$ret .= $this->limit;
		return $ret;
	}
}

這個類別還很粗糙,但是剛好滿足我的需求,也讓程式流程看起來比較清楚了。

檔案上傳進度資訊是PHP5.2才新增的功能。但是最近在逛phpclass.org時,發現有人做了PHP4的patch。patch及說明的出處:http://www.phpclasses.org/blog/post/61-File-upload-progress-meter-for-PHP-4-at-last.html

作者是早期PHP的核心開發人員,跟據他的說明,其實在php4就已經有人做了patch,但是不知為何到了PHP5.2,PHP團隊才把這個功能正式加進來。PHP5已經問世好幾年了,但是市面上大部分的網站,其實都還在用PHP4,主要是因為移植的複雜度以及潛在問題,所以許多人裹足不前。所以為了廣大的PHP4用戶,他做了Patch,使用這個patch就可以支援PHP5.2的檔案上傳進度資訊功能。

看到這個資訊很興奮,不過他patch的rfc1867.c是屬於PHP的核心,所以非得重新編譯不可。另外,PHP4已經不再更新了,末代版本是4.4.7,作者patch的版本是4.3.11以及4.4.4,在PHP官方網站上已經找不到原始碼的套件,所以只好用4.4.7來試試看。

以下是大致的過程中以及我碰到的一些問題:

  1. 一開始我是手動一行一行改rfc1867.c以及rfc1867.h,發現編譯可以過,但是結果上傳功能出問題.....。所以用patch指令來做,但是會有三處無法patch的地方,這些就再手動patch吧。結果.....恩,上傳功能沒問題,但是不知道上傳進度資訊可不可以用。
  2. 上網找了一下,結果還是到PECL下載了uploadprogress套件,網址:http://pecl.php.net/package/uploadprogress。套件中幾乎沒有說明,不過看起來是需要有PHP5.2才能裝。沒關係,先phpize,然後configure/make/make install。(我在我的系統裡面另外編譯了一套apache-2.0.61來測試,同時另外編譯一套php-4.4.7,但是使用phpize的時候,他會把include directory指到系統中預設的php目錄,所以我另外手動改了Makefile,才能順利編譯安裝)
  3. 安裝完畢以後,跑phpinfo(),ok,出現uploadprogress套件訊息,看起來這個延伸套件有作用。另外用get_defined_functions()函數,可以發現有一個叫做uploadprogress_get_info()的函數。
  4. 還是不知道怎麼用比較好,所以到網上找一找。後來在這個網站上找到說明跟展示:http://blog.liip.ch/archive/2006/09/10/upload-progress-meter-extension-for-php-5-2.html。更新過可demo的東西要參考另一個blog:http://blog.joshuaeichorn.com/archives/2005/05/01/ajax-file-upload-progress/。他的demo檔案放在一個web svn系統上:http://websvn.bluga.net/wsvn/HTML_AJAX/trunk/?rev=0&sc=0,把檔案下載下來,測試了一下......成功!!!

如果大家想要測試的話,以下是我patch過的rfc1867.c以及rfc1867.h:下載(注意:只能給php-4.4.7用,我是用它的source來patch的喔)

另外是幾個測試的圖片:

  1. 這是phpinfo()的畫面:
    upload progress
  2. 這是get_defined_functions()的畫面:
    upload progress
  3. 這是上傳進度的畫面:
    upload progress
  4. 這是上傳結束的畫面:
    upload progress

(我把畫面中的網址拿掉,因為我測試完就把測試用的apache關掉了。其實效果應該跟demo網站是一樣的。)

先引一段Douglas Crockford的文章:

Substandard Standard

The official specification for the language is published by ECMA. The specification is of extremely poor quality. It is difficult to read and very difficult to understand. This has been a contributor to the Bad Book problem because authors have been unable to use the standard document to improve their own understanding of the language. .....

這一段文字出自他的文章:JavaScript: The World's Most Misunderstood Programming Language,大意是說,因為ECMA標準寫得很爛,讓人看不懂,也讓人誤解。甚至連帶讓一堆書同樣寫得很爛,因為作者們無法拿標準作為依據。

作者自己有寫一篇好文章,提綱挈領地點出Javascript的特色:A Survey of the JavaScript Programming Language,看這篇文章絕對比看ECMA-262快進入狀況。

想要深入了解Javascript的話,Douglas Crockford的網站真的不能不看(感慨):

http://www.crockford.com/

變數作用範圍是一般程式語言初學就會碰到的基本觀念。理論上Javascript也是這樣,但是實際上恐怕有不少人並不知道自己不清楚Javascript的scope概念!

其實這些原本在ECMA-262就定義得很清楚,但是因為很基本所以反而容易被忽視,有時候變數運作範圍會跟我們想像的不一樣。大部分狀況下程式運作也不會出問題,但是有可能會讓你寫好的物件封裝不完整,或是不小心就改寫了整體變數,造成程式運作的問題。

先看一個簡單的例子:

1
2
3
4
5
6
7
8
9
<script>
function test () {
	hey = function () {alert('hey, you!!');};
}
try{
	test();
	window.hey();
} catch (e) {alert(e);}
</script>

這應該在預期之外吧?我在test函數裡面忘了用var保留字就assign一個函數給hey變數。結果.....hey變數變成了window物件的函數????這是怎麼一回事呢?

關於變數作用範圍這一方面,ECMA-262定義在identifier、scope chain、execution context等主題裡面。搞清楚這幾個方面的主題,才算真正理解Javascript變數作用範圍的定義。

簡單地說,在Javascript中,凡不是屬於保留字及運算符號等的名稱,只要符合規則,就會把他當作一個identifier,然後依照scope chain規則來解析這個identifier的型別跟值。scope chain則是依照execution context建立的。那這個規則到底是怎樣的呢?

javascript/ECMAScript是定為成一種依附於某種應用環境的程式語言,不是獨立自主運作的。所以通常會包含三種物件類型,一個是javascript/ECMAScript內建物件(Object、Function、Array、String、Boolean、Number、Math、Date、RegExp、Error等,還會有一個Global物件,通常跟host環境有關),一個是host物件,是由host環境建立的,像是瀏覽器裡面的DOM相關物件等等。還有一個就是使用者程式裡面定義的物件。

execution contexts可以說是程式執行的單位。ECMA-262定義了三種execution contexts,分別是global context、eval context與function context。凡是不在任何函數內的程式都屬於global context;使用eval來執行的程式碼時,不在任何函數內的程式都屬於eval context;所有位於函數內的程式碼,都屬於function context。程式進入不同的execution context時,執行環境會根據execution context的類型為他建立一個scope chain。scope chain是一個在邏輯上類似堆疊的結構,程式會透過這個scope chain依序來解析identifier。

在瀏覽器的執行環境中global物件就是window,global的identifier就會變成window的子物件或是屬性、函數等等(其實就是一個identifier,assign給它什麼型別的物件他就是什麼)。

回頭來看上面的例子,發生了什麼事情呢?執行test()的時候,在第三行碰到hey這個identifier,在test()這個scope找不到他的定義,所以到scope chain的下一個,就是global scope來找,也沒找到。在沒找到的情況下,系統就把這個identifier定義在global scope,先把他的值設定成undefined,接著透過=符號assign一個函數給它。由於在瀏覽器函數中,global物件就是window,所以無意中為window物件加了一個hey函數了。

在提到另一個例子前,先看一下幾個ECMA-262對於這個程式語言的定位:「ECMAScript is an object-oriented programming language for performing computations and manipulating computational objects within a host environment.」(第4章 "Overview")利用這個語言的特性,可以做到封裝、繼承與多型等物件導向語言該具備的能力,不過這裡只會提到封裝。

其實封裝的原則很簡單,因為Javascript利用scope chain規則來解析變數,這個規則不會處理到子函數裡面的identifier,所以在函數裡面用var定義的變數,或是用function定義的函數,就會成為這個函數私有的。使用this.identifier定義的則會成為public,可以在函數外面access到。

接下來看另一個例子:

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
<script>
function test () {
	var priv = "test private";
	this.pub = "test public";
	var ref = this;
	function privShowPriv () {
		alert(priv);
	}
	function privShowPub () {
		alert(ref.pub);
	}
	this.pubShowPriv = function () {
		alert(priv);
	}
	this.pubShowPub = function () {
		alert(this.pub);
	}
	this.protectShowPub = function () {
		privShowPub();
	}
	this.protectShowPriv = function () {
		privShowPriv();
	}
}
var a = new test();
a.pubShowPriv();
a.pubShowPub();
a.protectShowPriv();
a.protectShowPub();
</script>

在上面的例子裡,可以透過public的函數來存取public跟private的變數,或是利用public函數操作private函數來顯示private變數,但是無法在test函數物件之外使用他的private變數或函數。另外,在test()裡面用this定義的public變數,在test()裡面用function定義的private函數無法用this來存取,但是如果我們在test()裡面定義一個private變數然後把this assign給這個變數,在這些private函數中就可以透過這個變數來取得。(如第5行與第10行)

我們可以用不同方法為一個物件加入函數,但是這樣就無法存取這裡定義的私有變數:

1
2
3
4
5
6
7
8
9
10
11
<script>
function test () {
	var priv = "test private";
}
//test.prototype.pubShowPriv = new function () {
test.prototype.pubShowPriv = function () {
	alert(priv);
}
var a = new test();
a.pubShowPriv();
</script>

以上這個例子就會出錯。pubShowPriv函數無法存取priv變數。

再看另一個例子:

1
2
3
4
5
6
7
8
9
10
11
12
<script>
function test () {
	var priv = "test private";
	function privShowPriv () {
		alert(priv);
	}
	try {
		window.setTimeout(privShowPriv,500);
	} catch (e) {alert(e);}
}
test();
</script>

setTimeout是在global scope執行的,但是你可以直接assign "privShowPriv"函數給它執行。而且他還是可以存取到在test()函數中定義的priv變數。

換一種方式來做做看:

1
2
3
4
5
6
7
8
9
10
11
12
13
<script>
function test () {
	var priv = "test private";
	var ref = this;
	this.pubShowPriv = function () {
		alert(priv);
	}
	try {
		window.setTimeout(ref.pubShowPriv,500);
	} catch (e) {alert(e);}
}
test();
</script>

因為setTimeout跟test()是在不同的execution context,所以透過this是沒有辦法把pubShowPriv函數傳給他,但是可以利用一個變數ref間接傳給setTimeout。

從以上幾個例子看起來,scope chain是根據identifier定義的位置來做解析的(究竟是直譯的script?)。定義在outer function裡面的inner function,可以存取outer function裡面定義的變數,但是透過prototype等方法在outer function之外assign一個function給它,這個function就沒有辦法存取定義在這個outer function裡面的變數。

看一個更極端的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script>
function test1 () {
	this.inherit = function (obj) {
		for (var i in obj) {
			this[i] = obj[i];
		}
	} 
}
function test () {
	var priv = "test private";
	this.pubShowPriv = function () {
		alert(priv);
	}
}
var a = new test;
var b = new test1;
b.inherit(a);
b.pubShowPriv();
for (j in b) {
	alert(j);
}
</script>

利用b的inherit方法把a的pubShowPriv拷貝給b,在執行b.pubShowPriv()時,仍然可以存取定義在a裡的priv變數。透過最後的for迴圈可以看出,inherit並無法拷貝priv變數給b,列舉出來的幾個identifier只有"inherit"以及"pubShowPriv"。

大致總結一下,其實private、public並不是ECMA-262定義好的東西,只是依照以上的方法,就可以做出資料封裝的效果。變數範圍是在程式寫好時他根據剖析出來的上下文決定的,而不是根據你動態為物件加入方法、屬性的方式決定的,所以動態加入的函數會無法存取定義在物件裡面的變數。另外,我在我的例子裡面用到closure時並沒有考慮到memory leak的問題,這方面在使用closure時要自己注意。