ホームページ  >  記事  >  バックエンド開発  >  PHP を使用した Amazon SES プロキシサーバーの実装

PHP を使用した Amazon SES プロキシサーバーの実装

WBOY
WBOYオリジナル
2016-07-25 08:46:301209ブラウズ

この記事を理解するには、SES を使用するための一定の基礎が必要です。理解できない場合は、この質問の説明を読んでください。 http://segmentfault.com/q/1010000000095210

SES の正式名は Simple Email Service で、Amazon が開始した基本的な電子メール サービスです。 AWS の基本サービスの一部として、AWS の伝統的な利点である安いを継承しています。

はい、本当に安いです。これが、私が mailgun やその他の優れた電子メール サービスを使用しない理由です。毎月 100,000 通の電子メールを送信する場合、基本的に支払う必要があるのは約 10 ドルだけです。数百ドルから始まることが多い他のサービスと比較すると、この価格のメリットは非常に大きいです。したがって、これで私はその多くの欠点を許容できます。

しかし、中国でSESを利用する人が増えるにつれ、昨年末、ある日突然ブロックされてしまい、致命傷となりました。そこで、このサービスを使い続けるために、自分の海外サーバーにプロキシを構築してみました。同時に、これは API を改善して、一斉メール送信などのより価値のある機能を実装する機会にもなります。

なので、海外サーバーを使用せずに直接リバースプロキシを作ってプレイしただけでは、表面的な問題は解決しただけで、機能拡張の必要性は実現されませんでした。したがって、この SES エージェントを設計するために 2 つの基本的な目標を設定しました

元の API インターフェイスと完全な互換性があるため、プロキシを使用するために元のコードを基本的に変更する必要はありません。 一斉メール機能の実装

最初のポイントを達成するのは実際には非常に簡単です。実際には、PHP を使用してリバース プロキシを実装し、送信されたパラメーターを受け取り、アセンブル後にcurl コンポーネントを使用して実際の SES サーバーに送信し、直接出力します。領収書を取得した後、クライアントに渡します。これは標準的な代理店のプロセスです。私のコードを以下に示します。重要な部分についてはコメントしました

これらのコードはドメイン名のルート ディレクトリに配置する必要があることに注意してください

もちろん、第 2 レベルのドメイン名も使用できます。
  1. include __DIR__ . '/includes.php';
  2. // さらに重要なヘッダーがいくつかありますが、その他は注意する必要はありません
  3. $headers = array(
  4. 'Date : ' . get_header ('Date'),
  5. 'Host: ' . SES_HOST,
  6. 'X-Amzn-Authorization: ' . get_header('X-Amzn-Authorization')
  7. );
  8. // 次に、URL を再度アセンブルします。この正しい SES サーバーをリクエストするには
  9. $url = 'https://' . '/'
  10. . (empty($_SERVER['QUERY_STRING']) ? '' : '?' . $_SERVER['QUERY_STRING'] );
  11. $ch =curl_init();
  12. curl_setopt($ch, CURLOPT_USERAGENT, 'SimpleEmailService/php');
  13. curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 1);
  14. curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1);
  15. // 処理が必要です `POST` メソッドと `DELETE` メソッドだけです。 `GET` メソッドはたくさんあるので、一つずつ実装しません
  16. // 実際には、これらはすべて取得するためのメソッドです。この情報はバックグラウンドで直接表示できます。
  17. switch ($_SERVER ['REQUEST_METHOD']) {
  18. case 'GET':
  19. Break;
  20. case 'POST':
  21. global $HTTP_RAW_POST_DATA;
  22. $data = empty( $HTTP_RAW_POST_DATA) ? file_get_contents('php://input')
  23. : $ HTTP_RAW_POST_DATA;
  24. $headers[] = 'Content-Type: application/x-www-form-urlencoded';
  25. parse_data($data);
  26. curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
  27. curl_setopt($ch , CURLOPT_POSTFIELDS, $data);
  28. ブレーク;
  29. case 'DELETE':
  30. curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
  31. ブレーク;
  32. デフォルト:
  33. Break;
  34. }
  35. curl_setopt($ch, CURLOPT_HTTPHEADER, $ headers);
  36. curl_setopt($ch, CURLOPT_HEADER, false);
  37. curl_setopt($ch, CURLOPT_URL, $url);
  38. curl_setopt($ch, curlopt_returntransfer、true);
  39. curl_setopt($ ch、curlopt_folowlocation、true); ;
  40. curl_close($ch);
  41. header('Content-Type : ' . $content_type, true, $status);
  42. echo $response;
  43. コードをコピー

