Python cho người Việt

Python cho người Việt

Entries tagged “http”

Chương trình đường hầm HTTP bằng Python

written by Phan Đắc Anh Huy, on Jan 14, 2014 1:43:00 AM.

(Bài gửi đến PCNV từ cộng tác viên Vũ Khuê. Xin cảm ơn bạn.)

Giới thiệu

Có những lúc bạn cần kết nối đến máy chủ ngoài mạng nội bộ ở một cổng không thuộc những giao thức ứng dụng phổ biến như HTTP hay HTTPS (cổng 80 hoặc 443). Nhưnng điều đáng buồn là tường lửa chặn yêu cầu đến những cổng ngoài 80 hoặc 443. Khi đó, điều bạn có thể làm là thiết lập một chương trình trên một máy ngoại mạng. Chưong trình này nhận yêu cầu tử cổng 80 hoặc 443 và chuyển nó đến cổng và máy chủ thực sự. Việc này thường được gọi là thiết lập đường hầm (tunneling).

Trên thực tế, chương trình ssh với tuỳ chọn -L thường được sử dụng cho nhiệm vụ này. Tuy nhiên trong bài này chúng ta sẽ viết chương trinh đường hầm này dựa trên giao thức HTTP. Mục đích chính là miêu tả việc xử lý dữ liệu mạng tầm thấp với Python.

Cấu trúc

Chương trình này gồm 2 thành phần máy khách (client) và máy chủ (server)

  • Tunnel.py: thành phần máy khách. Thành phần này nhận yêu cầu từ một cổng nhất định và bọc dữ liệu này duới dạng một yêu cầu HTTP rồi gửi đến thành phần máy chủ.
  • Tunneld.py: thành phần máy chủ. Thành phần thực chất là một máy chủ HTTP (HTTP server). Khi có yêu cầu gửi đến, nó sẽ đọc yêu cầu này và thực hiện tác vụ tương ứng. Ví dụ như thực hiện kết nối với một máy khác hoặc chuyển dữ liệu từ tải của yêu cầu HTTP đến máy này.

Để thiết lập đường hầm, chạy 2 thành phần như sau:

python tunnel.py -p [client_listen_port] -h [target_host]:[target_port]
python tunneld.py -p [server_listen_port]

Một ứng dụng muốn gửi yêu cầu đến máy nào đó (target_host), nó cần gửi yêu cầu đó thông qua cổng mà tunnel.py được khởi tạo với (client_listen_port).

Triển khai

Bạn có thể tìm thấy mã chương trình tại đây: https://github.com/khuevu/http-tunnel.

Tunnel.py

Thành phần này nghe ở một cổng nhất định. Nó có 2 tiểu trình (thread) riêng biệt để nhận và trả lời yêu cầu:

	...
	BUFFER = 1024 * 50

	#set global timeout
	socket.setdefaulttimeout(30)

	class SendThread(threading.Thread):

	    """
	    Thread to send data to remote tunneld
	    """
	    ...

	    def run(self):
	        while not self.stopped():
	        	# receive data and send to through tunnel
	            data = self.socket.recv(BUFFER)
	            self.conn.send(data)
	    ...

	class ReceiveThread(threading.Thread):

	    """
	    Thread to receive data from remote tunneld
	    """
	    ...

	    def run(self):
	        while not self.stopped():
	            data = self.conn.receive()
	            self.socket.sendall(data)

	    ...

Hằng số BUFFER là lượng dữ liệu theo byte mà chường trình sẽ nhận từ ứng dụng trước khi gửi qua đường hầm. Có thể có nhiều ứng dụng kết nối với chương trình tunnel.py. Vì thế, ta cần tạo kết nối riêng cho mỗi ứng dụng. Dưới đây là đoạn mã của lớp Connection:

