Windows MSYS シェルスクリプトとして書いた CGI を Python の CGIHTTPServer でテスト(改々)

怪物ランドのプリンスだ。

Windows MSYS シェルスクリプトとして書いた CGI を Python の CGIHTTPServer でテスト(改)」ではとりあえず自分の用を足せればいいや、という雑なものだったので、多少整理してみた。

本題の前におさらい。

これをどうにかしたかった、ということね。

同じ「testcgihttpserver.py」:

testcgihttpserver.py
  1 # -*- coding: utf-8 -*-
  2 import copy
  3 import os
  4 import sys
  5 import select
  6 
  7 
  8 try:
  9     import urllib.parse  # Python 3.x
 10     def unquote(s):
 11         return urllib.parse.unquote(s)
 12     import http.server as cgihttpserver_mod
 13     _PY27 = False
 14 
 15 except ImportError:
 16     import urllib
 17     def unquote(s):
 18         return urllib.unquote(s)
 19     import CGIHTTPServer as cgihttpserver_mod # Python 2.7
 20     _PY27 = True
 21 
 22 
 23 nobody_uid = cgihttpserver_mod.nobody_uid
 24 
 25 
 26 def _run_cgi_process(self, env, script, scriptfile, query, decoded_query, length):
 27     if self.have_fork:
 28         # Unix -- fork as we should
 29         args = [script]
 30         if '=' not in decoded_query:
 31             args.append(decoded_query)
 32         nobody = nobody_uid()
 33         self.wfile.flush() # Always flush before forking
 34         pid = os.fork()
 35         if pid != 0:
 36             # Parent
 37             pid, sts = os.waitpid(pid, 0)
 38             # throw away additional data [see bug #427345]
 39             while select.select([self.rfile], [], [], 0)[0]:
 40                 if not self.rfile.read(1):
 41                     break
 42             if sts:
 43                 self.log_error("CGI script exit status %#x", sts)
 44             return
 45         # Child
 46         try:
 47             try:
 48                 os.setuid(nobody)
 49             except (os.error, OSError):
 50                 pass
 51             os.dup2(self.rfile.fileno(), 0)
 52             os.dup2(self.wfile.fileno(), 1)
 53             os.execve(scriptfile, args, env)
 54         except:
 55             self.server.handle_error(self.request, self.client_address)
 56             os._exit(127)
 57 
 58     else:
 59         # Non-Unix -- use subprocess
 60         import subprocess
 61         cmdline = [scriptfile]
 62         if self.is_python(scriptfile):
 63             interp = sys.executable
 64             if interp.lower().endswith("w.exe"):
 65                 # On Windows, use python.exe, not pythonw.exe
 66                 interp = interp[:-5] + interp[-4:]
 67             cmdline = [interp, '-u'] + cmdline
 68         elif os.path.splitext(scriptfile)[1] not in (".exe", ".bat"):
 69             # with MSYS or Cygwin (ba-)sh
 70             cmdline.insert(0, "sh")
 71         if '=' not in query:
 72             cmdline.append(query)
 73         self.log_message("command: %s", subprocess.list2cmdline(cmdline))
 74         try:
 75             nbytes = int(length)
 76         except (TypeError, ValueError):
 77             nbytes = 0
 78         p = subprocess.Popen(cmdline,
 79                              stdin=subprocess.PIPE,
 80                              stdout=subprocess.PIPE,
 81                              stderr=subprocess.PIPE,
 82                              env = env
 83                              )
 84         if self.command.lower() == "post" and nbytes > 0:
 85             data = self.rfile.read(nbytes)
 86         else:
 87             data = None
 88         # throw away additional data [see bug #427345]
 89         while select.select([self.rfile._sock], [], [], 0)[0]:
 90             if not self.rfile._sock.recv(1):
 91                 break
 92         stdout, stderr = p.communicate(data)
 93         self.wfile.write(stdout)
 94         if stderr:
 95             self.log_error('%s', stderr)
 96         p.stderr.close()
 97         p.stdout.close()
 98         status = p.returncode
 99         if status:
