簡易ログ解析

はじめに

ウェブサービスをはじめとしたサーバを伴うアプリの運用では、ログの解析が必要となる場面が非常に多くある。
KPI などの集計の他、障害時対応で、限られた時間でアクセスログから目的のデータを抽出しなければならない、といったエンジニアとしての腕を試されるケースも多い。
本格的なログ解析はシェルなりスクリプト言語なりでプログラムを組めば良いが、ワンライナーでできる簡単なコマンドを備忘のためにまとめる。

ログといっても出力形式は多様なので、今回は以下のような、Web サーバ名、接続元 IP アドレス、日時、リクエストメソッドURI、ユーザーエージェントなど、一般的な内容のアクセスログを想定している。

sample.log.web-45 123.456.789.0 - - [25/Dec/2015:00:00:42 +0900] "POST /sampleapp/getuserdata/index?userid=012345167890 HTTP/1.1" 200 496 "-“ "SampleApp 1.0.0(iOS 8.2; iPhone7,2)" 21719

なお、実際の運用では bzip2 形式でアーカイブされたログを扱うといったことが多いが、この場合、cat は bzcat、grep は bzgrep に置き換えると、アーカイブを展開することなく同じ操作が行える。

ユースケース

サービス全体の PV 数

サービス全体の PV 数を求める場合、アクセスログの行数がそのまま PV 数となる。

$ cat sample.log | wc -l
42549685

  1. まず cat でログ全体の内容を出力する。
  2. "-l" オプション付きの wc に繋いで、行数を出力する。

特定の API に対する PV 数

$ grep -c "updateuserdata/index" sample.log
2152947

  1. 特定の API に対する PV 数の場合、cat の代わりに grep を使うことで、検索文字列に指定した URI に対する PV 数を抽出する。grep は “-c” オプションで一致した行数を取得することができるので、wc に繋ぐ必要はない。

特定の API に対するユニーク PV 数

特定の API に対する PV 数の内でも、ユニークユーザーが何人かといった場合に使う。

$ grep "updateuserdata/index" sample.log | awk '{ print $8 }' | sort | uniq | wc -l
1311529

  1. 特定の API に対する PV 数同様、grepURI 文字列にマッチする行を抽出する。ただし、今回は一致した行の内容 (userid) を調べなければならないため、”-c” は外しておく。
  2. ユニーク条件は "userid" なので、awk を使って userid が含まれる 8 番目 ($8) の内容のみを切り出す。これによって、”/sampleapp/getuserdata/index?userid=012345167890” が結果として取得される。ここでは userid 以外の文字列も含まれているが、userid 以前の文字列に変化はないため、このままこの結果をユニーク条件として使うことができる。
  3. 次に uniq を使って重複を取り除いた行を取り出すが、uniq コマンドはそのまま使うと離れた行の結果を正しく処理できない。そこで、先に sort を通して同一の結果行を連続させてから uniq を行う。
  4. 最後に wc に繋いで行数を出力する。

1時間ごとの PV 数を抽出

各時間帯における1時間当たりの PV 数を得る場合に使う。

$ cat sample.log | awk '{ print substr($5, 14, 2) ":00" }' | sort | uniq -c
99827 00:00
89332 01:00
.
.

  1. まず、cat で全行を出力する。
  2. 次に awk を使ってアクセス日時 ($5) を切り出す。さらに、必要なのは 00 〜 23 時の時間単位の指定箇所のみとなるため、この awk の時点で substr 関数を使って、文字列の切り出しを行う。出力フォーマットの整形のために、”:00” を一緒に print している。なお、substr は、オフセットに負値を指定して文字列末尾から範囲を指定する、といったことができないので、注意が必要。
  3. あとは、ユニーク数の出力となるので、最初に sort を行ってから uniq へと繋ぐ。uniq は "-c" オプションを使うことで、各行の重複行数を出すことができるため、これを利用してそのまま PV 数とする。

アクセス端末のランキングを作成

API にアクセスした端末やブラウザのシェアを出す時に使う。