このコードは非常にシンプルですが、注意する必要があるいくつかのトリックがあります。その中で、POST メソッドを処理するときに、この関数が実際に大量メールを実装するための鍵となります。

そう言えば、SES の電子メール送信 API について触れなければなりません。SES は、複数の送信オブジェクトをサポートする単純な電子メール送信 API のみを提供します。ただし、複数の受信者に送信する場合は、受信者列にも表示されます。他の受信者のアドレス。もちろん、ccやbccのカーボンコピー機能にも対応していますが、このカーボンコピー機能を使ってグループメールを送信すると、受信者には受信者ではなく、カーボンコピーの対象の中に含まれることになります。通常の Web サイトでは、これらは明らかに許容できません。

つまり、電子メールを送信するには、実際の同時インターフェースが必要です。SES によって割り当てられた割り当ては、1 秒あたり 28 件の電子メールを送信することであることを知っておく必要があります (各人に異なる割り当てが設定されています)。フル活用すると、1 時間あたり 100,000 件の電子メールを送信できるようになります。 、これは完全に中規模のウェブサイトのニーズを満たすことができます。

そこで私はアイデアを思いつきました。 クライアント インターフェイスをまったく変更せずに、プロキシ サーバー経由で送信された複数の受信者を含む電子メールを 1 つの受信者を含む複数の電子メールに解凍し、これらの電子メールを非同期を使用して SES に送信します。行列。これは parse_data 関数が行うことです。以下に、使用するすべてのプライベート関数を含むコードを直接記述します

  1. define('REDIS_HOST', '127.0.0.1');
  2. define('REDIS_PORT', 6379);
  3. define('SES_HOST', 'email.us-east-1.amazonaws .com');
  4. define('SES_KEY', '');
  5. define('SES_SECRET', '');
  6. /**
  7. * get_header
  8. *
  9. * @parammixed $name
  10. * @access public
  11. * @return void
  12. */
  13. function get_header($name) {
  14. $name = ' HTTP_' 。 strtoupper(str_replace('-', '_', $name));
  15. return isset($_SERVER[$name]) ? $_SERVER[$name] : '';
  16. }
  17. /**
  18. * my_parse_str
  19. *
  20. * @parammixed $query
  21. * @parammixed$params
  22. * @access public
  23. * @return void
  24. */
  25. function my_parse_str($query, &$params) {
  26. if (empty($query)) {
  27. return;
  28. }
  29. $decode = function ($str) {
  30. return rawurldecode(str_replace('~', '%7E', $str));
  31. };
  32. $data =explode('&', $query);
  33. $ params = array();
  34. foreach ($data as $value) {
  35. list ($key, $val) =explode('=', $value, 2);
  36. if (isset($params[$key]) ) {
  37. if (!is_array($params[$key])) {
  38. $params[$key] = array($params[$key]);
  39. }
  40. $params[$key][] = $val;
  41. } else {
  42. $params[$key] = $decode($val);
  43. }
  44. }
  45. }
  46. /**
  47. * my_urlencode
  48. *
  49. * @parammixed $str
  50. * @access public
  51. * @return void
  52. */
  53. function my_urlencode($str) {
  54. return str_replace('%7E ', '~', rawurlencode($str));
  55. }
  56. /**
  57. * my_build_query
  58. *
  59. * @parammixed $params
  60. * @access public
  61. * @return void
  62. */
  63. function my_build_query($parameters) {
  64. $params = array();
  65. foreach ($parameters as $var = > $value) {
  66. if (is_array($value)) {
  67. foreach ($value as $v) {
  68. $params[] = $var.'='.my_urlencode($v);
  69. }
  70. } else {
  71. $params[] = $var.'='.my_urlencode($value);
  72. }
  73. }
  74. sort($params, SORT_STRING);
  75. return implode('&', $params);
  76. }
  77. /**
  78. * my_headers
  79. *
  80. * @parammixed $headers
  81. * @access public
  82. * @return void
  83. */
  84. function my_headers() {
  85. $date = gmdate('D, d M Y H:i:s e');
  86. $sig = Base64_encode(hash_hmac('sha256', $date, SES_SECRET, true));
  87. $headers = array();
  88. $headers[] = '日付: ' 。 $date;
  89. $headers[] = 'ホスト: ' 。 SES_HOST;
  90. $auth = 'AWS3-HTTPS AWSAccessKeyId=' 。 SES_KEY;
  91. $auth .= ',アルゴリズム=HmacSHA256,署名=' 。 $sig;
  92. $headers[] = 'X-Amzn-Authorization: ' . $auth;
  93. $headers[] = 'Content-Type: application/x-www-form-urlencoded';
  94. return $headers;
  95. }
  96. /**
  97. * parse_data
  98. *
  99. * @parammixed $data
  100. * @access public
  101. * @return void
  102. */
  103. function parse_data(&$data ) {
  104. my_parse_str($data, $params);
  105. if (!empty($params)) {
  106. $redis = new Redis();
  107. $redis->connect(REDIS_HOST, REDIS_PORT);
  108. //多送信地址
  109. if (isset($params['Destination.ToAddresses.member.2'])) {
  110. $address = array();
  111. $mKey = uniqid();
  112. $i = 2;
  113. while (isset($params['Destination.ToAddresses.member.' . $i])) {
  114. $aKey = uniqid();
  115. $key = 'Destination.ToAddresses.member.' 。 $i;
  116. $address[$aKey] = $params[$key];
  117. unset($params[$key]);
  118. $i ++;
  119. }
  120. $data = my_build_query($params);
  121. unset($params['Destination.ToAddresses.member.1']);
  122. $redis->set('m:' . $mKey, my_build_query($params));
  123. foreach ($address as $k = > $a) {
  124. $redis->hSet('a:' . $mKey, $k, $a);
  125. $redis->lPush('mail', $k . '|' . $mKey );
  126. }
  127. }
  128. }
  129. }