100             self.log_error("CGI script exit status %#x", status)
101         else:
102             self.log_message("CGI script exited OK")
103 
104 
105 def run_cgi(self):
106     """Execute a CGI script."""
107     dir, rest = self.cgi_info
108     path = dir + '/' + rest
109     i = path.find('/', len(dir)+1)
110     while i >= 0:
111         nextdir = path[:i]
112         nextrest = path[i+1:]
113 
114         scriptdir = self.translate_path(nextdir)
115         if os.path.isdir(scriptdir):
116             dir, rest = nextdir, nextrest
117             i = path.find('/', len(dir)+1)
118         else:
119             break
120 
121     # find an explicit query string, if present.
122     i = rest.rfind('?')
123     if i >= 0:
124         rest, query = rest[:i], rest[i+1:]
125     else:
126         query = ''
127 
128     # dissect the part after the directory name into a script name &
129     # a possible additional path, to be stored in PATH_INFO.
130     i = rest.find('/')
131     if i >= 0:
132         script, rest = rest[:i], rest[i:]
133     else:
134         script, rest = rest, ''
135 
136     scriptname = dir + '/' + script
137     scriptfile = self.translate_path(scriptname)
138     if not os.path.exists(scriptfile):
139         self.send_error(404, "No such CGI script (%r)" % scriptname)
140         return
141     if not os.path.isfile(scriptfile):
142         self.send_error(403, "CGI script is not a plain file (%r)" %
143                         scriptname)
144         return
145     ispy = self.is_python(scriptname)
146     if self.have_fork or not ispy:
147         if not self.is_executable(scriptfile):
148             self.send_error(403, "CGI script is not executable (%r)" %
149                             scriptname)
150             return
151 
152     # Reference: http://hoohoo.ncsa.uiuc.edu/cgi/env.html
153     # XXX Much of the following could be prepared ahead of time!
154     env = copy.deepcopy(os.environ)
155     env['SERVER_SOFTWARE'] = self.version_string()
156     env['SERVER_NAME'] = self.server.server_name
157     env['GATEWAY_INTERFACE'] = 'CGI/1.1'
158     env['SERVER_PROTOCOL'] = self.protocol_version
159     env['SERVER_PORT'] = str(self.server.server_port)
160     env['REQUEST_METHOD'] = self.command
161     uqrest = unquote(rest)
162     env['PATH_INFO'] = uqrest
163     env['PATH_TRANSLATED'] = self.translate_path(uqrest)
164     env['SCRIPT_NAME'] = scriptname
165     if query:
166         env['QUERY_STRING'] = query
167     env['REMOTE_ADDR'] = self.client_address[0]
168     authorization = self.headers.get("authorization")
169     if authorization:
170         authorization = authorization.split()
171         if len(authorization) == 2:
172             import base64, binascii
173             if not hasattr(base64, 'decodebytes'):
174                 # Python 2.7
175                 base64.decodebytes = base64.decodestring
176             env['AUTH_TYPE'] = authorization[0]
177             if authorization[0].lower() == "basic":
178                 try:
179                     authorization = authorization[1].encode('ascii')
180                     authorization = base64.decodebytes(authorization).\
181                                     decode('ascii')
182                 except (binascii.Error, UnicodeError):
183                     pass
184                 else:
185                     authorization = authorization.split(':')
186                     if len(authorization) == 2:
187                         env['REMOTE_USER'] = authorization[0]
188     # XXX REMOTE_IDENT
189     if self.headers.get('content-type') is None:
190         env['CONTENT_TYPE'] = self.headers.get_content_type()
191     else:
192         env['CONTENT_TYPE'] = self.headers['content-type']
193     length = self.headers.get('content-length')
194     if length:
195         env['CONTENT_LENGTH'] = length
196     referer = self.headers.get('referer')
197     if referer:
198         env['HTTP_REFERER'] = referer
199     accept = self.headers.get_all('accept', ())
200     env['HTTP_ACCEPT'] = ','.join(accept)
201     # ---------------------------------------- ADD BEGIN
202     accept_encoding = self.headers.get_all('accept-encoding', ())
203     env['HTTP_ACCEPT_ENCODING'] = ','.join(accept_encoding)
204     accept_charset = self.headers.get_all('accept-charset', ())
205     env['HTTP_ACCEPT_CHARSET'] = ','.join(accept_charset)
206     accept_language = self.headers.get_all('accept-language', ())
207     env['HTTP_ACCEPT_LANGUAGE'] = ','.join(accept_language)
208     # ---------------------------------------- ADD END
209     ua = self.headers.get('user-agent')
210     if ua:
211         env['HTTP_USER_AGENT'] = ua
212     co = filter(None, self.headers.get_all('cookie', []))
213     cookie_str = ', '.join(co)
214     if cookie_str:
215         env['HTTP_COOKIE'] = cookie_str
216     # XXX Other HTTP_* headers
217     # Since we're setting the env in the parent, provide empty
218     # values to override previously set values
219     for k in ('QUERY_STRING', 'REMOTE_HOST', 'CONTENT_LENGTH',
220               'HTTP_USER_AGENT', 'HTTP_COOKIE', 'HTTP_REFERER'):
221         env.setdefault(k, "")
222 
223     self.send_response(200, "Script output follows")
224     self.flush_headers()
225 
226     decoded_query = query.replace('+', ' ')
227 
228     self._run_cgi_process(env, script, scriptfile, query, decoded_query, length)
229 
230 
231 if _PY27:
232     import email.message
233     import mimetools
234 
235     def _splitparam(param):
236         # Split header parameters.  BAW: this may be too simple.  It isn't
237         # strictly RFC 2045 (section 5.1) compliant, but it catches most headers
238         # found in the wild.  We may eventually need a full fledged parser.
239         # RDM: we might have a Header here; for now just stringify it.
240         a, sep, b = str(param).partition(';')
241         if not sep:
242             return a.strip(), None
243         return a.strip(), b.strip()
244 
245     class _Message(mimetools.Message):
246         def get(self, name, failobj=None):
247             return self.getheader(name, failobj)
248 
249         def get_all(self, name, fallback=()):
250             return self.getheaders(name)
251 
252         def __getitem__(self, name):
253             return self.get(name)
254 
255         def get_content_type(self):
256             """Return the message's content type.
257     
258             The returned string is coerced to lower case of the form
259             `maintype/subtype'.  If there was no Content-Type header in the
260             message, the default type as given by get_default_type() will be
261             returned.  Since according to RFC 2045, messages always have a default
262             type this will always return a value.
263     
264             RFC 2045 defines a message's default type to be text/plain unless it
265             appears inside a multipart/digest container, in which case it would be
266             message/rfc822.
267             """
268             missing = object()
269             value = self.get('content-type', missing)
270             if value is missing:
271                 # This should have no parameters
272                 return 'text/plain'
273             ctype = _splitparam(value)[0].lower()
274             # RFC 2045, section 5.2 says if its invalid, use text/plain
275             if ctype.count('/') != 1:
276                 return 'text/plain'
277             return ctype
278 
279 
280     class LocalPatchedCGIHTTPRequestHandler(cgihttpserver_mod.CGIHTTPRequestHandler):
281 
282         def __init__(self, request, server_address, RequestHandlerClass):
283             self.MessageClass = _Message
284             cgihttpserver_mod.CGIHTTPRequestHandler.__init__(
285                 self, request, server_address, RequestHandlerClass)
286 
287         def is_executable(self, path):
288             """Test whether argument path is an executable file."""
289             return os.access(path, os.X_OK)
290 
291         def flush_headers(self):
292             pass
293 
294 
295     LocalPatchedCGIHTTPRequestHandler._run_cgi_process = _run_cgi_process
296     LocalPatchedCGIHTTPRequestHandler.run_cgi = run_cgi
297 
298 
299 else:  # Python 3.x
300     LocalPatchedCGIHTTPRequestHandler = cgihttpserver_mod.CGIHTTPRequestHandler
301     LocalPatchedCGIHTTPRequestHandler._run_cgi_process = _run_cgi_process
302     LocalPatchedCGIHTTPRequestHandler.run_cgi = run_cgi
303 
304 
305 def test(**kw):
306     """Test the HTTP request handler class.
307 
308     This runs an HTTP server on port 8000 (or the first command line
309     argument).
310 
311     """
312     kw["HandlerClass"] = LocalPatchedCGIHTTPRequestHandler
313     cgihttpserver_mod.test(**kw)

