この記事を読む前に知っておくべき知識:いつUrlEncodeとUrlDecode関数を使うべきか
筆者はGoogle Chromeを使用し、F12キーを押してサードパーティサイトのhttpプロトコルのインターフェースをキャプチャし、分析操作を行っています。
シナリオ
運用担当の男性が、たまに某外注会社のWebサイトシステムを使ってデバイス登録作業を行います。流れは簡単です:

- デバイスの基本情報を入力(7~8個のフィールド)、保存ボタンをクリック。
- 基本情報の保存成功後、デバイスタイプ選択操作に進み、デバイス識別子生成ボタンをクリック。
- デバイス識別子の生成成功後、デバイスに関連するモジュール情報を登録。シンプルなデバイスは2つのモジュール、複雑なデバイスは6つのモジュール(各モジュールに3~4個のフィールド)を入力し、最後に保存。
1台のデバイス登録が完了するまで、独身の俊敏な手さばきでも数分はかからず、実は大したことではありません。
ところが上司が「1000台のデバイスをやる必要がある」と言い出しました。運用担当は泣きます😂。そこで開発者の出番です:
- 運用担当がExcelテンプレートを用意し、登録する1000台のデバイスの基本情報とデバイスタイプ情報を入力します。この作業量は半日、せいぜい1日程度です。
- 開発者がC/Sクライアントの小さなツールを作成し、プログラム内でビジネス要件に従ってモジュール登録ルールを設定します。
- プログラム実行中に1台デバイスを登録するたびに、生成されたデバイス識別子とデバイスを関連付けます。
- すべての登録が完了したら、Excelエクスポート機能を提供し、デバイス基本情報と生成されたデバイス識別子をすべて関連付けてエクスポートし、作業完了。
数日間の開発を経て、開発者は丁寧に作り込んだ小ツールを運用担当に渡し、運用担当は賞賛のまなざしを向けました...
問題
前置きが少々長くなりましたが、この小ツールを開発中、開発者は一つの問題に遭遇しました:
xxxインターフェース

