読者です 読者をやめる 読者になる 読者になる

まったり技術ブログ

Technology is power.

『Goutte』を使って文字列からHTML解析

“通信を発生させずにHTMLを解析"をしたい・・・

 今までPHPでHTML解析するときは「Simple HTML DOM Parser」を利用しており、数年ぶりの利用で使い方を確認しているとこのような記事が。

localdisk.hatenablog.com

「Simple HTML DOM Parser」はパフォーマンス面に優れておらず、今では「Goutte」を使った方がいいとか。

github.com

いざ「Goutte」の使い方を簡単に調べてみると「Simple HTML DOM Parser」のようにHTML文字列を引数に与えて解析対象のオブジェクトを作成することができなさそう。 - Simple HTML DOM Parser

// Simple HTML DOM Parserを使った文字列からオブジェクトを生成
$html = str_get_html( '<html><body>Hello!</body></html>' );
  • Goutte
    オブジェクトを生成するためにはリクエスト通信を発生させないといけなさそう。
$client = new Client();
$crawler = $client->request('GET', 'https://www.symfony.com/blog/');
// 通信で取得したオブジェクトが解析対象になる

通信を発生させずにHTMLを解析!!

Goutteでもファイル内に記載されているHTMLを解析できそう。

ja.stackoverflow.com

下記のようにすると、通信を発生せずに引数に与えたHTML文字列を解析することができます。localhost:80に対して、通信を発生させているため、該当のサーバ上で80番ポートをListenさせている必要があります。

<?php
use Goutte\Client;

$client = new Client();
$crawler = $client->request('HEAD', null);
$crawler->clear();

// addHtmlContentの引数にHTML文字列
$crawler->addHtmlContent('<html><body>Hello!</body></html>');

echo $crawler->filter('html')->html();
  //=> <body>Hello!</body>
  
echo $crawler->filter('body')->text();
  //=> Hello!

『Laravel Collective』でのHTML生成を簡単にまとめてみる

f:id:motikan2010:20170128223017p:plain LaravelでRailsの"link_to"のようにHTMLを生成できないのかと思い、探してみたら『Laravel Collective』というものがありました。
導入方法などは下記を参照。

Laravel Collective

HTML出力のイメージができないところがありましたので、実際に出力させて、まとめてみました。

Formタグ

基本

Form::open([‘url’ => ‘home’])

自動的にCSRFトークンが追加されて出力されます。

<form method="POST" action="http://localhost:3000/home" accept-charset="UTF-8">
<input name="_token" type="hidden" value="eAFowzR2efsBNQYrVzQ4VymRbYQB2afVyEDijzqd">

Form::close()

</form>

送信先にルーティング名

Form::open([‘route’ => ‘users.index’])

action属性にルーティングを適用することができます。

<form method="POST" action="http://localhost:3000/users" accept-charset="UTF-8">
<input name="_token" type="hidden" value="eAFowzR2efsBNQYrVzQ4VymRbYQB2afVyEDijzqd">

送信先にURLパラメータ

Form::model($user, [‘route’ => [‘users.update’, $user->id]])

ユーザIDといったURLパラメータをaction属性に含めることもできます。

<form method="POST" action="http://localhost:3000/users/1" accept-charset="UTF-8">
<input name="_token" type="hidden" value="eAFowzR2efsBNQYrVzQ4VymRbYQB2afVyEDijzqd">

PUTメソッド

Form::open([‘url’ => ‘home’, ‘method’ => ‘put’])

メソッドを区別するため「name=“_method"」属性を持っているinputタグが一緒に出力されます。

<form method="POST" action="http://localhost:3000/home" accept-charset="UTF-8">
<input name="_method" type="hidden" value="PUT">
<input name="_token" type="hidden" value="eAFowzR2efsBNQYrVzQ4VymRbYQB2afVyEDijzqd">

マルチパートフォーム

Form::open([‘url’ => ‘home’, ‘files’ => true])

<form method="POST" action="http://localhost:3000/home" accept-charset="UTF-8" enctype="multipart/form-data">
<input name="_token" type="hidden" value="eAFowzR2efsBNQYrVzQ4VymRbYQB2afVyEDijzqd">

ラベル

Form::label(‘email’, ‘E-Mail Address’)

<label for="email">E-Mail Address</label>

入力フォーム

テキスト

Form::text(‘username’)

第1引数にname属性の値を指定します。

<input name="username" type="text">

Form::text(‘email’, ‘example@example.com’)

第2引数に初期値(value属性の値)を指定します。

<input name="email" type="text" value="example@example.com" id="email">

パスワード

Form::password(‘password’, [‘class’ => ‘awesome’])

<input class="awesome" name="password" type="password" value="">

メールアドレス

Form::email(‘mail’‘, $value = null, $attributes = [])

<input name="mail" type="email">

数値

Form::number(‘name’, ‘value’)

<input name="name" type="number" value="value">

日付

Form::date(‘name’, \Carbon\Carbon::now())

<input name="name" type="date" value="2017-01-28">

ファイル

Form::file(‘name’, $attributes = [])

<input name="name" type="file">

チェックボックス

Form::checkbox(‘name’, ‘value’)

<input checked="checked" name="name" type="checkbox" value="value">

ラジオボタン

Form::radio(‘name’, ‘value’)