Windows MSYS シェルスクリプトとして書いた CGI を Python の CGIHTTPServer でテスト(改)」とまぁ同じで「スパゲティを継承」しているオブジェクト指向まんせーですわよ。

テクニカルなことはまぁ、貼り付けたコードみてもらって、へぇとかほぉとかアホやなぁとかつぶやいてもらえれば、まぁいいです。かなりヘンチクリンなことしてますよ。

スペック的にはやや注意点あり、です。

  • REMOTE_HOST ヘッダは Python 2.7 版にはあったが、Python 3.x 版で消えていたので、そちらに倣った
  • Python 2.7 版では self.flush_headers() は何もしない(ので何か 3.x でフィックスされたかもしれないバグは「オリジナル 2.7 版のまま」)
  • content-type、authorization 部分は、Python 3.x 流儀のアダプタは書くには書いたけれど、ワタシはテストしてないので、Python 2.7 での動作は保障出来ない

かくして、同じくこんなことが出来る:

mycgitest.py
 1 # -*- coding: utf-8 -*-
 2 import unittest
 3 try:
 4     import urllib2
 5 except ImportError:  # Python 3.x
 6     import urllib.request as urllib2
 7 import json
 8 import sys
 9 
10 SERVICE_URL = "http://localhost:8000/cgi-bin/mycgi.cgi";
11 class TestAsCGI(unittest.TestCase):
12 
13     def setUp(self):
14         pass
15 
16     def _call(self, postdata, headers):
17         req = urllib2.Request(SERVICE_URL, data=postdata)
18         for k in headers:
19             req.add_header(k, headers[k])
20         #print(req.headers)
21         handler = urllib2.HTTPHandler(debuglevel=0)
22         opener = urllib2.build_opener(handler)
23 
24         f = opener.open(req)
25         #print(f.headers)
26         return f
27 
28     # --------------------------------------------------------
29     def test_01(self):
30         f = self._call({"a": "b"}, {
31                 "Accept-Charset": "MS_Kanji",
32                 "Accept-Encoding": "gzip;q=1,deflate;q=0.9",
33                 "Accept": "text/html"})
34 
35 
36 
37 def _testserver():
38     # testcgihttpserver はカレントフォルダの「下」に cgi-bin がいる
39     # ことをあてにしていて、mycgi.cgi、mycgitest.py ともに cgi-bin
40     # フォルダ内にいる、とすると、サーバは一つ上から起動する。
41     import os
42     os.chdir("..")
43     import testcgihttpserver
44     testcgihttpserver.test()
45 
46 
47 if __name__ == '__main__':
48     from multiprocessing import Process
49 
50     p = Process(target=_testserver)  # 裏でサーバを起こす
51     p.start()
52     try:
53         unittest.main()
54     finally:
55         p.terminate()

