LINE の スマートスピーカー Clova Friends と Clova Desk の HTTP リクエストを調べる

## 概要

- LINE Clova から Custom Extension (スキル) 起動時に送信される HTTP リクエストを調べる
- Ruby + Heroku による調査用 Web サーバ (Extension サーバー) を立てて HTTP リクエストの内容を調べる

## Clova とは

LINE が提供しているスマートスピーカー。

[LINE Clova公式サイト](https://clova.line.me/)

> ClovaはLINEが開発したAIアシスタントです。
> 話しかけるだけでLINEの送受信や家電の操作など、あなたの様々なリクエストに応えます。

## Clova の HTTP リクエスト

Clova は CEK (Clova Extensions Kit) を介して HTTPS リクエストを Extension サーバーに送信している。

[CEKの概要 \- Clova Developer Center β](https://clova-developers.line.biz/guide/CEK/CEK_Overview.md)

> CEKは、Clova Extension(以下、Extension)を開発および配布する際に必要なツールとインターフェースを提供するプラットフォームです。ClovaプラットフォームとExtension間のデータの送受信をサポートします。Extensionは、音楽、ショッピングなどの外部のサービス(サードパーティサービス)、または家庭のIoTデバイスの制御など、Clovaの機能を拡張して、ユーザーに様々な体験を提供するWebアプリケーションです。

[CEK APIのリファレンス \- Clova Developer Center β](https://clova-developers.line.biz/guide/CEK/References/CEK_API.md)

> ・HTTP/1.1バージョンでHTTP通信し、POSTメソッドを使用します。
> ・Hostとリクエストパスは、Extensionの開発者があらかじめ定義したURIに設定されます。
> ・リクエストボディのデータはJSON形式で、UTF-8エンコーディングを使用します。
> ・SignatureCEKフィールドとRSA公開鍵を使用して、Clovaから送信されたリクエストかどうかを検証することができます。

## Extension サーバーとは

Clova から送信される HTTPS リクエストを受信して応答を返す Web サーバ。

[Custom Extensionを作成する \- Clova Developer Center β](https://clova-developers.line.biz/guide/CEK/Guides/Build_Custom_Extension.md)

> Clova Developer Centerに登録するExtensionサーバーです。このサーバーは、Clovaがユーザーの音声入力を解析した結果や、デフォルトで提供されるインテントを渡された際に、そのインテントを処理して適切な応答を返す必要があります。

## 今回の調査対象

- Clova Friends (画面なしモデル)
- Clova Desk (画面ありモデル)
- Clova Developer Center テストツール (Web ブラウザ上で使用できるテストツール)

### Clova Friends とは

内蔵バッテリーを搭載している Clova。
画面は付いていない。

![clova-friends.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139365/a56f42c5-e612-b5d9-287e-3294f1578f1b.jpeg)

[Clova Friends \| LINE Clova公式サイト](https://clova.line.me/clova-friends-series/clova-friends/)

> コンパクトなサイズで、見ためもPOPな、スマートスピーカーです。
> バッテリーを内蔵しているので、音楽再生、LINE通話、占いなどの便利な機能を、お出かけ先などでご利用いただくことができます。

### Clova Desk とは

内蔵バッテリー、画面、赤外線送受信機を搭載している Clova。

![clova-desk.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139365/62b1fb86-537a-4ad5-0c24-d656a230db5b.jpeg)

[Clova Desk \| LINE Clova公式サイト](https://clova.line.me/desk/)

> 天気やレシピ、歌詞も画面表示でもっと楽しめる
> 7インチの画面で、子供と一緒の画面を見ながら、お気に入りの曲を聴いたり、料理を楽しむことができます。

[Clova Desk \| LINE Clova公式サイト](https://clova.line.me/desk/)

> IRと赤外線リモコンを搭載。リモコンを探すことなく、テレビやエアコンを操作することができます。

### Clova Developer Center テストツールとは

実際に Custom Extension (スキル) を配布する前にテストすることができるツール。

![clova-dev.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139365/87a4f66e-83ab-d2ad-9d15-036981af0fa6.png)

[Extensionをテストする \- Clova Developer Center β](https://clova-developers.line.biz/guide/DevConsole/Guides/CEK/Test_Extension.md)

> テスト画面では、次の2種類のテストを実行できます。
> 
> ・対話モデルテストモード：任意のインテントのサンプル発話を入力して、インテントやスロットの解析結果やExtensionへのリクエストメッセージを確認できます。
> ・シナリオテストモード：LaunchRequestからSessionEndedRequestまでの一連のシナリオをテストできます。

## HTTP リクエストを受ける調査用 Web サーバ (Extension サーバー)

- HTTP リクエスト確認用 Ruby スクリプト を Heroku に設置する
- Clova 実機等で Custom Extension (スキル) を起動して、その際に送信される HTTP リクエストを Web サーバ (Extension サーバー) で受信してログに出力する

### HTTP リクエスト確認用 Ruby スクリプト

```ruby
require 'socket'

# 標準出力を同期モードに設定
$stdout.sync = true

# 接続を受け付けるポート番号を決定
# 環境変数 PORT が設定されているならそれを設定
port = 8000
port = ENV['PORT'].to_i if ENV['PORT']

# サーバー接続をオープン
server = TCPServer.open(port)

# HTTP リクエストを待ち続ける
loop do

  begin

    # TCPSocket オブジェクトを取得
    socket = server.accept

    # 受け付けた日時を出力
    puts "[info]#{Time.new}"

    # HTTP リクエスト開始行を出力
    if not req_start_line = socket.gets
      puts '[info]req_start_line is nil'
      next
    end
    puts "#{req_start_line}"

    # HTTP リクエストヘッダーを1行ずつ出力
    while req_header = socket.gets.chomp
      puts "#{req_header}"
      break if req_header == '' # ヘッダー終了
      # Content-Length ヘッダーがあれば値を変数にセット
      h = req_header.split(':')
      content_length = h[1].strip.to_i if h[0].strip.downcase == 'content-length'
    end

    # Content-Length がある場合はボディを出力
    if content_length != nil
      puts socket.read(content_length)
    end

    # HTTP レスポンスを返す
    # 本文データ
    body = <<-'__EOS__'
    {
      "version": "1.0",
      "sessionAttributes": {},
      "response": {
        "outputSpeech": {
          "type": "SimpleSpeech",
          "values": {
              "type": "PlainText",
              "lang": "ja",
              "value": "こんにちは、クローバ🍀"
          }
        },
        "card": {},
        "directives": [],
        "shouldEndSession": true
      }
    }
    __EOS__
    # ステータス行
    socket.write "HTTP/1.1 200 OK\r\n"
    # ヘッダー
    socket.write "Server: #{RUBY_DESCRIPTION}\r\n"
    socket.write "Content-Type: application/json\r\n"
    socket.write "Content-Length: #{body.bytesize}\r\n"
    socket.write "Connection: close\r\n"
    # 空行
    socket.write "\r\n"
    # 本文
    socket.write body

  rescue => e
    puts e.full_message

  ensure
    # HTTP 接続を閉じる
    puts '[info]close this socket'
    socket.close
  end

end

server.close
```

## 調査結果

### 留意点

- ユーザー毎等で一意になるような値などは文字「X」による伏せ字に置き換えておく
- Heroku 経由なので一部の HTTP ヘッダ等が加工・追加されている可能性がある
- Signaturecek ヘッダ: Clova から送信されたかどうかを検証するためのヘッダ
- X-B3 ではじまるヘッダ: LINE 側でつけていると思われるヘッダ (分散トレーシングシステム Zipkin で使われるヘッダ)
- Heroku のリバースプロキシサーバ Vegur がつけていると思われるヘッダ: X-Request-Id, X-Forwarded-For, X-Forwarded-Proto, X-Forwarded-Port, Via, Connect-Time, X-Request-Start, Total-Route-Time

### Clova Friends (ディスプレイ無しモデル) の HTTP リクエスト

#### HTTP リクエスト全体

```
POST /hello-clova/ HTTP/1.1
Host: example.herokuapp.com
Connection: close
User-Agent: Go-http-client/1.1
Content-Type: application/json; charset=utf-8
Signaturecek: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
X-B3-Flags: 0
X-B3-Parentspanid: XXXXXXXXXXXXXXXX
X-B3-Sampled: 1
X-B3-Spanid: XXXXXXXXXXXXXXXX
X-B3-Traceid: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
X-Clova-Experiments: 1=A&10=A&2=B&3=A&4=B&5=A&6=C&7=B&8=A&9=B
X-Clova-Request-Id: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
Accept-Encoding: gzip
X-Request-Id: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
X-Forwarded-For: XXX.XXX.XXX.XXX
X-Forwarded-Proto: https
X-Forwarded-Port: 443
Via: 1.1 vegur
Connect-Time: 0
X-Request-Start: 1580853072763
Total-Route-Time: 0
Content-Length: 744

{"version":"1.0","session":{"new":true,"sessionAttributes":{},"sessionId":"XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX","user":{"userId":"UXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"}},"context":{"System":{"application":{"applicationId":"info.maigo.lab.helloclova"},"device":{"deviceId":"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX","display":{"size":"none","contentLayer":{"width":0,"height":0}}},"user":{"userId":"UXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"}}},"request":{"type":"LaunchRequest","requestId":"XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX","timestamp":"2020-02-04T21:51:12Z","locale":"ja-JP","extensionId":"info.maigo.lab.helloclova","intent":{"intent":"","name":"","slots":{}},"event":{"namespace":"","name":"","payload":null}}}
```

#### HTTP メッセージボディの JSON を整形したもの

```json
{
  "version": "1.0",
  "session": {
    "new": true,
    "sessionAttributes": {},
    "sessionId": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
    "user": {
      "userId": "UXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
    }
  },
  "context": {
    "System": {
      "application": {
        "applicationId": "info.maigo.lab.helloclova"
      },
      "device": {
        "deviceId": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
        "display": {
          "size": "none",
          "contentLayer": {
            "width": 0,
            "height": 0
          }
        }
      },
      "user": {
        "userId": "UXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
      }
    }
  },
  "request": {
    "type": "LaunchRequest",
    "requestId": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
    "timestamp": "2020-02-04T21:51:12Z",
    "locale": "ja-JP",
    "extensionId": "info.maigo.lab.helloclova",
    "intent": {
      "intent": "",
      "name": "",
      "slots": {}
    },
    "event": {
      "namespace": "",
      "name": "",
      "payload": null
    }
  }
}
```

### Clova Desk (ディスプレイ付きモデル) の HTTP リクエスト

#### HTTP リクエスト全体

```
POST /hello-clova/ HTTP/1.1
Host: example.herokuapp.com
Connection: close
User-Agent: Go-http-client/1.1
Content-Type: application/json; charset=utf-8
Signaturecek: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
X-B3-Flags: 0
X-B3-Parentspanid: XXXXXXXXXXXXXXXX
X-B3-Sampled: 1
X-B3-Spanid: XXXXXXXXXXXXXXXX
X-B3-Traceid: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
X-Clova-Experiments: 1=A&10=A&2=B&3=A&4=B&5=A&6=C&7=B&8=A&9=B
X-Clova-Request-Id: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
Accept-Encoding: gzip
X-Request-Id: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
X-Forwarded-For: XXX.XXX.XXX.XXX
X-Forwarded-Proto: https
X-Forwarded-Port: 443
Via: 1.1 vegur
Connect-Time: 0
X-Request-Start: 1580853042398
Total-Route-Time: 0
Content-Length: 787

{"version":"1.0","session":{"new":true,"sessionAttributes":{},"sessionId":"XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX","user":{"userId":"UXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"}},"context":{"System":{"application":{"applicationId":"info.maigo.lab.helloclova"},"device":{"deviceId":"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX","display":{"size":"custom","dpi":160,"orientation":"landscape","contentLayer":{"width":1024,"height":552}}},"user":{"userId":"UXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"}}},"request":{"type":"LaunchRequest","requestId":"XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX","timestamp":"2020-02-04T21:50:41Z","locale":"ja-JP","extensionId":"info.maigo.lab.helloclova","intent":{"intent":"","name":"","slots":{}},"event":{"namespace":"","name":"","payload":null}}}
```

#### HTTP メッセージボディの JSON を整形したもの

```json
{
  "version": "1.0",
  "session": {
    "new": true,
    "sessionAttributes": {},
    "sessionId": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
    "user": {
      "userId": "UXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
    }
  },
  "context": {
    "System": {
      "application": {
        "applicationId": "info.maigo.lab.helloclova"
      },
      "device": {
        "deviceId": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
        "display": {
          "size": "custom",
          "dpi": 160,
          "orientation": "landscape",
          "contentLayer": {
            "width": 1024,
            "height": 552
          }
        }
      },
      "user": {
        "userId": "UXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
      }
    }
  },
  "request": {
    "type": "LaunchRequest",
    "requestId": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
    "timestamp": "2020-02-04T21:50:41Z",
    "locale": "ja-JP",
    "extensionId": "info.maigo.lab.helloclova",
    "intent": {
      "intent": "",
      "name": "",
      "slots": {}
    },
    "event": {
      "namespace": "",
      "name": "",
      "payload": null
    }
  }
}
```

### Clova Developer Center テストツール の HTTP リクエスト

#### HTTP リクエスト全体

```
POST /hello-clova/ HTTP/1.1
Host: example.herokuapp.com
Connection: close
Signaturecek: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Content-Type: application/json;charset=UTF-8
User-Agent: Apache-HttpClient/4.5.6 (Java/1.8.0_202)
Accept-Encoding: gzip,deflate
X-Request-Id: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
X-Forwarded-For: XXX.XXX.XXX.XXX
X-Forwarded-Proto: https
X-Forwarded-Port: 443
Via: 1.1 vegur
Connect-Time: 0
X-Request-Start: 1580853979149
Total-Route-Time: 0
Content-Length: 594

{"version":"1.0","session":{"sessionId":"XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX","sessionAttributes":{},"user":{"userId":"XXXXXXXXXXXXXXXXXXXXXX","accessToken":"XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"},"new":true},"context":{"System":{"application":{"applicationId":"info.maigo.lab.helloclova"},"user":{"userId":"XXXXXXXXXXXXXXXXXXXXXX","accessToken":"XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"},"device":{"deviceId":"XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX","display":{"size":"l100","orientation":"landscape","dpi":96,"contentLayer":{"width":640,"height":360}}}}},"request":{"type":"LaunchRequest"}}
```

#### HTTP メッセージボディの JSON を整形したもの

```json
{
  "version": "1.0",
  "session": {
    "sessionId": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
    "sessionAttributes": {},
    "user": {
      "userId": "XXXXXXXXXXXXXXXXXXXXXX",
      "accessToken": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
    },
    "new": true
  },
  "context": {
    "System": {
      "application": {
        "applicationId": "info.maigo.lab.helloclova"
      },
      "user": {
        "userId": "XXXXXXXXXXXXXXXXXXXXXX",
        "accessToken": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
      },
      "device": {
        "deviceId": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
        "display": {
          "size": "l100",
          "orientation": "landscape",
          "dpi": 96,
          "contentLayer": {
            "width": 640,
            "height": 360
          }
        }
      }
    }
  },
  "request": {
    "type": "LaunchRequest"
  }
}
```

## 参考資料
- [LINE Clova公式サイト](https://clova.line.me/)
- [Clova Developer Center β](https://clova-developers.line.biz/)
- [CEK APIのリファレンス \- Clova Developer Center β](https://clova-developers.line.biz/guide/CEK/References/CEK_API.md)
- [生に近い HTTP リクエスト情報を調査するために Ruby で Web サーバを作って Heroku にデプロイ \- Qiita](https://qiita.com/niwasawa/items/c36aa48a578475f48870)