$ cat sample.log | awk 'sub(/\)\"/, "", $16) { print $16 }' | sort | uniq -c | sort -r
52731 iPhone8,1
37480 iPhone5,2
32282 iPhone7,1
22477 iPhone5,3
19529 iPhone7,2
17290 SO-02G
15576 SO-01G
15541 SH-02G
15088 SO-04E
.
.

  1. まず、cat で全行を出力する。
  2. 次に awk を使って、端末情報が含まれる UserAgent 部分 ($16) を切り出す。ここで結果をそのまま print すると、iPhone7,2)” といった閉じ括弧とダブルクォーテーションが含まれた文字列が出力される。これを消すために、sub 関数を使って文字列の置換削除を行っている。sub 関数は正規表現による文字列抽出・置換が可能となっており、sedにパイプしなくても置換が実現できる。
  3. 端末名のみの行が出力されるので、sort => uniq と繋ぐ。uniq は、 "-c" オプションをつけて各行の重複行数を出力する。
  4. "uniq -c" による重複行数表示は行頭につくため、これを利用し、"-r" (リバースオプション) をつけた sort を行うことで、重複行数の多い端末からソートしてそのままランキング表示を行うことができる。ここでは sort を2回使うことになるが、1回目は正しく uniq の結果を得るための sort、2回目はランキング作成のための sort である。

NSTextViewの末尾に文字を追加する方法

NSTextViewに文字列を追加しようと調べると、

func insertText(_ insertString: AnyObject)

が見つかる。
だが、このメソッドOS X v10.11 で deprecated となっていて、使うべきではない。

NSTextInputClient

NSTextViewのリファレンスを見ると、NSTextViewはNSTextInputClientプロトコルに適応 (conform) している。

このプロトコルに定義されている、

func insertText(_ aString: AnyObject, replacementRange replacementRange: NSRange)

が代わりに使えそうだ。

ここで

textView.insertText("追加する文字列", replacementRange: NSRange())

としてやると、文字列が追加できる。だが、NSRangeの内容を指定していないため、この場合先頭に文字列が追加される。

NSRangeの実体は以下の定義の構造体である。

typedef struct _NSRange {
      NSUInteger location;
      NSUInteger length;
} NSRange;

そこで、

textView.insertText("追加する文字列", replacementRange: NSMakeRange(-1, 0))

としてやることで、末尾に文字列を追加することができた。

SwiftでC++のコードを使う方法

SwiftC++のコードを使う場合、Objective-C++でラップして呼び出す必要がある。具体的には以下の手順が必要となる。

  1. C++ファイル、C++ヘッダファイルを作成
  2. C++をラップするObjective-C++ファイル、Objective-C++ヘッダファイルを作成
  3. ObjectiveC++ブリッジファイルでObjective-C++ヘッダファイルをインポート
  4. SwiftからObjective-C++のラップ関数を呼び出す

C++ファイルの作成

まずは、New File...からC++ファイルを作成する。この時、"Also create a header file" を選択すると、ヘッダファイルも同時に作成される。
また、C++ファイルの初回作成時には、ブリッジヘッダファイルも同時に作成してくれる。

今回はSocketControllerというクラスを作って、testメソッドを実装する。

SocketControllerCpp.hpp

#ifndef SocketControllerCpp_hpp
#define SocketControllerCpp_hpp

#include <stdio.h>

class SocketControllerCpp {
public:
    void test(void);
};

#endif /* SocketControllerCpp_hpp */

SocketControllerCpp.cpp

#include "SocketControllerCpp.hpp"

void SocketControllerCpp::test(void)
{
    printf("Hello C++");
}

Objective-C++ファイルの作成

次に、C++のコードをラップするObjective-C++ファイルを作成する。
同じく、NewFile...からC++ファイルを作成する。
ヘッダファイルはSocketControllerObjC.h、実装ファイルはSocketControllerObjC.mmとする。
ラップクラス内ではC++クラスをnewし、testメソッドを呼び出す。

SocketControllerObjC.h

