Python
Perl
Fortran
SimpleHTTPServer

Python3のhttp.serverで他言語のCGIを動かす - perlとかFortranとか

タイトルのとおりです。

(追記) 編集リクエストを受け、タイトルを若干修正しました。
Python3であることを明記しました。
記事を最初に書いた時の勘違いが激しかったので、再構成しました。

環境

以下の二つの環境で試してみました。

Cygwin on windows7

  • gcc, gfortran 7.3.0
  • python 3.6.4
  • perl v5.26.2

Ubuntu 16.04

  • gcc, gfortran 5.4.0
  • python 3.5.2
  • perl 5.22.1
  • php 7.0.30

準備

まず、pythonでSimpleHTTserverを立ち上げる作業ディレクトリを作り、そこに移動します。

mkdir python_cgi_test
cd python_cgi_test

この中に、cgi-binディレクトリとindex.htmlを作っておきます。

mkdir cgi-bin
vi index.html
index.html
<!DOCTYPE html>
<html>
<head>
    <title>Python Simple HTTP server</title>
</head>
<body>

<ul>
    <li> <a href="/cgi-bin/test_py">Python</a> </li>
    <li> <a href="/cgi-bin/test_pl">Perl</a> </li>
    <li> <a href="/cgi-bin/test_php?hoge=fuga">PHP</a> </li>
    <li> <a href="/cgi-bin/test_c">C</a> </li>
    <li> <a href="/cgi-bin/test_f90">Fortran</a> </li>
</ul>

</body>
</html>

このindex.htmlがあるフォルダに行き、

python -m http.server --cgi

として、http://localhost:8000/ にアクセスするとindex.htmlがブラウザで出力されます。ここには各言語へのCGIテストに行くリンクも用意しました。

python

test_py
#!/usr/bin/python

print("Content-type: text/html")
print("")
print("<html>")
print("Hello,World!")
print("</html>")

まあ、ググったらすぐに見つかるサンプルです。

以降、出力結果はブラウザで見ると<html><body>で囲まれた部分の挨拶的な文字列が出るだけなので省略します。

perl

test_pl
#!/usr/bin/perl 

print "Content-type: text/html\n\n"; 
print "<html><body>\n"; 
print "Success CGI!\n"; 
print "</body></html>"; 
exit; 

ほかのインタプリタ言語についても、シバンに書いてやれば対応できるようです。

ちなみに、余りに基本的な構文すぎるので、perlではなくpython2でも問題なく実行できてしまいます。python3にするとさすがに「SyntaxError: Missing parentheses in call to 'print'. 」になります。

C

コンパイル言語だってCGIに使えます。

test.c
#include <stdio.h>

int main(void)
{
    printf("Content-type: text/html\n\n");

    printf("<HTML><BODY>");
    printf("Hello from C");
    printf("</BODY></HTML>");
    printf("\n");
    return 0;
}
gcc test.c -o test_c
mv test_c /path/to/python_server_test/cgi-bin

Cygwinだとtest_c.exeファイルになります。適当にリネームしましょう。

要は適切な内容を標準出力に出す実行ファイルを作ってやれば良いのです。

Fortran

Cと同様にFortranでもできます。

test.f90
program cgi_test
   print '(A)', "Content-type: text/html"
   print '(A)', ""
   write(*, *)"<html>"
   write(*, *)"Hello from fortran"
   write(*, *)"</html>"
   stop
end program cgi_test
gfortran test.f90 -o test_f90
mv test_f90 /path/to/python_server_test/cgi-bin

最初2つの出力がprintなのは、write(*,*)だとContent ...の前にスペースが1つついてしまい、CGIの出力として解釈されなかったためです。

これで、我らがFortranでCGIを書き、それを簡単にテスト実行できるかもしれないというめどが立ってきました。

ディレクトリ構造

treeコマンド結果を修正
python_server_test
    ├── cgi-bin
    │   ├── test_c
    │   ├── test_f90
    │   ├── test_pl
    │   └── test_py
    ├── cgi-source
    │   ├── test.c
    │   └── test.f90
    └── index.html