<input name="name" type="radio" value="value">

セレクトボックス

基本

Form::select(‘size’, [‘L’ => ‘Large’, ’S' => ‘Small’])

<select name="size">
  <option value="L">Large</option>
  <option value="S">Small</option>
</select>

初期値を指定

Form::select(‘size’, [‘L’ => ‘Large’, ’S' => ‘Small’], ’S')

第3引数に初期値を指定することができます。

<select name="size">
  <option value="L">Large</option>
  <option value="S" selected="selected">Small</option>
</select>

空の初期値を指定

Form::select(‘size’, [‘L’ => ‘Large’, ’S' => ‘Small’], null, [‘placeholder’ => ‘Pick a size…’])

空の初期値を設定することもできます。

<select name="size">
  <option selected="selected" value="">Pick a size...</option>
  <option value="L">Large</option>
  <option value="S">Small</option>
</select>

multiple属性を付与

Form::select(‘size’, [‘L’ => ‘Large’, ’S' => ‘Small’], null, [‘multiple’ => true])

multiple属性を付与するし、複数選択することができます。

<select multiple="1" name="size">
  <option value="L">Large</option>
  <option value="S">Small</option>
</select>

選択肢をグループ化

Form::select(‘animal’, [ ‘Cats’ => [‘leopard’ => ‘Leopard’], ‘Dogs’ => [‘spaniel’ => ‘Spaniel’], ])

<select name="animal">
  <optgroup label="Cats">
    <option value="leopard">Leopard</option>
  </optgroup>
  <optgroup label="Dogs">
    <option value="spaniel">Spaniel</option>
  </optgroup>
</select>

連続した値の選択肢

Form::selectRange(‘number’, 10, 20)

10 〜 20の値を指定することができます。

<select name="number">
  <option value="10">10</option>
  <option value="11">11</option>
  <option value="12">12</option>
  <option value="13">13</option>
  <option value="14">14</option>
  <option value="15">15</option>
  <option value="16">16</option>
  <option value="17">17</option>
  <option value="18">18</option>
  <option value="19">19</option>
  <option value="20">20</option>
</select>

月の選択肢

Form::selectMonth(‘month’)

<select name="month">
  <option value="1">January</option>
  <option value="2">February</option>
  <option value="3">March</option>
  <option value="4">April</option>
  <option value="5">May</option>
  <option value="6">June</option>
  <option value="7">July</option>
  <option value="8">August</option>
  <option value="9">September</option>
  <option value="10">October</option>
  <option value="11">November</option>
  <option value="12">December</option>
</select>

ボタン

Form::submit(‘Click Me!’)

<input type="submit" value="Click Me!">

オリジナルのフィールド

Form::myField()

フィールドを作成し、任意の名前で定義するこができます。

Form::macro('myField', function()
{
    return '<input type="awesome">';
});
<input type="awesome">

リンク

基本

<a href="http://localhost:3000/foo/bar">http://localhost:3000/foo/bar</a>

リンク文字列を指定

<a href="http://localhost:3000/home">Home</a>

リンク先をHTTPS

<a href="https://localhost:3000/foo/bar">https://localhost:3000/foo/bar</a>
<a href="http://localhost:3000/foo/bar.zip">http://localhost:3000/foo/bar.zips</a>

リンク先にルーティング名を使用

<a href="http://localhost:3000/login">http://localhost:3000/login</a>

URLパラメータを付与

“$parameters"引数に配列を与えることで、リンク先にURLパラメータを付与することができます。

<a href="http://localhost:3000/login?id=1&name=motikan2010">http://localhost:3000/login?id=1&name=motikan2010</a>

リンク先に"コントローラ@アクション"形式

<a href="http://localhost:3000/login">http://localhost:3000/login</a>

ScaffoldとAuthを使ってアプリケーションを作る

f:id:motikan2010:20170124011356p:plain
PHP製のWebフレームワークである『Laravel』を使って"認証あり"のアプリケーションを作成していきます。
今回はLaravelバージョン5.3系を使います。

プロジェクトの作成

$ laravel new auth_scaffold
$ cd auth_scaffold
$ php artisan --version
Laravel Framework version 5.3.29

Scaffoldの導入

github.com

下記のコマンドで容易に導入することができます。

$ composer require 'laralib/l5scaffold' --dev

サービスプロバイダーの追加

“config/app.php"内の"providers"に対して、
「Laralib\L5scaffold\GeneratorsServiceProvider::class」を追加します。
$ vim config/app.php

<?php
(省略)
    'providers' => [

        /*
         * Laravel Framework Service Providers...
         */
        Illuminate\Auth\AuthServiceProvider::class,
        Illuminate\Broadcasting\BroadcastServiceProvider::class,
(省略)
        Laralib\L5scaffold\GeneratorsServiceProvider::class,
    ],
(以下省略)

上記の追加が完了したら、Scaffoldの準備完了です。

scaffoldの実行

下記のコマンドで コントローラ・モデル・ビュー の作成が完了します。
相変わらず便利すぎ!

$ php artisan make:scaffold Tweet --schema="title:string:default('Tweet #1'), body:text"

Configuring Tweet...
Migration created successfully
Seed created successfully.
Model created successfully.
Controller created successfully.
Layout created successfully.
Error created successfully.
Views created successfully.
Dump-autoload...
Route::resource("tweets","TweetController"); // Add this line in routes.php

予定通りに作成されたのかを確認してみます。

// コントローラ
$ ls app/Http/Controllers/TweetController.php
app/Http/Controllers/TweetController.php

// モデル
$ ls app/Tweet.php
app/Tweet.php

// ビュー
$ ls resources/views/tweets/
create.blade.php  edit.blade.php  index.blade.php  show.blade.php

しっかりと作成されているようです。
相変わらず早い!!

ルーティングの設定

作成されたコントローラを呼び出せないと意味がありませんので、 ルーティングの設定を行っていきます。
“routes/web.php"の末尾に「Route::resource("tweets”,“TweetController”);」を記述します。
$ vim routes/web.php

<?php
(省略)

Route::get('/', function () {
    return view('welcome');
});

Route::resource("tweets","TweetController"); // 追記

ルーティングの設定が反映されているか確認してみます。

$ php artisan route:list

+--------+-----------+---------------------+----------------+----------------------------------------------+--------------+
| Domain | Method    | URI                 | Name           | Action                                       | Middleware   |
+--------+-----------+---------------------+----------------+----------------------------------------------+--------------+
|        | GET|HEAD  | /                   |                | Closure                                      | web          |
|        | GET|HEAD  | api/user            |                | Closure                                      | api,auth:api |
|        | POST      | tweets              | tweets.store   | App\Http\Controllers\TweetController@store   | web          |
|        | GET|HEAD  | tweets              | tweets.index   | App\Http\Controllers\TweetController@index   | web          |
|        | GET|HEAD  | tweets/create       | tweets.create  | App\Http\Controllers\TweetController@create  | web          |
|        | DELETE    | tweets/{tweet}      | tweets.destroy | App\Http\Controllers\TweetController@destroy | web          |
|        | PUT|PATCH | tweets/{tweet}      | tweets.update  | App\Http\Controllers\TweetController@update  | web          |
|        | GET|HEAD  | tweets/{tweet}      | tweets.show    | App\Http\Controllers\TweetController@show    | web          |
|        | GET|HEAD  | tweets/{tweet}/edit | tweets.edit    | App\Http\Controllers\TweetController@edit    | web          |
+--------+-----------+---------------------+----------------+----------------------------------------------+--------------+

データベースの作成

ここの説明は簡単に流します。
データベースの名前は「scaffold_app」にしておきます。 mysql> create database scaffold_app; データベースの設定は下記のファイルに行います。 - ユーザ名:laravelusr - パスワード:himitsu
にしています。
$ vim .env

(省略)

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=scaffold_app
DB_USERNAME=laravelusr
DB_PASSWORD=himitsu

(省略)

上記設定が完了したらテーブルの作成を行います。

マイグレーション

下記のコマンドを実行することによって、テーブルが作成されます。
ここでは、tweetsテーブル以外にも作成されていますが、無視しておきます。

$ php artisan migrate

Migration table created successfully.
Migrated: 2014_10_12_000000_create_users_table
Migrated: 2014_10_12_100000_create_password_resets_table
Migrated: 2017_01_23_143819_create_tweets_table

Webアプリケーションへアクセス

これで、Webアプリケーションとして利用することが可能になりました。

$ php artisan serve --host 0.0.0.0 --port 3000

http://127.0.0.1:3000/tweets"にアクセス f:id:motikan2010:20170124004456p:plain
上記のような画面が表示されたら成功です。
試しになにか投稿してみましょう。「Create」を押して投稿画面へ遷移できます。
f:id:motikan2010:20170124004459p:plain
投稿することができました。
「View」を押すことで詳細画面へ遷移することもできます。

f:id:motikan2010:20170124004502p:plain

これはこれで良いアプリケーションとなっていますが、これから認証機能を実装してきます。

認証機能(Auth)の導入

魔法の呪文『make:auth』

下記のコマンドを実行します。
$ php artisan make:auth
認証に必要な コントローラ・モデル・ビュー の作成が完了します。
Scaffold同様、作成されているかを確認します。

// コントローラ
$ ls app/Http/Controllers/Auth/
ForgotPasswordController.php  LoginController.php  RegisterController.php  ResetPasswordController.php

// モデル
$ ls app/User.php
app/User.php

// ビュー
$ ls resources/views/auth/
login.blade.php     passwords/      register.blade.php

作成されているのが確認できます。
次にルーティングの設定ですが、Authの場合は自動的に記述されます。
念のために確認してみます。
$ cat routes/web.php

<?php
(省略)

Route::get('/', function () {
    return view('welcome');
});

Route::resource("tweets","TweetController");

Auth::routes();    // 自動的に追加されている

Route::get('/home', 'HomeController@index');

ルーティングが反映されているか改めて確認してみます。

$ php artisan route:list

+--------+-----------+------------------------+----------------+------------------------------------------------------------------------+--------------+
| Domain | Method    | URI                    | Name           | Action                                                                 | Middleware   |
+--------+-----------+------------------------+----------------+------------------------------------------------------------------------+--------------+
|        | GET|HEAD  | /                      |                | Closure                                                                | web          |
|        | GET|HEAD  | api/user               |                | Closure                                                                | api,auth:api |
|        | GET|HEAD  | home                   |                | App\Http\Controllers\HomeController@index                              | web,auth     |
|        | POST      | login                  |                | App\Http\Controllers\Auth\LoginController@login                        | web,guest    |
|        | GET|HEAD  | login                  | login          | App\Http\Controllers\Auth\LoginController@showLoginForm                | web,guest    |
|        | POST      | logout                 | logout         | App\Http\Controllers\Auth\LoginController@logout                       | web          |
|        | POST      | password/email         |                | App\Http\Controllers\Auth\ForgotPasswordController@sendResetLinkEmail  | web,guest    |
|        | GET|HEAD  | password/reset         |                | App\Http\Controllers\Auth\ForgotPasswordController@showLinkRequestForm | web,guest    |
|        | POST      | password/reset         |                | App\Http\Controllers\Auth\ResetPasswordController@reset                | web,guest    |
|        | GET|HEAD  | password/reset/{token} |                | App\Http\Controllers\Auth\ResetPasswordController@showResetForm        | web,guest    |
|        | POST      | register               |                | App\Http\Controllers\Auth\RegisterController@register                  | web,guest    |
|        | GET|HEAD  | register               | register       | App\Http\Controllers\Auth\RegisterController@showRegistrationForm      | web,guest    |
|        | GET|HEAD  | tweets                 | tweets.index   | App\Http\Controllers\TweetController@index                             | web          |
(省略)
|        | GET|HEAD  | tweets/{tweet}/edit    | tweets.edit    | App\Http\Controllers\TweetController@edit                              | web          |
+--------+-----------+------------------------+----------------+------------------------------------------------------------------------+--------------+

問題なく認証機能へアクセスできそうです。

認証の確認

認証を行っていないユーザはログイン画面へ遷移させるようにしましょう。 $ vim app/Http/Controllers/TweetController.php

<?php namespace App\Http\Controllers;

use App\Http\Requests;
use App\Http\Controllers\Controller;

use App\Tweet;
use Illuminate\Http\Request;

class TweetController extends Controller {

        public function __construct(){
            $this->middleware('auth');
        }
(省略)

これで認証を行っていないユーザはTweetControllerのアクションへアクセスできないようになります。

認証後の遷移先を変更

実は認証に必要なアカウントの「登録後」や「認証後」には"/home"へ遷移されるように、Authのコントローラで設定されています。 “/home"には特になにもアプリケーションがありませんので、そこへ遷移されても楽しくありません。  事前に「登録後」や「認証後」には”/tweets"へ遷移されるように設定します。
行う作業は非常に簡単です。
下記2つのファイルを修正するだけです。
$ vim app/Http/Controllers/Auth/RegisterController.php

<?php

namespace App\Http\Controllers\Auth;

use App\User;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Validator;
use Illuminate\Foundation\Auth\RegistersUsers;

class RegisterController extends Controller
{

(省略)

    protected $redirectTo = '/tweets'; // "home" を "tweets" に変更
(省略)

$ vim app/Http/Controllers/Auth/LoginController.php

<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\AuthenticatesUsers;

class LoginController extends Controller
{

(省略)
    
    protected $redirectTo = '/tweets'; // "home" を "tweets" に変更
(省略)

これで完成です。

アプリケーションを確認

下記のコマンドでアプリケーションを起動しましょう。
$ php artisan serve –host 0.0.0.0 –port 3000

http://127.0.0.1:3000/」にアクセスすると下記のような画面が表示されます。
f:id:motikan2010:20170124004509p:plain
ためしに「http://127.0.0.1:3000/tweets」に直接アクセスしてみましょう。
当然ながら認証を行っていませんので、ログイン画面へ遷移されますf:id:motikan2010:20170124004523p:plain
まだ、ユーザは存在していないので「Register」からユーザの作成を行ってみます。 f:id:motikan2010:20170124004517p:plain
ユーザの作成が完了すると、認証処理が行われTweets画面へ遷移されます。 f:id:motikan2010:20170124004526p:plain

こんな短時間でこのようなアプリケーションが作成できるとは驚きです。
今回は誰でもがアカウントを作成することができ、認証後のページにアクセスすることが可能となっていますが、コードをいじってみることで自分だけがアクセスできるサイトを作れそうですね。

golang製ベンチマークツール『hey』を使ってみた

f:id:motikan2010:20170117010150p:plain Web界隈では「Apache Bench」「JMeter」などのベンチマークツールが有名ですが、ここではGo言語で開発されたベンチマークツール『hey』を紹介します。

github.com

百聞は一見に如かず「hey」!!

結果は下記ような形式で出力されます。abと比べて非常にシンプルな結果表示になっています。
このツールの実装自体もシンプルで可読性が高く(さすがはGo)、カスタマイズしても良し、このツールを参考に自分自身でベンチマークツールを作成するのにも参考になるかと思います。

# ./hey -n 200 -c 50 http://127.0.0.1:3000/login
2 requests done.
10 requests done.
15 requests done.
21 requests done.
(省略)
192 requests done.
197 requests done.
All requests done.

Summary:
  Total:    19.2161 secs
  Slowest:  18.6380 secs
  Fastest:  0.1297 secs
  Average:  2.6002 secs
  Requests/sec: 10.4080
  Total data:   180532 bytes
  Size/request: 902 bytes

Status code distribution:
  [200] 199 responses
  [500] 1 responses

Response time histogram:
  0.130 [1]    |
  1.981 [155]  |∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎
  3.831 [5]    |∎
  5.682 [5]    |∎
  7.533 [5]    |∎
  9.384 [5]    |∎
  11.235 [4]   |∎
  13.086 [5]   |∎
  14.936 [6]   |∎∎
  16.787 [4]   |∎
  18.638 [5]   |∎

Latency distribution:
  10% in 0.3156 secs
  25% in 0.3908 secs
  50% in 0.4547 secs
  75% in 0.6077 secs
  90% in 11.2754 secs
  95% in 14.9333 secs
  99% in 18.3849 secs

インストール

Go言語が使える状態から始めます。動作するバージョンは1.7以上です。

$ go version
go version go1.7.4 linux/amd64

インストールは非常に簡単です。下記のコマンドを実行するだけです。

$ go install github.com/rakyll/hey

これでエラーが表示されなければheyコマンドが使用できるようになります。
どのようなオプションが用意されているのかを確認してみます。

$ hey
Usage: hey [options...] <url>

Options:
  -n  Number of requests to run. Default is 200.
  -c  Number of requests to run concurrently. Total number of requests cannot
      be smaller than the concurrency level. Default is 50.
  -q  Rate limit, in seconds (QPS).
  -o  Output type. If none provided, a summary is printed.
      "csv" is the only supported alternative. Dumps the response
      metrics in comma-separated values format.

  -m  HTTP method, one of GET, POST, PUT, DELETE, HEAD, OPTIONS.
  -H  Custom HTTP header. You can specify as many as needed by repeating the flag.
      For example, -H "Accept: text/html" -H "Content-Type: application/xml" .
  -t  Timeout for each request in seconds. Default is 20, use 0 for infinite.
  -A  HTTP Accept header.
  -d  HTTP request body.
  -D  HTTP request body from file. For example, /home/user/file.txt or ./file.txt.
  -T  Content-type, defaults to "text/html".
  -a  Basic authentication, username:password.
  -x  HTTP Proxy address as host:port.
  -h2 Enable HTTP/2.

  -host HTTP Host header.

  -disable-compression  Disable compression.
  -disable-keepalive    Disable keep-alive, prevents re-use of TCP
                        connections between different HTTP requests.
  -cpus                 Number of used cpu cores.
                        (default for current machine is 2 cores)
  -more                 Provides information on DNS lookup, dialup, request and
                        response timings.

動作確認

GETリクエスト

もっとも基本なGETリクエスト。下記のように実行します。

$ hey http://127.0.0.1:3000/

POSTリクエスト

  • HTTPメソッドをPOSTに指定。
  • リクエストヘッダに「Content-Type: application/x-www-form-urlencoded」を付与
  • もちろんPOSTパラメータも送ることができます。
    ちなみにURLエンコードをする必要はありませんでした。
$ hey -m POST -H "Content-Type: application/x-www-form-urlencoded" -d "email=example@example.com&password=foobar" http://127.0.0.1:3000/login

Basic認証

「-a [ユーザ名]:[パスワード]」をオプションに指定します。
ダブルクォーテーションはあってもなくても動作します。

$ hey -a "hogeuser:hogepassword" http://127.0.0.1/

使ってみて

最近Go言語にハマってしまいGo言語製のツールを探してみてこのツールにたどり着きましたが、やはりベンチーマークツールは楽しいですね。なによりズラズラと流れるアプリケーションログを眺めるのが非常に快感を覚えます。
まだまだ『hey』のオプションは全て使え切れていない。(今回は2つしか使っていないし...)
キープアライブやHTTP/2の知識を入れて是非使いこなしてみたいツールですね。

Webサーバベンチマークツール『weighttp』をさわってみる

Webサーバのベンチマークツールである"weighttp"のインストール・動作確認までを行っていきます。
私の環境ではPythonのバージョン3がデフォルトして動作しており、それが原因でつまずいた点がありましたので、記載しておきます。

github.com

インストール

$ yum install -y --enablerepo=epel libev libev-devel
$ wget http://github.com/lighttpd/weighttp/zipball/master
$ unzip master
$ cd lighttpd-weighttp-f680bec/

configure実行(失敗)

$ ./waf configure
Checking for program gcc,cc              : ok /usr/bin/gcc
Checking for program cpp                 : ok /usr/bin/cpp
Checking for program ar                  : ok /usr/bin/ar
Checking for program ranlib              : ok /usr/bin/ranlib
Checking for gcc                         : ok
Checking for library ev                  : ok
Traceback (most recent call last):
  File "/tmp/weighttp-master/.waf3-1.5.9-d1e0349fc8937631a656fb8ea7e99063/wafadmin/Utils.py", line 414, in recurse
    txt=readf(base+'_'+name,m='rU')
  File "/tmp/weighttp-master/.waf3-1.5.9-d1e0349fc8937631a656fb8ea7e99063/wafadmin/Utils.py", line 379, in readf
    f=open(fname,m)
FileNotFoundError: [Errno 2] No such file or directory: '/tmp/weighttp-master/wscript_configure'

"FileNotFoundError"が発生している。下のサイトで解決することができた。 バージョン2のPythonを使用すればいいらしい。

[solved] Can't Compile Hamster (Python app) / Applications & Desktop Environments / Arch Linux Forums

configure実行(失敗2度目・・・)

$ python2 ./waf configure

Checking for program gcc,cc              : ok /usr/bin/gcc
Checking for program cpp                 : ok /usr/bin/cpp
Checking for program ar                  : ok /usr/bin/ar
Checking for program ranlib              : ok /usr/bin/ranlib
Checking for gcc                         : ok
Checking for library ev                  : ok
Checking for header ev.h                 : not found
 error: the configuration failed (see '/tmp/weighttp-master/build/config.log')

"Checking for header ev.h : not found"と出力されているので、ヘッダファイルを移動させます。

ヘッダファイル"ev.h"を移動

$ cp /usr/include/libev/ev.h /usr/include/

configure実行(成功!!)

$ python2 ./waf configure

Checking for program gcc,cc              : ok /usr/bin/gcc
Checking for program cpp                 : ok /usr/bin/cpp
Checking for program ar                  : ok /usr/bin/ar
Checking for program ranlib              : ok /usr/bin/ranlib
Checking for gcc                         : ok
Checking for library ev                  : ok
Checking for header ev.h                 : ok
Checking for library pthread             : ok
Checking for header pthread.h            : ok
Checking for header unistd.h             : ok
Checking for header stdint.h             : ok
Checking for header fcntl.h              : ok
Checking for header inttypes.h           : ok
'configure' finished successfully (0.481s)

ビルド

$ python2 ./waf build

Waf: Entering directory `/tmp/weighttp-master/build'
[1/4] cc: src/client.c -> build/default/src/client_1.o
[2/4] cc: src/weighttp.c -> build/default/src/weighttp_1.o
[3/4] cc: src/worker.c -> build/default/src/worker_1.o
[4/4] cc_link: build/default/src/client_1.o build/default/src/weighttp_1.o build/default/src/worker_1.o -> build/default/weighttp
Waf: Leaving directory `/tmp/weighttp-master/build'
'build' finished successfully (0.754s)

インストール

$ python2 ./waf install

Waf: Entering directory `/tmp/weighttp-master/build'
* installing build/default/weighttp as /usr/local/bin/weighttp
Waf: Leaving directory `/tmp/weighttp-master/build'
'install' finished successfully (0.007s)

"weighttp"コマンドを実行

これで"weighttp"を利用することができるようになりました。 試しにヘルプコマンドを実行してみます。

$ weighttp --help
weighttp 0.4 - a lightweight and simple webserver benchmarking tool

error: unkown option: --

weighttp <options> <url>
  -n num   number of requests    (mandatory)
  -t num   threadcount           (default: 1)
  -c num   concurrent clients    (default: 1)
  -k       keep alive            (default: no)
  -6       use ipv6              (default: no)
  -H str   add header to request
  -h       show help and exit
  -v       show version and exit

example: weighttpd -n 10000 -c 10 -t 2 -k -H "User-Agent: foo" localhost/index.html

試してみる

helpコマンドで出力されたコマンド例通りに動かしてみます。

$ weighttp -n 10000 -c 10 -t 2 -k -H "User-Agent: foo" localhost/index.php

weighttp 0.4 - a lightweight and simple webserver benchmarking tool

starting benchmark...
spawning thread #1: 5 concurrent requests, 5000 total requests
spawning thread #2: 5 concurrent requests, 5000 total requests
progress:  10% done
progress:  20% done
progress:  30% done
progress:  40% done
progress:  50% done
progress:  60% done
progress:  70% done
progress:  80% done
progress:  90% done
progress: 100% done

finished in 5 sec, 34 millisec and 237 microsec, 1986 req/s, 387 kbyte/s
requests: 10000 total, 10000 started, 10000 done, 10000 succeeded, 0 failed, 0 errored
status codes: 10000 2xx, 0 3xx, 0 4xx, 0 5xx
traffic: 2000000 bytes total, 1920000 bytes http, 80000 bytes data

Webサーバログにも通信ログが残っておりしっかりとベンチマークを取得できていそう。
今までのは"Apache Bench"を使っていましたが、"weighttp"が代替ツールになるかこれからいろいろ検証していく予定。

キャプチャ認証『SimpleCaptcha2』を使ってみる

f:id:motikan2010:20170112012951p:plain
今回はBot等に自動認証を許さないために用いられる 「CAPTCHA」をRailsアプリケーションに実装してみます。
ImageMagickあたりでつまずいたので、備忘録感覚にメモ。

環境確認とアプリの作成

まずは動作環境の確認とサンプルサプリケーションの作成を行っていきます。
普及している4系を使っていきます。

$ cat /etc/redhat-release
CentOS release 6.8 (Final)
$ rails _4.2.0_ new app

「simple-captcha」の追加(失敗)

メインとなるキャプチャのライブラリですが、上位の人気を誇っている「simple-captcha」を入れてみました。

$ vim Gemfile

(追加)
gem 'simple_captcha', :git => 'git://github.com/galetahub/simple-captcha.git'

そして、いつも通りにインストールします。

# bundle install

simple-captchaのセットアップ

ここで問題が発生しました。

$ rails generate simple_captcha
Running via Spring preloader in process 11946
   identical  app/views/simple_captcha/_simple_captcha.erb
/opt/rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/bundler/gems/simple-captcha-2602bf19a63d/lib/generators/simple_captcha_generator.rb:18:in `create_migration': wrong number of arguments (given 3, expected 0) (ArgumentError)
  from /opt/rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/railties-4.2.0/lib/rails/generators/migration.rb:63:in `migration_template'
  from /opt/rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/bundler/gems/simple-captcha-2602bf19a63d/lib/generators/simple_captcha_generator.rb:19:in `create_migration'

https://github.com/pludoni/simple-captcha/issues/18 上記URL先を見てみると、新バージョンである『SimpleCaptcha2』を使えば解決できそうな予感。

『SimpleCaptcha2』のセットアップ

$ vim Gemfile
(追加)
gem 'simple_captcha2', git: 'https://github.com/pludoni/simple-captcha.git', require: true

そして再度インストール

$ bundle install

改めて「rails generate simple_captcha」コマンド

$ rails generate simple_captcha
Running via Spring preloader in process 1067
Expected string default value for '--helper'; got true (boolean)
Expected string default value for '--jbuilder'; got true (boolean)
      create  app/views/simple_captcha/_simple_captcha.erb
      create  db/migrate/20170112000239_create_simple_captcha_data.rb

様々なファイルが作成されたということで今回は成功したらしい。

db/migrate/に下記のようなマイグレーションファイルが作成されたことが確認できます。

$ cat db/migrate/20170112000239_create_simple_captcha_data.rb

class CreateSimpleCaptchaData < ActiveRecord::Migration
  def self.up
    create_table :simple_captcha_data do |t|
      t.string :key, :limit => 40
      t.string :value, :limit => 6
      t.timestamps
    end

    add_index :simple_captcha_data, :key, :name => "idx_key"
  end

  def self.down
    drop_table :simple_captcha_data
  end
end

下記コマンドでテーブルを作成します。

$ rake db:migrate
== 20170112000239 CreateSimpleCaptchaData: migrating ==========================
-- create_table(:simple_captcha_data)
   -> 0.0026s
-- add_index(:simple_captcha_data, :key, {:name=>"idx_key"})
   -> 0.0012s
== 20170112000239 CreateSimpleCaptchaData: migrated (0.0040s) =================

テーブルが正常に作成されたことを確認して、下記のようにapplication.rbコントローラに「include SimpleCaptcha::ControllerHelpers」の一行を追記します。

$ vim app/controllers/application.rb
 
class ApplicationController < ActionController::Base
  # Prevent CSRF attacks by raising an exception.
  # For APIs, you may want to use :null_session instead.
  protect_from_forgery with: :exception
  include SimpleCaptcha::ControllerHelpers
end

テストアプリを作成

ここでは簡単にRESTアプリを作成します。

# rails g scaffold Memo title:string description:text
# rake db:migrate

ビューを変更

下記のようにキャプチャ入力項目を追記します。

$ vim app/views/memos/_form.html.erb

(省略)
<div class="field">
    <%= f.label :description %><br>
    <%= f.text_area :description %>
  </div>
  <div class="field">
    <%= f.label "Simple-Captcha" %><br>
    <%= f.simple_captcha :label => "上の文字を記入してくだい", :placeholder => "ここに入力" %>
  </div>
  <div class="actions">
    <%= f.submit %>
  </div>
(省略)

ブラウザでキャプチャの確認

ここまで進めますと「http://localhost:3000/memos/new」にアクセスするとキャプチャ入力項目が表示されるはずですので、確認してみます。 f:id:motikan2010:20170112001609p:plain:w200
表示されていません・・・。
サーバ側では以下のようなエラーが表示されています。

StandardError (Error while running convert: convert: not authorized `ACTWH' @ error/constitute.c/ReadImage/453.
convert: missing an image filename `jpeg:-' @ error/convert.c/ConvertImageCommand/3015.
):
  /opt/rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/bundler/gems/simple-captcha-b145495ab9e5/lib/simple_captcha/utils.rb:17:in `run'
  /opt/rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/bundler/gems/simple-captcha-b145495ab9e5/lib/simple_captcha/image.rb:83:in `generate_simple_captcha_image'
  /opt/rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/bundler/gems/simple-captcha-b145495ab9e5/lib/simple_captcha/middleware.rb:42:in `make_image'
  /opt/rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/bundler/gems/simple-captcha-b145495ab9e5/lib/simple_captcha/middleware.rb:21:in `call'
(以下省略)

キャプチャ画像を表示

画像の加工処理がうまくいっていないようですので、その処理に必要なパッケーシをインストールします。

# yum -y install ghostscript
# yum -y install ImageMagick-devel

ここでブラウザで確認してみても画像が表示されていません。 http://www.srcw.net/wiki/index.php?SimpleCaptcha 調べてみるとImageMagick脆弱性があり、設定ポリシーが厳しくなりデフォルト設定だと表示されていないと推測。 その脆弱性に関する情報はこちらを参照 ImageMagickの脆弱性(CVE-2016-3714他)についてまとめてみた - piyolog

その設定ファイルである「ポリシーファイル」を以下のように編集してみます。

$ vim /etc/ImageMagick/policy.xml

(省略)
<policymap>
  <!-- <policy domain="system" name="precision" value="6"/> -->
  <!-- <policy domain="resource" name="temporary-path" value="/tmp"/> -->
  <!-- <policy domain="resource" name="memory" value="2GiB"/> -->
  <!-- <policy domain="resource" name="map" value="4GiB"/> -->
  <!-- <policy domain="resource" name="area" value="1gb"/> -->
  <!-- <policy domain="resource" name="disk" value="16eb"/> -->
  <!-- <policy domain="resource" name="file" value="768"/> -->
  <!-- <policy domain="resource" name="thread" value="4"/> -->
  <!-- <policy domain="resource" name="throttle" value="0"/> -->
  <!-- <policy domain="resource" name="time" value="3600"/> -->
<!--
  <policy domain="coder" rights="none" pattern="EPHEMERAL" />
  <policy domain="coder" rights="none" pattern="HTTPS" />
  <policy domain="coder" rights="none" pattern="HTTP" />
  <policy domain="coder" rights="none" pattern="URL" />
  <policy domain="coder" rights="none" pattern="FTP" />
  <policy domain="coder" rights="none" pattern="MVG" />
  <policy domain="coder" rights="none" pattern="MSL" />
  <policy domain="coder" rights="none" pattern="TEXT" />
  <policy domain="coder" rights="none" pattern="LABEL" />
  <policy domain="path" rights="none" pattern="@*" />
-->
</policymap>

編集を終えたら再度ブラウザでアクセスし確認します。 f:id:motikan2010:20170112001211p:plain:w200
今度はうまく表示できているようです。 ですが肝心なキャプチャをデタラメな値を入力しても作成できるようになっています。
正常に動作させるためにモデルとコントローラの編集を行います。

正常に動作させるためモデルとコントローラを編集

モデル

「attr_accessor :captcha_key, :captcha」の一行を追記します。

$ vim app/models/memo.rb

class Memo < ActiveRecord::Base
  attr_accessor :captcha_key, :captcha
end

コントローラ

末尾にあるmemo_params関数の中身を以下のように編集します。
(, :captcha_key, :captchaを追加)

$ vim app/controllers/memos_controller.rb

(省略)
    def memo_params
        params.require(:memo).permit(:title, :description, :captcha_key, :captcha)
    end
end

同じコントローラファイルに入力されたキャプチャ文字列が正しいかの検証処理をcreateメソッド内に記述します。 以下のように編集します。

$ vim app/controllers/memos_controller.rb

  # POST /memos
  # POST /memos.json
  def create
    @memo = Memo.new(memo_params)

    if simple_captcha_valid?
      respond_to do |format|
        if @memo.save
          format.html { redirect_to @memo, notice: 'Memo was successfully created.' }
          format.json { render :show, status: :created, location: @memo }
        else
          format.html { render :new }
          format.json { render json: @memo.errors, status: :unprocessable_entity }
        end
      end
    else
        redirect_to :action => "new"
    end
  end

最後に入力された値の引き渡しがうまくいくように下記のコードを記述します。

$ vim config/environment.rb

module SimpleCaptcha
  module ControllerHelpers
    def simple_captcha_valid?
      return true if Rails.env.test?
      if params[:memo][:captcha]
        data = SimpleCaptcha::Utils::simple_captcha_value(params[:memo][:captcha_key] || session[:captcha])
        result = data == params[:memo][:captcha].delete(" ").upcase
        SimpleCaptcha::Utils::simple_captcha_passed!(session[:captcha]) if result
        return result
      else
        return false
      end
    end
  end
end

これで入力された文字列と画像に表示されている文字列の比較が行われ、等しい場合に正常な処理が行われるようになります。

参考サイト

joppot.info

PythonだけでHTTPSサーバ

Secure属性を付与したCookieの取り扱いなど簡単な検証で重宝しています。

HTTPS通信にしようするサーバ証明書の作成
# openssl req -new -x509 -keyout server.pem -out server.pem -days 365 -nodes
Webサーバ側のコード

ここではWebフレームワークのbottleを使用します。

import sys, os, datetime
from bottle import route, run, request, HTTPResponse, ServerAdapter

class SSLWebServer(ServerAdapter):

def run(self, handler):
from gevent.pywsgi import WSGIServer
srv = WSGIServer((self.host, self.port), handler,
certfile='./server.pem',
keyfile='./server.pem')
srv.serve_forever()

@route('/', method='GET')
@route('/index', method='GET')
def index():
body = "0"
if request.get_cookie("counter"):
body = str(int(request.get_cookie("counter")) + 1)
res = HTTPResponse(status=200, body=body)
res.set_cookie('counter', body, secure=1)
return res

run(host='127.0.0.1', port=8080, server=SSLWebServer)
ブラウザで確認

f:id:motikan2010:20161225194304p:plain

curlコマンドで確認

-k オプションでHTTPS通信を行います。

# curl -v -k https://127.0.0.1:8080/
* Trying 127.0.0.1...
* Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
* TLS 1.2 connection using TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
* Server certificate: Internet Widgits Pty Ltd
> GET / HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.43.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Length: 1
< Content-Type: text/html; charset=UTF-8
< Set-Cookie: counter=0; Secure
< Date: Sun, 25 Dec 2016 10:31:46 GMT
<
* Connection #0 to host 127.0.0.1 left intact
0

参考:dgtool: SSL encryption in python bottle