ASUSルーターのスクレイピング
ASUSのルーター上から、HTTPS/SSL証明書ファイルを自動で取得することを考えて作ってみました。
経緯
DDNSをASUSルーターのサービスで設定し、「Let's Encryptから無料の証明書を取得」というオプションを選択していると、ASUSルーターがHTTPS/SSL証明書を自動で取得してくれます。
そして証明書を使用したい場合は、この画面からエクスポートする必要があります。
私はHome Assistantというスマート家電系の制御を統括してくれるサービスにてHTTPS接続を利用しているのですが、90日ごとに自動で更新される証明書を、更新が行われるたびにルーターの管理ページへアクセスし、エクスポート、ダウンロードして解凍、そして証明書をHome Assistantのsslフォルダへ移動…といった手順を踏んでいて非常に面倒でした。
ルーター上で更新されるこの証明書ファイルをなんとか自動で持ってこれないか?というのが今回の経緯・目的となっています。
HTTPS/SSL証明書をASUS任せにしない方が、Home Assistant側のアドオンなりを使って楽に解決できそうだけど…
ウェブページの動作を確認
まずはFirefox上で管理ページの動作を確認します。
ウェブ開発者ツールを開きます。
ネットワークタブを開き「永続ログ」を有効にしたら、ログインしてみます。
結果はこのようになっていました。メソッドに注目すると、初めにlogin.cgi
へPOSTを行っているようです。その後index.asp
をGET
し、ログイン後のメインページを読み込んでいることがわかります。
POST
の詳細を見てみましょう。クリックすると右側へ細かい情報が表示されます。
応答ヘッダーを見ると、Set-Cookie:
としてasus_token
というのが帰ってきているのがわかります。これを使ってログインするのでしょうか。
「要求」タブも見てみます。
送信したフォームデータには以上のものが含まれているようです。重要なのはlogin_authorization
でしょう。ランダムな文字列が設定されていたので、おそらくログインユーザー名とパスワードを暗号化した文字列だと思われます。Main_Login.asp
の中を見ると、どうやらlogin()
というjsの関数で暗号化してPOST
していそうな様子が見れました。
以上を踏まえて、index.asp
のGET
を詳しく見てみましょう。
先ほどPOST
の返答で帰ってきていたasus_token
をCookieに含めていることがわかります。
何回かログインを試すとわかりますが、このトークンはログインを行うたびに変わっていることがわかります。
そして件のサーバー証明書ですが、ただルートディレクトリにあるcert_key.tar
をGET
しているだけでした。
要求ヘッダーにはやはりasus_token
が設定されていたので、試しにログインせずにこのURLをGET
したところ、ログインページへリダイレクトされるだけでした。
ウェブページ動作のまとめ
あくまで私の推測ですが、大体このような形かと思われます。
ファイル | 役割・動作 |
---|---|
Main_Login.asp |
ログインフォームの提供 ログイン情報の暗号化 login.cgi へログイン情報のPOST
|
login.cgi |
ログイン情報の受け取り ルーター内のユーザー情報と照合 正しければログイントークンを返答 |
index.asp |
メインページ |
cert_key.tar |
目的のファイル |
??? | ログイントークンの照合 間違っていたらリダイレクト |
以上より、login.cgi
へ「暗号化されたログイン情報」であるlogin_authorization
をPOST
し、返答のasus_token
をクッキーへ含めてcert_key.tar
へGET
リクエストを送れば、目的のファイルを取得できそうです。
メインページの情報についても、同様に色々取れそうですね。
コーディング
以上の予想について、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
を無事取得できました。
login success!
asus_token = 8NSx7HO~~~MaUpTUj
ところでこういうトークンは別に晒しても問題ないはずですが、なんとなく隠したくなります。
それでは得られたasus_token
を用いて目的のファイルを入手したいと思います。
証明書ファイルダウンロード部分
# 証明書ファイルの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
ファイルが確認できました。
これだけでもかなりの時短になりますが、ついでなのでHome AssistantのSambaサーバーへファイルを送りましょう。
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')
実行した結果無事に目的のフォルダへファイルを転送できました。
これでとりあえずスクリプトを実行しさえすれば一瞬で証明書ファイルの更新ができる…
あとはHome Assistant側で証明書のセンサーを作成し、expiredが検知されたら自動でこのスクリプトを実行する形にすればよさそうです。Home Assistant側にpythonスクリプトを実行させるか、またはセンサー値表示用ラズパイ等にREST API機能を載せて、HAから呼ばれたらこのスクリプトを実行するという形も面白そうです。
これでようやくアレクサに声をかけても応答しないという煩わしさから解放されます!
終わり?
スクレイピングに興味があったためこのような力業で解決しましたが、Home Assistantの証明書ファイルは本来 Let's encryptのアドオンなどを使用して自動で更新させるもののはずなので、余裕が生まれたらそのやり方に変えたいです……。