まいける's Tech Blog

LAMP関係のメモなどを不定期に掲載します

中国方面からの Bittorrent 攻撃を防ぐ

 運用しているサーバ宛に

xxx.xxx.xxx.xxx - - [18/May/2015:03:18:27 +0900] "GET /announce/?info_hash=(以下略) HTTP/1.0" 404 371 "-" "Bittorrent"

こんな感じのログが大量に残されていました。見る人が見ればわかるように、Bittrorrent によるアクセスです。

 当然のことながら、運用しているサーバでは Bittorrent を立ち上げていません。あまりに多くのホストから大量のアクセスを受けるので、原因を調べてみたところ、中国の DNS サーバが(わざと?)間違った IP アドレスを返しているようで、その間違えて返す先がうちのサーバになっている様子。

 Fail2Ban を使って頻繁にやってくるホストについては、アクセスを遮断していたのですが、減りそうな気配もないため、さらに調べると、Bittorrent の中の人が、404(Not Found)を返すより、410(Gone)を返したほうが早く収束すると書いていたので(A Note on the DDoS Attacks)、それに従って

<Location ~ "^/announc">
ErrorDocument 410 "d14:failure reason13:not a tracker8:retry in5:nevere"
</Location>

を追加したのですが、テストしても 404 を返すばかり。
 実はこのディレクティブ、410 のエラーになったときのメッセージを変更するだけで、ステータスコードを変更する指定をしていないのです。

 なので、

RewriteEngine On
RewriteRule ^/announc - [G]

<Location ~ "^/announc">
ErrorDocument 410 "d14:failure reason13:not a tracker8:retry in5:nevere"
</Location>

のように、最初に mod_rewrite を使って指定の URL に対するアクセスに対して 410 を返したうえで、ErrorDocument の指定をする必要があります。

PHP で Google Analytics API(Service accounts版)

 Google APIs Client Library for PHP(v1.0.6beta)を使って Google Analytics API(v3) からデータを取得する方法について覚え書き。今回はサーバサイドでの処理を想定して、OAuth のクライアント種別を Service accounts にしています。ゴールは、このサンプルと同様の結果を得ることです。

 以下、コードです。

<?php

require_once 'vendor/autoload.php'; // composer の autoload.phpの場所を指定

$client_id = '(OAuth のクライアント ID を記入)';
$service_account_name = '(OAuth のメールアドレスを記入)'; //Email Address
$key_file_location = 'key.p12'; // OAuth のクライアント ID を作成したときに生成された p12 ファイルをアップロードし、その場所を指定

$client = new Google_Client();
$client->setApplicationName("Analytics_API_Examples");

$client->setClientId($client_id);

$key = file_get_contents($key_file_location);

$cred = new Google_Auth_AssertionCredentials (
    $service_account_name,
    array('https://www.googleapis.com/auth/analytics'),
    $key
);

$client->setAssertionCredentials($cred);

// 以下は上記のサンプルと同内容(Typo のみ修正)

$analytics = new Google_Service_Analytics($client);

runMainDemo($analytics);

function runMainDemo(&$analytics) {
    try {

        // Step 2. Get the user's first view (profile) ID.
        $profileId = getFirstProfileId($analytics);

        if (isset($profileId)) {

            // Step 3. Query the Core Reporting API.
            $results = getResults($analytics, $profileId);

            // Step 4. Output the results.
            printResults($results);
        }

    } catch (apiServiceException $e) {
        // Error from the API.
        print 'There was an API error : ' . $e->getCode() . ' : ' . $e->getMessage();

    } catch (Exception $e) {
        print 'There was a general error : ' . $e->getMessage();
    }
}

function getFirstprofileId(&$analytics) {
    $accounts = $analytics->management_accounts->listManagementAccounts();

    if (count($accounts->getItems()) > 0) {
        $items = $accounts->getItems();
        $firstAccountId = $items[0]->getId();

        $webproperties = $analytics->management_webproperties->listManagementWebproperties($firstAccountId);

        if (count($webproperties->getItems()) > 0) {
            $items = $webproperties->getItems();
            $firstWebpropertyId = $items[0]->getId();

            $profiles = $analytics->management_profiles->listManagementProfiles($firstAccountId, $firstWebpropertyId);

            if (count($profiles->getItems()) > 0) {
                $items = $profiles->getItems();
                return $items[0]->getId();

            } else {
                throw new Exception('No views (profiles) found for this user.');
            }
        } else {
          throw new Exception('No webproperties found for this user.');
        }
    } else {
        throw new Exception('No accounts found for this user.');
    }
}

function getResults(&$analytics, $profileId) {
    return $analytics->data_ga->get(
        'ga:' . $profileId,
        '2012-03-03',
        '2012-03-03',
        'ga:sessions');
}

function printResults(&$results) {
    if (count($results->getRows()) > 0) {
        $profileName = $results->getProfileInfo()->getProfileName();
        $rows = $results->getRows();
        $sessions = $rows[0][0];

        print "<p>First view (profile) found: $profileName</p>";
        print "<p>Total sessions: $sessions</p>";

    } else {
        print '<p>No results found.</p>';
    }
}