复制發

parse_data 関数が 2 番目の受信者から開始され、それらを個別のメールにまとめて、他の独立したプロセスが読み取って送信できるように Redis キューに入れていることがわかります。

最初の受信者から始めてみませんか?

元のプロトコルと互換性がある必要があるため、クライアントがメールリクエストを送信するときは、常に何かを返す必要があるため、最初の受信者のメールリクエストが直接送信されます。はキューに入らないので、実際の SES サーバーのレシートを取得してクライアントに返すことができ、クライアント コードは変更を加えずにこの戻りを処理できます。

SES メールに署名が必要な場合はどうすればよいですか?

はい、すべての SES メールには署名が必要です。したがって、解凍すると電子メールのデータが変更されるため、署名も変更する必要があります。 my_build_query 関数はこれを実行し、リクエスト パラメーターに再署名します。

以下は、このプロキシ システムの最後のコンポーネントであるメール送信キューの実装です。これも php ファイルであり、nohup php コマンドを使用して、独自のクォータ サイズに従ってバックグラウンドで複数の php プロセスを開始し、同時メールを実現できます。送信。その構造も非常にシンプルで、キュー内のメールを読んでから、curl を使用してリクエストを送信します

  1. include __DIR__ . '/includes.php';
  2. $redis = new Redis();
  3. $redis->connect(REDIS_HOST, REDIS_PORT);
  4. do {
  5. $pop = $redis->brPop('mail', 10);
  6. if (empty($pop)) {
  7. continue;
  8. }
  9. list ($k, $id) = $pop;
  10. list($aKey, $mKey) =explode('|', $id);
  11. $address = $redis->hGet('a:' . $mKey, $aKey);
  12. if (empty($address)) {
  13. continue ;
  14. }
  15. $data = $redis->get('m:' . $mKey);
  16. if (empty($data)) {
  17. continue;
  18. }
  19. my_parse_str($data, $params);
  20. $params['Destination.ToAddresses.member.1'] = $address;
  21. $data = my_build_query($params);
  22. $headers = my_headers();
  23. $url = 'https://' 。 /';
  24. $ch =curl_init();
  25. curl_setopt($ch, CURLOPT_USERAGENT, 'SimpleEmailService/php');
  26. curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
  27. curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
  28. curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
  29. curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  30. curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
  31. curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
  32. curl _setopt ($ch, CURLOPT_URL, $url);
  33. curl_setopt($ch, CURLOPT_TIMEOUT, 10);
  34. curl_exec($ch);
  35. curl_close($ch);
  36. unset($ch);
  37. unset($data) ;
  38. } while (true);
コードをコピー

上記は、SES メール プロキシ サーバーの作成に関する私のアイデア全体です。ぜひ一緒に議論してください。

プロキシサーバー、PHP、Amazon


声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。