怪物ランドのプリンスだ。
「Windows MSYS シェルスクリプトとして書いた CGI を Python の CGIHTTPServer でテスト(改)」ではとりあえず自分の用を足せればいいや、という雑なものだったので、多少整理してみた。
本題の前におさらい。
- Python 2.7、3.x ともに、CGIHTTPRequestHandler は Windows の MSYS シェルスクリプトを起動出来ない
- Python 3.x は Accept: ヘッダを通せなくなるデグレードをやらかしている
- Python 2.7、3.x ともに Accept-Charset、Accept-Encoding、Accept-Language ヘッダを通せない
これをどうにかしたかった、ということね。
同じ「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 での動作は保障出来ない
かくして、同じくこんなことが出来る:
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$