BETA

Classiに翻弄される後輩たちを助けたかった件

投稿日:2020-04-20
最終更新:2020-04-20

Classiというものに翻弄される後輩たちを助けるためにツールを作成した。GitHubで公開中→against_classi

Classiとは

学校教育向けに提供されるSaaS型のクラウドサービスである。ベネッセとソフトバンクの合弁子会社のClassi株式会社が運営している。学校からの連絡、テストの配信、学習動画、コンテンツ共有等そこそこ多機能である。
これだけならいいのだが、問題はインフラが貧弱だそうで平時でもレスポンスが鈍いこと。そしてこのコロナ禍での休校によって各学校が連絡や課題の配信に利用しアクセス数が急増、ログインできない、できても機能のページに遷移できない等の問題が多発した。そして先日、恐れていたことがとうとう起きてしまった。

教育機関向けSaaS「Classi」に不正アクセス 約122万人のユーザーIDなど流出の恐れとのことだ。これを受けて運営は緊急メンテナンスを実施。多くの高校はこの休校期間をClassiに頼りっきりであったため、この間何もできなくなったのである。メンテナンスが明けると今度はアクセスが殺到、断続的に503エラーが発生した。

いくら重くて使い物にならないとはいえ、Classi経由で配信される連絡をチェックし課題をこなさなければならないと後輩は言う。朝食後にログインを試みて失敗、また間を空けて試みても失敗。配信されたテストをこなすのに3時間掛けたという後輩もいた。「一定時間おきにログインアタックしてくれるプログラムが欲しい」とのことだった。ならば私が作ろう。先輩が立ち上がった。

1stアプローチ:HTMLでPOST先を指定

真っ先に思いついた方法である。F12ツールを駆使してページを解析した。formタグでログイン情報をPOSTするURLを指定、inputタグのvalueにIDとパスワードを格納しているようだった。以下のhtmlファイルを作成した。

<body onload="document.login.submit()">  
  <form action="https://auth.classi.jp/login/validate" method="post" name="login">  
    <input type="text" name="classi_id" id="classi_id" value="your_id">  
    <input type="password" name="password" id="password" value="your_pass">  
  </form>  
</body>  

指定時間おきに実行するため、bat及びbashのスクリプトを作成した。載せても良かったのだが、結論から言うと動かなかったので載せなかった。
ログインにはidとパスワード以外にCSRFトークンが必要(F12から推測するにClassiではおそらくRailsで生成)であった。そのため、ログインすることができなかった。

2ndアプローチ:Seleniumで自動操作

ならばブラウザの外部からブラウザを操作するしかないと思い調べていると、Seleniumというものに行き当たった。もともとはWebアプリケーションをテストするためのツールである。Classiの開発現場でも使われているのだろうか。
クライアントAPIの提供によって、Java/C#/Ruby/JavaScript/R/Pythonでテストが可能になっている。Pythonで何か作ってみたいと思っていたので、Pythonを採用した。

Seleniumのインストール

Python用のAPIは、pipがあれば簡単にインストールできる。

$ pip install selenium  

WebDriverのインストール

クライアントAPIから送信されるコマンドを受け入れ、ブラウザに送信するためのもの。pipがあれば簡単にインストールできる。

# Chrome  
$ pip install chromedriver_binary  

# Firefox  
$ pip install geckodriver_autoinstaller  

inputimeoutのインストール

Pythonではinput()で簡単にキー入力ができるが、タイムアウトの処理ができない。それを可能にするライブラリを作成して公開している方がいたので、インストールした。

$ pip install inputimeout  

Pythonで記述

実際にプログラムを書いていく。

ライブラリのimport

当然ながらChromeとFirefoxでWebDriverが違う。

import time  
import sys  
import datetime  
import configparser  
from selenium import webdriver  
from inputimeout import inputimout, TimeoutOccurred  

# Chrome  
import chromedriver_binary  

# Firefox  
import geckodriver_autoinstaller  
geckodriver_autoinstaller.install()  

変数の定義

実行間隔を指定するspan、IDを指定するid、パスワードを指定するpasswdはiniファイルを読みに行くようにした。ツールを使ったF5連打になりサーバーに過負荷をかけないように、spanを60未満にすると終了ステータス1を吐いて終了するようにした。

config_file = configparser.ConfigParser()  
config_file.read('./config.ini')  
span = config_file['config']['span']  
id = config_file['config']['id']  
passwd = config_file['config']['passwd']  

if int(span) < 60:  
    print("実行間隔が短すぎます")  
    print("config.iniのspanを60以上にしてください")  
    print("終了します")  
    sys.exit(1)  

# config.iniの例  
[config]  
span = 3600  
id = your_id  
passwd = your_passwd  

連続実行か1回実行か

ログインに使用するidを出力した後、0で連続実行、1で1回実行を選ぶようにした。10秒操作が無いと0が選ばれ、その他の文字を入力すると戻ってまた聞かれる。ここでinputimeoutを使用している。while文で無限ループさせながら変数flagが0か1かをif文で処理し、breakを利用している。

