23.06.15
DEVOTEE를 활성화 시키면
지금 작성한 커뮤니티 글에 대해 1개의 댓글을 달아줍니다.
버튼을 누르면 글 수정 시 ChatGPT가 작성한 댓글이 수정됩니다.
| 컨텐츠 유형 | 제목 | 저장일 | 삭제 |
|---|
본인인증 로그인에 실패하였습니다.
회원이 아니시거나 본인인증 등록이
완료되지 않은 사용자입니다.
안녕하세요. Teus입니다.
계속해서 네트워크 관련 포스팅으로 찾아뵙고 있습니다.
이번 포스팅은 Python에서 기초적으로 제공하는 http.server와 CGI에 대해서 다룹니다.
Python은 socketserver.TCPServer의 child Class인 http.server.HTTPServer
를 활용해서 기본적인 서버를 만들 수 있게 준비되어있습니다.
import http.server
import socketserver
def run(server_class=http.server.HTTPServer, handler_class=http.server.SimpleHTTPRequestHandler):
PORT = 8000
server_address = ('', PORT)
httpd = server_class(server_address, handler_class)
httpd.serve_forever()
run()WSGI의 경우 Server class와 Handler를 선택할 필요 없이 Response를 줄 App만 만들면 되었습니다.
반면 위 http.server를 사용하면 handler를 선택해서 response를 줄 수 있습니다.
이때 http.server.CGIHTTPRequestHanlder를 통해서 CGI가 어떤식으로 동작하는지 알아보도록 하겠습니다.
일단 TCPServer의 serve_forever()같은 경우 WSGI 1편 포스팅 이미 다뤘습니다.
Socket이 selector로 request를 받아들이고
BaseServer의 self.finish_request()에서 ReqeustHandler를 사용해서 처리했습니다.
(이 내용은 WSGI2편 포스팅에서 다뤘습니다.)
#https://github.com/python/cpython/blob/7dead6a33ac786465c3d927d5c2025caf0c1a71b/Lib/socketserver.py#L342
def process_request(self, request, client_address):
self.finish_request(request, client_address)
self.shutdown_request(request)
...
def finish_request(self, request, client_address):
"""Finish one request by instantiating RequestHandlerClass."""
self.RequestHandlerClass(request, client_address, self)일단 CGIHTTPRequestHandler는 아래와 같은 구조로 되어있습니다.
↑
↑
↑
↑
일단 BaseRequestHandler의 생성자를 다시 보겠습니다.
class BaseRequestHandler:
def __init__(self, request, client_address, server):
self.request = request
self.client_address = client_address
self.server = server
self.setup()
try:
self.handle()
finally:
self.finish()
def setup(self):
pass
def handle(self):
pass
def finish(self):
pass생성자를 실행하면서 self.setup(), self.handle(), self.finish()를 순서대로 실행하게 됩니다.
이때 self.setup()와 self.finish()은
socket을 준비하고 stdout에 준비된 Data를 socket을 통해서 외부로 송출합니다.
위 method들은 StreamRequestHandler에 정의 되어있습니다.
#https://github.com/python/cpython/blob/7dead6a33ac786465c3d927d5c2025caf0c1a71b/Lib/socketserver.py#L777
class StreamRequestHandler(BaseRequestHandler):
...
rbufsize = -1
wbufsize = 0
# A timeout to apply to the request socket, if not None.
timeout = None
# Disable nagle algorithm for this socket, if True.
# Use only when wbufsize != 0, to avoid small packets.
disable_nagle_algorithm = False
def setup(self):
self.connection = self.request
if self.timeout is not None:
self.connection.settimeout(self.timeout)
if self.disable_nagle_algorithm:
self.connection.setsockopt(socket.IPPROTO_TCP,
socket.TCP_NODELAY, True)
self.rfile = self.connection.makefile('rb', self.rbufsize)
if self.wbufsize == 0:
self.wfile = _SocketWriter(self.connection)
else:
self.wfile = self.connection.makefile('wb', self.wbufsize)
def finish(self):
if not self.wfile.closed:
try:
self.wfile.flush()
except socket.error:
# A final socket error may have occurred here, such as
# the local error ECONNABORTED.
pass
self.wfile.close()
self.rfile.close()WSGI와 달리, 이번에는 self.handle()이 BaseHTTPRequestHandler에 정의된 상태로 사용됩니다.
#https://github.com/python/cpython/blob/7dead6a33ac786465c3d927d5c2025caf0c1a71b/Lib/http/server.py#L432
class BaseHTTPRequestHandler(socketserver.StreamRequestHandler):
...
def handle(self):
"""Handle multiple requests if necessary."""
self.close_connection = True
self.handle_one_request()
while not self.close_connection:
self.handle_one_request()
def handle_one_request(self):
try:
self.raw_requestline = self.rfile.readline(65537)
if len(self.raw_requestline) > 65536:
self.requestline = ''
self.request_version = ''
self.command = ''
self.send_error(HTTPStatus.REQUEST_URI_TOO_LONG)
return
if not self.raw_requestline:
self.close_connection = True
return
if not self.parse_request():
# An error code has been sent, just exit
return
mname = 'do_' + self.command
if not hasattr(self, mname):
self.send_error(
HTTPStatus.NOT_IMPLEMENTED,
"Unsupported method (%r)" % self.command)
return
#여기서 하부의 do_GET, do_POST와 같은 method를 가지고옴
method = getattr(self, mname)
method()
self.wfile.flush() #actually send the response if not already done.
...위 self.handle_one_request()에서 볼 수 있듯 do_ResponseMethod를 통해서 HTTP Msg를 처리하게 됩니다.
do_GET,do_GET은 SimpleHTTPRequestHandler
do_POST는 CGIHTTPRequestHandler에 정의되어 있습니다.
그래서 SimpleHTTPRequestHandler를 사용한 HTTP Server는 Post를 사용할 경우 Error가 발생합니다
이때 do_POST를 통해서 CGI가 어떻게 Python에서 구현되었는지 볼까요?
#https://github.com/python/cpython/blob/7dead6a33ac786465c3d927d5c2025caf0c1a71b/Lib/http/server.py#L978
class CGIHTTPRequestHandler(SimpleHTTPRequestHandler):
...
def do_POST(self):
#cgi가 가능한 url path인지를 판별
if self.is_cgi():
self.run_cgi()
else:
#Error send
def is_cgi(self):
collapsed_path = _url_collapse_path(self.path)
dir_sep = collapsed_path.find('/', 1)
while dir_sep > 0 and not collapsed_path[:dir_sep] in self.cgi_directories:
dir_sep = collapsed_path.find('/', dir_sep+1)
if dir_sep > 0:
head, tail = collapsed_path[:dir_sep], collapsed_path[dir_sep+1:]
self.cgi_info = head, tail
return True
return False
cgi_directories = ['/cgi-bin', '/htbin']CGI의 경우 response를 보낼 app을 /cgi-bin 혹은 /htbin 위치에 두어야만 작동합니다
이를 python코드로 cgi_directories를 만들어놓고, 여기서 path가 이중에 있는지를 찾는걸 볼 수 있습니다.
그리고, 문제가 없다면 self.run_cgi를 통해서 CGI Script를 실행합니다.
#https://github.com/python/cpython/blob/7dead6a33ac786465c3d927d5c2025caf0c1a71b/Lib/http/server.py#L1053
def run_cgi(self):
"""Execute a CGI script."""
#==============================================
#dir = '/cgi-bin' or '/htbin'
#rest = cgi script file.py
#==============================================
dir, rest = self.cgi_info
path = dir + '/' + rest
i = path.find('/', len(dir)+1)
while i >= 0:
nextdir = path[:i]
nextrest = path[i+1:]
scriptdir = self.translate_path(nextdir)
if os.path.isdir(scriptdir):
dir, rest = nextdir, nextrest
i = path.find('/', len(dir)+1)
else:
break
# find an explicit query string, if present.
rest, _, query = rest.partition('?')
# dissect the part after the directory name into a script name &
# a possible additional path, to be stored in PATH_INFO.
i = rest.find('/')
if i >= 0:
script, rest = rest[:i], rest[i:]
else:
script, rest = rest, ''
scriptname = dir + '/' + script
scriptfile = self.translate_path(scriptname)
if not os.path.exists(scriptfile):
self.send_error(
HTTPStatus.NOT_FOUND,
"No such CGI script (%r)" % scriptname)
return
if not os.path.isfile(scriptfile):
self.send_error(
HTTPStatus.FORBIDDEN,
"CGI script is not a plain file (%r)" % scriptname)
return
ispy = self.is_python(scriptname)
if self.have_fork or not ispy:
if not self.is_executable(scriptfile):
self.send_error(
HTTPStatus.FORBIDDEN,
"CGI script is not executable (%r)" % scriptname)
return
#==============================================
# 여기까지 CGI Script를 찾고, CGI Sciprt가 유효한지를 판단함
#==============================================먼저 CGI Script 실행하기 앞서
주어진 url 주소를 파싱해서 CGI Script의 정확한 위치를 찾고
해당 Script가 실제 존재하는지, python으로 실행 가능한 파일인지를 검증합니다.
def run_cgi(self):
...
env = copy.deepcopy(os.environ)
env['SERVER_SOFTWARE'] = self.version_string()
env['SERVER_NAME'] = self.server.server_name
env['GATEWAY_INTERFACE'] = 'CGI/1.1'
env['SERVER_PROTOCOL'] = self.protocol_version
env['SERVER_PORT'] = str(self.server.server_port)
env['REQUEST_METHOD'] = self.command
uqrest = urllib.parse.unquote(rest)
env['PATH_INFO'] = uqrest
env['PATH_TRANSLATED'] = self.translate_path(uqrest)
env['SCRIPT_NAME'] = scriptname
env['QUERY_STRING'] = query
env['REMOTE_ADDR'] = self.client_address[0]
authorization = self.headers.get("authorization")
if authorization:
authorization = authorization.split()
if len(authorization) == 2:
import base64, binascii
env['AUTH_TYPE'] = authorization[0]
if authorization[0].lower() == "basic":
try:
authorization = authorization[1].encode('ascii')
authorization = base64.decodebytes(authorization).\
decode('ascii')
except (binascii.Error, UnicodeError):
pass
else:
authorization = authorization.split(':')
if len(authorization) == 2:
env['REMOTE_USER'] = authorization[0]
# XXX REMOTE_IDENT
if self.headers.get('content-type') is None:
env['CONTENT_TYPE'] = self.headers.get_content_type()
else:
env['CONTENT_TYPE'] = self.headers['content-type']
length = self.headers.get('content-length')
if length:
env['CONTENT_LENGTH'] = length
referer = self.headers.get('referer')
if referer:
env['HTTP_REFERER'] = referer
accept = self.headers.get_all('accept', ())
env['HTTP_ACCEPT'] = ','.join(accept)
ua = self.headers.get('user-agent')
if ua:
env['HTTP_USER_AGENT'] = ua
co = filter(None, self.headers.get_all('cookie', []))
cookie_str = ', '.join(co)
if cookie_str:
env['HTTP_COOKIE'] = cookie_str
# XXX Other HTTP_* headers
# Since we're setting the env in the parent, provide empty
# values to override previously set values
for k in ('QUERY_STRING', 'REMOTE_HOST', 'CONTENT_LENGTH',
'HTTP_USER_AGENT', 'HTTP_COOKIE', 'HTTP_REFERER'):
env.setdefault(k, "")
self.send_response(HTTPStatus.OK, "Script output follows")
#header를 response buff에 입력한 뒤
#header를 담고있던 list를 초기화 시켜줌
self.flush_headers()
decoded_query = query.replace('+', ' ') 그리고 header를 준비합니다.
header같은 경우 dict형태로 저장하는것을 볼 수 있습니다.
여기서 self.send_response()라는 method이름 때문에 혼란스러울 수 있으나
사실 self.send_response()는 client한태 보내는것이 아니라, buffer에다가 보내는 것을 확인할 수 있습니다.
#https://github.com/python/cpython/blob/7dead6a33ac786465c3d927d5c2025caf0c1a71b/Lib/http/server.py#L520
class BaseHTTPRequestHandler(socketserver.StreamRequestHandler):
...
def send_response(self, code, message=None):
"""Add the response header to the headers buffer and log the
response code.
Also send two standard headers with the server software
version and the current date.
"""
self.log_request(code)
self.send_response_only(code, message)
self.send_header('Server', self.version_string())
self.send_header('Date', self.date_time_string())
def send_response_only(self, code, message=None):
"""Send the response header only."""
if self.request_version != 'HTTP/0.9':
if message is None:
if code in self.responses:
message = self.responses[code][0]
else:
message = ''
if not hasattr(self, '_headers_buffer'):
self._headers_buffer = []
self._headers_buffer.append(("%s %d %s\r\n" %
(self.protocol_version, code, message)).encode(
'latin-1', 'strict'))이제 HTTP Header와 Script가 준비과 완료 되었습니다.
이제 이 Script를 실행시켜 HTTP Response Msg를 준비할 시간입니다.
#실제 CGI를 실행
def run_cgi(self):
...
if self.have_fork:
# Unix -- fork as we should
# Unix 계열 시스템에서 fork를 사용한 CGI실행
...
else:
# Non-Unix -- use subprocess
import subprocess
cmdline = [scriptfile]
if self.is_python(scriptfile):
interp = sys.executable
if interp.lower().endswith("w.exe"):
# On Windows, use python.exe, not pythonw.exe
interp = interp[:-5] + interp[-4:]
cmdline = [interp, '-u'] + cmdline
if '=' not in query:
cmdline.append(query)
#==============================================
#여기까지 Command prompt에 python파일을 실행하기위한
#실행문장 + 매개변수를 정의함
#==============================================
self.log_message("command: %s", subprocess.list2cmdline(cmdline))
try:
nbytes = int(length)
except (TypeError, ValueError):
nbytes = 0
#subprocess를 통해서 response를 할 때 마다 Process를 새로 만듦
p = subprocess.Popen(cmdline,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env = env
)
if self.command.lower() == "post" and nbytes > 0:
data = self.rfile.read(nbytes)
else:
data = None
# throw away additional data [see bug #427345]
while select.select([self.rfile._sock], [], [], 0)[0]:
if not self.rfile._sock.recv(1):
break
# 자식 process에서 스크립트를 실행하고
# 스크립트가 끝날 때까지 대기함
# process.join()과 유사함
stdout, stderr = p.communicate(data)
self.wfile.write(stdout)
if stderr:
self.log_error('%s', stderr)
p.stderr.close()
p.stdout.close()
status = p.returncode
if status:
self.log_error("CGI script exit status %#x", status)
else:
self.log_message("CGI script exited OK")```결과로 확인할 수 있듯
Process를 만들어서 HTTP Response Msg를 만들어 냅니다.
그리고 Main Process에서는 subprocess에서 만들어낸 text를 기반으로
HTTP Response Msg를 만들고, Socket을 통해 외부로 전달합니다.
의외인 점은, HTTP Msg를 보내는 작업 전체가 subprocess로 되어있지 않습니다.
먼저 project directory를 아래처럼 만들어 줍니다.
simple_server(폴더)
->server.py
->cgi-bin(폴더)
-->cgi_script.py그리고 server.py 파일을 아래처럼 http server로 구축 합니다.
#simple_server/server.py
import http.server
import socketserver
PORT = 8000
Handler = http.server.CGIHTTPRequestHandler
with http.server.HTTPServer(("", PORT), Handler) as httpd:
print("serving at port", PORT)
httpd.serve_forever()그리고 response를 만들기 위한 script파일을 아래처럼 만들어 줍니다.
#simple_server/cgi-bin/cgi_script.py
import time
time.sleep(3)
print("Content-type: text/html\n\n")
print("hello world!")그리고 서버를 실행하면 serving at port 8000 이라는 메세지와 함께 서버가 작동합니다.
cgi는 post로만 작동하기 때문에 python requests package로 실행 해봅니다.
import requests
res = requests.post("http://localhost:8000/cgi-bin/cgi_script.py")
print(res.text)실행하면 서버shell에서 정상적으로 script가 실행되고 response를 보낸것을 확인할 수 있습니다.
이때 두개의 python shell을 키고 동시에 실행할 경우 순차적으로 두개가 실행되는것을 통해서
CGI가 동시에 여러 requests를 처리하지는 않는것을 볼 수 있습니다.
DEVOTEE를 활성화 시키면
지금 작성한 댓글에 AI가 댓글을 달아줍니다.