Python + astモジュールを使ってソースコードを解析し、メソッドブロックや関数ブロックの定義行と最終行を取得する

Pythonソースコードを解析して、メソッドブロックや関数ブロックの定義行と最終行を取得することがありました。

Pythonでは標準モジュールのastを使ってソースコードを解析できるため、試した時のメモです。
32.2. ast — 抽象構文木 — Python 3.6.1 ドキュメント

なお、以下のページが大変参考になりました。ありがとうございました。
Python: ast (Abstract Syntax Tree: 抽象構文木) モジュールについて - CUBE SUGAR CONTAINER

 

環境

 

方法

上記の参考ブログ同様、astモジュールと再帰を組み合わせて解析します。

  • メソッドや関数の定義は、ast.FunctionDefクラス
  • メソッド名や関数名は、ast.FunctionDef.nameで取得
  • 行数はast.FunctionDef.linenoなどのlineno属性で取得
    • クラスによっては、lineno属性がないことに注意

 
collect_method_last_line_no.py

import ast

class Collection:
    def __init__(self, name='', def_line_no=0, last_line_no=0):
        self.name = name
        self.def_line_no = def_line_no
        self.last_line_no = last_line_no


class MethodLastLineNoCollector:
    def __init__(self):
        self.result = {}
        self.searched_line_no = 0

    def run(self, node):
        if isinstance(node, ast.FunctionDef):
            self.result[node.lineno] = Collection(node.name, def_line_no=node.lineno)

        for child in ast.iter_child_nodes(node):
            self.run(child)
        
        if hasattr(node, 'lineno'):
            # 探索した最終行を取得
            # 再帰で探すので、node.linenoは 1 > 2 > 3 > 2 > 1 となる
            if node.lineno > self.searched_line_no:
                self.searched_line_no = node.lineno

            # 再帰で探した時の帰りに、最終行を設定する
            else:
                if self.result.get(node.lineno):
                    self.result[node.lineno].last_line_no = self.searched_line_no


if __name__ == '__main__':
    FILENAME = 'target.py'
    with open(FILENAME, 'r') as f:
        source = f.read()

    tree = ast.parse(source, FILENAME)

    collector = MethodLastLineNoCollector()
    collector.run(tree)

    for v in collector.result.values():
        print(f'{v.name} -> def:{v.def_line_no}, last:{v.last_line_no}')

 
動作確認をします。例えば、

target.py

def foo_function():
    pass

    def innner_foo_function():
        pass


class Bar:
    def bar_method(self):
        pass

        def bar_inner_method(self):
            pass

    def bar_other_method(self):
        pass

    class InnerBar:
        def innter_bar_method(self):
            pass


if __name__ == "__main__":
    pass

というソースコードがあったとします。

 
collect_method_last_line_no.pyを実行します。

$ python collect_method_last_line_no.py 
foo_function -> def:1, last:5
innner_foo_function -> def:4, last:5
bar_method -> def:9, last:13
bar_inner_method -> def:12, last:13
bar_other_method -> def:15, last:16
innter_bar_method -> def:19, last:20

動作しているようです。

 

ソースコード

GitHubに上げました。collect_method_last_line_noディレクトリの中が今回のものです。
thinkAmi-sandbox/python_ast-sample

Docker + Alpine3.5 + Apache2.4 + Python3.6で、CGIのCookieを使ってみた

前回、CGIのリダイレクトを使ってみました。
Docker + Alpine3.5 + Apache2.4 + Python3.6で、CGIのリダイレクトを使ってみた - メモ的な思考的な

 
今回はCGICookieを使ってみます。

目次

 

環境

  • Mac OS X 10.11.6
  • Docker for Mac 17.03.1-ce-mac12
  • Alpine3.5 + Apache2.4.25 + Python 3.6.1

 
なお、Dockerfileは以前のものを流用し、Dockerは以下のコマンドで利用しています。

## DockerfileからDockerイメージをビルド
$ docker image build -t alpine:python3_httpd24_cookie .

## Dockerコンテナを起動し、CGIのディレクトリをホストと共有
$ docker container run -p 8081:80 --name cookie -v `pwd`/cgi/:/usr/local/apache2/cgi-bin/ alpine:python3_httpd24_cookie

 
また、今回のCGIの流れは、

  1. リダイレクト元ページでCookieをセット
  2. リダイレクト先へリダイレクト
  3. リダイレクト先でCookieの内容を表示

とします。

ステータスコードもリダイレクトのものを設定したいため、NPHスクリプトとして作成します。

 

PythonCookieまわりの標準モジュールについて

CookieまわりのPythonの標準モジュールについて、

  • http.cookies
    • Web クライアント 向けの HTTP クッキー処理
  • http.cookiejar
    • HTTP のクッキークラスで、基本的にはサーバサイドのコードで有用

の2つがあり、

http.cookiejar および http.cookies モジュールは互いに依存してはいません。

21.24. http.cookiejar — HTTP クライアント用の Cookie 処理 — Python 3.6.1 ドキュメント

とのことです。

CGIはサーバサイドのため、今回はhttp.cookiesを使います。
21.23. http.cookies — HTTPの状態管理 — Python 3.6.1 ドキュメント

 

CookieのExpires属性の日付書式について

Cookieの仕様を規定しているRFC6265では、rfc1123-dateと記載されています。
4.1.1. 構文 | RFC 6265 — HTTP State Management Mechanism (日本語訳)

rfc1123-dateの具体的な日付書式はRFC2616に記載されており、RFC1123形式とのことです。
ハイパーテキスト転送プロトコル – HTTP/1.1 - RFC2616 日本語訳の複製

 
Pythonにおいて、RFC1123形式で日付を取得する方法を探したところ、以下に記載がありました。
http - RFC 1123 Date Representation in Python? - Stack Overflow

いくつか挙げられていましたが、今回は標準モジュールのemail.utils.formatdate()を使うことにします*1
email.utils.formatdate() | 19.1.14. email.utils: 多方面のユーティリティ — Python 3.6.1 ドキュメント

 

http.cookies.SimpleCookieについて

まずはCookieオブジェクトであるhttp.cookies.SimpleCookieを使ってみます。

 

Cookieの読み込み

CGIではCookie環境変数HTTP_COOKIEに設定されるため、それを読み込んでみます。

読み込み方法としては

  • __init__()
  • load()

の2つがあります。

COOKIE_STR = 'foo=ham; bar=spam'

# __init__()を使ったCookieの読み込みと表示
cookie = SimpleCookie(COOKIE_STR)

for key, morsel in cookie.items():
    print(morsel)
    # => Set-Cookie: foo=ham
    #    Set-Cookie: bar=spam


# load()を使ったCookieの読み込みと表示
cookie = SimpleCookie()
cookie.load(COOKIE_STR)

for key, morsel in cookie.items():
    # => Set-Cookie: foo=ham
    #    Set-Cookie: bar=spam

 
また、dictの値もCookieとして読み込めます。

data = {
    'foo': 'ham',
    'bar': 'spam',
}
cookie = SimpleCookie(data)

for key, morsel in cookie.items():
    print(morsel)
    # => Set-Cookie: foo=ham
    #    Set-Cookie: bar=spam

 

Set-Cookieヘッダの出力

output()メソッドを使います。

なお、内部では__str__()output()エイリアスとして設定されているため、print文にSimpleCookieオブジェクトを渡しても良いです。
https://github.com/python/cpython/blob/v3.6.1/Lib/http/cookies.py#L402https://github.com/python/cpython/blob/v3.6.1/Lib/http/cookies.py#L531

cookie = SimpleCookie()
cookie['foo'] = 'ham'

print(cookie.output())
# => Set-Cookie: foo=ham
print(cookie)
# => Set-Cookie: foo=ham

 

output()で引数headerを設定

デフォルト値はSet-Cookie:ですが、別の値を設定したい場合には引数headerを使います。

cookie = SimpleCookie()
cookie['foo'] = 'ham'

print(cookie.output(header='hoge'))
# => hoge foo=ham

 

output()で引数sepを設定

複数Set-Cookieヘッダ間の区切り文字はCRLF(の8進数エスケープシーケンス表記)がデフォルト値ですが、変更したい場合は引数sepを使います。

data = {
    'foo': 'ham',
    'bar': 'spam',
}
cookie = SimpleCookie(data)
print(cookie.output(sep='++++'))
# => Set-Cookie: bar=spam++++Set-Cookie: foo=ham

 

output()で引数attrsを設定

複数のCookie属性がある場合で、ある属性のみ出力したい場合にはoutput()の引数attrsを使います。

# 複数属性を持つCookieを作成
cookie = SimpleCookie()
cookie['foo'] = 'ham'
cookie['foo']['path'] = '/bar/baz'
cookie['foo']['domain'] = 'example.com'
# RFC1123形式でexpiresを設定する
cookie['foo']['expires'] = formatdate(timeval=None, localtime=False, usegmt=True)
cookie['foo']['max-age'] = 100
cookie['foo']['httponly'] = True
cookie['foo']['secure'] = True

# attrsを使わない場合
print(cookie.output())
# => Set-Cookie: foo=ham; Domain=example.com; expires=Sat, 20 May 2017 11:37:36 GMT; HttpOnly; Max-Age=100; Path=/bar/baz; Secure

# attrsに文字列を渡す場合
print(cookie.output(attrs='expires'))
# => Set-Cookie: foo=ham; expires=Sat, 20 May 2017 11:39:12 GMT

# attrsに要素1のリストを渡す場合
print(cookie.output(attrs=['expires']))
# => Set-Cookie: foo=ham; expires=Sat, 20 May 2017 11:39:12 GMT