cd python_server_test
python -m http.server --cgi

POST/GET

CGIとなると、やはりブラウザからの入力を受け付けたいものです。ブラウザからのリクエストとして、まずPOSTとGETを確かめてみます。
Cookieなどは気が向いたらいずれ。

html側

リクエストを発生させるため、htmlファイルに以下のフォームを作成します。

    <form method="POST" action="/cgi-bin/hogehoge?foo=bar">
        <input type="text" name="hoge1" value="fuga">
        <input type="submit" name="submit_button" value="press">
    </form>

cgi-binの下にテストしたい実行ファイルをhogehogeとしてリネームして置いてやって、"press"ボタンを押します。

php

現状の僕にとって、サーバーからのデータを確かめるコードを一番書きやすかったのがphpなので、まずはこれで書いてみます。

#!/usr/bin/php
Content-type: text/html

<html>
<body>

<h1> $_GET </h1>
<pre> <?php var_dump($_GET); ?> </pre>
<h1> $_POST </h1>
<pre> <?php var_dump($_POST); ?> </pre>

<h1> GET(QUERY_STRING) </h1>
<p><?php echo $_SERVER["QUERY_STRING"]; ?></p>

<h1> POST(stdin) </h1>
<p><?php
if( "POST" === $_SERVER["REQUEST_METHOD"] ){
   $length =  intval($_SERVER["CONTENT_LENGTH"]);
   $post = fgets(STDIN, $length + 1);
   echo $post;
}else{
   echo "No POST data found";
}
?></p> 
</body>
</html>

Apacheなどから普通にphpを叩くと、GETは$_GET配列に、POSTは$_POSTに入っています。ところが、上のコードのようにとりあえずvar_dumpで中身を見てみても、空配列になっています。
ではどこにあるかと言いますと、GETは環境変数のQUERY_STRING、POSTは標準入力から与えられます。
ということで、GETはecho $_SERVER["QUERY_STRING"];で、
POSTは$length = intval($_SERVER["CONTENT_LENGTH"]);で文字列長を得てから$post = fgets(STDIN, $length+1);と取得します。
ここで$post = fgets(STDIN); と長さ不定で取ろうとすると、EOLを待ち続けるためなのかわかりませんが、フリーズしてしまいます。

一応実行結果例を書いておきます。

$_GET
 array(0) {
}

$_POST
 array(0) {
}

GET(QUERY_STRING)
foo=bar

POST(stdin)
hoge1=fuga&submit_button=press

ちなみにfilter_inputは変数が設定されていない場合の挙動を示しました。

調べてないので想像ですが、$_GET$_POSTは、apacheなどのHTTPサーバーのモジュールとしてPHPが実行した時に定義されるのでしょうか。今回はどうもローカル上のインタプリタとして動いているようですので。
これをpythonサーバーでやろうとするとサーバー側で何らかのコードを書かないと行けないのでしょうか。

C

コメントで@tenmyoさんが書かれた通りです。ありがとうございました。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, const char *argv[], const char *envp[])
{
  size_t i=0;
  printf("Content-type: text/plain\r\n\r\n");
  // ENV
  for (i=0; envp[i]; ++i) {
    printf("%s\n", envp[i]);
  }
  printf("----\n");
  // STDIN
  const char *method = getenv("REQUEST_METHOD");
  if (method) {
    if (!strcmp(method, "POST")) {
      const char *len = getenv("CONTENT_LENGTH");
      size_t size = 0;
      if (len) {
        size = atoll(len);
      }
      unsigned char *buf = malloc(size);
      if (!buf) {
        return -1;
      }
      size = fread(buf, 1, size, stdin);
      fwrite(buf, 1, size, stdout);
    }
  }
  return 0;
}

これは実行すると環境変数がすべて出力されますので、適切に応用してやれば良いかなと思います。

Fortran

これがやりたかった。