実行例(テストは11書いてある):

 1 me@localhost: cgi-bin$ /c/Python34/python mycgitest.py
 2 Serving HTTP on 0.0.0.0 port 8000 ...
 3 127.0.0.1 - - [07/Jul/2015 05:52:43] "POST /cgi-bin/mycgi.cgi HTTP/1.1" 200 -
 4 127.0.0.1 - - [07/Jul/2015 05:52:43] command: sh c:\Users\hhsprings\sandbox\cgi-bin\mycgi.cgi ""
 5 127.0.0.1 - - [07/Jul/2015 05:52:44] CGI script exited OK
 6 .127.0.0.1 - - [07/Jul/2015 05:52:45] "POST /cgi-bin/mycgi.cgi HTTP/1.1" 200 -
 7 127.0.0.1 - - [07/Jul/2015 05:52:45] command: sh c:\Users\hhsprings\sandbox\cgi-bin\mycgi.cgi ""
 8 127.0.0.1 - - [07/Jul/2015 05:52:46] CGI script exited OK
 9 .127.0.0.1 - - [07/Jul/2015 05:52:47] "POST /cgi-bin/mycgi.cgi HTTP/1.1" 200 -
10 127.0.0.1 - - [07/Jul/2015 05:52:47] command: sh c:\Users\hhsprings\sandbox\cgi-bin\mycgi.cgi ""
11 127.0.0.1 - - [07/Jul/2015 05:52:48] CGI script exited OK
12 .127.0.0.1 - - [07/Jul/2015 05:52:49] "POST /cgi-bin/mycgi.cgi HTTP/1.1" 200 -
13 127.0.0.1 - - [07/Jul/2015 05:52:49] command: sh c:\Users\hhsprings\sandbox\cgi-bin\mycgi.cgi ""
14 127.0.0.1 - - [07/Jul/2015 05:52:51] CGI script exited OK
15 .127.0.0.1 - - [07/Jul/2015 05:52:52] "POST /cgi-bin/mycgi.cgi HTTP/1.1" 200 -
16 127.0.0.1 - - [07/Jul/2015 05:52:52] command: sh c:\Users\hhsprings\sandbox\cgi-bin\mycgi.cgi ""
17 127.0.0.1 - - [07/Jul/2015 05:52:53] CGI script exited OK
18 .127.0.0.1 - - [07/Jul/2015 05:52:54] "POST /cgi-bin/mycgi.cgi HTTP/1.1" 200 -
19 127.0.0.1 - - [07/Jul/2015 05:52:54] command: sh c:\Users\hhsprings\sandbox\cgi-bin\mycgi.cgi ""
20 127.0.0.1 - - [07/Jul/2015 05:52:56] CGI script exited OK
21 .127.0.0.1 - - [07/Jul/2015 05:52:57] "POST /cgi-bin/mycgi.cgi HTTP/1.1" 200 -
22 127.0.0.1 - - [07/Jul/2015 05:52:57] command: sh c:\Users\hhsprings\sandbox\cgi-bin\mycgi.cgi ""
23 127.0.0.1 - - [07/Jul/2015 05:52:58] CGI script exited OK
24 .127.0.0.1 - - [07/Jul/2015 05:52:59] "POST /cgi-bin/mycgi.cgi HTTP/1.1" 200 -
25 127.0.0.1 - - [07/Jul/2015 05:52:59] command: sh c:\Users\hhsprings\sandbox\cgi-bin\mycgi.cgi ""
26 127.0.0.1 - - [07/Jul/2015 05:53:00] CGI script exited OK
27 .127.0.0.1 - - [07/Jul/2015 05:53:01] "POST /cgi-bin/mycgi.cgi HTTP/1.1" 200 -
28 127.0.0.1 - - [07/Jul/2015 05:53:01] command: sh c:\Users\hhsprings\sandbox\cgi-bin\mycgi.cgi ""
29 127.0.0.1 - - [07/Jul/2015 05:53:03] CGI script exited OK
30 .127.0.0.1 - - [07/Jul/2015 05:53:04] "POST /cgi-bin/mycgi.cgi HTTP/1.1" 200 -
31 127.0.0.1 - - [07/Jul/2015 05:53:04] command: sh c:\Users\hhsprings\sandbox\cgi-bin\mycgi.cgi ""
32 .127.0.0.1 - - [07/Jul/2015 05:53:05] CGI script exited OK
33 127.0.0.1 - - [07/Jul/2015 05:53:06] "POST /cgi-bin/mycgi.cgi HTTP/1.1" 200 -
34 127.0.0.1 - - [07/Jul/2015 05:53:06] command: sh c:\Users\hhsprings\sandbox\cgi-bin\mycgi.cgi ""
35 127.0.0.1 - - [07/Jul/2015 05:53:07] CGI script exited OK
36 .
37 ----------------------------------------------------------------------
38 Ran 11 tests in 24.532s
39 
40 OK
41 me@localhost: cgi-bin$ /c/Python27/python mycgitest.py
42 Serving HTTP on 0.0.0.0 port 8000 ...
43 127.0.0.1 - - [07/Jul/2015 06:16:29] "POST /cgi-bin/mycgi.cgi HTTP/1.1" 200 -
44 127.0.0.1 - - [07/Jul/2015 06:16:29] command: sh c:\Users\hhsprings\sandbox\cgi-bin\mycgi.cgi ""
45 .127.0.0.1 - - [07/Jul/2015 06:16:30] CGI script exited OK
46 127.0.0.1 - - [07/Jul/2015 06:16:31] "POST /cgi-bin/mycgi.cgi HTTP/1.1" 200 -
47 127.0.0.1 - - [07/Jul/2015 06:16:31] command: sh c:\Users\hhsprings\sandbox\cgi-bin\mycgi.cgi ""
48 127.0.0.1 - - [07/Jul/2015 06:16:31] CGI script exited OK
49 .127.0.0.1 - - [07/Jul/2015 06:16:32] "POST /cgi-bin/mycgi.cgi HTTP/1.1" 200 -
50 127.0.0.1 - - [07/Jul/2015 06:16:32] command: sh c:\Users\hhsprings\sandbox\cgi-bin\mycgi.cgi ""
51 127.0.0.1 - - [07/Jul/2015 06:16:34] CGI script exited OK
52 .127.0.0.1 - - [07/Jul/2015 06:16:35] "POST /cgi-bin/mycgi.cgi HTTP/1.1" 200 -
53 127.0.0.1 - - [07/Jul/2015 06:16:35] command: sh c:\Users\hhsprings\sandbox\cgi-bin\mycgi.cgi ""
54 127.0.0.1 - - [07/Jul/2015 06:16:36] CGI script exited OK
55 .127.0.0.1 - - [07/Jul/2015 06:16:37] "POST /cgi-bin/mycgi.cgi HTTP/1.1" 200 -
56 127.0.0.1 - - [07/Jul/2015 06:16:37] command: sh c:\Users\hhsprings\sandbox\cgi-bin\mycgi.cgi ""
57 127.0.0.1 - - [07/Jul/2015 06:16:39] CGI script exited OK
58 .127.0.0.1 - - [07/Jul/2015 06:16:40] "POST /cgi-bin/mycgi.cgi HTTP/1.1" 200 -
59 127.0.0.1 - - [07/Jul/2015 06:16:40] command: sh c:\Users\hhsprings\sandbox\cgi-bin\mycgi.cgi ""
60 127.0.0.1 - - [07/Jul/2015 06:16:41] CGI script exited OK
61 .127.0.0.1 - - [07/Jul/2015 06:16:42] "POST /cgi-bin/mycgi.cgi HTTP/1.1" 200 -
62 127.0.0.1 - - [07/Jul/2015 06:16:42] command: sh c:\Users\hhsprings\sandbox\cgi-bin\mycgi.cgi ""
63 127.0.0.1 - - [07/Jul/2015 06:16:43] CGI script exited OK
64 .127.0.0.1 - - [07/Jul/2015 06:16:44] "POST /cgi-bin/mycgi.cgi HTTP/1.1" 200 -
65 127.0.0.1 - - [07/Jul/2015 06:16:44] command: sh c:\Users\hhsprings\sandbox\cgi-bin\mycgi.cgi ""
66 127.0.0.1 - - [07/Jul/2015 06:16:46] CGI script exited OK
67 .127.0.0.1 - - [07/Jul/2015 06:16:47] "POST /cgi-bin/mycgi.cgi HTTP/1.1" 200 -
68 127.0.0.1 - - [07/Jul/2015 06:16:47] command: sh c:\Users\hhsprings\sandbox\cgi-bin\mycgi.cgi ""
69 127.0.0.1 - - [07/Jul/2015 06:16:48] CGI script exited OK
70 .127.0.0.1 - - [07/Jul/2015 06:16:49] "POST /cgi-bin/mycgi.cgi HTTP/1.1" 200 -
71 127.0.0.1 - - [07/Jul/2015 06:16:49] command: sh c:\Users\hhsprings\sandbox\cgi-bin\mycgi.cgi ""
72 127.0.0.1 - - [07/Jul/2015 06:16:51] CGI script exited OK
73 .127.0.0.1 - - [07/Jul/2015 06:16:52] "POST /cgi-bin/mycgi.cgi HTTP/1.1" 200 -
74 127.0.0.1 - - [07/Jul/2015 06:16:52] command: sh c:\Users\hhsprings\sandbox\cgi-bin\mycgi.cgi ""
75 127.0.0.1 - - [07/Jul/2015 06:16:52] CGI script exited OK
76 .
77 ----------------------------------------------------------------------
78 Ran 11 tests in 25.419s
79 
80 OK
81 me@localhost: cgi-bin$