class Connection():
    
    def __init__(self, connection_id, remote_addr):
        self.id = connection_id
        self.http_conn = httplib.HTTPConnection(remote_addr['host'], remote_addr['port'])
    ...

    def create(self, target_addr):
        params = urllib.urlencode({"host": target_addr['host'], "port": target_addr['port']})
        headers = {"Content-Type": "application/x-www-form-urlencoded", "Accept": "text/plain"}

        self.http_conn.request("POST", self._url("/" + self.id), params, headers)

        response = self.http_conn.getresponse()
        response.read()
        if response.status == 200:
            print 'Successfully create connection'
            return True 
        else:
            print 'Fail to establish connection: status %s because %s' % (
                response.status, response.reason)
            return False 

    def send(self, data):
        params = urllib.urlencode({"data": data})
        headers = {"Content-Type": "application/x-www-form-urlencoded", "Accept": "text/plain"}
        try: 
            self.http_conn.request("PUT", self._url("/" + self.id), params, headers)
            response = self.http_conn.getresponse()
            response.read()
            print response.status 
        except (httplib.HTTPResponse, socket.error) as ex:
            print "Error Sending Data: %s" % ex

    def receive(self):
        try: 
            self.http_conn.request("GET", "/" + self.id)
            response = self.http_conn.getresponse()
            data = response.read()
            if response.status == 200:
                return data
            else: 
                print "GET HTTP Status: %d" % response.status
                return ""
        except (httplib.HTTPResponse, socket.error) as ex:
            print "Error Receiving Data: %s" % ex
            return "" 

    ...

Như bạn thấy ở đây, Connection có những hàm để thiết lập đường hầm, gửi và nhận dữ liệu. Sự tưong tác này được xây dựng trên giao thức HTTP. Cụ thể là:

  • POST request: yêu cầu thiết lập kết nối.
  • PUT request: gửi dữ liệu qua kết nối.
  • GET request: nhận kết nối.
  • DELETE request: kết thúc kết nối.

Sẽ rõ ràng hơn khi ta nhìn vào mã của tunneld.py, thành phần nhận những yêu cầu HTTP này:

class ProxyRequestHandler(BaseHTTPRequestHandler):
    ...

    BUFFER = 1024 * 50 

    def _get_connection_id(self):
        return self.path.split('/')[-1]
    ...

    def do_GET(self):
        """GET: Read data from TargetAddress and return to client through http response"""
        s = self._get_socket()
        if s:
            try:
                data = s.recv(self.BUFFER)
                print data
                self.send_response(200)
                self.end_headers()
                if data:
                    self.wfile.write(data)
        ...

    def do_POST(self):
        """POST: Create TCP Connection to the TargetAddress"""
        id = self._get_connection_id() 

        length = int(self.headers.getheader('content-length'))
        req_data = self.rfile.read(length)
        params = cgi.parse_qs(req_data, keep_blank_values=1) 
        target_host = params['host'][0]
        target_port = int(params['port'][0])

        print 'Connecting to target address: %s % s' % (target_host, target_port)
        #open socket connection to remote server
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.connect((target_host, target_port))
        s.settimeout(7)
        print 'Successfully connected'
        #save socket reference
        self.sockets[id] = s
        try: 
            self.send_response(200)
            self.end_headers()
        except socket.error, e:
            print e

    def do_PUT(self):
        """Read data from HTTP Request and send to TargetAddress"""
        id = self._get_connection_id()
        s = self.sockets[id]
        length = int(self.headers.getheader('content-length'))
        data = cgi.parse_qs(self.rfile.read(length), keep_blank_values=1)['data'][0] 
        try: 
            s.sendall(data)
            self.send_response(200)
        ...

    def do_DELETE(self): 
        self._close_socket()
        self.send_response(200)
        self.end_headers()

Ở đây, ProxyRequestHandler chính là một máy chủ HTTP, nhận và xử lý những yêu cầu cơ bản của HTTP.

  • do_POST: hàm này xử lý những yêu cầu POST. Nó sẽ lấy thông tin về máy đối tượng (tên miền, cổng) và tạo kết nối TCP đến máy đó. Nó trả về trạng thái 200 nếu kết nối này thành công.
  • do_GET: hàm này lấy dữ liệu từ kết nối đã được thiết lập với máy đối tương trong do_POST. Sau đó nó trả dữ liệu này trong trả lời HTTP của yêu cầu GET.
  • do_PUT: hàm này lấy nhận yêu cầu PUT, đọc dữ liệu từ tải của yêu cầu đó và gửi qua kết nối nói trên.
  • do_DELETE: hàm này đóng kết nối với máy đối tượng.