#ifndef SocketControllerObjC_hpp
#define SocketControllerObjC_hpp

#import <Foundation/Foundation.h>

@interface SocketControllerObjC : NSObject
- (void) test;
@end

#endif /* SocketControllerObjC_hpp */

SocketControllerObjC.mm

#include "SocketControllerObjC.h"
#include "SocketControllerCpp.hpp"

@implementation SocketControllerObjC {
    SocketControllerCpp *socketController;
}

-(id)init {
    self = [super init];
    socketController = new SocketControllerCpp();
    return self;
}

-(void)dealloc {
    delete socketController;
    //[super dealloc];
}

-(void)test {
    socketController->test();
}
@end

Objective-C++ブリッジファイルの作成

ブリッジファイルは初回のC++ファイル作成時に一緒に作成されている。
ここではObjective-C++ファイルのインポートを行う。

プロジェクト名-Bridging-Header.h

#import "SocketControllerObjC.h"

Swiftからのメソッド呼び出し

あとは、SwiftからObjective-C++ラップクラスを呼び出すと、ラップしたメソッドを通してC++メソッドを使うことができる。
ViewControllerから呼び出した場合。

override func viewDidLoad() {
    super.viewDidLoad()
    let socketController = SocketControllerObjC();
    socketController.test();
}

スマホネイティブゲームのチート対策

はじめに

オンラインゲームの開発では、常にチート対策を考えなければならない。
特にスマホネイティブゲームの場合、クライアント解析、通信改竄、サーバ脆弱性への攻撃と、ブラウザゲームと比べてもアタックポイントの範囲が広く、常にチート、及びセキュリティリスクの可能性を意識してシステム設計、実装を行う必要がある。

ただ、現在はチートツールも広く出回っており、手法が多様化している。そのため、チート手法とその対策はイタチごっこの様相を呈しており、ここに必要以上の労力をつぎ込んでも、それに見合うリターンを得るのは難しいだろう。
そこで、チートを100%防ぐことは不可能であるという認識を持ち、あくまでもその認識を前提とした上で、何を守るべきなのか、どう対策するのかを考えるべきである。

守るべきもの

チートを100%防げないとはいえ、必ず守らなければいけないポイントがある。これを最重要ポイントとして位置づけ、ここには十分なリソースを投入すべきである。

個人情報漏洩

個人情報というと個人を特定できるような情報を指すが、それ以外にもユーザーのゲームデータ流出などは、著しく信頼を損なうため防がなければならない。特にAndroidにおいては、アプリ情報を他アプリから読み出されるようなリスクもあるため、注意が必要である。

ゲーム内順位への影響

個人的なパラメータ改竄やリリース前コンテンツの使用など、チートがユーザー個々の範囲に収まるものであれば、大きな影響は出ない。それに比べ、ランキングポイントの改竄や順位改竄といった、ゲームバランスの崩壊につながるチートは、ソーシャル要素が大きな役割を占めるスマホネイティブゲームでは致命的であり、ゲームそのものクローズにつながる危険もある。

売り上げへの影響

特にF2Pゲームは、アプリ内課金によって成り立っているため、当然課金システムのクラックや、課金販売しているゲーム内通貨の不正入手といったチートは防がなければならない。

チート対策の方針

カジュアルクラックを防ぐ

一口にチートといっても様々な種類や技術レベルがある。リバースエンジニアリングするような専門的技術を用いたチートは稀であり、ほとんどはネットで配布されているツールを使った、特に専門知識を必要としないチート、いわゆるカジュアルクラックである。
有名なゲームになると、そのゲーム専用に作られたツールも存在するが、一般的には PME (Process Memory Edit) などが出回っている。
これらのツールを使ったチートは対策も難しくなく、これによりほとんどのチートは防ぐことができるため、まず対策すべきものである。

チートされない設計を行う

冒頭で挙げたように、チートされるポイントは大きく分けて、以下の3つが考えられる。

  • クライアント解析
  • 通信改竄
  • サーバ脆弱性への攻撃

