2011/05/17

c++でcgi ー ファイルのアップロード

C++でファイルのアップロードを受け取り、ファイルの中身をHTMLで表示するプログラムです。

参考にしたページ
http://www-cms.phys.s.u-tokyo.ac.jp/~naoki/CIPINTRO/CCGI/multdisp.html


・HTML
<!DOCTYPE html>
<html>
  <head>複数ファイルのアップロード
 <meta charset="utf-8">
  </head>

  <body>
 <form action="upload.fcgi" method="POST" enctype="multipart/form-data">
   <input type="file" name="files" multiple>
   <input type="submit" value="submit">
 </form>
  </body>
</html>

・C++ソースコードの説明


・12~85行目:Requestがアップロードかを判定

環境変数[CONTENT_TYPE]の文字列から
  • [multipart/form-data]が含まれているか
  • [boundary]の文字列を取得できるか
を調べてRequestがアップロードかを判定しています。


例としてGoogle ChromeでアップロードをするとCONTENT_TYPEは、以下のようになります。
(WebKitFormBoundary7uUEXkjnVYxPBKMIは、ユニークな値がはいります。)

multipart/form-data; boundary=----WebKitFormBoundary7uUEXkjnVYxPBKMI, referer: 呼び出し元のURL

boundaryの文字列は、アップロードされた各ファイルの本体と、フッタの区切りに使うので覚えておきます。


・90~123行目:ファイルの中身まで標準入力を進める

アップロードされたファイルは、標準入力stdinから以下の順で取得できます。
(複数ファイルがアップロードされた時は、この順に繰り返されます。)
  1. ヘッダ
  2. ファイル本体
  3. フッタ

例えば2つのファイルをアップロードすると、以下のように情報が送られてきます。

------WebKitFormBoundary7uUEXkjnVYxPBKMI[\r][\n]
Content-Disposition: form-data; name="files"; filename="ファイル1"[\r][\n]
Content-Type: application/octet-stream[\r][\n]
[\r][\n]
ファイル1の中身
[\r][\n]
------WebKitFormBoundary7uUEXkjnVYxPBKMI[\r][\n]
Content-Disposition: form-data; name="files"; filename="ファイル2"[\r][\n]
Content-Type: application/octet-stream[\r][\n]
[\r][\n]
ファイル2の中身
[\r][\n] 


ヘッダの構成は以下のようになっています。
  1.  [--]+[CONTENT_TYPEのboundary]で指定されるユニークな文字列[\r][\n]
  2.  Content-Disposition[\r][\n]
  3.  Content-Type[\r][\n][\r][\n]

[\r][\n][\r][\n]の後からがファイル本体になるので、そこまでスキップしています。

送信されたフォームのフィールド名やファイル名などを取得する際には、2の[Content-Disposition]から取得する必要がありますが、本記事では行っていません。


・136~174行目

ファイル本体とヘッダをstdinから取得し、HTMLとし標準出力に表示しています。
また、stdinから文字を取得する際に[boundary]の文字列と比較し、ヘッダは表示せずに終了します。


・C++

#include <string>
#include <stdio.h>
#include <iostream>
#include <stdlib.h> // getenv

const int BOUNDARY_LENGTH = 8; // boundaryの文字数
using namespace std;