まず、環境変数の取得ですが、get_environment_variableを2回呼ぶのが正攻法かと思います。

   character(:), allocatable :: get
   integer :: length, status
   intrinsic :: get_environment_variable
   character(:), parameter :: env_query = "QUERY_STRING"

   call get_environment_variable(env_query, status=status, length=length)
   if (status == 0)then
      allocate(character(length) :: get)
      call get_environment_variable(env_query, value=get)
      write(*, *) "<h1>GET</h1>"
      write(*, *) get
      deallocate(get)
   endif

1回目のcallで文字列を取得し、バッファ用文字列をallocateし、2回目のcallでようやく環境変数を取得します。deferred length characterの挙動のため、若干冗長な書き方になってしまっています。

続いてPOSTの取得です。
REQUEST_METHODPOSTだったら、文字列長を取得し、

    call get_environment_variable("CONTENT_LENGTH", value=clen)

clencharacter型なので整数型に変換し、

    read(clen, *) length

標準入力から取得します。

    allocate(character(length) :: post)
    read(*, '(A)', advance='no', size=length) post

ここでもphpでのfgets()の話と同様に、文字列長を固定してやる必要があります。
read(*,*) postだとフリーズします。

以上をちゃんと動く形でまとめたものが以下になります。

test.f90
program test_getpost
   implicit none
   character(:), allocatable :: method, get, post, clen
   integer :: length, status
   intrinsic :: get_environment_variable
   character(:), parameter :: env_query = "QUERY_STRING"
   character(:), parameter :: env_method = "REQUEST_METHOD"
   character(:), parameter :: env_length = "CONTENT_LENGTH"

   print '(A)', "Content-type: text/html"
   print '(A)', ""
   write(*, *)"<html>"
   write(*, *) "<head>"
   write(*, *) "<title>GET POST with fortran</title>"
   write(*, *) "</head>"
   write(*, *) "<body>"
   call get_environment_variable(env_query, status=status, length=length)
   if (status == 0)then
      allocate(character(length) :: get)
      call get_environment_variable(env_query, value=get)
      write(*, *) "<h1>GET</h1>"
      write(*, *) get
      deallocate(get)
   endif

   call get_environment_variable(env_method, status=status, length=length)
   if(status == 0) then
      allocate(character(length) :: method)
      call get_environment_variable(env_method, value=method)
      if (method == "POST") then
         call get_environment_variable(env_length, status=status, length=length)
         allocate(character(length) :: clen)
         call get_environment_variable(env_length, value=clen)
         read(clen, *) length
         allocate(character(length) :: post)
         write(*, *) "<h1>POST</h1>"
         read(*, '(A)', advance='no', size=length) post
         write(*, *) "<p>" // post // "</p>"
         deallocate(post)
      endif
      deallocate(method)
   endif
   write(*, *) "</body>"
   write(*, *)"</html>"
   stop

end program test_getpost

これで我らがFortranでもCGIを安心して書けますね。

注意

Pythonのsimplehttpの仕様として、CGI実行ファイルは実行権限を付けて cgi-bin ディレクトリ以下におく必要があるようです。他のディレクトリ名にするとプレーンテキストとしてそのまま表示されました。
https://docs.python.org/3/library/http.server.html#http.server.CGIHTTPRequestHandler
によると、cgi_directories['/cgi-bin', '/htbin']として定義されているそうです。

他のディレクトリに置きたい場合は、たぶん自分でpythonスクリプトを書く必要があるかと思います。

お詫び

この記事を最初書いた時は、python -m http.server --cgiではPOST/GETが取得できないという話をしていました。
これは単純に、僕の勘違いから取得に失敗していただけでした。
みなさんの指摘をもとに修正したらうまく行きましたので、それを元に記事を再構成しました。
@shiracamusさん、@tenmyoさん、ありがとうございました。

記事を修正したためコメントが若干話がつながらないかもしれませんが、残しておきます。

言い訳

  • GET:phpなら$_GETにあると思い込んでおりました。今作ってるWordpressプラグインではGET使わないし・・・これを機会にこういうスーパーグローバル変数依存はやめようかなと。
  • POST:Fortranのread(*,*)でフリーズしましたので、ダメなのかと思いました。