# attrsにリストを渡す場合
print(cookie.output(attrs=['max-age', 'httponly']))
# => Set-Cookie: foo=ham; HttpOnly; Max-Age=100

 

埋め込み可能なJavaScript snippetを出力

js_output()を使います。

# 複数属性を持つCookieを作成
cookie = SimpleCookie()
cookie['foo'] = 'ham'
cookie['foo']['path'] = '/bar/baz'
cookie['foo']['domain'] = 'example.com'
cookie['foo']['expires'] = formatdate(timeval=None, localtime=False, usegmt=True)
cookie['foo']['max-age'] = 100
cookie['foo']['httponly'] = True
cookie['foo']['secure'] = True

print(cookie.js_output())
# =>
# 
# <script type="text/javascript">
# <!-- begin hiding
# document.cookie = "foo=ham; Domain=example.com; expires=Sat, 20 May 2017 11:39:12 GMT; HttpOnly; Max-Age=100; Path=/bar/baz; Secure";
# // end hiding -->
# </script>
# 

 

Cookie値として日本語を設定
data = {
    'foo': 'ham',
    'bar': 'spam eggs',
    'baz': 'あ',
}
cookie = SimpleCookie(data)
print(cookie)
# => Set-Cookie: bar="spam eggs"
#   Set-Cookie: baz="あ"
#   Set-Cookie: foo=ham

と、値がダブルクォートで囲まれました。

パーセントエンコーディングをしたほうがよいのかもしれませんが、今回は深入りしません。

 

http.cookies.Morselについて

Morselの属性やメソッドを使用

SimpleCookieと同様に色々なメソッドがあるため、それぞれ使ってみます。

print('[{}]:'.format(inspect.getframeinfo(inspect.currentframe())[2]))
m = Morsel()
m.set('foo', 'bar', 'baz')

print(f'key: {m.key}')
# => key: foo
print(f'value: {m.value}')
# => value: bar
print(f'coded_value: {m.coded_value}')
# => coded_value: baz

print(m)
# => Set-Cookie: foo=baz

print(m.output())
# => Set-Cookie: foo=baz

print(m.OutputString())
# => foo=baz

print(m.js_output())
# =>
# <script type="text/javascript">
# <!-- begin hiding
# document.cookie = "foo=baz";
# // end hiding -->
# </script>

 

Morsel.coded_valueについて

Morselオブジェクトでは、set()メソッドを使って、実際に送信する形式にエンコードされたcookie値をMorsel.coded_valueへ設定します。

上記で見た通り、Morsel.output()では

m = Morsel()
m.set('foo', 'bar', 'baz')

print(m.output())
# => Set-Cookie: foo=baz

と、coded_valueの設定値がSet-Cookieヘッダの値として使われます。

 
一方、SimpleCookieクラスでCookieを設定した場合、coded_valueがどうなるかを見たところ、

cookie = SimpleCookie()
cookie['foo'] = 'ham'

print(f'key: {m.key}')
# => key: foo
print(f'value: {m.value}')
# => value: ham
print(f'coded_value: {m.coded_value}')
# => coded_value: ham

と、valueとcoded_valueが同じ値になりました。

 

ハードコーディングによるCookieを使ったCGIの流れ

上記でhttp.cookiesモジュールを見ましたが、一度戻ってハードコーディングによるCookieを使ったCGIの流れを実装してみます。

まずはCookieをセットしてリダイレクトする部分です。

nph-set_cookie_by_hardcode.py

#!/usr/bin/python3

# HTTPレスポンスヘッダ
import os
# ステータスコードとメッセージはPythonの定数を使う
# https://docs.python.jp/3/library/http.html
from http import HTTPStatus

# CGIの環境変数からプロトコルとバージョンを取得する
protocol = os.environ.get('SERVER_PROTOCOL')

# HTTPレスポンスヘッダ
# NPHなので、ステータスラインも記述する
print(f'{protocol} {HTTPStatus.FOUND.value} {HTTPStatus.FOUND.phrase}')
print('Location: /cgi-bin/done_redirect_with_cookie.py')
print('Set-Cookie: foo=bar; HttpOnly; Path=/example')
print('Set-Cookie: hoge=fuga')
print('')

次はリダイレクト後に受け取ったCookieを表示する部分です。

done.py

#!/usr/bin/python3

from http.cookies import SimpleCookie
import os

# HTTPレスポンスヘッダ
print('Content-Type: text/plain;charset=utf-8')
print('')

# レスポンスボディ
# 環境変数にセットされたCookieを表示する
env_cookie = os.environ.get('HTTP_COOKIE')
print('environ ver:')
print(env_cookie)
print('-'*20)

# SimpleCookieを使って表示する
print('SimpleCookie ver:')
cookie = SimpleCookie(env_cookie)
print(type(cookie))
print(cookie)

 
curlを使って確認します。