// ========================================================
//   Requestがアップロードか調べ、boundaryの文字列を返します。
// ========================================================
bool isUploadRequest( std::string & boundaryStr )
{
 /*
   環境変数[REQUEST_METHOD]に送信されたメソッド[GET/POST]が入ります。
   アップロードは、[POST]で送信されてきます。
 */
 std::string request_method = getenv( "REQUEST_METHOD" );
 if ( request_method != "POST" ) 
 {
  fprintf( stderr, "REQUEST_METHOD isn't POST.\n" );
  return false;
 }

 /*
   Uploadでは、環境変数[CONTENT_TYPE]に[multipart/form-data]が含まれています。
 */
 std::string content_type = getenv( "CONTENT_TYPE" );
 if ( content_type.empty() || 
   content_type.find( "multipart/form-data" ) == std::string::npos )
 {
  fprintf( stderr, "CONTENT_TYPE is error.\n" );
  return false;
 }

 fprintf( stderr, "%s\n", content_type.c_str());

 /*
   boundaryを取得します。
  */
 std::string::size_type index = content_type.find( "boundary" );
 if ( index == std::string::npos )
 {
  return false;
 }

 // boundaryの文字列より後に[=]があるかを調べます。
 index = content_type.find( "=", index + BOUNDARY_LENGTH );
 if ( index == std::string::npos )
 {
  return false;
 }

 // [=]の後からboundaryを取得します。
 index++;
 if ( index >= content_type.size())
 {
  return false;
 }

 std::string::size_type end_index = std::string::npos;
 if ( content_type[ index ] == '"' )
 {
  // [boundary="XXXXXX"]形式は、["]内の文字列を取得します。
  end_index = content_type.find( '"', index + 1 );
  if ( end_index == std::string::npos )
  {
   return false;
  }
  index++;
 }
 else
 {
  // [boundary=XXXXX, XXXX=XXXX]形式は、[,]までの文字列を取得します。
  end_index = content_type.find( ',', index + 1 );
 }

 /*
   ファイル本体とフッタの区切り文字列を作成します。
   区切りの文字列は、[\r\n--]+[boundaryの文字列]の形式です。
 */
 boundaryStr = "\r\n--";
 boundaryStr += content_type.substr( index, end_index - index );
 return true;
}

// ========================================================
// ファイルの中身の直前には、[\r\n\r\n]があるので、そこまで進めます。 
// ========================================================
bool skipHeader()
{
 char buf;
 while ( 1 )
 {
  if ( fread( &buf, 1, 1, stdin ) != 1 )
   return false;

  if ( buf != '\r' )
   continue;

  if ( fread( &buf, 1, 1, stdin ) != 1 )
   return false;
  
  if ( buf != '\n' )
   continue;

  if ( fread( &buf, 1, 1, stdin ) != 1 )
   return false;
  
  if ( buf != '\r' )
   continue;

  if ( fread( &buf, 1, 1, stdin ) != 1 )
   return false;
  
  if ( buf == '\n' )
  {
   // [\r\n\r\n]が見つかったので、ループを抜けます。
   return true;
  }
 }
 return false;
}
 
// ========================================================
// アップロードされたファイルの中身を表示します。
// ========================================================
bool printFile( const std::string & boundaryStr )
{
 // ヘッダ部分をスキップし、ファイルの中身まで進めます。
 if ( !skipHeader())
 {
  return false;
 }

 // ファイルの中身を表示します。
 char buf;
 string tmpStr;
 string::size_type boundaryLength = boundaryStr.size();
 string::size_type index          = 0;

 while ( 1 ) 
 {
  if ( fread( &buf, 1, 1, stdin ) != 1 )
   return true;

  if ( buf == boundaryStr[ index ] )
  {
   /*
     boundaryの文字なので、一旦tmpStrに保存し、比較するboundaryのインデックスを進めます。
   */
   index++;
   tmpStr += buf;

   if ( index >= boundaryLength )
   {
    // boundaryの文字が全て見つかったので、ループを抜けます。
    break;
   }
  }
  else
  {
   if ( index > 0 )
   {
    // boundaryでない文字列が見つかったので、tmpStrの文字列を表示しインデックスを0に戻します。
    printf( "%s", tmpStr.c_str());
    index = 0;
    tmpStr.clear();
   }

   putchar( buf );
  }
 }
 return true;
}

// ========================================================
// main
// ========================================================
int main()
{
 // Requestsがアップロードかを調べます。
 std::string boundaryStr;
 if ( !isUploadRequest( boundaryStr ))
 {
  return -1;
 }
 

 // HTMLを出力します。
 printf( "Content-Type: text/html\n\n" );
 
 printf( "<html>" );
 printf( "<head><title>test</title></head>" );
 printf( "<body>" );

 std::string boundary;
 if ( !isUploadRequest( boundary ))
 {
  printf( "error\n" );
 }
 else
 {
  while ( 1 )
  {
   if ( !printFile( boundaryStr )) break;
  }
 }
 printf( "</body>" );
 printf( "</html>" ); 
 return 0;
}

0 コメント:

コメントを投稿

 
Design by Free WordPress Themes | Bloggerized by Lasantha - Premium Blogger Themes | Blogger Templates