Thử nghiệm chương trình

Chúng ta sẽ thử chương trình này bằng việc kết nối với một IRC server thông qua chương trình. Trước hết, thiết lập đường hầm cần thiết. Tại một máy ngoại mạng, không bị chặn bởi tường lửa, chạy:

python tunneld.py -p 80

Tại máy nội mạng chạy:

python tunnel.py -p 8889 -h mayngoaimang:80 irc.freenode.net:6667

Như vậy ta đã thiết lập một đương hầm ở cổng 8889 qua máy ngoại mạng đến IRC server ở cổng 6667. Yêu cầu đến cổng 6667 thường bị chặn bởi tường lửa. Để thử nghiệm, ta kết nối đến cổng 8889 và gửi yêu cầu theo giao thức IRC:

nc localhost 8889
NICK abcxyz
USER abcxyz abcxyz irc.freenode.net :abcxyz

(nc - netcat - là một công cụ giúp bạn gửi giữ liệu trên TCP: http://www.irongeek.com/i.php?page=backtrack-3-man/netcat).

Ta nhận được trả lời thông báo kết nối thành công:

:calvino.freenode.net NOTICE * :*** Looking up your hostname...
:calvino.freenode.net NOTICE * :*** Checking Ident
:calvino.freenode.net NOTICE * :*** Found your hostname
:calvino.freenode.net NOTICE * :*** No Ident response
NICK abcxyz
USER abcxyz abcxyz irc.freenode.net :abcxyz
:calvino.freenode.net 001 abcxyz :Welcome to the freenode Internet Relay Chat Network abcxyz
:calvino.freenode.net 002 abcxyz :Your host is calvino.freenode.net[ ... /6667], running version ircd-seven-1.1.3
:calvino.freenode.net 003 abcxyz :This server was created Sun Dec 4 2011 at 14:42:47 CET
:calvino.freenode.net 004 abcxyz calvino.freenode.net ircd-seven-1.1.3 DOQRSZaghilopswzCFILMPQbcefgijklmnopqrstvz bkloveqjfI
:calvino.freenode.net 005 abcxyz CHANTYPES=# EXCEPTS INVEX CHANMODES=eIbq,k,flj,CFLMPQcgimnprstz CHANLIMIT=#:120 PREFIX=(ov)@+ MAXLIST=bqeI:100 MODES=4 NETWORK=freenode KNOCK STATUSMSG=@+ CALLERID=g :are supported by this server
:calvino.freenode.net 005 abcxyz CASEMAPPING=rfc1459 CHARSET=ascii NICKLEN=16 CHANNELLEN=50 TOPICLEN=390 ETRACE CPRIVMSG CNOTICE DEAF=D MONITOR=100 FNC TARGMAX=NAMES:1,LIST:1,KICK:1,WHOIS:1,PRIVMSG:4,NOTICE:4,ACCEPT:,MONITOR: :are supported by this server
:calvino.freenode.net 005 abcxyz EXTBAN=$,arx WHOX CLIENTVER=3.0 SAFELIST ELIST=CTU :are supported by this server
:calvino.freenode.net 251 abcxyz :There are 232 users and 70582 invisible on 31 servers
:calvino.freenode.net 252 abcxyz 45 :IRC Operators online
:calvino.freenode.net 253 abcxyz 10 :unknown connection(s)
:calvino.freenode.net 254 abcxyz 34513 :channels formed
:calvino.freenode.net 255 abcxyz :I have 6757 clients and 1 servers
:calvino.freenode.net 265 abcxyz 6757 10768 :Current local users 6757, max 10768
:calvino.freenode.net 266 abcxyz 70814 83501 :Current global users 70814, max 83501
:calvino.freenode.net 250 abcxyz :Highest connection count: 10769 (10768 clients) (2194912 connections received)
...

Kết

Như vậy chúng ta đã đi qua một chương trình xử lý dữ liệu mạng với Python. Chương trình chủ yếu làm việc với dữ liệu thông qua socket API. Một điều quan trọng khác mà bài viết này đề cập đến là sự tách biệt giữa giao thức ứng dụng và dữ liệu gửi trên giao thức đó. Chúng ta có thể vận chuyển dữ liệu mà thông thường đuợc gửi bằng giao thức này qua một giao thức khác.

Chú ý: Chương trình chỉ có mục đích thí nghiệm và không phù hợp với chạy thực dụng.

Lập trình web với Python (3)

written by Nguyễn Thành Nam, on Jan 28, 2010 4:52:25 PM.

Sau khi đã cài đặt máy chủ web, và Python ở hai bài trước, trong bài này chúng ta sẽ bước vào thế giới lập trình web với mô hình đơn giản nhất, mô hình CGI.

CGI là viết tắt của Common Gateway Interface, dịch chính xác là giao tiếp cổng chung. Giao thức này được sinh ra để chuẩn hóa (hay xác định rõ) cách thức một máy chủ web giao phó việc tạo trang web cho các ứng dụng console (các ứng dụng nhận dữ liệu từ bộ nhập chuẩn và xuất ra bộ xuất chuẩn). Các ứng dụng này được gọi là các ứng dụng CGI, hay kịch bản CGI vì chúng thường được viết bằng ngôn ngữ kịch bản, mặc dù chúng cũng có thể được viết bằng các ngôn ngữ khác như C, Java.

Mô hình hoạt động

Thông thường, một máy chủ web nhận yêu cầu từ máy khách và tìm kiếm trên hệ thống xem có tập tin mà máy khách cần truy xuất hay không. Nếu có thì máy chủ web sẽ xuất nội dung của tập tin này cho máy khách.

Trong giao thức CGI, máy chủ web nhận yêu cầu từ máy khách, tìm kiếm trên hệ thống xem có ứng dụng CGI đó không. Nếu có thì máy chủ web sẽ thực thi ứng dụng CGI, chuyển toàn bộ yêu cầu đến ứng dụng thông qua các biến môi trường và bộ nhập chuẩn. Ứng dụng CGI sẽ xử lý yêu cầu rồi gửi trả thông tin cho máy chủ web qua bộ xuất chuẩn. Máy chủ web sẽ nhận thông tin từ bộ xuất chuẩn của ứng dụng CGI để xuất trả lại cho máy khách. Mô hình hoạt động của giao thức CGI được miêu tả trong hình sau.

/static/web-programming/cgi/cgi-model.png

Hello World

Để hiểu rõ hơn cách hoạt động, chúng ta sẽ tạo một tập tin helloworld.py trong thư mục C:\Program Files\Apache Software Foundation\Apache2.2\cgi-bin với nội dung như sau:

#!c:/python26/python

print "Content-Type: text/html"
print ""
print "Hello world."

Trước khi giải thích từng dòng lệnh, chúng ta sẽ xem xem kết quả đạt được là gì. Chúng ta sẽ dùng trình duyệt để vào trang http://localhost/cgi-bin/helloworld.py. Dĩ nhiên chúng ta cũng phải đảm bảo rằng máy chủ Apache đang chạy. Và đây là hình mà chúng ta nhận được.

/static/web-programming/cgi/helloworld.png

Các yếu tố chính của chương trình CGI

Như thế, chương trình CGI đầu tiên đã thực thi hoàn chỉnh. Hãy quay lại xem các yếu tố căn bản của một chương trình CGI viết bằng ngôn ngữ Python.

Dòng đầu tiên được gọi là dòng shebang với hai ký tự đặc biệt #!. Phần đi ngay sau hai ký tự này là đường dẫn tuyệt đối đến trình thông dịch Python trên hệ thống. Dòng này nói cho máy chủ web biết rằng tập tin này cần được đọc bởi trình thông dịch Python.

Dòng lệnh print thứ nhất cung cấp một đầu mục (header) cho máy chủ web. Đầu mục này là Content-Type với nội dung là text/html. Đây là đầu mục bắt buộc trong giao thức HTTP, do đó các ứng dụng CGI thông thường phải cung cấp đầu mục này.

Dòng lệnh in thứ hai (in ra một dòng trống) báo hiệu sự kết thúc của các đầu mục, và những gì theo sau sẽ là dữ liệu nội dung. Dòng lệnh này là bắt buộc.

Dòng lệnh in thứ ba xuất nội dung chính, là chuỗi Hello world..

Các dòng lệnh này là những yếu tố chính cấu tạo nên một chương trình CGI viết bằng ngôn ngữ Python. Điểm quan trọng nhất mà chúng ta cần lưu ý là việc xuất dữ liệu thông qua bộ xuất chuẩn (stdout), và định dạng của dữ liệu cần xuất (đầu mục, dòng trống, nội dung).

Thêm vào các thẻ HTML

Chương trình của chúng ta khá buồn tẻ vì chúng ta khai báo Content-Typetext/html nhưng chúng ta không dùng thẻ HTML. Chúng ta hãy sửa lại chương trình như sau:

#!c:/python26/python

print "Content-Type: text/html"
print ""
print """<html>
	<head>
		<title>Python!</title>
	</head>
	<body>
		<font size="+3">H</font>ello <font color="red">HTML</font>.
	</body>
</html>"""

Quay lại trình duyệt và làm tươi (F5, hay Ctrl-R), chúng ta sẽ thấy hình như sau:

/static/web-programming/cgi/hellohtml.png

Content-Type là quan trọng

Những gì chúng ta vừa thực hiện là thay đổi nội mà chúng ta xuất ra bộ xuất chuẩn.

Bây giờ, giữ cùng nội dung đấy, nhưng chúng ta thay đổi đầu mục Content-Type thành text/plain.

#!c:/python26/python

print "Content-Type: text/plain"
print ""
print """<html>
	<head>
		<title>Python!</title>
	</head>
	<body>
		<font size="+3">H</font>ello <font color="red">HTML</font>.
	</body>
</html>"""

Thử xem trang này với các trình duyệt chuẩn ví dụ Opera, hoặc Firefox thì chúng ta sẽ nhận được hình tương tự như sau:

/static/web-programming/cgi/hellohtmlplain.png

Thay vì xử lý những thẻ HTML một cách đặc biệt thì trình duyệt đã không làm như vậy. Và đây là cách làm đúng bởi vì chúng ta nói cho trình duyệt biết rằng nội dung của chúng là ở dạng văn bản thường (plaintext) thông qua đầu mục Content-Type như trên. Trình duyệt IE không tuân theo chuẩn HTTP một cách triệt để nên vẫn sẽ hiển thị nội dung của chúng ta dưới dạng một trang HTML.

Một ít dữ liệu động

Mục đích của chương trình CGI là xử lý và xuất dữ liệu nên sẽ là vô ích nếu chương trình CGI của chúng ta chỉ xuất các trang web tĩnh. Chúng ta sẽ sửa lại chương trình một chút như sau:

#!c:/python26/python

import random

actions = ("rocks", "rules", "kicks butt", "constricts camel")

print "Content-Type: text/html"
print ""
print """<html>
	<head>
		<title>Python!</title>
	</head>
	<body>
		Python %s.
	</body>
</html>""" % random.choice(actions)

Trong chương trình này, chúng ta sử dụng mô-đun random và hàm choice để chọn một hành động ngẫu nhiên. Khi làm tươi trình duyệt, chúng ta có thể gặp các hình tương tự như sau.

/static/web-programming/cgi/dynamic1.png

/static/web-programming/cgi/dynamic2.png

Tóm tắt

Trong kỳ này chúng ta đã được giới thiệu về mô hình hoạt động của giao thức CGI, các yếu tố quan trọng trong một chương trình CGI viết bằng ngôn ngữ Python, một ít kiến thức về giao thức HTTP, và sự đa dạng của thế giới trình duyệt web. Ở kỳ sau chúng ta sẽ tìm hiểu cách nhận dữ liệu từ máy khách qua URL và mẫu đơn (form).