この記事はTokyo City University Advent Calendar 2020の9日目の記事です。
昨日の記事は @aqp_nmu さんによる “それっぽい"配信環境を整えてきもちよくなろう - ふとんのなか でした!
よく見かけるあの配信画面はこんな感じで作るのか・・・と勉強になりました。
特に2560x1440にしておけばFHDがそのまま置けるしサイズ感もいい感じ、というのが驚きで・・・。確かにそうなんですが、すごい発想ですよね。
さて、今日の記事はフロッピーでもMS-DOSでもWindows95でもなく、シェルスクリプトを使ったお話です。 ケーさんの好きなJSじゃないよ! Shell Scriptだよ!
構成としては説明→配布→内容解説なので、是非内容解説まで読んでいただけると幸いです(長いですけど)
はじめに
都市大生のみなさんはいつも使わされてるお世話になっている都市大の出席確認システムですが、毎回
- アクセスしてログイン
- 番号を選ぶ
- 教科を選ぶ
- 登録
- 登録完了!
という、地味に手数の多い作業を行います。別に構わないといえば構わないのですが、少ない手数で出席確認の登録ができれば、非常に楽になります。
そこで、今回は出席確認を
- ページにアクセス
- 数字をクリック
- 登録完了!
のように非常にかんたんな手順で済むようなシステムの構築をしてみました。
タイトルに2クリックとありますが、これはChrome拡張化することで、拡張のボタンクリック→数字クリックの2回にできるものとなっています。もちろんChrome拡張にしなくてもブックマークでも同じですので、ぜひ試してみてください。(Chrome拡張配布もあるよ!)
まずは動作サンプル
サイトにアクセスするとこうなります
数字ボタンを押すだけで出席登録できますので、このページをブックマークに登録しておけば、「ブックマーククリック→番号ボタンクリック」の2クリックになりますね。(ブラウザが変わった? 気にしないでください・・。)
時間外にアクセスすると、ちゃんとエラーが出ます。
特徴
- puppeteerなどの重いソフトを使用していないので、サーバリソースが小規模で済む
- シェルスクリプトかつ使用コマンドは基本ツールだらけなので、アーキテクチャ依存は一切ない(ただしshじゃなくbash依存です。zshはわかりません)
ブラウザとかを使うマシン以外に必要なもの
- Bashが使えてインターネットに出れるLinux機
- VPSのように、どこからでもアクセスできるほうが便利ですが、URLが漏れると誰でも出席確認登録が行えてしまうので、そこは注意してください。(とはいえ誰がやろうが設置した人の出席確認になってしまうだけなのでリスクは微妙)
- 適切に設置すればパスワードが漏れることはほぼ無いです
- WindowsならWSLでやるというスタンドアロンな手段もあります。
- もちろんLinuxをメイン機として使ってる人はその中でもどうぞ
- VPSのように、どこからでもアクセスできるほうが便利ですが、URLが漏れると誰でも出席確認登録が行えてしまうので、そこは注意してください。(とはいえ誰がやろうが設置した人の出席確認になってしまうだけなのでリスクは微妙)
Webサーバーの準備
今回は説明用環境にRaspberry Pi Zero+Raspbianを利用していますので、もろもろのパスとかコマンドはDebian系になっています。
違うディストリを使う場合は適宜変更してください。
また、説明に使用しているアドレスはhttp://example.com/
です。ここも適宜読み替えてください。
Apache2とnkfのインストール
WebサーバーとなるApacheと、SJISで送られてくるサイトをUTF-8で処理するための文字コード変換のためのnkfを入れます。
|
|
疎通確認
アクセスしてみて、デフォルトページが表示されていればOKです。
cgidの有効化
CGIを使えるように、モジュールの有効化をします
|
|
Apache2を再起動します
|
|
cgi-binのディレクトリの変更+シェルスクリプトをCGIとして動かせるようにする
デフォルトだと、CGIのディレクトリが/usr/lib/cgi-bin
のため、コンソールからのアクセス性が悪くなっています(HTMLファイルは/var/www/html
なので)。
そこで、ディレクトリを/var/www/cgi-bin
に変更した上で、ついでにシェルスクリプト(.sh
)もCGIだよって教えてあげます。そうしないとCGIとしてシェルスクリプトが動かないので・・・。
まず、以下がデフォルトの設定(抜粋)です。Directoryが/usr/lib/cgi-bin
になっていますね。
|
|
これを、以下のように変更します
|
|
変更を適用するために、Apache2を再起動します。
|
|
動作確認
本当にCGIとしてシェルスクリプトが動くのか実験します。
以下のシェルスクリプトを/var/www/cgi-bin/test.sh
として保存します。(cgi-binフォルダは多分無いので作ってください)
|
|
実行権限を付与します。個人サーバーでユーザーは一人しかいないので、めんどくさいので777にします。(複数人で使用してたりする場合は適宜適切に設定してください。)
|
|
これでアクセスすると、Hello Worldが表示されるはずです。(アドレスはhttp://サーバーアドレス/cgi-bin/test.sh
)
実プログラムの配置
どうやって作ったかはいらん! 早く中身をよこせ!っていう人もいる(かもしれない)ので、まずは設置と使い方を書きます。
ディレクトリ作成
HTMLを取得して保存して作業をするので、その作業ディレクトリの作成と、www-dataがそのフォルダを読み書きできるようにchownで所有者を変更します。
|
|
シェルスクリプト
このシェルスクリプトが処理の全てです。内容は後で説明します。
以下をuidとpassを自分のものに置き換えてから保存したあと、ちゃんと実行権限の付与をしてあげてください。
(この平文保存部分に関して、文句言いたい人がいるかも知れませんが、まぁCGIなのでそう簡単に漏れないのと、後述する方法でギリギリ対策(?)ができますので許してください。)
Gist: https://gist.github.com/mikuta0407/9464d86a597b9a08660387e3a9dcdff1
|
|
HTML
CGIへのリンクボタンを表示してあげるHTMLです。 例によってリンクは適宜置き換えてください。
Gist: https://gist.github.com/mikuta0407/5cda31aaf49789deaf3dcb1c95363921
|
|
Chrome拡張化する
拡張化といっても、単純に拡張機能で小さいウィンドウ内でページを表示するようにするだけです。iframeで実装しています。
実際に使用すると、こんな感じになります。
どのページにいても一瞬で「右上のボタンクリック→番号ボタンクリック」の2クリックで登録できるようになるので便利です。
一旦完成版としてChrome拡張のファイルを置いておきます。 tcuattend_chex.zip
使い方:
- Zip内を展開(フォルダ付きで)
- index.htmlにあるiframeで表示するリンクを書き換える
- manifest.jsonのpermissionsを表示するリンクのトップページのようなリンクに書き換える(http://hoge.com/attend/index.html だったら、http://hoge.com/ にしておけばOK(本当のところこれでいいのかはわからない))
- 同梱のアイコンが著作権を回避?したやつなので、お好きに変える
- Chromeの拡張でデベロッパーモードをオンにして、「パッケージ化されていない拡張機能を読み込む」から拡張機能を読ませる
(こんな感じで崩れる場合は、iframeとbody自体のwidthをいじってあげてください。)
一応Chrome拡張内のコードを載せておきます。(アイコンは自由ですよ)
hoge.hogeとかは適宜置き換えてください。
|
|
|
|
プログラム構築までの道のり
さて、アドカレなのでただ単純にプログラムを配布するだけではなく、どうやって作ったかを紹介します。
注: シェルコマンドは改行をしたものを記載していますが、見やすくするためだけなので、実際の処理は改行無しで問題ありません。
めんどくさかったところ
授業時間じゃないと、実際の登録処理の仕様が調べられないしデバッグもできない
これが最大の難所でした。
幸いログインそのものとログイン失敗は時間外でも行えるので、それは授業時間外でゆっくり行えましたが、本番処理に関してはそれができないので、演習授業系で自分の作業が終了し、少し時間が空いたときにPOSTログを収集する、といった作業をしていました。
ログイン処理
Puppeteerなどであれば、実際に要素で叩けばいいのですが、今回はシェルスクリプトでやっているので、POSTを行うためにcurlを使用するようにします。
動作を調べる
まずはChromeの検証画面を開きながら、ログインをしてみます。ログインをすると、Networkタブのindex.php
に、何をForm Dataとして送ったのかが記載されているので確認をします。
Form Dataの欄に、いろいろと記載されています。どうやら、uid, pass, menuname, module, actionの5つを送信しているようです。
そして、menunameはデコードできないと言っていますが、**実は「出席」のSJISコードをURLエンコードされたものです。**あまりにもひどい。SJISをやめろ
Form Data欄の上部にあるview URL encoded
をクリックすると、生の値が見れます。
curlでやってみる
ここまでで「https://call.off.tcu.ac.jp/index.php に対して、POSTでForm Dataとして5つのデータを投げればログインができる」ことがわかりました。
ではこれを実際にcurlでやってみます。
|
|
-d
をつけるとFormDataとして付属させることができます。
実際に実行すると、このようになります。
HTMLがShift-JISで書かれているので、UTF-8で表示しているコンソールでは化けてしまっています。シェルスクリプトでも文字列を扱っているので、UTF-8で扱えるよう、nkfを通してあげます。
|
|
無事にきれいに表示できるようになりました。
時間外のエラーが出ていますが、これが出ているということはログインに成功しているということです。
本番処理ではこのページに数字を選んだりする要素があるので、このHTMLデータを保存できるようにします。
注: シェルスクリプト作成段階ではsudoなしでシェルスクリプトを叩きたいので、ディレクトリをホームディレクトリ内で行っています。
|
|
ログイン失敗処理
動作を調べる
とりあえず適当に間違っているユーザー名とパスワードでログインしてみます。
「IDまたはパスワードに誤りがあります」という表記がありますね。
そして、「パスワード」という単語は、ログイン画面以外ではログイン失敗のときにしか表示されません。
つまり、htmlに「パスワード」という文字が含まれていれば、ログイン失敗という判定ができるわけです。
curlとgrepでやってみる
まず、上記のcurlコマンドを使用して、誤ったログインID/パスワードを入力した時の返答を保存します。
catするとこうなります。
(ちなみにこの段階で、「セッション管理はPHPSESSIDを使えばいいんだな」という事もわかってしまいますね。(どうして時間外にはPHPSESSIDがなくて、失敗ではSESSIDが出てくるんでしょうね)
このselectkamoku.htmlをgrepして、「パスワード」という文字があった場合には・・・という処理をできるようにします。
まずは単純にgrepしてみます。
出てきますね。
grepは、検索結果の有無で、ちゃんと戻り値があります。
戻り値は$?
にありますので、確認をしてみます。
($?は、?
という変数に戻り値が入っている・・・とでも思ってください。変数名の頭に$を付けてあげると変数として呼び出せます。)
grepは、「検索結果があれば0」「検索結果がなければ1」というように結果があることが前提の戻り値を返してきます。
なので、以下のようなスクリプトを利用すれば、「あった場合はある、ない場合はないと表示する」などができます。(grepが吐き出す標準出力は/dev/nullに投げて闇に葬っています)
|
|
これを実際にログインに成功した状態のHTMLでやると、
無い方の処理が走ります。
時間外処理
動作を調べる
これはログイン失敗の処理とほぼ似たようなことをやります。今回はログインできるIDとパスワードでログインします。
ログイン失敗のときしか現れない単語として「パスワード」がありましたが、時間外のときには
「現在時間」がキーワードになります。(なんとなく確実性を求めるために「現在時間に」まで使ってみています)
ちなみに時間外のページにたどり着ければ、ログインは成功していますので、本番処理でも同様に行ってください。
ifで判定してみる
|
|
ちゃんと処理ができていますね。
本番処理
動作を調べる
先程と同様、調べていきます。まずHTMLを眺めてみます。
これはChromeの開発者ツールでは欲しい情報がすべては表示されないため、curlで落としてきます。
コマンドを再掲します。
|
|
実行すると、selectkamoku.htmlに登録時の番号選択の画面のHTMLが入っているはずです。
HTMLが取得できたところで、次に出席登録時にはどんなデータが送られているかを見てみます。
先程までと同様、Chromeの開発者ツールを出して、出席登録を実際に行ったときにどんなForm dataが送られているかを見てみます。
このForm dataと、先程落としてきた番号選択の画面のHTMLを見比べてわかることとして、まずform内のhiddenとしての項目がいくつかあります。
まずはPHPSESSID
ですが、これはセッションIDですね。これはcookie扱いなので、Form dataにはありませんでした。
他の項目はすべてForm dataとして送られるデータです。
まずmodule
がSk
となっています。これはおそらくSelect kamokuの意味でしょう。
action
はProcedureAcc
となっています。手順アクセサリでしょうか。
プルダウンメニューのSelKamoku
には、option内に講義選択内容があります。これは普通に履修登録をしていれば一つしか表示されないはずです。このvalue値で授業名を判定しているのでしょう。
普段授業名を選んでいるここの中身ですね。
次にInpNo
です。これはInputNumberでしょう。出席番号ですね。単純に1~9です。
最後にsubmitButtonName
ですが、さっきログインのときもやったように、SJISエンコードされた文字を送ります。出席登録
のSJISコードがURLエンコードされて%8Fo%90%C8%93o%98%5E
となります。どうしてボタンのvalueで日本語を送信するんですか?
curlでやってみる
ではこれまで同様、curlでやってみましょう。やってることは簡単です。-dで投げるデータを変えるだけです。
なお、PHPSESSIDはForm dataではなくCookieとなるので、-dではなく–cookieを利用します。
|
|
うまくいくと、コンソールに出席確認ができたことがわかるHTMLが流れてきます。
以上がCLIでやってみる出席登録です。
では、実際にこれを使って、2クリックシステムを作っていきましょう。
実環境用ファイル作成
シェルスクリプト
さて、ここまでの作業を組み合わせると、以下のようなシェルスクリプトを書けばいいことになります。
|
|
(「2. 実プログラムの配置」に貼ったソースを小分けにして解説していきます。各処理部分の調査部分の再掲みたいになってしまっていますが・・・)
ログイン処理
まず、ログイン処理は以下のようになりますね。(最後のファイル保存場所が絶対パスで/var/www/cgi-bin/htmlcache/
になっています)
|
|
ログイン失敗処理
次にログイン判定はgrepで「パスワード」を検索した結果を利用します。
失敗だったらレスポンスヘッダを吐いたあとに、selectkamoku.htmlの中身(エラー内容もある)をそのまんま吐くようにしています。
|
|
ログイン成功→時間外処理
ログインに成功している場合は、時間外かどうかを見に行きます。
「現在時間に」という文字があったら、レスポンスヘッダと「時間外」という文字とともに、元のHTMLを吐いています。(この動作が、上記「3. 動作確認」のスクショ3枚目の時間外のときのものになります)
|
|
本番処理
ログイン成功かつ時間外でもない場合は、いよいよ本番処理です。
セッションID取得
まずはセッションIDを取得・変数代入します。
|
|
出席登録+結果表示
最後に、出席登録を行います。出席登録では毎回番号が変わります。そのたびにシェルスクリプトを書き換えるわけにも行かないため、シェルスクリプト自体の引数で番号を受け取れるようにし、その番号を利用します。
すこし脱線しますが、シェルスクリプトでの引数は、第1引数から順番に${1} ${2} ...
といったように呼び出せます。
シンプルな例だと、こんな感じになります。
|
|
今回のツールでは、出席番号以外はすべて自動で取得します。つまり、今回の場合はシェルスクリプトの第1引数に指示番号が与えられれば、それを元に全てを実行できることになります。
なので、スクリプト内ではInpNoの指定部分をInpNo=${1}
とするだけで、引数を指示番号として利用できます。
(最後に結果を返せるように、登録結果をsuccess.htmlとして保存し、吐き出しています。)
|
|
最後に、if文を閉じれば、
|
|
シェルスクリプトは完成となります。
この段階でコンソールから直接引数ありで叩くと、実際に挙動を確かめることができます。(shコマンド無しでいきなり叩けてるのは権限が777のため)
HTMLファイル
上記のシェルスクリプトにより、http://サーバーアドレス/cgi-bin/tcu_attend.sh
を呼び出せば出席登録ができるわけですが、このままでは引数が与えられず、意味がありません。
CGIでは、コマンドライン引数を与える方法として、URLの最後に?
を付けると、その後がコマンドライン引数として使えるようになります。
今回の場合は引数が1つで、かつ単純に番号だけなので、以下のようなリンクにすれば、「1番で登録」用のリンクになります。
|
|
つまり、
|
|
のようなシンプルなHTMLで、
最低限の実装ができます。
ただ、これだとあまりにも見た目が貧相なのと、スマホフレンドリーでもなく、使い勝手も地獄なので、ボタン化して使いやすくします。
実HTMLはすでに貼ってあるので、抜粋すると、
|
|
のようにinputでボタンを書けば、
こんな感じでボタン化することができます。
これを9個にして、間隔とかを整えたものが「3. 動作確認」に貼ったような画面になります。
以上でシェルスクリプトとHTMLの完成です!
おまけ1: IDとパスワードをシェルスクリプトから分離して保存する
一応、CGIとして動作しているので、shファイルを直接読まれることは無く、パスワード漏洩もしにくいとは思いますが、それでも/var/www/cgi-binというApache2管理下の内にあるファイルなので、万が一のことがあるかもしれません。
そこで、IDとパスワードは別ファイルに保管し、実行されるたびにそこから情報を拾ってきて使うようにしてみます。
(「2. 実プログラムの配置」で記載したソースにはこれは書いていません。それも含めて書くのは面倒だったので…。 ただやるとしても簡単に改変できるので、もし心配だったらやってみてください。)
まずは、ファイルを置くための場所を作ります。とりあえず/var/www/cgi-bin
じゃなければいいので、適当に/var/www/tcuattenddata/
とかを作ります。(いろいろとアレですが、ここらへんはお好きなように・・・)
|
|
そしてここにid_pass.txt
を作り、1行目にID、2行目にパスワードを記述した本当に単純なファイルを作成します。(複数ユーザーがいないからできるような運用方法ですねホントに)
次に、これらをシェルスクリプトで変数に代入するようにします。
引っ張り出し方は、
|
|
のように、sedで行数指定で取り出して行います。
これを変数に入れられればいいので、シェルスクリプトの最上位(#!/bin/bashの次)に以下のような行を追記します
|
|
これでUSERID
にIDが、PASSWORD
にパスワードが入ります。
次に、これをcurlの引数内で呼べるようにします。
本番処理のところでも書きましたが、Form Dataに与える部分はダブルクオーテーションで囲まれているので、単純に${USERID}
や${PASSWORD}
とするだけで呼び出せます。(シングルクオートだとエスケープが必要になる) (一応{ }
がなくても良い)
(ダブルクオーテーションがあるので実験も含めてダブルクオーテーション込みで出しています)
幸いにも、都市大のパスワードレギュレーションはダブルクオートとシングルクオートを弾いているので、このあたりのエスケープは無視することができます。
最後に、シェルスクリプトのユーザー名とパスワードの部分を${USERID}
と${PASSWORD}
に変更してあげれば、完成となります。
|
|
(もちろん動作確認はしてあげてくださいね!)
おまけ2(公開鯖向け): 一応robots.txtを書く
なんかの拍子にクローラに見つかっても困るので、robots.txtを書いておきます。(この手の対策ってお行儀の良いクローラにしか意味ないですけどね)
|
|
おまけ3: DiscordのBotにしてみる
DiscordのBotにして、Discordのチャットから出席登録できたら便利かもしれません。
こんな感じに。(brとか残ってるのは許してくださいガバガバ処理なだけです)
Discordは、シェルスクリプトだけでは(おそらく)文字を受け取るBotは流石に無理があります。Webhookで一方通行ならできますが。
ということで、文字を受け取るBotはNodejsを使うことになります。でも、ここまで作ってきたものはシェルスクリプトです。JavaScriptじゃありません。じゃあシェルスクリプトをNodejsから叩いてしまいましょう! (アドカレ主催に怒られそう)
ということでソースです。と言いたいこところですが、その前に説明です。
せっかくここまでWebからできる出席登録を作ったので、Botで呼ばれたらそこにアクセスすればいいわけです。それこそJSで出来ます。
できるんですが・・・ここまでシェルスクリプトでやってきてるので、ここもやっぱりシェルスクリプトとcurlでがんばります。
つまり、動作概要としては、
Discord →(文字列)→ Nodejs(文字列処理) → bash(ここまで作ったやつをcurlで叩くシェルスクリプト実行) → Web鯖(出席登録のシェルスクリプト) → bash(帰ってきた結果を若干加工してstdout) → Nodejs(Discordにsend) → Discord
というすごい遠回りをします。完全にNodejsはDiscordとbashを繋ぐためのやつになります。
まずは、ここまで作った「CGIとしてのシェルスクリプト」を叩くシェルスクリプトです。
|
|
HTML側でボタンクリックしたときのリンクを直接叩いています。シェルスクリプト実行時の引数に番号だけ入れればいいので、あとでJS側から引数ありで叩いてあげればいいわけですね。
なんかJSって言ってるとケーを思い出しますね
このシェルスクリプトが呼ばれると、さっきつくったCGIをcurlで叩いて、落ちてきたやつをgrepなりで処理してechoします。ここは別になにかForm dataとかを送るわけでもないので、-sSでログを出さないようにするだけで、後はただ叩くだけです。後一応nkfを通しています。
落ちてきたHTMLの中の授業科目/指示番号が書いてある行だけgrepで取り出しています。そのあとにsedでタグを取り除いてあげれば、先程の画像のようなことは起きませんね。
次にNodejsで動かすJavaScriptのソースです。discord.jsはインストールしてくださいね。
|
|
受け取って、!attendから始まる場合だけ処理して、数字を取り出してシェルスクリプトにぶん投げる感じです。
参考:
あとはNodejsで回してあげれば、Discordから出席登録ができるようになります。
〆
いかがだったでしょうか。
こんな感じで、シェルスクリプトだけでもいろいろとWebツールは作れます。そしてそれをNodejsから叩く方法を確立させれば、シェルスクリプトだけで何でもできます(?)
JavaScriptもいいですけど、ShellScriptもいいんですからね・・・?
これを読んだ都市大生の皆様は、1から作らなくてもいいので、今回のプログラムをぜひ使ってみて下さい!! WSLでもいいですしmなんなら家に一個くらいラズパイ転がってますよね!(???)
ということで、明日はうめさんの「どうすりゃいいの~♪」です。どうなるんですかね? (あとこのアドカレはなんですか?)