最近遭遇した問題の記録:UrlEncode、UrlDecode

最近遭遇した問題の記録:UrlEncode、UrlDecode

簡単な共有

最終更新 2021/01/09 17:03
沙漠尽头的狼
読了目安 6 分
カテゴリ
.NET
タグ
.NET C# Web API UrlDecode UrlEncode

この記事を読む前に知っておくべき知識:いつUrlEncodeとUrlDecode関数を使うべきか

筆者はGoogle Chromeを使用し、F12キーを押してサードパーティサイトのhttpプロトコルのインターフェースをキャプチャし、分析操作を行っています。

シナリオ

運用担当の男性が、たまに某外注会社のWebサイトシステムを使ってデバイス登録作業を行います。流れは簡単です:

デバイス情報の登録

  1. デバイスの基本情報を入力(7~8個のフィールド)、保存ボタンをクリック。
  2. 基本情報の保存成功後、デバイスタイプ選択操作に進み、デバイス識別子生成ボタンをクリック。
  3. デバイス識別子の生成成功後、デバイスに関連するモジュール情報を登録。シンプルなデバイスは2つのモジュール、複雑なデバイスは6つのモジュール(各モジュールに3~4個のフィールド)を入力し、最後に保存。

1台のデバイス登録が完了するまで、独身の俊敏な手さばきでも数分はかからず、実は大したことではありません。

ところが上司が「1000台のデバイスをやる必要がある」と言い出しました。運用担当は泣きます😂。そこで開発者の出番です:

  1. 運用担当がExcelテンプレートを用意し、登録する1000台のデバイスの基本情報とデバイスタイプ情報を入力します。この作業量は半日、せいぜい1日程度です。
  2. 開発者がC/Sクライアントの小さなツールを作成し、プログラム内でビジネス要件に従ってモジュール登録ルールを設定します。
  3. プログラム実行中に1台デバイスを登録するたびに、生成されたデバイス識別子とデバイスを関連付けます。
  4. すべての登録が完了したら、Excelエクスポート機能を提供し、デバイス基本情報と生成されたデバイス識別子をすべて関連付けてエクスポートし、作業完了。

数日間の開発を経て、開発者は丁寧に作り込んだ小ツールを運用担当に渡し、運用担当は賞賛のまなざしを向けました...

問題

前置きが少々長くなりましたが、この小ツールを開発中、開発者は一つの問題に遭遇しました:

xxxインターフェース

xxxインターフェース

これはあるインターフェースの情報です。Content-Typeapplication/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());
}
さらに探索

関連読書

その他の記事
同じカテゴリ / 同じタグ 2024/01/19

.NET ベースの FluentValidation 検証チュートリアル

FluentValidationは、.NETベースの検証フレームワークで、オープンソースかつ無料、そしてエレガントです。チェーン操作をサポートし、理解しやすく、機能が充実しています。さらに、MVC5、WebApi2、ASP.NET Coreと深く統合でき、コンポーネント内には十数種類の一般的なバリデーターが用意されており、拡張性が高く、カスタムバリデーターをサポートし、ローカライズ多言語にも対応しています。

続きを読む
同じカテゴリ / 同じタグ 2023/01/11

gRPC入門と実践(.NET編)

長らく、フロントエンドとバックエンドのやり取りにはWebApi + JSON方式を使用し、バックエンドサービス間の呼び出しも同様でした

続きを読む