この内、サーバ脆弱性への攻撃については、破られた時のリスクが大きいものの、従来のオンラインゲームでの知見が活かせること、及びクライアントに比べリモート環境で実行難度が高いことから、比較的守りやすいと言える。

逆にクライアントはユーザーの手元で自由に手を加えられることから、メモリ解析、ロジック解析、リソース解析、プロセスアタッチと攻撃手法も多く、非常に守りづらい。

つまり、ゲームシステムの根幹に関わる重要なロジックをクライアント側に持たせることは非常に大きなリスクであり、やってはいけない。もちろん、全てのゲームロジックをサーバ側に持っていくことは、サーバリソース的な問題も発生することから現実的ではないが、必ず守らなければいけないポイントに関するロジックについては、必ずサーバ側で処理しなければならない。ガチャの判定をクライアントで行うなどもってのほかだ。

チートを検知する仕組みを導入する

前述の通り、チートを100%防ぐことはまずできない。その代わり、チートされた時にそれを検知する仕組みをサーバ側に導入することが重要になる。

具体的には以下の2点が重要である。

  • 異常なパラメータが送られてきた時にそれを検知すること (チートの検知)
  • パラメータ遷移についてのログを保存すること (チート手法の解析及びエビデンスの取得)

ログの保存については、サーバリソースの問題から全てのログの保存は難しいかもしれないが、重点的に守るべきポイントについては必ず取ることが重要である。

チートされない実装を行う

チートされない実装としては、最低限以下のような対策が必要である。

クライアント
  • Jailbreak / root 端末検知
  • メモリスクランブル
  • リソースデータ (主にマスタデータなど) の暗号化
  • リソースデータや設定の適切なパーミッション設定
  • 端末内データベースや設定ファイルへの重要データの平文保存回避
  • ソースコードへの重要データの保存回避
  • 不要なシンボル情報の削除、及び難読化
通信
  • 通信内容の暗号化、及びハッシュ付加 (SSLだけでは不十分)
  • 不要なメッセージ情報の削除 (エラーメッセージなど)
サーバ

Arduino Ethernet Shield 2

Arduino みたいなガジェットは、とにかくインプットとアウトプットの豊富さ、そしてその組み合わせによって実現できるユースケースが重要で、むしろそれが全てだと思う。
この点、インターネットへの接続というのは、入力においても出力においても最大のリソースであって 、それだけで世界が何倍にも広げられる。

というわけで、Arduino Ethernet Shield 2 を購入した。

Arduino イーサネットシールド2

Arduino イーサネットシールド2

導入手順

接続

まずは、Arduino Unoと接続する。シールドタイプの製品なので、そのままはめるだけ。
非常に簡単だが、Ethernet Shield の名の通り、当然有線の Ethernet ケーブルで接続する必要がある。最近は無線が当たり前の環境になってきたので、思わぬ落とし穴かも。
f:id:nao_bamboo:20151104010414j:plain

Arduino IDEのアップデート

次に Arduino IDE の最新版を導入する。
"Arduino IDE" とかで検索すると、以下のページに辿り着くが、ここにはなぜか旧バージョン (ver1.6.5) が表示される。
https://www.arduino.cc/en/Main/Software

正しくは、以下のページで、最新バージョンは ver1.7.7 (2015/11/04現在) だ。www.arduino.org

インストール完了。
f:id:nao_bamboo:20151104010842p:plain

ところで、インストール前に見たら、今まで 1.0.6 というのを使っていたみたいだが。。。

サンプルスケッチ

Ethernet Shield 2 を使ったサンプルスケッチは、"ファイル > スケッチの例 > Ethernet 2" にある。
いろいろなサンプルが用意されているが、今回はとりあえず WebServer を選ぶ。
なお、"スケッチ > ライブラリを使用 > Ethernet 2" というメニューもあるが、こちらはインクルードの宣言が記入されるだけで、サンプルスケッチではない。

Ethernet 2 ライブラリに関する説明は以下のページにあった。
Arduino | Labs | Ethernet 2 Library