?>

こんな感じです。

最後にはまったのは、
There was a general error : Error calling GET https://www.googleapis.com/analytics/v3/management/accounts: (403) User does not have any Google Analytics account.
のエラー。OAuth のクライアント ID を作成するときに使用している Google アカウントに Analytics の閲覧権限を与えているのに…と悩んだのですが、OAuth のメールアドレス(上記コードの $service_account_name で指定したメールアドレス)に閲覧権限を与える必要があります。冷静に考えれば当たり前なのですが、気づきにくいかもしれません。

マルチドメインでSSLを1ドメインだけで利用しているときの注意ポイント

 複数ドメインを1つのIPアドレスで運用し、その中の1ドメインだけでSSLを使っているケースって多いと思うのですが、そのような場合に、SSLのデフォルト設定をしておかないと、大変なことになるというお話です。

 名前ベースのバーチャルホストを設定している場合

NameVirtualHost 192.51.100.57:80
NameVirtualHost 192.51.100.57:443

<VirtualHost 192.51.100.57:80>
    ServerName www.example.com
    DocumentRoot /var/www/www.example.com/htdocs
</VirtualHost>

<VirtualHost 192.51.100.57:443>
    ServerName www.example.com
    DocumentRoot /var/www/www.example.com/htdocs

    SSLEngine on
    SSLCertificateFile /etc/apache2/ssl/example.crt
    SSLCertificateKeyFile /etc/apache2/ssl/example.key
</VirtualHost>

<VirtualHost 192.51.100.57:80>
    ServerName www.example2.com
    DocumentRoot /var/www/www.example2.com/htdocs
</VirtualHost>

こんな感じで設定することが多いと思うのですが、この設定だけだと、https://www.example2.com/ をはじめとする未定義のホストにアクセスすると、/var/www/www.example.com/htdocs の内容が表示されてしまいます。これだと何かと都合が悪い(重複コンテンツになってしまいますし)ので、以下のように、デフォルトの設定を最初に加えておくことをおすすめします。

NameVirtualHost 192.51.100.57:80
NameVirtualHost 192.51.100.57:443

<VirtualHost 192.51.100.57:80>
    ServerName dummy
    CustomLog /var/log/apache2/access_log combined

    DocumentRoot /var/www/dummy
    <Directory /var/www/dummy>
        Options -Indexes
        Order allow,deny
        allow from all
    </Directory>
</VirtualHost>

<VirtualHost 192.51.100.57:443>
    ServerName dummy
    CustomLog /var/log/apache2/access_log combined

    DocumentRoot /var/www/dummy
    <Directory /var/www/dummy/>
        Options -Indexes
        Order allow,deny
        allow from all
    </Directory>
    SSLEngine on
    SSLCertificateFile /etc/apache2/ssl/example.crt
    SSLCertificateKeyFile /etc/apache2/ssl/example.key
</VirtualHost>

<VirtualHost 192.51.100.57:80>
    ServerName www.example.com
    DocumentRoot /var/www/www.example.com/htdocs
</VirtualHost>

<VirtualHost 192.51.100.57:443>
    ServerName www.example.com:443
    DocumentRoot /var/www/www.example.com/htdocs

    SSLEngine on
    SSLCertificateFile /etc/apache2/ssl/example.crt
    SSLCertificateKeyFile /etc/apache2/ssl/example.key
</VirtualHost>

<VirtualHost 192.51.100.57:80>
    ServerName www.example2.com
    DocumentRoot /var/www/www.example2.com/htdocs
</VirtualHost>

あとは、/var/www/dummy を作っておけばOKです。これで、https://www.example2.com/ にアクセスしてきた場合には、Not Found が返るようになります。Forbidden を返したければ

NameVirtualHost 192.51.100.57:80
NameVirtualHost 192.51.100.57:443

<VirtualHost 192.51.100.57:80>
    ServerName dummy
    <Location />
        Order deny,allow
        Deny from All
    </Location>
</VirtualHost>

<VirtualHost 192.51.100.57:443>
    ServerName dummy
    <Location />
        Order deny,allow
        Deny from All
    </Location>

    SSLEngine on
    SSLCertificateFile /etc/apache2/ssl/example.crt
    SSLCertificateKeyFile /etc/apache2/ssl/example.key
</VirtualHost>

以下略

こんな感じに。

 通常の80番ポートのほうは、取得したドメインの数だけホストの設定をすることが多いので、デフォルトの設定をしなくてもあまり問題が生じないのですが、443番のほうは逆に https を使わないホストについて設定することがほぼないと思います。それゆえにデフォルト設定が重要というわけです。
 ホストごとに設定ファイルを分けている場合(「001-example.com」みたいに)には、「000-default」のように、デフォルト設定が一番最初に読み込まれるようにファイル名を調整してください。