はじめてのスクレイピング ASUS製ルーターから情報を取得

# ASUSルーターのスクレイピング

ASUSのルーター上から、HTTPS/SSL証明書ファイルを自動で取得することを考えて作ってみました。
## 経緯

DDNSをASUSルーターのサービスで設定し、「Let's Encryptから無料の証明書を取得」というオプションを選択していると、ASUSルーターがHTTPS/SSL証明書を自動で取得してくれます。
そして証明書を使用したい場合は、この画面からエクスポートする必要があります。
>ルーター管理ページ -> WAN -> DDNS より
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3363316/bdd0bf36-6c42-64c2-67bb-b8ff9348611b.png)

私は[Home Assistant](https://www.home-assistant.io/)というスマート家電系の制御を統括してくれるサービスにてHTTPS接続を利用しているのですが、90日ごとに自動で更新される証明書を、更新が行われるたびにルーターの管理ページへアクセスし、エクスポート、ダウンロードして解凍、そして証明書をHome Assistantのsslフォルダへ移動…といった手順を踏んでいて非常に面倒でした。

ルーター上で更新されるこの証明書ファイルをなんとか自動で持ってこれないか？というのが今回の経緯・目的となっています。

```mermaid
graph TD;
    A{証明書期限切れ}-->|人が気付く|ルーター管理ページへアクセス;
    A-->|自動検知|スクリプトの実行;
    ルーター管理ページへアクセス-->ログイン;
    ログイン-->WANページ;
    WANページ-->DDNSページ;
    DDNSページ-->ファイルをエクスポート;
    ファイルをエクスポート-->HAのSambaサーバーへアップロード;
    HAのSambaサーバーへアップロード-->B([解決]);
    スクリプトの実行-->B;
```

<font color=gray> _HTTPS/SSL証明書をASUS任せにしない方が、Home Assistant側のアドオンなりを使って楽に解決できそうだけど…_</font>

## ウェブページの動作を確認
まずはFirefox上で管理ページの動作を確認します。
ウェブ開発者ツールを開きます。
ネットワークタブを開き「永続ログ」を有効にしたら、ログインしてみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3363316/6dac41e9-316b-dd55-122e-65d07d3f1fe8.png)
結果はこのようになっていました。メソッドに注目すると、初めに`login.cgi`へPOSTを行っているようです。その後`index.asp`を`GET`し、ログイン後のメインページを読み込んでいることがわかります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3363316/80405220-ca87-ac66-c2d7-e4fea6ff7ae2.png)
`POST`の詳細を見てみましょう。クリックすると右側へ細かい情報が表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3363316/53147d3b-b415-2e2c-7b29-76d5f76a571e.png)
応答ヘッダーを見ると、`Set-Cookie:`として`asus_token`というのが帰ってきているのがわかります。これを使ってログインするのでしょうか。

「要求」タブも見てみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3363316/16def041-56b8-f643-40f5-2dace6d94286.png)
送信したフォームデータには以上のものが含まれているようです。重要なのは`login_authorization`でしょう。ランダムな文字列が設定されていたので、おそらくログインユーザー名とパスワードを暗号化した文字列だと思われます。`Main_Login.asp`の中を見ると、どうやら`login()`というjsの関数で暗号化して`POST`していそうな様子が見れました。

以上を踏まえて、`index.asp`の`GET`を詳しく見てみましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3363316/416c2424-d2ef-df9a-ecb3-76d5f60a4479.png)
先ほど`POST`の返答で帰ってきていた`asus_token`をCookieに含めていることがわかります。

何回かログインを試すとわかりますが、このトークンはログインを行うたびに変わっていることがわかります。

そして件のサーバー証明書ですが、ただルートディレクトリにある`cert_key.tar`を`GET`しているだけでした。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3363316/d3f73c67-590f-3ed4-c670-7f2dd5406cac.png)
要求ヘッダーにはやはり`asus_token`が設定されていたので、試しにログインせずにこのURLを`GET`したところ、ログインページへリダイレクトされるだけでした。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3363316/e9acedd7-34c8-73eb-1724-e32041bd8b2e.png)

### ウェブページ動作のまとめ
あくまで私の推測ですが、大体このような形かと思われます。
| ファイル | 役割・動作 |
|:-:|:-:|
| `Main_Login.asp` | ログインフォームの提供<br/>ログイン情報の暗号化<br/>`login.cgi`へログイン情報の`POST` |
| `login.cgi` | ログイン情報の受け取り<br/>ルーター内のユーザー情報と照合<br/>正しければログイントークンを返答 |
| `index.asp` | メインページ |
|`cert_key.tar`|目的のファイル|
| ??? | ログイントークンの照合<br/>間違っていたらリダイレクト |

以上より、`login.cgi`へ「暗号化されたログイン情報」である`login_authorization`を`POST`し、返答の`asus_token`をクッキーへ含めて`cert_key.tar`へ`GET`リクエストを送れば、目的のファイルを取得できそうです。
メインページの情報についても、同様に色々取れそうですね。
## コーディング
以上の予想について、Pythonを用いてコードにしてみます。

### ログイン部分