WebServer

以下がサンプルコード

/*
  Web Server

 A simple web server that shows the value of the analog input pins.
 using an Arduino Wiznet Ethernet shield.

 Circuit:
 * Ethernet shield attached to pins 10, 11, 12, 13
 * Analog inputs attached to pins A0 through A5 (optional)

 created 18 Dec 2009
 by David A. Mellis
 modified 9 Apr 2012
 by Tom Igoe

 */

#include <SPI.h>
#include <Ethernet2.h>

// Enter a MAC address and IP address for your controller below.
// The IP address will be dependent on your local network:
byte mac[] = {
  0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED
};
IPAddress ip(192, 168, 1, 177);

// Initialize the Ethernet server library
// with the IP address and port you want to use
// (port 80 is default for HTTP):
EthernetServer server(80);

void setup() {
  // Open serial communications and wait for port to open:
  Serial.begin(9600);
  while (!Serial) {
    ; // wait for serial port to connect. Needed for Leonardo only
  }


  // start the Ethernet connection and the server:
  Ethernet.begin(mac, ip);
  server.begin();
  Serial.print("server is at ");
  Serial.println(Ethernet.localIP());
}


void loop() {
  // listen for incoming clients
  EthernetClient client = server.available();
  if (client) {
    Serial.println("new client");
    // an http request ends with a blank line
    boolean currentLineIsBlank = true;
    while (client.connected()) {
      if (client.available()) {
        char c = client.read();
        Serial.write(c);
        // if you've gotten to the end of the line (received a newline
        // character) and the line is blank, the http request has ended,
        // so you can send a reply
        if (c == '\n' && currentLineIsBlank) {
          // send a standard http response header
          client.println("HTTP/1.1 200 OK");
          client.println("Content-Type: text/html");
          client.println("Connection: close");  // the connection will be closed after completion of the response
          client.println("Refresh: 5");  // refresh the page automatically every 5 sec
          client.println();
          client.println("<!DOCTYPE HTML>");
          client.println("<html>");
          // output the value of each analog input pin
          for (int analogChannel = 0; analogChannel < 6; analogChannel++) {
            int sensorReading = analogRead(analogChannel);
            client.print("analog input ");
            client.print(analogChannel);
            client.print(" is ");
            client.print(sensorReading);
            client.println("<br />");
          }
          client.println("</html>");
          break;
        }
        if (c == '\n') {
          // you're starting a new line
          currentLineIsBlank = true;
        }
        else if (c != '\r') {
          // you've gotten a character on the current line
          currentLineIsBlank = false;
        }
      }
    }
    // give the web browser time to receive the data
    delay(1);
    // close the connection:
    client.stop();
    Serial.println("client disconnected");
  }
}

変更するのは2箇所。1つは、

byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED };

で、Ethernet Shield 2 本体に添付されているシールに書かれた MAC アドレスに書き換える。
もう一箇所は、

IPAddress ip(192, 168, 1, 177);

で、DHCP サーバから割り当てられたIPアドレスに書き換える。

内容はシンプル。クライアントからのリクエストを読んで、ステータスコードとレスポンスヘッダを返すようになっている。
その後のレスポンスボディで、Arduino のアナログインプットの値を返しているが、今は何も繋いでいないので、こちらは今は意味がない。

コンパイル & 転送したら早速アクセスしてみる。
以下が Paw (HTTPクライアント) からアクセスした結果。
f:id:nao_bamboo:20151104011940p:plain
想定通りのリクエスト & レスポンスとなっている。

Arduino のシリアルモニタにもログが残っている。
f:id:nao_bamboo:20151104012425p:plain

今後の展望

HTTP (だけではない) が話せるということは、独自にセンサーの値を返したり、外部サービスと連携ができるだけではなく、他のアプリケーションサーバとも連動ができるということ。
となれば、Arduino の豊富な入出力と高級的なプログラムが組み合わせられるので、できることは無限大に考えられる。
やりたいことは色々あるなあ。