$ curl -b cookie.txt -c cookie.txt -L -D - http://localhost:8081/cgi-bin/nph-set_cookie_by_hardcode.py -v
*   Trying ::1...
* Connected to localhost (::1) port 8081 (#0)
> GET /cgi-bin/nph-set_cookie_by_hardcode.py HTTP/1.1
> Host: localhost:8081
> User-Agent: curl/7.43.0
> Accept: */*
> 
< HTTP/1.1 302 Found
HTTP/1.1 302 Found
< Location: /cgi-bin/done.py
Location: /cgi-bin/done.py
* Added cookie foo="bar" for domain localhost, path /cgi-bin/, expire 0
< Set-Cookie: foo=bar; HttpOnly;
Set-Cookie: foo=bar; HttpOnly;
* Added cookie hoge="fuga" for domain localhost, path /cgi-bin/, expire 0
< Set-Cookie: hoge=fuga
Set-Cookie: hoge=fuga
* no chunk, no close, no size. Assume close to signal end

< 
* Closing connection 0
* Issue another request to this URL: 'http://localhost:8081/cgi-bin/done.py'
* Hostname localhost was found in DNS cache
*   Trying ::1...
* Connected to localhost (::1) port 8081 (#1)
> GET /cgi-bin/done.py HTTP/1.1
> Host: localhost:8081
> User-Agent: curl/7.43.0
> Accept: */*
> Cookie: foo=bar; hoge=fuga
> 
< HTTP/1.1 200 OK
HTTP/1.1 200 OK
< Date: Sun, 21 May 2017 08:54:51 GMT
Date: Sun, 21 May 2017 08:54:51 GMT
< Server: Apache/2.4.25 (Unix)
Server: Apache/2.4.25 (Unix)
< Transfer-Encoding: chunked
Transfer-Encoding: chunked
< Content-Type: text/plain;charset=utf-8
Content-Type: text/plain;charset=utf-8

< 
environ ver:
foo=bar; hoge=fuga
--------------------
SimpleCookie ver:
<class 'http.cookies.SimpleCookie'>
Set-Cookie: foo=bar
Set-Cookie: hoge=fuga

Cookieが正しくセットされているようです。

 
なお、curlで使われるCookie.txtファイルは、実行後は以下のようになりました。

# Netscape HTTP Cookie File
# http://curl.haxx.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.

#HttpOnly_localhost FALSE   /cgi-bin/   FALSE   0   foo bar
localhost   FALSE   /cgi-bin/   FALSE   0   hoge    fuga

 

http.cookies.SimpleCookieを使ったCGIの流れ

今度は同じ内容をhttp.cookies.SimpleCookieを使って書いてみます。

nph-set_cookie_by_module.py

#!/usr/bin/python3

# HTTPレスポンスヘッダ
import os
# ステータスコードとメッセージはPythonの定数を使う
# https://docs.python.jp/3/library/http.html
from http import HTTPStatus
from http.cookies import SimpleCookie

# CGIの環境変数からプロトコルとバージョンを取得する
protocol = os.environ.get('SERVER_PROTOCOL')

# HTTPレスポンスヘッダ
# NPHなので、ステータスラインも記述する
print(f'{protocol} {HTTPStatus.FOUND.value} {HTTPStatus.FOUND.phrase}')
print('Location: /cgi-bin/done.py')

# Cookie
cookie = SimpleCookie()
cookie['foo'] = 'bar'
cookie['foo']['httponly'] = True
cookie['hoge'] = 'fuga'

print(cookie.output())
print('')

 
リダイレクト後(done.py)は同じのため、省略します。

 
curlで確認します。

$ curl -b cookie.txt -c cookie.txt -L -D - http://localhost:8081/cgi-bin/nph-set_cookie_by_module.py -v
*   Trying ::1...
* Connected to localhost (::1) port 8081 (#0)
> GET /cgi-bin/nph-set_cookie_by_module.py HTTP/1.1
> Host: localhost:8081
> User-Agent: curl/7.43.0
> Accept: */*
> Cookie: foo=bar; hoge=fuga
> 
< HTTP/1.1 302 Found
HTTP/1.1 302 Found
< Location: /cgi-bin/done.py
Location: /cgi-bin/done.py
* Replaced cookie foo="bar" for domain localhost, path /cgi-bin/, expire 0
< Set-Cookie: foo=bar; HttpOnly
Set-Cookie: foo=bar; HttpOnly
* Replaced cookie hoge="fuga" for domain localhost, path /cgi-bin/, expire 0
< Set-Cookie: hoge=fuga
Set-Cookie: hoge=fuga
* no chunk, no close, no size. Assume close to signal end

< 
* Closing connection 0
* Issue another request to this URL: 'http://localhost:8081/cgi-bin/done.py'
* Hostname localhost was found in DNS cache
*   Trying ::1...
* Connected to localhost (::1) port 8081 (#1)
> GET /cgi-bin/done.py HTTP/1.1
> Host: localhost:8081
> User-Agent: curl/7.43.0
> Accept: */*
> Cookie: foo=bar; hoge=fuga
> 
< HTTP/1.1 200 OK
HTTP/1.1 200 OK
< Date: Sun, 21 May 2017 08:57:51 GMT
Date: Sun, 21 May 2017 08:57:51 GMT
< Server: Apache/2.4.25 (Unix)
Server: Apache/2.4.25 (Unix)
< Transfer-Encoding: chunked
Transfer-Encoding: chunked
< Content-Type: text/plain;charset=utf-8
Content-Type: text/plain;charset=utf-8

< 
environ ver:
foo=bar; hoge=fuga
--------------------
SimpleCookie ver:
<class 'http.cookies.SimpleCookie'>
Set-Cookie: foo=bar
Set-Cookie: hoge=fuga
* Connection #1 to host localhost left intact

同じ結果となりました。

 

ソースコード

GitHubに上げました。alpine_apache_python36_cookieディレクトリの中が今回のものです。
thinkAmi-sandbox/Docker_Apache-sample

*1:公式ドキュメントにはLegacy APIと書かれています。ただ、推奨しないなどの記載は特にないため、使っても大丈夫かと思います。参考:https://docs.python.jp/3/library/email.html

Docker + Alpine3.5 + Apache2.4 + Python3.6で、CGIのリダイレクトを使ってみた

以前、SSIを使ってみました。
Docker + Alpine3.5 + Apache2.4 + Python3.6で、SSIを使ってみた - メモ的な思考的な

今回はCGIのリダイレクトを使ってみます。

目次

 

環境

  • Mac OS X 10.11.6
  • Docker for Mac 17.03.1-ce-mac12
  • Alpine3.5 + Apache2.4.25 + Python 3.6.1

 
なお、Dockerfileは以前のものを流用し、Dockerは以下のコマンドで利用しています。

## DockerfileからDockerイメージをビルド
$ docker image build -t alpine:python3_httpd24_redirect .

## Dockerコンテナを起動し、CGIのディレクトリをホストと共有
$ docker container run -p 8081:80 --name redirect -v `pwd`/cgi/:/usr/local/apache2/cgi-bin/ alpine:python3_httpd24_redirect

 
また、動作確認にはcurlを使います。
インターネットにアクセスしよう 番外編 : curlの使い方 - Cocoaはやっぱり!

 

Locationヘッダを使ったリダイレクト

今回はLocationヘッダを使ったリダイレクトを試します。

LocationヘッダのURLの仕様は、RFC7231にあり、

Location = URI-reference

7.1.2. Location | RFC 7231 — HTTP/1.1: Semantics and Content (日本語訳)

とのことです。

URI-referenceの定義は、RFC3986にあり、

URI-reference は、URI か相対的参照のどちらかである。 URI-reference の先頭がコロン分離記号を従えるスキームの構文に合致しなければ、URI-reference は相対的参照である。

4.1. URI 参照 | Uniform Resource Identifier (URI): 一般的構文 - RFC3986 日本語訳の複製

とのことです。

そのため、今回は相対的参照として、ホスト以下を記載します。

以上をもとに、

  • リダイレクト前:redirect.py
  • リダイレクト後:done_redirect.py

の2ファイルを用意します。

redirect.py

#!/usr/bin/python3

# HTTPレスポンスヘッダ
print('Location: /cgi-bin/done_redirect.py')
print('')

 
done_redirect.py

#!/usr/bin/python3

# HTTPレスポンスヘッダ
print('Content-Type: text/plain;charset=utf-8')
print('')

print('Hello')

 
curlLオプションでリダイレクトに対応し、vオプションで実際の往復を見てみます。

$ curl -L -v http://localhost:8081/cgi-bin/redirect.py
*   Trying ::1...
* Connected to localhost (::1) port 8081 (#0)
> GET /cgi-bin/redirect.py HTTP/1.1
> Host: localhost:8081
> User-Agent: curl/7.43.0
> Accept: */*
> 
< HTTP/1.1 200 OK
< Date: Sat, 20 May 2017 21:22:27 GMT
< Server: Apache/2.4.25 (Unix)
< Transfer-Encoding: chunked
< Content-Type: text/plain;charset=utf-8
< 
Hello
* Connection #0 to host localhost left intact

リダイレクトが行われているようですが、ステータスコードが200になっていました。

 

NPHスクリプトによる、Locationヘッダを使ったリダイレクト

ステータスコードも自由に変更できる方法を探したところ、書籍「CGIプログラミング 第2版」の3章にApacheNPH (Non Parsed Headers)スクリプトを使う方法が紹介されていました*1

CGIプログラミング

CGIプログラミング

 
また、RFC3875などにも記載があります。

 
書籍ではPerlを使っていましたが、今回はPythonで書いてみます。

NPHスクリプトの場合、

  • ファイル名の接頭辞にnph-を付ける
  • 少なくとも、ステータス行、Conent-Typeヘッダ、Serverヘッダを出力する

とのことですので、以下のPythonスクリプトを書きました。

nph-redirect_hardcord.py

#!/usr/bin/python3

# HTTPレスポンスヘッダ
# NPHなので、ステータスラインも記述する
print('HTTP/1.1 302 Found')
print('Location: /cgi-bin/done_redirect.py')
print('Server: hoge')
print('')

 
curlにて確認します。

$ curl -L -v http://localhost:8081/cgi-bin/nph-redirect_hardcord.py
*   Trying ::1...
* Connected to localhost (::1) port 8081 (#0)
> GET /cgi-bin/nph-redirect_hardcord.py HTTP/1.1
> Host: localhost:8081
> User-Agent: curl/7.43.0
> Accept: */*
> 
< HTTP/1.1 302 Found
< Location: /cgi-bin/done_redirect.py
< Server: hoge
* no chunk, no close, no size. Assume close to signal end
< 
* Closing connection 0
* Issue another request to this URL: 'http://localhost:8081/cgi-bin/done_redirect.py'
* Hostname localhost was found in DNS cache
*   Trying ::1...
* Connected to localhost (::1) port 8081 (#1)
> GET /cgi-bin/done_redirect.py HTTP/1.1
> Host: localhost:8081
> User-Agent: curl/7.43.0
> Accept: */*
> 
< HTTP/1.1 200 OK
< Date: Sat, 20 May 2017 22:14:46 GMT
< Server: Apache/2.4.25 (Unix)
< Transfer-Encoding: chunked
< Content-Type: text/plain;charset=utf-8
< 
Hello
* Connection #1 to host localhost left intact

302リダイレクトになりました。

 

NPHスクリプト + 環境変数による、Locationヘッダを使ったリダイレクト

上記のNPHスクリプトでは色々とハードコーディングしていたため、

を使うように変更します。

 
nph-redirect_environ.py

#!/usr/bin/python3

import os
# ステータスコードとメッセージはPythonの定数を使う
# https://docs.python.jp/3/library/http.html
from http import HTTPStatus

# CGIの環境変数からプロトコルとバージョン・ホストを取得する
protocol = os.environ.get('SERVER_PROTOCOL')
server = os.environ.get('SERVER_SOFTWARE')

# HTTPレスポンスヘッダ
print(f'{protocol} {HTTPStatus.FOUND.value} {HTTPStatus.FOUND.phrase}')
print('Location: /cgi-bin/done_redirect.py')
print(f'Server: {server}')
print('')

 
curlで確認します。

$ curl -L -v http://localhost:8081/cgi-bin/nph-redirect_environ.py

> GET /cgi-bin/nph-redirect_environ.py HTTP/1.1
> Host: localhost:8081
> User-Agent: curl/7.43.0
> Accept: */*
> 
< HTTP/1.1 302 Found
< Location: /cgi-bin/done_redirect.py
< Server: Apache/2.4.25 (Unix)
< 
> GET /cgi-bin/done_redirect.py HTTP/1.1
> Host: localhost:8081
> User-Agent: curl/7.43.0
> Accept: */*
> 
< HTTP/1.1 200 OK
< Date: Sat, 20 May 2017 22:23:31 GMT
< Server: Apache/2.4.25 (Unix)
< Transfer-Encoding: chunked
< Content-Type: text/plain;charset=utf-8
< 
Hello

同じく、302リダイレクトになりました。

 

その他

NPHスクリプトを使う際、環境によっては以下のエラーが出るようです。
apache - Apache2 sends two HTTP headers with a mapped “nph-” CGI - Stack Overflow

 

ソースコード

GitHubに上げました。alpine_apache_python36_redirectディレクトリの中が今回のものです。
thinkAmi-sandbox/Docker_Apache-sample

*1:旧版の英語版であれば、オライリーの「O'Reilly Open Books Project」により公開されています:http://www.oreilly.com/openbook/cgi/ch03_08.html

Google I/O 2017 Extended Live Viewing 信州に参加しました

5/17の深夜に開催された、Google I/O 2017 Extended Live Viewing 信州に参加しました。

久しぶりの参加であり、長野県でGoogle I/O 2017 Extendedが毎年開催されるのはありがたいと感じています。

 
信州会場は基調講演前から開場していたこともあり、他の参加者のみなさんと交流を深められ、楽しい時間を過ごせました。

基調講演のYoutube配信は英語 + 英語字幕でしたが、

というライフハックなどもあり、自分なりに楽しむことができました。英語のカンファレンスに参加すると、英語学習意欲の増加するので良いです。

 
その中でも、個人的には「Google Lens」と「Google Photo books」が気になり、またどのプレゼンターもAIを推していたのが印象に残りました。
photo books | Google Photos - All your photos organized and easy to find

なお、詳細については、すでに他のサイトで記載されているので省略します。

 
今後の予定としては、6/10(土)にGoogle I/O 報告会があります。
Google I/O 2017 報告会 信州会場 - connpass

例年同様、ゲストパネラーとして Google I/O参加のDeveloperExpertの方も参加されるようですので、いろいろなお話がうかがえそうです。

 
最後になりましたが、開催してくださった関係者のみなさま、ありがとうございました。

Docker + Alpine3.5 + Apache2.4 + Python3.6で、SSIを使ってみた

前回、フォームのデータをcgiモジュールを使って受け取りました。
Docker + Alpine3.5 + Apache2.4 + Python3.6で、フォームのデータを標準モジュールcgiで受け取ってみた - メモ的な思考的な

 
今回は、ApacheのSSI(Server Side Include)を、公式チュートリアルを見ながら試してみます。
Apache チュートリアル: Server Side Includes 入門 - Apache HTTP サーバ バージョン 2.4  
 
目次

 

環境

  • Mac OS X 10.11.6
  • Docker for Mac 17.03.1-ce-mac12
  • Alpine3.5 + Apache2.4.25 + Python 3.6.1

 
なお、Dockerfileは前回のものを流用します。

また、コンソールで「Dockerのコンテナを起動する」とした場合は、以下のコマンドを入力しています。
参考:docker container / image コマンド新旧比較 - Qiita

# Docker container runしたのとは別のターミナルで実行
## Dockerのssiコンテナを停止
$ docker container stop ssi

# Docker container runしたターミナルで実行
## Dockerコンテナを削除
$ docker container rm $(docker container ls -a -q)
## もしくは起動していないDockerコンテナを全削除
$ docker container prune

## 削除したいDockerイメージのIDを知る
$ docker image ls
REPOSITORY          TAG                          IMAGE ID
alpine              python3_httpd24_ssi          658225783800

## Dockerイメージを削除
$ docker image rm 658225783800

## DockerfileからDockerイメージをビルド
$ docker image build -t alpine:python3_httpd24_ssi .


## Dockerコンテナを実行
### HTMLのみの場合、HTMLのディレクトリのみホストと共有
$ docker container run -p 8081:80 --name ssi -v `pwd`/htdocs/:/usr/local/apache2/htdocs alpine:python3_httpd24_ssi

### HTML & CGIの場合、HTML & CGIのディレクトリをホストと共有
$ docker container run -p 8081:80 --name ssi -v `pwd`/htdocs/:/usr/local/apache2/htdocs -v `pwd`/cgi/:/usr/local/apache2/cgi-bin/ alpine:python3_httpd24_ssi

 

AddOutputFilterを使って、shtmlファイルのみSSIを有効にする

まずは、SSIが動くか試してみます。

そのため、

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <title></title>
</head>

<body>
    <h1>SSIサンプル(Echo)</h1>
    <!--#echo var="DATE_LOCAL" -->
</body>
</html>

  • echo.html
  • echo.shtml

として2つ用意し、両方ともドキュメントルートの中に入れます。

ホスト上のパーミッションは以下の通りです。

$ ls -al
...
-rw-r--r--  1 you  staff  182  5 14 15:28 echo.html
-rw-r--r--  1 you  staff  182  5 14 15:15 echo.shtml

 
次に、httpd.confを修正します。

  • SSIを使うため、include_moduleをLoadModule
  • Directoryディレクティブに以下を追加
    • Options Includes
    • AddType text/html .shtml
    • AddOutputFilter INCLUDES .shtml

とします。

なお、Web上ではAddOutputFilter INCLUDESAddHandler server-parsedの2つの方法がありました。

Apacheのドキュメントによると、

とのことなので、2.4系の今回はAddOutputFilterを使います。

httpd.conf

...
LoadModule include_module modules/mod_include.so
...
DocumentRoot "/usr/local/apache2/htdocs"
<Directory "/usr/local/apache2/htdocs">
    AllowOverride None
    Require all granted

    # shtmlファイルでSSIを使うための設定を追加
    Options Includes
    AddType text/html .shtml
    AddOutputFilter INCLUDES .shtml
</Directory>

 
Dockerコンテナを起動し、curlで確認します。

# htmlファイルではダメ
$ curl http://localhost:8081/echo.html
...
<body>
    <h1>SSIサンプル(Echo)</h1>
    <!--#echo var="DATE_LOCAL" -->
</body>


# shtmlファイルでは、SSI動作
$ curl http://localhost:8081/echo.shtml
...
<body>
    <h1>SSIサンプル(Echo)</h1>
    Sunday, 14-May-2017 06:36:59 
</body>

 
shtmlファイルのみSSIが動作しました。

ソースコード全体は以下です。
https://github.com/thinkAmi-sandbox/Docker_Apache-sample/tree/master/alpine_apache_python36_ssi/ssi_shtml_echo_using_outputfilter

 

AddOutputFilterを使って、shtml・htmlファイルでSSIを有効にする

htmlファイルでもSSIが動作するよう、httpd.confにてhtmlをAddOutputFilterへ追加します。

httpd.conf

DocumentRoot "/usr/local/apache2/htdocs"
<Directory "/usr/local/apache2/htdocs">
    AllowOverride None
    Require all granted

    Options Includes
    AddType text/html .shtml
    # AddOutputFilterに.htmlを追加
    AddOutputFilter INCLUDES .shtml .html
</Directory>

 
ホストのパーミッションには変更ありません。

$ ls -al
-rw-r--r--  1 you  staff  182  5 14 15:28 echo.html
-rw-r--r--  1 you  staff  182  5 14 15:15 echo.shtml

 
Dockerコンテナを起動し、curlで確認します。

# shtmlファイル
$ curl http://localhost:8081/echo.shtml
...
<body>
    <h1>SSIサンプル(Echo)</h1>
    Sunday, 14-May-2017 06:45:58 
</body>

# htmlファイル
$ curl http://localhost:8081/echo.html
...
<body>
    <h1>SSIサンプル(Echo)</h1>
    Sunday, 14-May-2017 06:46:01 
</body>

 
shtml・htmlファイルでSSIが動作しました。

ソースコード全体は以下です。
https://github.com/thinkAmi-sandbox/Docker_Apache-sample/tree/master/alpine_apache_python36_ssi/ssi_html_echo_using_outputfilter

 

XBitHack onを使って、htmlファイルのみでSSIを有効にする

AddOutputFilter以外の方法を探したところ、公式チュートリアルXBitHack onを使う方法が記載されていました。

XBitHack は、ファイルの実行ビットが立っている場合、 SSI ディレクティブにより解析することを Apache に伝えます。 従って、SSI ディレクティブを現在のページに加えるためには、 ファイル名を変更しなくてもよく、単に chmod を使用してファイルを実行可能にするだけで済みます。

SSI を許可するためのサーバの設定 | Apache チュートリアル: Server Side Includes 入門 - Apache HTTP サーバ バージョン 2.4

 
httpd.confを変更し、OptionsとXBitHackを使うようにします。

httpd.conf

DocumentRoot "/usr/local/apache2/htdocs"
<Directory "/usr/local/apache2/htdocs">
    AllowOverride None
    Require all granted

    Options Includes
    # XBitHackをonにする
    XBitHack on
</Directory>

 
Dockerコンテナを起動し、curlで確認します。

まずはパーミッションがそのままの場合です。

$ curl http://localhost:8081/echo.shtml
...
<body>
    <h1>SSIサンプル(Echo)</h1>
    <!--#echo var="DATE_LOCAL" -->
</body>

$ curl http://localhost:8081/echo.html
...
<body>
    <h1>SSIサンプル(Echo)</h1>
    <!--#echo var="DATE_LOCAL" -->
</body>

いずれもSSIは動作しませんでした。

 
続いて、ホストのパーミッションを変更します。

$ chmod +x echo.html 
$ chmod +x echo.shtml

$ ls -al
-rwxr-xr-x  1 you  staff  182  5 14 15:28 echo.html
-rwxr-xr-x  1 you  staff  182  5 14 15:15 echo.shtml

 
念のため、Dockerコンテナにあるファイルのパーミッションも確認します。

$ docker exec -it `docker ps | grep ssi | awk '{print $1}'` /bin/bash
bash-4.3# ls ./htdocs -al
...
-rwxr-xr-x    1 root     root           182 May 14 06:47 echo.html
-rwxr-xr-x    1 root     root           182 May 14 06:47 echo.shtml

ホストと同期されています。

 
パーミッションが変更されていたので、curlにて確認します。

$ curl http://localhost:8081/echo.shtml
...
<body>
    <h1>SSIサンプル(Echo)</h1>
    <!--#echo var="DATE_LOCAL" -->
</body>

$ curl http://localhost:8081/echo.html
...
<body>
    <h1>SSIサンプル(Echo)</h1>
    Sunday, 14-May-2017 07:06:21 
</body>

htmlファイルのみSSIが有効になりました。

ソースコード全体は以下です。
https://github.com/thinkAmi-sandbox/Docker_Apache-sample/tree/master/alpine_apache_python36_ssi/ssi_html_cgi_using_xbithack_on

 

XBitHack onを使って、html・shtmlファイルでSSIを有効にする

XBitHackディレクティブのドキュメントを見ると、

on

ユーザの実行ビットが設定されている text/html ファイルは全てサーバで解析する html ドキュメントとして扱われます。

XBitHack ディレクティブ | mod_include - Apache HTTP サーバ バージョン 2.4

との記載がありました。

そのため、AddTypeshtmlを追加すれば、shtmlでも有効になると考えられたため、試してみました。

httpd.conf

# DocumentRoot
DocumentRoot "/usr/local/apache2/htdocs"
<Directory "/usr/local/apache2/htdocs">
    AllowOverride None
    Require all granted

    Options Includes
    # text/htmlに、.shtmlファイルも追加する
    AddType text/html .shtml
    XBitHack on
</Directory>

 
パーミッションは両方とも実行可能です。

$ ls -al htdocs/
drwxr-xr-x  4 you  staff  136  5 14 16:08 .
drwxr-xr-x  7 you  staff  238  5 14 16:08 ..
-rwxr-xr-x  1 you  staff  182  5 14 16:08 echo.html
-rwxr-xr-x  1 you  staff  182  5 14 16:08 echo.shtml

 
Dockerコンテナを起動し、curlで確認します。

$ curl http://localhost:8081/echo.shtml
...
<body>
    <h1>SSIサンプル(Echo)</h1>
    Sunday, 14-May-2017 07:15:58 
</body>

$ curl http://localhost:8081/echo.html
...
<body>
    <h1>SSIサンプル(Echo)</h1>
    Sunday, 14-May-2017 07:16:03 
</body>

両方ともSSIが動作しました。

ソースコード全体は以下です。
https://github.com/thinkAmi-sandbox/Docker_Apache-sample/tree/master/alpine_apache_python36_ssi/ssi_shtml_echo_using_xbithack_on

 

XBitHack onを使って、CGIを動かす

SSIの動作が確認できたため、今度はCGIを動かしてみます。

httpd.confにCGIの記述を追加します。

httpd.conf

# CGIを使うので、alias・cgidモジュールを追加
LoadModule alias_module modules/mod_alias.so
LoadModule cgid_module modules/mod_cgid.so

# CGIを使うための設定
# alias_module settings
ScriptAlias /cgi-bin/ "/usr/local/apache2/cgi-bin/"
# CGI directory
<Directory "/usr/local/apache2/cgi-bin">
    AllowOverride None
    Options ExecCGI
    SetHandler cgi-script
    Require all granted
</Directory>

 
続いて、標準出力に現在時刻を出すCGIPythonスクリプトを作成します。

now.py

#!/usr/bin/python3
import datetime

# HTTPヘッダ
print('Content-Type: text/plain;charset=utf-8')
print('')

print(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'))

if __name__ == '__main__':
    pass

 
確認用のHTMLファイルも作成します。

CGIが正しく動作しているかを確認するため、JavaScriptでも現在時刻を取得します。PythonUTCJavaScriptJSTタイムゾーンが異なりますが、今回は確認するだけなのでそのままとします*2

cgi.html

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <title></title>
</head>

<body>
    <h1>SSIサンプル(CGI)</h1>
    <p>CGIの時間(UTC):<!--#include virtual="/cgi-bin/now.py" --></p>
    <p>JSの時間(JST) :<span id="js_time"></span></p>

    <script>
        document.getElementById("js_time").innerHTML = function(){
            var now = new Date();
            var year = now.getFullYear();
            var mon = now.getMonth() + 1;
            var day = now.getDate();
            var hour = now.getHours();
            var min = now.getMinutes();
            var sec = now.getSeconds();

            return `${year}-${mon}-${day} ${hour}:${min}:${sec}`
        }();
    </script>
</body>
</html>

 
また、htmlとPythonスクリプトを実行可能にします。

$ cd ../ssi_html_cgi_using_xbithack_on/

# 両方とも実行可能に変更
$ chmod +x cgi/now.py 
$ chmod +x htdocs/cgi.html 

# それぞれのファイルのパーミッションを確認
$ ls -Ral
...

./cgi:
-rwxr-xr-x  1 you  staff  179  5 14 16:23 now.py

./htdocs:
-rwxr-xr-x  1 you  staff  194  5 14 16:23 cgi.html

 
Dockerコンテナを起動します。CGI用のディレクトリもホストと共有します。

$ docker container run -p 8081:80 --name ssi -v `pwd`/htdocs/:/usr/local/apache2/htdocs -v `pwd`/cgi/:/usr/local/apache2/cgi-bin/ alpine:python3_httpd24_ssi

 
ブラウザでhttp://localhost:8081/cgi.htmlへアクセスして確認します。

SSIサンプル(CGI)

CGIの時間(UTC):2017-05-14 08:43:18

JSの時間(JST) :2017-5-14 17:43:18

 
再度アクセスします。

SSIサンプル(CGI)

CGIの時間(UTC):2017-05-14 08:43:58

JSの時間(JST) :2017-5-14 17:43:58

更新されているようです。

ソースコード全体は以下です。
https://github.com/thinkAmi-sandbox/Docker_Apache-sample/tree/master/alpine_apache_python36_ssi/ssi_shtml_echo_using_xbithack_on

 

XBitHack fullを使って、CGIを動かす

XBitHackディレクティブの公式ドキュメントを見ると、

注意 他の CGI を #include するかもしれないものや、各アクセスに対して違う出力を生成する (もしくは後のリクエストで変わるかもしれないもの) すべての SSI スクリプトに対してグループ実行ビットが 設定されていないことを確認できない場合は、full は使わない方が良い でしょう。

XBitHack ディレクティブ | mod_include - Apache HTTP サーバ バージョン 2.4

とあったため、試してみます。

 
httpd.confでXBitHackをfullに修正します。

httpd.conf

DocumentRoot "/usr/local/apache2/htdocs"
<Directory "/usr/local/apache2/htdocs">
    AllowOverride None
    Require all granted

    Options Includes
    # XBitHackを変更
    XBitHack full
</Directory>

 
Dockerコンテナを起動し、ブラウザでhttp://localhost:8081/cgi.htmlへアクセスして確認します。

SSIサンプル(CGI)

CGIの時間(UTC):2017-05-14 08:46:52

JSの時間(JST) :2017-5-14 17:46:52

 
再度アクセスします。

SSIサンプル(CGI)

CGIの時間(UTC):2017-05-14 08:46:52

JSの時間(JST) :2017-5-14 17:47:8

 
CGIの時間が初回アクセス時と同一のため、キャッシュが使われているようです。

よって、ドキュメントにもある通り、CGIを使う場合にはXBitHack onの方が良さそうです。

ソースコード全体は以下です。
https://github.com/thinkAmi-sandbox/Docker_Apache-sample/tree/master/alpine_apache_python36_ssi/ssi_html_cgi_using_xbithack_full

 

CGIを動かす際、includeやexecなどいくつかのパターンを試す

Apacheの公式ドキュメントを読むと、CGIPythonスクリプトを動かすには、

  • include virtual
  • exec cgi
  • exec cmd
  • exec cmd python3

などが使えそうでしたので、試してみます。
基本要素 | mod_include - Apache HTTP サーバ バージョン 2.4

 
HTMLにはそれぞれの動かし方と、PythonスクリプトでのContent-Type出力の有無のパターンを記載します。

cgi.html

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <title></title>
</head>

<body>
    <h1>SSIサンプル(CGI: XBitHack on)</h1>
    <h2>CGIにcontent-typeあり</h2>
    <ul>
        <li>include virtual: <!--#include virtual="/cgi-bin/with_content_type.py" --></li>
        <li>exec cgi: <!--#exec cgi="/cgi-bin/with_content_type.py" --></li>
        <li>exec cmd: <!--#exec cmd="/usr/local/apache2/cgi-bin/with_content_type.py" --></li>
        <li>exec cmd python3: <!--#exec cmd="python3 /usr/local/apache2/cgi-bin/with_content_type.py" --></li>
    </ul>
    <h2>CGIにcontent-type無し</h2>
    <ul>
        <li>include virtual: <!--#include virtual="/cgi-bin/witout_content_type.py" --></li>
        <li>exec cgi: <!--#exec cgi="/cgi-bin/witout_content_type.py" --></li>
        <li>exec cmd: <!--#exec cmd="/usr/local/apache2/cgi-bin/witout_content_type.py" --></li>
        <li>exec cmd python3: <!--#exec cmd="python3 /usr/local/apache2/cgi-bin/witout_content_type.py" --></li>
    </ul>
</body>
</html>

 
Pythonスクリプトでは、Content-Typeを出すものと出さないものを用意します。

with_content_type.py

#!/usr/bin/python3
import datetime

# HTTPヘッダ
# SSIで「include virtual」する時はHTTPヘッダが必要
print('Content-Type: text/plain;charset=utf-8')
print('')

print(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'))

 
witout_content_type.py

#!/usr/bin/python3
import datetime

print(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'))

 
Dockerコンテナを起動し、ブラウザでhttp://localhost:8081/cgi.htmlへアクセスして確認します。

CGIにcontent-typeあり

・include virtual: 2017-05-14 09:49:46
・exec cgi: 2017-05-14 09:49:46
・exec cmd: Content-Type: text/plain;charset=utf-8 2017-05-14 09:49:46
・exec cmd python3: Content-Type: text/plain;charset=utf-8 2017-05-14 09:49:46

CGIにcontent-type無し

・include virtual: [an error occurred while processing this directive]
・exec cgi:
・exec cmd: 2017-05-14 09:49:46
・exec cmd python3: 2017-05-14 09:49:47

エラーが発生したり、表示されないものがありました。Dockerのログも見てみます。

[cgid:error] Premature end of script headers: witout_content_type.py
[include:error] unable to include "/cgi-bin/witout_content_type.py" in parsed file /usr/local/apache2/htdocs/cgi.html, subrequest returned 500
[cgid:error] Premature end of script headers: witout_content_type.py

 
これより、

  • Content-Typeが必要なパターン
    • include virtual, exec cgi
  • Content-Typeは不要なパターン
    • exec cmd, exec cmd python3

と分かりました。

ソースコード全体は以下です。
https://github.com/thinkAmi-sandbox/Docker_Apache-sample/tree/master/alpine_apache_python36_ssi/ssi_options_includes_using_xbithack_on

 

CGIを動かす際、OptionsディレクティブでIncludesNOEXECを使う

今までOptionsディレクティブはIncludesを指定してきました。

ただ、SSIではIncludesNOEXECという指定もできます。
Options ディレクティブ | core - Apache HTTP サーバ バージョン 2.4

ドキュメントによると、include virtualだけが使えるようですので、試してみます。

 
httpd.confを修正します。

httpd.conf

DocumentRoot "/usr/local/apache2/htdocs"
<Directory "/usr/local/apache2/htdocs">
    AllowOverride None
    Require all granted

    # SSIを使うための設定
    # .htmlファイルでSSIを使えるようにするが、
    # include virtualのみ有効にする
    Options IncludesNOEXEC
    XBitHack on
</Directory>

 
Dockerコンテナを起動し、ブラウザでhttp://localhost:8081/cgi.htmlへアクセスして確認します。

CGIにcontent-typeあり

・include virtual: 2017-05-14 09:51:38
・exec cgi: [an error occurred while processing this directive]
・exec cmd: [an error occurred while processing this directive]
・exec cmd python3: [an error occurred while processing this directive]

CGIにcontent-type無し

・include virtual: [an error occurred while processing this directive]
・exec cgi: [an error occurred while processing this directive]
・exec cmd: [an error occurred while processing this directive]
・exec cmd python3: [an error occurred while processing this directive]

 
SSIで動かす時にinclude virtual以外ではエラーが出ています。

Dockerのログも見ます。

[cgid:error] AH01271: exec used but not allowed in /usr/local/apache2/htdocs/cgi.html
[cgid:error] AH01271: exec used but not allowed in /usr/local/apache2/htdocs/cgi.html
[cgid:error] Premature end of script headers: witout_content_type.py
[include:error] unable to include "/cgi-bin/witout_content_type.py" in parsed file /usr/local/apache2/htdocs/cgi.html, subrequest returned 500
[cgid:error] AH01271: exec used but not allowed in /usr/local/apache2/htdocs/cgi.html
[cgid:error] AH01271: exec used but not allowed in /usr/local/apache2/htdocs/cgi.html
[cgid:error] AH01271: exec used but not allowed in /usr/local/apache2/htdocs/cgi.html

ドキュメント通り、include virtualだけが正常に動作しました。

ソースコード全体は以下です。
https://github.com/thinkAmi-sandbox/Docker_Apache-sample/tree/master/alpine_apache_python36_ssi/ssi_options_includes_noexec_using_xbithack_on

 

ソースコード

GitHubに上げました。alpine_apache_python36_ssiディレクトリの中が今回のものです。
thinkAmi-sandbox/Docker_Apache-sample

*1:Apache公式のが見当たらなかったため、Oracleのサイトにあるものをリンクしました

*2:Alpineのタイムゾーンを修正すれば良いかと思いますが、手間だったので対応せず…

Docker + Alpine3.5 + Apache2.4 + Python3.6で、フォームのデータを標準モジュールcgiで受け取ってみた

前回、フォームのデータをCGIPythonスクリプトsys.stdin.read()os.environを使って受け取りました。
Dockerで、Alpine3.5 + Apache2.4 + Python3.6の環境を作って、フォームのデータをCGIで受け取ってみた - メモ的な思考的な

 
今回は標準モジュールcgiにあるFieldStorage使ってデータを受け取ってみます。

 
目次

 

環境

  • Mac OS X 10.11.6
  • Docker for Mac 17.03.1-ce-mac5
  • Alpine3.5 + Apache2.4.25 + Python 3.6.1

なお、Dockerfileは前回のものを流用します。

HTMLファイルは前回のものを流用し、actionだけを対象のPythonスクリプトに差し替えます。

Dockerまわりのコマンドは以下の通りです。

# Docker上のApacheでCGIとして動かすために、Dockerと共有するローカルファイルのパーミッションを変更
$ chmod 755 cgi_module.py 
$ chmod 755 cgi_file_upload.py
$ chmod 755 cgi_file_upload_with.py

# Dockerイメージのビルド
$ docker image build -t alpine:python3_httpd24_cgi_module .

# Dockerコンテナの起動
$ docker container run -p 8081:80 --name cgi_module -v `pwd`/htdocs/:/usr/local/apache2/htdocs -v `pwd`/cgi/:/usr/local/apache2/cgi-bin/ alpine:python3_httpd24_cgi_module

 

cgi.FieldStorageを使ったフォームデータの受け取り

データの読み込み

cgi.FieldStorageインスタンスを生成することで、標準入力などからデータを読み込めます。

cgi_module.py

#!/usr/bin/python3
import cgi

# HTTPレスポンスヘッダ
print('Content-Type: text/plain;charset=utf-8\n')
print('')

form = cgi.FieldStorage()
print(form)
# => FieldStorage(None, None, [MiniFieldStorage('quantity', '1個'), 
#                              MiniFieldStorage('hidden_valude', '隠しデータ')])

 
また、フォームに入力していない要素も取得したい場合は、

form = cgi.FieldStorage(keep_blank_values=True)
print(form)
# => FieldStorage(None, None, [MiniFieldStorage('subject', ''), 
#                              MiniFieldStorage('quantity', '1個'), 
#                              MiniFieldStorage('hidden_valude', '隠しデータ'), 
#                              MiniFieldStorage('memo', '')])

と、FieldStorageのインスタンス生成時の引数としてkeep_blank_values=Trueを指定します。

ただ、上記を見る限り、未選択のcheckboxやradio button、select multipleは取得できないようです。

 
なお、FieldStorageのインスタンス生成については、公式ドキュメントにある

標準入力または環境変数からフォームの内容を読み出します (どちらから読み出すかは、複数の環境変数の値が CGI 標準に従ってどのように設定されているかで決まります)。インスタンスが標準入力を使うかもしれないので、インスタンス生成を行うのは一度だけにしなければなりません。

21.2. cgi — CGI (ゲートウェイインタフェース規格) のサポート — Python 3.6.1 ドキュメント

という点に注意します。

 
また、FieldStorageインスタンス生成後にsys.stdin.read()を使うと、

form = cgi.FieldStorage()

print('stdin:\n{}'.format(sys.stdin.read()))
# => stdin:

と、何も取得できません。

 
一方(今回のケースだけかもしれませんが)、os.environについては

form = cgi.FieldStorage()

print('os.environ:\n')
for k, v in os.environ.items():
    print('{}: {}'.format(k, v))
# =>
# HTTP_HOST: localhost:8081
# HTTP_CONNECTION: keep-alive
# ...

と、環境変数の値が取得できました。

 

FieldStorageの属性やメソッド

属性やメソッドを使ってみると、

form = cgi.FieldStorage()

print(form.type)
# => application/x-www-form-urlencoded

print(form.headers)
# => {'content-type': 'application/x-www-form-urlencoded', 'content-length': '94'}

print(form.keys())
# => ['quantity', 'memo', 'hidden_valude', 'subject']

となりました。

 

フォームのフィールド値の取得

例えば、「自分で持ち帰る」チェックボックスにチェックを入れた場合、

form = cgi.FieldStorage()

print(form['takeout'])
# => MiniFieldStorage('takeout', '自分で持ち帰る')

print(form['takeout'].value)
# => 自分で持ち帰る

と、辞書の添字アクセスと同じようにして、フィールドの値を取得できます。

 
なお、添字アクセスで存在しないキーを指定した場合、

form = cgi.FieldStorage()

try:
    print(form['foo'])
except:
    print('not found: foo')
# => not found: foo

と、例外が出ます。

 
そのため、getvalue()メソッドを使うことで、

form = cgi.FieldStorage()
print(form.getvalue('foo'))
# => None
print(form.getvalue('foo', 'default_value'))
# => default_value

と、例外を回避したり、デフォルト値を返すことができます。

 

同じnameを持つ複数フィールド値の取得

チェックボックスなど、複数のフィールドで同じnameを持つ場合は

form = cgi.FieldStorage()

print(form.getfirst('purpose'))
# => 贈り物にする

print(form.getlist('purpose'))
# => ['贈り物にする', '自家用にする']

のように、getfirst()で最初のものだけ取得したり、getlist()でリストとして取得します。

 
なお、nameが一つだけの場合にgetlist()メソッドを使うと、

form = cgi.FieldStorage(keep_blank_values=True)

print(form.getlist('takeout'))
# => ['自分で持ち帰る']

と、要素数1のリストとなります。

 

cgi.FieldStorageを使ったアップロードファイルの受け取り

ファイルアップロードができるフォームを

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <title></title>
</head>

<body>
    <h1>ファイルアップロードサンプル(一気に読み込み)</h1>
    <form action="/cgi-bin/cgi_file_upload.py" method="POST" enctype="multipart/form-data">
        <!--input text-->
        <label for="id_upload_file">対象ファイル</label>
        <input type="file" id="id_upload_file" name="upload_file">

        <!--submit-->
        <p>
            <input type="submit">
        </p>
    </form>
</body>

</html>

と用意した場合も、cgi.FieldStorageでアップロードしたファイルのデータを扱えます。

 
例えば、

upload_file.txt

あ
a

というファイルを扱う場合、

cgi_file_upload.py

#!/usr/bin/python3
import cgi

# HTTPレスポンスヘッダ
print('Content-Type: text/plain;charset=utf-8\n')
print('')

form = cgi.FieldStorage()
print(form)
# => FieldStorage(None, None, [FieldStorage('upload_file', 'upload_file.txt', b'\xe3\x81\x82\na')])

とすることで、FieldStorageのインスタンスとして取得できます。

 
FieldStorageインスタンスからファイルの中身を一気に読み込む場合、

form = cgi.FieldStorage()

content = form.getvalue('upload_file')

print(content.__class__)
# => <class 'bytes'>

print(content.decode('utf-8'))
# =>
# あ
# a

と、getvalue()メソッドでバイト文字列を取得し、それをdecode()にて文字列にします。

 
また、一気に読み込むのが難しい場合は、一定のバイト数で区切って読み込むこともできます。

cgi_file_upload_with.py

form = cgi.FieldStorage()
upload_file = form['upload_file']

print(upload_file)
# => FieldStorage('upload_file', 'upload_file.txt', b'\xe3\x81\x82\na')

print(dir(upload_file))
# => ['FieldStorageClass', '_FieldStorage__file', '_FieldStorage__write', '__bool__', 
#     '__class__', '__contains__', '__del__', '__delattr__', '__dict__', '__dir__', 
#     '__doc__', '__enter__', '__eq__', '__exit__', '__format__', '__ge__', '__getattr__',
#     '__getattribute__', '__getitem__', '__gt__', '__hash__', '__init__', '__init_subclass__',
#     '__iter__', '__le__', '__len__', '__lt__', '__module__', '__ne__', '__new__',
#     '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', 
#     '__subclasshook__', '__weakref__', '_binary_file', 'bufsize', 'bytes_read', 
#     'disposition', 'disposition_options', 'done', 'encoding', 'errors', 'file', 
#     'filename', 'fp', 'getfirst', 'getlist', 'getvalue', 'headers', 'innerboundary',
#     'keep_blank_values', 'keys', 'length', 'limit', 'list', 'make_file', 'name', 
#     'outerboundary', 'qs_on_post', 'read_binary', 'read_lines', 'read_lines_to_eof', 
#     'read_lines_to_outerboundary', 'read_multi', 'read_single', 'read_urlencoded', 
#     'skip_lines', 'strict_parsing', 'type', 'type_options']

print(upload_file.name)
# => upload_file

print(upload_file.filename)
# => upload_file.txt

print(upload_file.file)
#=> <_io.BytesIO object at 0x7fe34bba03b8>

# with文にも対応しているとのことなので、試してみる
# http://docs.python.jp/3/library/cgi.html
with upload_file as f:
    content = b''
    while True:
        # 1バイトずつ読み込む場合
        byte_strings = f.file.read(1)
        print(byte_strings)
        # => b'\xe3', b'\x81', b'\x82', b'\n', b'a', b'' の順に読み込まれる

        if not byte_strings:
            break
        content += byte_strings

print(content.decode('utf-8'))
# あ
# a

 

ソースコード

GitHubに上げました。alpine_apache_python36_cgi_moduleディレクトリの中が今回のものです。
thinkAmi-sandbox/Docker_Apache-sample

Dockerで、Alpine3.5 + Apache2.4 + Python3.6の環境を作って、フォームのデータをCGIで受け取ってみた

以前、DockerでCGIを動かしてみました。
Dockerで、Alpine3.4 + Apache2.4.25 + Python3.6.0の環境を作って、CGIを動かしてみた - メモ的な思考的な

 
今回は、Dockerで、Alpine3.5 + Apache2.4 + Python3.6の環境を作って、フォームのデータをPythonCGIで受け取ってみます。

 
目次

 

環境

  • Mac OS X 10.11.6
  • Docker for Mac 17.03.1-ce-mac5
  • Alpine3.5
    • httpd:2.4.25-alpineをベースに、Python3をAlpineのパッケージapkでインストールしたもの

 

Dockerfile作成

以前は「Python3.6のAlpine版のイメージ + ApacheをセットアップするDockerfile」という構成でした。

ただ、Alpine3.4縛りのせいとはいえ、ApacheをセットアップするDockerfileがほぼコピペだったので、あまり良くないと感じていました。

 
他の方法を探してみたところ、Alpine3.5にapkのPython3をインストールしているDockerfileがありました。
frol/docker-alpine-python3: The smallest Docker image with Python 3.5 (~61MB)

 
そこで、httpd:2.4.25-alpineをベースに、apkのPython3をインストールするDockerfileを作成しました。

なお、Apacheのconfファイルは前回のものを流用します。

Dockerfile

FROM httpd:2.4.25-alpine

RUN apk --update --no-cache --repository http://dl-cdn.alpinelinux.org/alpine/edge/main/ add python3 && \
    python3 -m ensurepip && \
    rm -r /usr/lib/python*/ensurepip && \
    pip3 install --upgrade pip setuptools && \
    rm -r /root/.cache

# ローカルのhttpd.confをコピー
COPY httpd.conf /usr/local/apache2/conf/

 

HTMLフォームの作成

ひと通りのフォーム要素を持つHTMLを用意します。

また、GETだけではなくPOSTも試したかったので、formのmethodだけを変えたHTMLも用意します。

htdocs/form_get_stdin.html

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <title></title>
</head>

<body>
    <h1>フォームサンプル(GET)</h1>
    <form action="/cgi-bin/stdin_environ.py" method="GET">
        <!--input text-->
        <label for="id_subject">件名</label>
        <input type="text" id="id_subject" name="subject">

        <!--radio button-->
        <p>
            <label for="id_apple">リンゴ</label>
            <input id="id_apple" type="radio" name="fruit" value="りんご">
            <label for="id_mandarin">ミカン</label>
            <input id="id_mandarin" type="radio" name="fruit" value="みかん">
            <label for="id_grape">ブドウ</label>
            <input id="id_grape" type="radio" name="fruit" value="ぶどう">
        </p>
        <p>
            <label for="id_big"></label>
            <input id="id_big" type="radio" name="fruit_size" value="大きいもの">
            <label for="id_small"></label>
            <input id="id_small" type="radio" name="fruit_size" value="小さいもの">
        </p>

        <!--select-->
        <p>
            <label for="id_quantity">個数</label>
            <select id="id_quqntity" name="quantity">
                <option id="id_selected1_1" name="select1_1" value="1個">1</option>
                <option id="id_selected1_2" name="select1_2" value="2個">2</option>
                <option id="id_selected1_3" name="select1_3" value="3個">3</option>
            </select>
        </p>

        <!--select multiple-->
        <p>
            <label for="id_accessories">付属品</label>
            <select id="id_accessories" name="accessories" multiple>
                <option id="id_selected2_1" name="select2_1" value="紙袋">紙袋</option>
                <option id="id_selected2_2" name="select2_2" value="容器">容器</option>
                <option id="id_selected2_3" name="select2_3" value="紐"></option>
            </select>
        </p>

        <!--checkbox-->
        <p>
            <label for="id_takeout">持ち帰る</label>
            <input type="checkbox" id="id_takeout" name="takeout" value="自分で持ち帰る">
        </p>
        <p>
            <label for="id_gift">贈り物</label>
            <input type="checkbox" id="id_gift" name="gift" value="贈り物にする">
        </p>

        <!--hidden-->
        <input type="hidden" id="id_hidden_value" name="hidden_valude" value="隠しデータ">

        <!--textare-->
        <label for="id_memo">メモ</label>
        <textarea id="id_memo" name="memo"></textarea>

        <!--submit-->
        <p>
            <input type="submit">
        </p>
    </form>
</body>

</html>

POST用のHTML(htdocs/form_get_stdin.html)は省略します。

 

CGI用のPythonスクリプトを作成

Apacheのドキュメントによると、フォームのデータは環境変数と標準入力(STDIN)に設定されます。
裏で何が起こっているのか? | Apache Tutorial: CGI による動的コンテンツ - Apache HTTP サーバ バージョン 2.4

 
そこで今回は、環境変数と標準入力の値をブラウザへと返すようにしてみます。

なお、Pythonでは、

  • 環境変数の値: os.environ辞書
  • 標準入力の値: sys.stdin.read()

でそれぞれ取得できます。

cgi/stdin_environ.py

#!/usr/bin/python3
# shebangに指定するPython3を以下で確認
# bash-4.3# which python3
# /usr/bin/python3

import os
import sys

# HTTPレスポンスヘッダ
print('Content-Type: text/plain;charset=utf-8\n')
print("\n")

# HTTPレスポンスボディ
# 標準入力
print('-'*20)
print('stdin:\n{}'.format(sys.stdin.read()))

# 環境変数
print('-'*20)
print('os.environ:\n')
for k, v in os.environ.items():
    print('{}: {}'.format(k, v))

 

CGI用のPythonスクリプトパーミッションを変更

今回、HTMLやPythonスクリプトは Dockerの-vオプションを使用して、ホストとコンテナで共有します。

ただ、Dockerfileでは共有した時のパーミッションをうまく設定できませんでした(後述)。

そこで今回は、ホストのPythonスクリプトパーミッションを変更します。これにより、DockerコンテナとPythonスクリプトを共有しても、同じパーミッションになります。

# 変更前のMac上のパーミッション
$ ls -al
-rw-r--r--   1 you  staff  513  5 10 05:46 stdin_environ.py

# 実行可能へと変更
$ chmod 755 stdin_environ.py 

# 変更後のMac上のパーミッション
$ ls -al
-rwxr-xr-x   1 you  staff  513  5 10 05:46 stdin_environ.py

 

Dockerの起動

ローカルとDockerでファイルを共有するため、-vオプションを使用します。

今回はhtmlファイルのディレクトリとCGIPythonディレクトリの2つを共有するため、-vオプションを2つ使ってそれぞれ指定します。
Mounting multiple volumes on a docker container? - Stack Overflow

ホスト コンテナ
htdocs/ /usr/local/apache2/htdocs
cgi/ /usr/local/apache2/cgi-bin/

 
実際に入力する内容は以下の通りです。

# ビルド
$ docker build -t alpine:python3_httpd24_cgi_form .

# 起動
$ docker run -p 8081:80 --name cgi_form -v `pwd`/htdocs/:/usr/local/apache2/htdocs -v `pwd`/cgi/:/usr/local/apache2/cgi-bin/ alpine:python3_httpd24_cgi_form

 
コンテナのパーミッションも確認します。

# コンテナに入る
$ docker exec -it `docker ps | grep cgi_form | awk '{print $1}'` /bin/bash

# カレントディレクトリの確認
bash-4.3# pwd
/usr/local/apache2

# カレントディレクトリのパーミッション
bash-4.3# ls -al
total 40
drwxr-xr-x    1 www-data www-data      4096 Mar  3 21:57 .
drwxr-xr-x    1 root     root          4096 Mar  3 21:57 ..
drwxr-xr-x    2 root     root          4096 Mar  3 21:57 bin
drwxr-xr-x    2 root     root          4096 Mar  3 21:57 build
drwxr-xr-x    4 root     root           136 May  9 20:47 cgi-bin
drwxr-xr-x    1 root     root          4096 May  9 20:53 conf
drwxr-xr-x    3 root     root          4096 Mar  3 21:57 error
drwxr-xr-x    5 root     root           170 May  9 20:46 htdocs
drwxr-xr-x    3 root     root          4096 Mar  3 21:57 icons
drwxr-xr-x    2 root     root          4096 Mar  3 21:57 include
drwxr-xr-x    1 root     root          4096 May  9 20:54 logs
drwxr-xr-x    2 root     root          4096 Mar  3 21:57 modules

# cgi-binディレクトリの中にある「stdin_environ.py」のパーミッションを確認
bash-4.3# cd cgi-bin/

bash-4.3# ls -al
-rwxr-xr-x    1 root     root           513 May  9 20:46 stdin_environ.py

ホストと同じパーミッションが設定されていました。

 

フォームでGET

http://localhost:8081/form_get_stdin.htmlにアクセスし、以下のようにフォームへ入力します。

f:id:thinkAmi:20170510210628p:plain:w300

 
送信ボタンを押したあとの結果は以下の通りです。

環境変数のみ値が設定されています。

--------------------
stdin:

--------------------
os.environ:

HTTP_HOST: localhost:8081
HTTP_CONNECTION: keep-alive
HTTP_UPGRADE_INSECURE_REQUESTS: 1
HTTP_USER_AGENT: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36
HTTP_ACCEPT: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
HTTP_REFERER: http://localhost:8081/form_get_stdin.html
HTTP_ACCEPT_ENCODING: gzip, deflate, sdch, br
HTTP_ACCEPT_LANGUAGE: ja,en-US;q=0.8,en;q=0.6
PATH: /usr/local/apache2/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
SERVER_SIGNATURE: 
SERVER_SOFTWARE: Apache/2.4.25 (Unix)
SERVER_NAME: localhost
SERVER_ADDR: 172.17.0.2
SERVER_PORT: 8081
REMOTE_ADDR: 172.17.0.1
DOCUMENT_ROOT: /usr/local/apache2/htdocs
REQUEST_SCHEME: http
CONTEXT_PREFIX: /cgi-bin/
CONTEXT_DOCUMENT_ROOT: /usr/local/apache2/cgi-bin/
SERVER_ADMIN: you@example.com
SCRIPT_FILENAME: /usr/local/apache2/cgi-bin/stdin_environ.py
REMOTE_PORT: 59532
GATEWAY_INTERFACE: CGI/1.1
SERVER_PROTOCOL: HTTP/1.1
REQUEST_METHOD: GET
QUERY_STRING: subject=%E3%82%BF%E3%82%A4%E3%83%88%E3%83%AB&fruit=%E3%82%8A%E3%82%93%E3%81%94&fruit_size=%E5%B0%8F%E3%81%95%E3%81%84%E3%82%82%E3%81%AE&quantity=2%E5%80%8B&accessories=%E5%AE%B9%E5%99%A8&takeout=%E8%87%AA%E5%88%86%E3%81%A7%E6%8C%81%E3%81%A1%E5%B8%B0%E3%82%8B&gift=%E8%B4%88%E3%82%8A%E7%89%A9%E3%81%AB%E3%81%99%E3%82%8B&hidden_valude=%E9%9A%A0%E3%81%97%E3%83%87%E3%83%BC%E3%82%BF&memo=%E4%B8%80%E8%A1%8C%E7%9B%AE%0D%0A%E4%BA%8C%E8%A1%8C%E7%9B%AE
REQUEST_URI: /cgi-bin/stdin_environ.py?subject=%E3%82%BF%E3%82%A4%E3%83%88%E3%83%AB&fruit=%E3%82%8A%E3%82%93%E3%81%94&fruit_size=%E5%B0%8F%E3%81%95%E3%81%84%E3%82%82%E3%81%AE&quantity=2%E5%80%8B&accessories=%E5%AE%B9%E5%99%A8&takeout=%E8%87%AA%E5%88%86%E3%81%A7%E6%8C%81%E3%81%A1%E5%B8%B0%E3%82%8B&gift=%E8%B4%88%E3%82%8A%E7%89%A9%E3%81%AB%E3%81%99%E3%82%8B&hidden_valude=%E9%9A%A0%E3%81%97%E3%83%87%E3%83%BC%E3%82%BF&memo=%E4%B8%80%E8%A1%8C%E7%9B%AE%0D%0A%E4%BA%8C%E8%A1%8C%E7%9B%AE
SCRIPT_NAME: /cgi-bin/stdin_environ.py

 

フォームでPOST

http://localhost:8081/form_post_stdin.htmlにアクセスし、同じようにフォームに入力し、送信ボタンを押します。

POSTでは環境変数と標準入力に値が設定されています。

--------------------
stdin:
subject=%E3%82%BF%E3%82%A4%E3%83%88%E3%83%AB&fruit=%E3%82%8A%E3%82%93%E3%81%94&fruit_size=%E5%B0%8F%E3%81%95%E3%81%84%E3%82%82%E3%81%AE&quantity=2%E5%80%8B&accessories=%E5%AE%B9%E5%99%A8&takeout=%E8%87%AA%E5%88%86%E3%81%A7%E6%8C%81%E3%81%A1%E5%B8%B0%E3%82%8B&gift=%E8%B4%88%E3%82%8A%E7%89%A9%E3%81%AB%E3%81%99%E3%82%8B&hidden_valude=%E9%9A%A0%E3%81%97%E3%83%87%E3%83%BC%E3%82%BF&memo=%E4%B8%80%E8%A1%8C%E7%9B%AE%0D%0A%E4%BA%8C%E8%A1%8C%E7%9B%AE
--------------------
os.environ:

HTTP_HOST: localhost:8081
HTTP_CONNECTION: keep-alive
CONTENT_LENGTH: 444
HTTP_CACHE_CONTROL: max-age=0
HTTP_ORIGIN: http://localhost:8081
HTTP_UPGRADE_INSECURE_REQUESTS: 1
HTTP_USER_AGENT: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36
CONTENT_TYPE: application/x-www-form-urlencoded
HTTP_ACCEPT: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
HTTP_REFERER: http://localhost:8081/form_post_stdin.html
HTTP_ACCEPT_ENCODING: gzip, deflate, br
HTTP_ACCEPT_LANGUAGE: ja,en-US;q=0.8,en;q=0.6
PATH: /usr/local/apache2/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
SERVER_SIGNATURE: 
SERVER_SOFTWARE: Apache/2.4.25 (Unix)
SERVER_NAME: localhost
SERVER_ADDR: 172.17.0.2
SERVER_PORT: 8081
REMOTE_ADDR: 172.17.0.1
DOCUMENT_ROOT: /usr/local/apache2/htdocs
REQUEST_SCHEME: http
CONTEXT_PREFIX: /cgi-bin/
CONTEXT_DOCUMENT_ROOT: /usr/local/apache2/cgi-bin/
SERVER_ADMIN: you@example.com
SCRIPT_FILENAME: /usr/local/apache2/cgi-bin/stdin_environ.py
REMOTE_PORT: 59536
GATEWAY_INTERFACE: CGI/1.1
SERVER_PROTOCOL: HTTP/1.1
REQUEST_METHOD: POST
QUERY_STRING: 
REQUEST_URI: /cgi-bin/stdin_environ.py
SCRIPT_NAME: /cgi-bin/stdin_environ.py

 

その他悩んだこと

Dockerfileの中で、共有ディレクトリのパーミッションを設定する方法

前述のとおり、今回はホストとコンテナの共有ディレクトリのパーミッションは、ホストのパーミッションを変更することで対応しました。

なお、ホストのパーミッションを変更しなかった場合、Dockerのログに以下が出力されるとともに、ブラウザに「Internal Server Error」が表示されました。

[pid 97:tid 140512665647944] (13)Permission denied: AH01241: exec of '/usr/local/apache2/cgi-bin/stdin_environ.py' failed
[pid 12:tid 140512664455856] [client 172.17.0.1:59462] End of script output before headers: stdin_environ.py, referer: http://localhost:8081/form_get_stdin.html

 
以下の方法を試してみましたが、うまくいきませんでした。

 
ちなみに、Alpine3.4では useraddusermodが無いとのことです。
Alpine Linuxでユーザやグループを追加・修正・削除する - 水底

 

ソースコード

GitHubに上げました。alpine_apache_python36_cgi_formディレクトリの中が今回のものです。
thinkAmi-sandbox/Docker_Apache-sample