【OSSリーディング企画】BootstrapのJavaScriptを読み解く【その1】
いきさつ
Bootstrap、すごい便利。
htmlタグさえドキュメントどおりにおけば*1、
カルーセルは動くわモーダルは開くわアラートは出るわ、、、
いつも本当にお世話になっております。
ただ、、、ただね、
「なんで動くのか」について全然考えたことがなかったんですよ。
細かいところを調整しようと思った時に、
中がどうなっているのか分かっていないので、
手が完全に止まってしまう。。。
これじゃあ「ライブラリを使っている」のではなく、
「ライブラリに使われている」。そんな気がしてならない。。。
というわけで、OSSリーディングを始めました。
対象OSS
今回はBootstrapのモーダル周りについて読んでいきます。
読むファイルはこれ。
すでにビルドされている生のJavaScript。
どうせならJavaScriptも軽く復習したい。
そういう思いもあります。
OSSを読むにあたって大切なこと
「全部は読まない。読みたいとこだけ」
なんだか懐かしい響きですね。
OSSを、はじめから全部読もうと思ってはいけません。
なんせOSS(今回はBootstrap)は、
「1000人以上の人が」
「何年もかけて」
開発をしているライブラリです。
はじめから全部読むのはツライツライ。
今回のゴール
「何もしていないのに(JS書いていないのに)なんでボタンを押したらモーダルが開くのか」
に絞って読んでいきます。
モーダルを開く処理までは読みません。
「ボタンを押したら」という部分に着目し、
ボタンに対してclickイベントを登録している部分を読み解きます。
また、その途中で即時関数が使われているので、即時関数についても簡単に触れます。
読んでいきます
Modalモジュールが宣言されている部分
/** * -------------------------------------------------------------------------- * Bootstrap (v4.1.1): modal.js * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * -------------------------------------------------------------------------- */ var Modal = function ($$$1) { // いろいろな処理 }($);
だいたい1930行あたりにModalモジュール(モジュールという言い方が正しいのかどうかは微妙)が宣言されていますね。
Bootstrapでは様々な機能が使えますが、
これらはみなモジュールとして提供されています。
すごい乱暴に書くとこんな感じ。
var Modal = function ($$$1) { // いろいろな処理 }($); var Carousel = function ($$$1) { // いろいろな処理 }($); var Alert = function ($$$1) { // いろいろな処理 }($); exports.Alert = Alert; exports.Carousel = Carousel; exports.Modal = Modal; ...
各モジュールで使われる変数は関数の中で定義され、カプセル化されています。
それにより、他のモジュールで使われている変数とぶつかったりすることはありません。
かしこい(小並感)。
exportsの意味について
そして、
exports.Alert = Alert; exports.Carousel = Carousel; exports.Modal = Modal;
exportsにはwindow.bootstrapオブジェクトが入っているよ。*2
つまり、もっと分かりやすく書くとこんな感じ。
window.bootstrap.Alert = Alert; window.bootstrap.Carousel = Carousel; window.bootstrap.Modal = Modal;
なので、コンソール上でwindow.bootstrapすると、
これらのモジュール(正確にはコンストラクタ)にアクセスすることができるよ。
Modal用のボタンにクリックイベントをバインドしている部分
さて、Modalを開くためのボタンのhtmlを確認してみるよ。
<button type="button" data-toggle="modal" data-target="#exampleModal"> Launch demo modal </button>
これだけでいいらしいよ。ふざけてるね!
重要なのは、
data-toggle="modal"
という部分。
これを目印にして、
このdata属性を持っている要素にclickイベントをバインドしているよ。
こんな感じ!
// 2500行あたり $$$1(document).on('click', '[data-toggle="modal"]', function (event) { // クリックした時に起こるイベント }
え?
でもなんで勝手にclickイベントがバインドされるかって?
それは、この処理が即時関数の中で実行されているからだよ!
var Modal = function ($$$1) { // 即時関数の中 $$$1(document).on('click', '[data-toggle="modal"]', function (event) { // クリックした時に起こるイベント } }($);
一度JavaScriptが読み込まれると即時関数は勝手に実行されるので、
html上にdata-toggle="modal"さえ書いておけば、
勝手にclickイベントがバインドされるんだね!
即時関数の使い所
「一度しか実行されない処理」を即時関数で書くことが多いよ。
今回のように、
「特定の要素に対してイベントリスナを登録」
という処理は1度だけの実行でいいから、よく使われるよ。
即時関数のメリット
即時関数で処理を包むことにより、
変数のスコープが即時関数内に限定されるので、
グローバルスコープを汚さないよ!
巷のライブラリでも即時関数はよく見るよ。みてみてね!
まとめ
というわけで今回は、
モーダルを開くためのボタンに対してclickイベントが登録されている部分を確認してみました。
data-toggle='modal'さえ持っていればよいので、
ボタンだろうがlistだろうがdivだろうがなんでもモーダルが開けそうですね。
BootstrapのJavaScriptは、
定数がきちんと宣言されていたり、
jQueryからモーダルを操作できるようなAPIを提供していたり、
複数のモーダルを開くことができるような実装の仕方をしていたりと、
すごく勉強になる部分が多いので、ひきつづき読んできたいと思います。
*1:JavaScript書かなくてよいことも多い
*2:気になる人は1行目から読んでみよう。Boostrapがブラウザ上で実行される場合、exportsにはwindow.bootstrapオブジェクトが代入されているはずだ
つるのおんがえし〜「決して、この部屋をのぞかないで(Permission denied)」〜
この記事のゴール
この記事を読めば、
$ ls hoge d rwx-----x. 1 owner group 4096 Dec 22 15:48 hoge
この
rwx-----x
が読めるようになる(ハズ)です。
あらすじ
おじいさんが罠にかかっているつるを見つけました。
苦しんでいるつるがかわいそうだったので、おじいさんは助けてやりました。
その日の夜は大雪になり、とても寒い夜でした。
コン、コン。家の扉が叩かれる音がします。
夜、一人のおんながおじいさんの家にやってきました。
そのおんなは一晩泊めてほしいといいます。
やさしいおじいさんはおんなを家に招き入れることにしました。
家に招き入れた
おじいさんは手元の端末でおんなをlinux userに追加しました。
[root@ie ~]# useradd tsuru
おんながはたを折り始めた
おんなはいろいろ良くしてもらったお礼に、
はた(美しい布)をおりたいといいました。
おんな「はたをおりあげるまで、決してのぞかないでください」
[tsuru@ie ~]$ cd tsuru
といって、おんなはへやにはいっていきました。
おじいさんは好奇心に耐えられなかった
おじいさんは震える手で端末にこう打ち込みました。
[jiisan@ie ~]$ cd tsuru
しかし、
-zsh: cd: tsuru/: Permission denied
部屋には入れませんでした。
[jiisan@ie ~]$ ls tsuru drwx------. 2 tsuru tsuru 4096 3月 27 07:26 2018 tsuru
おじいさん「なるほど。。。こりゃ入れんわい」
おじいさんは納得しました。
なぜ、入れなかったのか
まず、おじいさんがuseraddしたことにより、
tsuruディレクトリが作成されました。
useraddしたときにできるこの部屋の所有者は「tsuru」になります。
ですので、こうなります。
この部屋は、作成された時から以下の要件をみたしています。
・つる[owner]は部屋を自由にする(見れる、いじれる、入れる)ことができる
・おじいさんとおばあさん[other]は入れない
つまり、こういうことですね。
もうすこしlinux風に書くとこんな感じです。
さらに、linuxのファイルは「所有者」だけでなく「所有グループ」をという情報を持っています。
これにより、
「つるの親族」というグループのユーザだけ部屋に入ることを許可できたりします。
useraddしたときにできる部屋の所有グループは、「tsuru」です。
これで完成です。
ちょっと見方を変えて、、、
ここでlsです!
「owner」ができること
「group」ができること
「other」ができること
をまとめたものを、パーミッションと言います。
おじいさんは強硬手段に出た
中でなにをやっているのか、気になって仕方がありません。
じいさんの秘蔵のこれくしょんが、
つるによって紐解かれていしまっているかも知れません。
3日悩んでおじいさんは決心しました。
「パーミッションを変えよう」
(つづく)
【Java言語で学ぶリファクタリング入門】クラスの抽出
いきさつ
昔書いたコードが悲しい感じだった&どこから手を付ければよいかわからない感じだったので、
リファクタリングの知識が欲しかった。
そしたら手元にこの本があった。
リファクタリング前
Playerクラス
class Player { private _currentMedia; private _musicData; private _videoData; public setMedia() { }; public play() { }; public loop() { }; public stop() { }; private playMusic() { }; private loopMusic() { }; private stopMusic() { }; private playVideo() { }; private loopVideo() { }; private stopVideo() { }; }
playerが責務を持ちすぎ。
- musicの再生・停止・ループ方法を知っている
- videoの再生・停止・ループ方法を知っている
なにがしんどいのか
新しいmediaを追加するたびにmethodを追加する必要がある。
playerが再生されるものに対しての知識を持ちすぎているので、密結合。
リファクタリング方法
クラスの抽出を行なう。
リファクタリング後
新たにMusicMediaとVideoMediaクラスを作った。
Mediaクラス
/** 共通インターフェース */ interface Playable { play(); loop(); stop(); } /**MusicMedia */ class MusicMedia implements Playable { public play() { }; public stop() { }; public loop() { }; } /** VideoMedia */ class VideoMedia implements Playable { public play() { }; public stop() { }; public loop() { }; }
Playerクラス
class Player { private _currentMedia: Playable; public setMedia(media: Playable) { this._currentMedia = media; } public play() { this._currentMedia.play(); } public stop() { this._currentMedia.stop(); } public loop() { this._currentMedia.loop(); } }
何がうれしいのか
新たにMediaを追加する時に、既存のコードに手を加えなくて良い(拡張に対して開いている)
既存のmediaの修正をする時に、クラス内で修正が収まる(修正に対して閉じている)
PlayerクラスはPlayableインターフェースを実装しているクラスなら何でも扱えるので、疎結合
【Node.js】Expressをinstallして起動(npm start)するまで
いきさつ
少し作りたいものがあったので、
Expressをinstallして起動してみる
手順
express-generatorをグローバルインストール
$ npm install express-generator -g
expressコマンドでプロジェクトを作成
$ express restapi
必要なnode_moduleをinstall
$ cd restapi; npm install
アプリケーションの起動
$ npm start > restapi@0.0.0 start /home/vagrant/dev/restapi > node ./bin/www
ポートにアクセス
ipアドレス:3000
にアクセスすると、
と表示され、起動が確認できる。
おまけ
ポート3000がLISTENになっているか見てみる。
$ netstat -antp tcp 0 0 :::3000 :::* LISTEN 22175/node
ポート3000をnodeプロセスがLISTENしている。
【CTF】サーバをハックする〜Simple Auth IIとSQLiteと〜
いきさつ
セキュリティの勉強も兼ねてCTFの問題を解いてみた。
サーバをハックするより前にソースコードが見えているので
ハックするような大したことはしていない。
問題
20ptなので超簡単ということだが。。。?
考えたこと
ログイン画面
適当なID&パスワードを入れ、ログインしてみる。
ソースコード
<?php function h($s) { return htmlspecialchars($s, ENT_QUOTES, 'UTF-8'); } if (!isset($_POST['id']) or !is_string($_POST['id'])) $_POST['id'] = ''; if (!isset($_POST['password']) or !is_string($_POST['password'])) $_POST['password'] = ''; $try = false; $ok = false; if ($_POST['id']!=='' or $_POST['password']!=='') { $try = true; $db = new PDO('sqlite:database.db'); $s = $db->prepare('SELECT * FROM user WHERE id=? AND password=?'); $s->execute(array($_POST['id'], $_POST['password'])); $ok = $s->fetch() !== false; }
postされたidとpasswordでuserテーブルを検索、
該当レコードが存在する場合はログイン成功ということらしい。
SQLインジェクション使える?→使えないよね
プリペアドステートメントを使用しているので、
execute内に渡された値はエスケープされる。
この時点で
やれることがなくなってしまった。。。
ググる
この記事をみると、
「ダウンロード」という言葉がある。
どういうこと?
SQLiteについて調べる
どうやらSQLiteのDB情報は、
****.dbというファイル形式で保存されるらしい。
そして、
<?php //... $db = new PDO('sqlite:database.db');
このsqliteの後で指定されているのはDBのファイルで、
相対パス指定されているということらしい。
DBファイルをダウンロード
sourceのurlのauth.phpの部分を、database.dbに変えるとDBファイルがダウンロードできる。
(普通じゃ考えられないけど)
SQLiteで読み込む
$ sqlite3 ~/Downloads/database.db sqlite>
適当なsqlを発行
phpソースコードから、userテーブルが有ることがわかっているので、
selectで全部持ってくる。
sqlite> select * from user; root|FLAG_****
でてきた。
まとめ
この手の問題を特には様々な知識が求められる。
今回で言えば、
SQLiteの接続方法
プリペアドステートメント
SQLインジェクション(使わなかったけど)
などなど。
なかなか勉強になった。
#Cでsocket通信をしてみる。ついでにnetstatでlistenしているポートを調べる。
C#でソケット通信
職場でC#を使うのと、
ソケット通信に興味があるのでちょっと練習してみた。
ソースコード
server側
using System; public class Server { public static void Main() { string ipString = "127.0.0.1"; System.Net.IPAddress ipAdd = System.Net.IPAddress.Parse(ipString); int port = 2001; System.Net.Sockets.TcpListener listener = new System.Net.Sockets.TcpListener(ipAdd, port); listener.Start(); Console.WriteLine("Listenを開始しました({0}:{1})。", ((System.Net.IPEndPoint)listener.LocalEndpoint).Address, ((System.Net.IPEndPoint)listener.LocalEndpoint).Port); // 接続要求があったら受け入れる System.Net.Sockets.TcpClient client = listener.AcceptTcpClient(); Console.WriteLine("クライアント({0}:{1})と接続しました。", ((System.Net.IPEndPoint)client.Client.RemoteEndPoint).Address, ((System.Net.IPEndPoint)client.Client.RemoteEndPoint).Port); // NetworkStreamを取得データの流れ System.Net.Sockets.NetworkStream ns = client.GetStream(); ns.ReadTimeout = 10000; ns.WriteTimeout = 10000; System.Text.Encoding enc = System.Text.Encoding.UTF8; bool disconnected = false; System.IO.MemoryStream ms = new System.IO.MemoryStream(); byte[] resBytes = new byte[256]; int resSize = 0; do { //データの一部を受信する resSize = ns.Read(resBytes, 0, resBytes.Length); //Readが0を返した時はクライアントが切断したと判断 if (resSize == 0) { disconnected = true; Console.WriteLine("クライアントが切断しました。"); break; } //受信したデータを蓄積する ms.Write(resBytes, 0, resSize); //まだ読み取れるデータがあるか、データの最後が\nでない時は、 // 受信を続ける } while (ns.DataAvailable || resBytes[resSize - 1] != '\n'); //受信したデータを文字列に変換 string resMsg = enc.GetString(ms.GetBuffer(), 0, (int)ms.Length); ms.Close(); resMsg = resMsg.TrimEnd('\n'); Console.WriteLine(resMsg); if (!disconnected) { //クライアントにデータを送信する //クライアントに送信する文字列を作成 string sendMsg = resMsg.Length.ToString(); //文字列をByte型配列に変換 byte[] sendBytes = enc.GetBytes(sendMsg + '\n'); //データを送信する ns.Write(sendBytes, 0, sendBytes.Length); Console.WriteLine(sendMsg); } ns.Close(); client.Close(); Console.WriteLine("クライアントとの接続を閉じました。"); listener.Stop(); Console.WriteLine("Listenerを閉じました。"); Console.ReadLine(); } }
clinent側
using System; public class Client { public static void Main() { Console.WriteLine("入力してください"); string sendMsg = Console.ReadLine(); if (sendMsg == null || sendMsg.Length == 0) { return; } string ipOrHost = "127.0.0.1"; int port = 2001; System.Net.Sockets.TcpClient tcp = new System.Net.Sockets.TcpClient(ipOrHost, port); Console.WriteLine("サーバー({0}:{1})と接続しました({2}:{3})。", ((System.Net.IPEndPoint)tcp.Client.RemoteEndPoint).Address, ((System.Net.IPEndPoint)tcp.Client.RemoteEndPoint).Port, ((System.Net.IPEndPoint)tcp.Client.LocalEndPoint).Address, ((System.Net.IPEndPoint)tcp.Client.LocalEndPoint).Port); System.Net.Sockets.NetworkStream ns = tcp.GetStream(); ns.ReadTimeout = 10000; ns.WriteTimeout = 10000; System.Text.Encoding enc = System.Text.Encoding.UTF8; byte[] sendBytes = enc.GetBytes(sendMsg + '\n'); ns.Write(sendBytes, 0, sendBytes.Length); Console.WriteLine(sendMsg); System.IO.MemoryStream ms = new System.IO.MemoryStream(); byte[] resBytes = new byte[256]; int resSize = 0; do { resSize = ns.Read(resBytes, 0, resBytes.Length); if (resSize == 0) { Console.WriteLine("サーバーが切断しました。"); break; } ms.Write(resBytes, 0, resSize); } while (ns.DataAvailable || resBytes[resSize - 1] != '\n'); string resMsg = enc.GetString(ms.GetBuffer(), 0, (int)ms.Length); ms.Close(); resMsg = resMsg.TrimEnd('\n'); Console.WriteLine(resMsg); ns.Close(); tcp.Close(); Console.WriteLine("切断しました。"); Console.ReadLine(); } }
コンパイル
mcs client.cs mcs server.cs
実行
サーバの実行
$ mono server.exe Listenを開始しました(127.0.0.1:2001)。
クライアントの実行
$ mono client.exe 入力してください hello world
サーバの反応
クライアント(127.0.0.1:32924)と接続しました。 hello world 11 クライアントとの接続を閉じました。 Listenerを閉じました。
まとめ
だからlocalhost上でポートさえ違えば、通信ができる。
今回は
・サーバ IP 127.0.0.1 ポート 2001
・クライアント IP 127.0.0.1 ポート 32924
で通信を行った。
なお、クライアントのポートは通信毎に自動で割り振られる。
netstatでポートがLISTENしているかどうかを調べてみる
$ netstat -anpt tcp 0 0 127.0.0.1:2001 0.0.0.0:* LISTEN 14145/mono
monoというプロセスが、 2001ポートを使って通信していることがわかる。
-aオプション
LISTEN状態も含むすべてのソケットを表示する。
-nオプション
IPアドレス・ポート番号をそのまま表示する。
これがない場合、ドメイン名として表示されたり、ssh・httpdとか表示されたりする。
-pオプション
通信しているプロセスを表示する。
iphoneを探していたはずなのに、いつしか私は死に場所を探していた。
※ 学生の頃の話です
その1
眩しい。どこかで蛍光灯が光っている。
背中にはぺらぺらの布団と、硬い床の感覚。
目を開いた。見覚えのある部屋。
どうやら無事に家に帰ってきたらしい。
その2
眼鏡を探す。ない。
スマホを探す。これもない。
目がさめると、私の生活インフラの大部分を失っていた。
その3
「彼らを取り戻さなくては」そう思った。
残念ながらこのような展開には慣れていた。
酒をのんで記憶を飛ばし、現場に何かしらの置き土産をするのは割りとよくある話だ。
少なくとも私には。
これだけ簡単に記憶が飛ぶのだから、人間の記憶なんてもろいもんだなーと思う。
メモリだったら揮発性。どうせ大した記憶じゃないから別に揮発していいんだけれど。
その4
画面上にiphoneの現在位置が表示される。
その5
指し示された位置は、入ったことのない森だった。
その6
うそでしょ。そう思う。
昨日は鴨川で楽しくお花見をしていた。
なんでこんな「河原から小一時間ほど歩いた森の中」にiphoneが。
でも確かに森の中だった。
周りには目印がなにもなく、あるのは等高線。
丁度緩やかな谷になっているところに、たしかにiphoneが存在している。
少なくともGPS上ではそうなっていた。
その7
多分位置情報がずれてるんだ。とにかく現場に行こう。
そしたら近くの公道にiphoneが落ちているはずだ。
そう思った私は、Macを持って現場に向かった。
その8
現場近くでMacを広げる。
iphoneには緊急モードというものが搭載されていて、
それをMacから遠隔で起動することができる。
起動すると、
画面ロックしたり、
iphoneの画面上に連絡先電話番号を表示したり、
……iphoneに断末魔の叫びを上げさせることができる。
それはただの甲高い発信音。でも聞くたびに「これ断末魔だよなぁ」と感じる。
その9
森の近くの公道にて、その時は来た。
Mac画面上のボタンを押した。
iphoneが叫んだ。
…その断末魔は、森の方から聞こえた。
声のする方へ、少しづつ足をすすめる。
足元が悪い。どうしてこんなところに昨日の私は存在していたのか。
どう考えても死にたかったとしか思えない。
無意識に死にたかったとしか、思えないのだ。
その10
断末魔が近くなる。
更に悪くなる足場。
鳴り続ける断末魔。
ついに……見つけた。
その11
iphoneと。その横に眼鏡が見つかった。
まるで昨日の私は、上空からUFOに吸い上げられ、
眼鏡とiphoneをその場に置き忘れてしまったようだ。
異様な光景がそこにあった。
その12
昨日そこで何があったのか、いろいろな人の証言を確認した。
わかったのは、
「河原で呑んでいたお前すごいニコニコしてた」
「でも突然フラフラと何処かに歩きだした」
という2点だけ。
その13
僕は考えるのをやめた。
酒もしばらくやめた。
その14
3日後に日本酒を呑んだ。美味しかった。