これはあるインターフェースの情報です。Content-Typeはapplication/x-www-form-urlencoded、パラメータはForm Dataを使用しており、つまりパラメータはUrlEncodeされています。例えばエンコード前のパラメータ:
"Content":"{"AP_Name":"HK_7889","IP":"192.168.0.1"}"
エンコード後(このオンラインURLエンコード・デコードツールで確認できます):
"Content":"%7B%22AP_Name%22%3A%22HK_7889%22%2C%22IP%22%3A%2292.168.0.1%22%7D"
Postmanでテストした際、パラメータにUrlEncodeを使用しなくてもインターフェーステストは成功しました。この小ツールを開発する際、似たようなインターフェースが3つあり、UrlEncodeを行いませんでした:
var client = new RestClient("http://admin.lqclass.com/api/device");
client.Timeout = -1;
var request = new RestRequest(Method.POST);
request.AddHeader("Content-Type", "application/x-www-form-urlencoded");
request.AddParameter("Content", "{\"AP_Name\":\"HK_7889\",\"IP\":\"92.168.0.1\"}");
IRestResponse response = client.Execute(request);
Console.WriteLine(response.Content);
しかし、少し複雑なインターフェース(例えばスクリーンショットのパラメータ)の場合:
"Content":"{"AP_Name":"HK_7889","IP":"192.168.0.1","Module":[{"M_Name":"cameri0","Desc":"cameri0","AP_PUID":"54632325461320320"},{"M_Name":"cameri1","Desc":"cameri1","AP_PUID":"54636325461320320"},{"M_Name":"cameri2","Desc":"cameri2","AP_PUID":"54632325421320320"}]}"
Contentの値をフォーマットするとわかりやすくなります。Moduleはデバイスに関連するモジュール情報です:
{
"AP_Name": "HK_7889",
"IP": "192.168.0.1",
"Module": [
{
"M_Name": "cameri0",
"Desc": "cameri0",
"AP_PUID": "54632325461320320"
},
{
"M_Name": "cameri1",
"Desc": "cameri1",
"AP_PUID": "54636325461320320"
},
{
"M_Name": "cameri2",
"Desc": "cameri2",
"AP_PUID": "54632325421320320"
}
]
}
実際にUrlEncodeされたパラメータは:
"Content":"%7B%22AP_Name%22%3A%22HK_7889%22%2C%22IP%22%3A%22192.168.0.1%22%2C%22Module%22%3A%22%255B%257B%2522M_Name%2522%253A%2522cameri0%2522%252C%2522Desc%2522%253A%2522cameri0%2522%252C%2522AP_PUID%2522%253A%252254632325461320320%2522%257D%252C%257B%2522M_Name%2522%253A%2522cameri1%2522%252C%2522Desc%2522%253A%2522cameri1%2522%252C%2522AP_PUID%2522%253A%252254636325461320320%2522%257D%252C%257B%2522M_Name%2522%253A%2522cameri2%2522%252C%2522Desc%2522%253A%2522cameri2%2522%252C%2522AP_PUID%2522%253A%252254632325421320320%2522%257D%255D%22%7D"
通常のインターフェースでは、上記の成功したC#コードのようにUrlEncodeせずに直接呼び出しても問題ありませんでした。
しかし、このインターフェースの呼び出しでは、サーバーからエラーメッセージ「xxx解析失敗」が返ってきました。呼び出しコードは以下の通り:
var client = new RestClient("http://admin.lqclass.com/api/device");
client.Timeout = -1;
var request = new RestRequest(Method.POST);
request.AddHeader("Content-Type", "application/x-www-form-urlencoded");
request.AddParameter("Content", "{\"AP_Name\":\"HK_7889\",\"IP\":\"192.168.0.1\",\"Module\":[{\"M_Name\":\"cameri0\",\"Desc\":\"cameri0\",\"AP_PUID\":\"54632325461320320\"},{\"M_Name\":\"cameri1\",\"Desc\":\"cameri1\",\"AP_PUID\":\"54636325461320320\"},{\"M_Name\":\"cameri2\",\"Desc\":\"cameri2\",\"AP_PUID\":\"54632325421320320\"}]}");
IRestResponse response = client.Execute(request);
Console.WriteLine(response.Content);
2つの呼び出しコードの違いはどこでしょう?ただContentの値が異なるだけです。最終的に、手動でUrlEncodeする必要があるのかと疑いましたが、URLパラメータではないのに何故エンコードが必要なのでしょう?とにかく、エンコードしてみました。
問題解決
パラメータをエンコードして呼び出し:
var client = new RestClient("http://admin.lqclass.com/api/device");
client.Timeout = -1;
var request = new RestRequest(Method.POST);
request.AddHeader("Content-Type", "application/x-www-form-urlencoded");
request.AddParameter("Content", "%7B%22AP_Name%22%3A%22HK_7889%22%2C%22IP%22%3A%22192.168.0.1%22%2C%22Module%22%3A%22%255B%257B%2522M_Name%2522%253A%2522cameri0%2522%252C%2522Desc%2522%253A%2522cameri0%2522%252C%2522AP_PUID%2522%253A%252254632325461320320%2522%257D%252C%257B%2522M_Name%2522%253A%2522cameri1%2522%252C%2522Desc%2522%253A%2522cameri1%2522%252C%2522AP_PUID%2522%253A%252254636325461320320%2522%257D%252C%257B%2522M_Name%2522%253A%2522cameri2%2522%252C%2522Desc%2522%253A%2522cameri2%2522%252C%2522AP_PUID%2522%253A%252254632325421320320%2522%257D%255D%22%7D");
IRestResponse response = client.Execute(request);
Console.WriteLine(response.Content);
はは、成功しました。ここでは簡単に推測すると、相手のサービスが受け取ったパラメータに対してUrlDecode処理を行っている可能性があります。
実は途中でパラメータのUrlEncode操作をさらに1回行っています。すなわち、下記のModuleパラメータ値です:
"Content":{"AP_Name":"HK_7889","IP":"192.168.0.1","Module":[{"M_Name":"cameri0","Desc":"cameri0","AP_PUID":"54632325461320320"},{"M_Name":"cameri1","Desc":"cameri1","AP_PUID":"54636325461320320"},{"M_Name":"cameri2","Desc":"cameri2","AP_PUID":"54632325421320320"}]}
1回目のUrlEncode:まずModuleの値に対してUrlEncodeを行います:
"Content":{"AP_Name":"HK_7889","IP":"192.168.0.1","Module":%5B%7B%22M_Name%22%3A%22cameri0%22%2C%22Desc%22%3A%22cameri0%22%2C%22AP_PUID%22%3A%2254632325461320320%22%7D%2C%7B%22M_Name%22%3A%22cameri1%22%2C%22Desc%22%3A%22cameri1%22%2C%22AP_PUID%22%3A%2254636325461320320%22%7D%2C%7B%22M_Name%22%3A%22cameri2%22%2C%22Desc%22%3A%22cameri2%22%2C%22AP_PUID%22%3A%2254632325421320320%22%7D%5D}
2回目のUrlEncodeが、上記で成功したパラメータの形です。Content全体の値に対してUrlEncodeを行います。上記の成功パラメータを参照してください。重複して貼り付けません。
最後にまとめ
他人のデータパケットをキャプチャする際、印象や既存知識だけで「こうすべきだ」と決めつけてはいけません。例えば前述のパラメータで、UrlEncodeを使わなくても呼び出しが成功したからといって、他のパケットでも同じ方法が正しいとは限りません。うまくいかない時は、推測した方法をどんどん試してみましょう。
まとめ:「とにかく、やっちまえ」です。
本記事で使用したUrlEncodeのC#コード:
public static string UrlEncode(string str)
{
StringBuilder sb = new StringBuilder();
byte[] byStr = System.Text.Encoding.UTF8.GetBytes(str); //デフォルトはSystem.Text.Encoding.Default.GetBytes(str)
for (int i = 0; i < byStr.Length; i++)
{
sb.Append(@"%" + Convert.ToString(byStr[i], 16));
}
return (sb.ToString());
}