print("id:" + id)  
while 1:  
    print("連続実行なら0、1回実行なら1を入力")  
    print("10秒以内、デフォルトは0")  
    try:  
        flag = inputimeout(timeout=10)  
    except TimeoutOccurred:  
        flag = 0  
    if int(flag) == 0:  
        print(span + "秒毎に実行します\n")  
        break  
    elif int(flag) == 1:  
        print("1回実行します\n")  
        break  
    else:  
        print("0か1を入力してください\n")  

ログイン試行

試行した日時が分かるようにdatetimeから得たものを出力している。
当然ながらChromeとFirefoxでブラウザを立ち上げる処理が異なる。driver.getでWebサイトを表示、driver.find_element_by_idでidから要素を指定しsend_keysで入力。ログインボタンをクリックするためにdriver.find_element_by_xpathから要素をXPathで指定、clickでクリック。flagが1すなわち1回実行を選んだならここで正常終了する。
flagが0なら続けるか終えるか10秒以内に尋ねた後、続けるならspanで指定した時間time.sleepで待機してから無限ループで戻るようにしている。実装の方法は先述とほぼ同じ。

while 1:  
    date = datetime.datetime.now()  
    print(date)  

    # Chrome  
    driver = webdriver.Chrome()  

    # Firefox  
    driver = webdriver.Firefox()  

    driver.get("https://auth.classi.jp/students")  

    driver.find_element_by_id("classi_id").send_keys(id)  
    driver.find_element_by_id("password").send_keys(passwd)  
    driver.find_element_by_xpath("/html/body/div[1]/div[1]/div[3]/div[1]/form/button").click()  
    if int(flag) == 1:  
        sys.exit(0)  

    while 1:  
        print("続けて実行するなら0、終了するなら1を入力")  
        print("10秒以内、デフォルトは0")  
        try:  
            flag = inputimeout(timeout=10)  
        except TimeoutOccurred:  
            flag = 0  
        if int(flag) == 1:  
            sys.exit(0)  
        elif int(flag) == 0:  
            print(span + "秒後に実行します")  
            print("強制終了はCtrl+C(ブラウザも閉じます)")  
            time.sleep(int(span))  
            break  
        else:  
            print("0か1を入力してください")  

Pyinstallerでバイナリに

このツールを利用する後輩の大半は自分のPCにPythonの環境など作っていないはずなので、何も無くてもダブルクリックで実行できるようにした。Pyinstallerにて--onefileオプションをつけて行った。--add-binaryオプションも試したがうまく行かなかったので、別でWebDriverのバイナリを置きpyファイル内で絶対パスで指定するようにした。iniファイルは1つ上層に置いている。バイナリ生成にあたって書き換えたpyファイルである。この場合、chromedriver_binaryとgeckodriver_autoinstallerはimportしなくてよい。

# (略)  
config_file.read("../config.ini")  
# (略)  
driver = webdriver.Chrome(executable_path="./chromedriver")  

当然ながらFirefoxならgeckodriverになるし、Windowsなら拡張子exeがつく。

Classiについて思うこと

情報漏洩はあってはならないことであり、再発防止に務めるのは当然である。ベネッセは過去にも攻撃ではないが漏洩をやらかしているので尚更だ。しかしながら、IDは漏れたようだがパスワードは暗号化後のものしか漏れていないようなので、不正利用には至っていないようだ。見られる状態で漏れたのは教員ユーザーが任意記入する自己紹介とのことで、正直言って盗んでも利益は無いだろう。この攻撃は誰が何のためにやったのか謎が深まるばかりだ。
生徒が振り回されているのはClassiしかこの休校期間において手段が無いからであり、彼らにはClassi以外の選択肢が必要だ。競合が現れればClassiも改善されていくのであろう。共通テストも然り、教育改革におけるベネッセの独占を許してはいけない。
GoogleClassroom等のが競合に挙げられるが、Google等のクラウドサービスはファイルを共有できること、見ず知らず人とやり取りができてしまうこと等を理由にアクセスを禁止している自治体がほとんどである。ICTの環境が充実している学校はどれも私立だ。Classiもファイル共有はできるが、多くの公立学校では「ベネッセだから」という理由でClassiを採用している。同じ理由で採用する私立学校も多いはずだ。日本のICT教育の遅れはこういったところから来るのではないか。
今のところはClassiは安定して稼働していて503エラー等には見舞われていないので、このツールを作ったことは無意味に終わるかもしれない。しかしながら、Seleniumというものを知ったこと、Pythonでのプログラミングを習得したことは私にとって大きな意味を持つ。プログラミングを学ぶためのプログラミングよりも、何か作ってみたいと思ってやるプログラミングの方が精が入るのである。

技術ブログをはじめよう Qrunch(クランチ)は、プログラマの技術アプトプットに特化したブログサービスです
駆け出しエンジニアからエキスパートまで全ての方々のアウトプットを歓迎しております!
or 外部アカウントで 登録 / ログイン する
クランチについてもっと詳しく

この記事が掲載されているブログ

Linuxを触りつつ、Minecraftで遊んだり、時々電子工作をしたりします。Qiitaから引っ越し完了。

よく一緒に読まれる記事

0件のコメント

ブログ開設 or ログイン してコメントを送ってみよう