```python:ルーターへログイン
import requests
import tarfile

# 接続先のURL
login_url = 'http://router.asus.com/login.cgi'

login_header = { # ブラウザの要求ヘッダーをコピペ
	"Host": "router.asus.com",
	"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/118.0",
	"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
	"Accept-Language": "ja,en-US;q=0.7,en;q=0.3",
	"Accept-Encoding": "gzip, deflate",
	"Referer": "http://router.asus.com/Main_Login.asp",
	"Content-Type": "application/x-www-form-urlencoded",
	"Content-Length": "157",
	"Origin": "http://router.asus.com",
	"DNT": "1",
	"Connection": "keep-alive",
	"Upgrade-Insecure-Requests": "1",
	"Pragma": "no-cache",
	"Cache-Control": "no-cache",
}
login_data = { # ブラウザの「要求」タブから確認したフォームデータ
	"group_id": "",
	"action_mode": "",
	"action_script": "",
	"action_wait": "5",
	"current_page": "Main_Login.asp",
	"next_page": "index.asp",
	"login_authorization": "ログイン情報", # 暗号化されたユーザー名＋パスワードの文字列
	"login_captcha": ""
}

# URLへデータをPOSTした結果を保存
login_res = requests.post(
  login_url, # 目的のURLへ
  data = login_data, # フォームデータを送信
  headers = login_header # ブラウザのヘッダーを使用
)
login_res.raise_for_status() # 例外処理

# 結果のクッキーからトークンが取得できたら成功
login_token = login_res.cookies.get('asus_token')
if login_token:
	print('login success!')
    print('asus_token =',asus_token)
else:
	print('login failed...')
```
以上のコードを実行した結果、`asus_token`を無事取得できました。
```console:実行結果
login success!
asus_token = 8NSx7HO~~~MaUpTUj
```
<font color=gray> _ところでこういうトークンは別に晒しても問題ないはずですが、なんとなく隠したくなります。_</font>

それでは得られた`asus_token`を用いて目的のファイルを入手したいと思います。
### 証明書ファイルダウンロード部分

```python:証明書ファイルのダウンロード
# 証明書ファイルのURLを設定
cert_url = 'http://router.asus.com/cert_key.tar'
# 先ほど同様にヘッダーをブラウザからコピペ
# Cookieに関してのみ、取得したasus_tokenを使用
cert_header = {
	"Host": "router.asus.com",
	"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/118.0",
	"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
	"Accept-Language": "ja,en-US;q=0.7,en;q=0.3",
	"Accept-Encoding": "gzip, deflate",
	"DNT": "1",
	"Connection": "keep-alive",
	"Cookie": "asus_token=" + login_cookie + "; clickedItem_tab=0", # 取得したasus_tokenは変数login_cookieに格納されている
	"Upgrade-Insecure-Requests": "1",
	"Pragma": "no-cache",
	"Cache-Control": "no-cache",
}
# URLにGETした結果を保存
get_cert_res = requests.get(
  cert_url,
  headers = cert_header,
)
get_cert_res.raise_for_status() # 例外処理

# 結果のコンテンツをtarとして扱う
with open('cert_key.tar', "wb") as f:
	f.write(get_cert_res.content)
try: # 解凍できるか試す。できなかったら取得に失敗している
	with tarfile.open('cert_key.tar', 'r') as tar: # 解凍
		tar.extractall(path="cert_key') # cert_keyというフォルダに保存
	print('got a certification')
except:
	# traceback.print_exc()
	print('getting certification failed...')
	exit()
```
結果、pythonスクリプトのあるフォルダに`.tar`ファイルと解凍された`.pem`ファイルが確認できました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3363316/9246ccbd-75b9-215d-a146-a7e8cd1f86e8.png)
これだけでもかなりの時短になりますが、ついでなのでHome AssistantのSambaサーバーへファイルを送りましょう。

```python:SMBでのファイル送信
samba_target_ip = '192.168.50.133' # 接続先のIP
samba_connection_port = 445 # Sambaのポート　デフォルトで445
conn = SMBConnection( # Sambaコネクション用の情報
    'user', # サーバーへログインするユーザー
    'password', # パスワード
    'python_user', # おそらくクライアントの名前…？
    'workgroup' # ワークグループの名前のはず…？
)
try: # つなぐ
	conn.connect( samba_target_ip, samba_connection_port )
	print(conn.echo('connection success'))
except:
  print('connect error...')
  exit()

# それぞれのファイルをsslフォルダへ転送
with open('./cert_key/cert.pem', 'rb') as file:
	try:
		conn.storeFile('ssl', 'cert.pem', file)
		print(file.name,'transfered successfully')
	except:
		print(file.name,' transfered error...')
		exit()
    
with open('./cert_key/key.pem', 'rb') as file:
	try:
		conn.storeFile('ssl', 'key.pem', file)
		print(file.name,'transfered successfully')
	except:
		print(file.name,' transfered error...')
		exit()
    
conn.close() # 終わったらコネクションを閉じる

# ローカルのゴミを削除
try:
	os.remove('./cert_key/cert.pem')
	os.remove('./cert_key/key.pem')
	os.remove('./cert_key.tar')
except:
  print('error occured while removing a file')
finally:
	print('remove success!\nall steps finished successfully')
```
実行した結果無事に目的のフォルダへファイルを転送できました。

これでとりあえずスクリプトを実行しさえすれば一瞬で証明書ファイルの更新ができる…
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3363316/332024e9-0c52-dff8-7bad-47d97b7c9784.png)

あとはHome Assistant側で証明書のセンサーを作成し、expiredが検知されたら自動でこのスクリプトを実行する形にすればよさそうです。Home Assistant側にpythonスクリプトを実行させるか、またはセンサー値表示用ラズパイ等にREST API機能を載せて、HAから呼ばれたらこのスクリプトを実行するという形も面白そうです。
これでようやくアレクサに声をかけても応答しないという煩わしさから解放されます！

## 終わり？
スクレイピングに興味があったためこのような力業で解決しましたが、Home Assistantの証明書ファイルは本来 Let's encryptのアドオンなどを使用して自動で更新させるもののはずなので、余裕が生まれたらそのやり方に変えたいです……。