【TypeScript】Promise.allで頑張る
こんばんは。今回はPromise.allを使ってみます。MongoDBも使います。
記載の通り、配列に入ったPromiseを全部解決するまで待つという動きが出来るようになります。
Promise.allを使わないやりかた
import {MongoClient, MongoClientOptions} from "mongodb"; const uri:string = "mongodb://127.0.0.1:27017" const options:MongoClientOptions = {useUnifiedTopology:true,useNewUrlParser:true} const client = new MongoClient(uri,options); const dbName = "__test__"; const collectionName = "test_collec"; const initing = async (client:MongoClient) => { await client.connect(); const __test__ = client.db(dbName); const cursor = __test__.collection(collectionName).find(); const datas = []; while(await cursor.hasNext()){ datas.push(await cursor.next()); }; console.log(datas); await client.close(); return ; }; initing(client);
今回の話で最も重要なところはここの部分になります。
const datas = []; while(await cursor.hasNext()){ datas.push(await cursor.next()); }; console.log(datas);
datas
配列にDBから取得してきたデータを挿入してきているのですが、DBから取得してきている間はawaitしているので、その間の処理は完全に止まっています。
Promise.allを使ったやり方
import {MongoClient, MongoClientOptions} from "mongodb"; const uri:string = "mongodb://127.0.0.1:27017" const options:MongoClientOptions = {useUnifiedTopology:true,useNewUrlParser:true} const client = new MongoClient(uri,options); const dbName = "__test__"; const collectionName = "test_collec"; const initing = async (client:MongoClient) => { const cli = await client.connect(); const __test__ = cli.db(dbName); const cursor = __test__.collection(collectionName).find(); const datas = []; const count = await cursor.count(); for (let i = 1; i <= count; i++){ datas.push(cursor.next()); }; const promised = await Promise.all(datas); console.log(promised); await cli.close(); return ; }; initing(client);
変更部分は主にここ
const datas = []; const count = await cursor.count(); for (let i = 1; i <= count; i++){ datas.push(cursor.next()); }; const promised = await Promise.all(datas); console.log(promised);
while文をやめて、for文にしています。これはwhile内にあったcursor.hasNext()
の結果を待つという処理をやめて、単純にデータ内にある個数分データを取得するという判定に変更しています。そのfor文内で行っているdatas.push()
は、cursor.next()に対して結果の取得を待つという処理がなされていません。これらを取得するまでに待っていたものはデータの個数を持っているcursor.count()
だけです。
- while(await cursor.hasNext()){ - datas.push(await cursor.next()); - }; + const count = await cursor.count(); + for (let i = 1; i <= count; i++){ + datas.push(cursor.next()); + };
datasの中身は[Promise { <pending> }, Promise { <pending> }, Promise { <pending> }]
と言った感じになります。
そして、Promise.all()
でPromiseだらけのdatas
の取得を待ちます。この取得待ちは並列で行われるので、取得の待ち時間が少なくて住みます。そして、その結果をpromised
変数に代入してあげます。
ソースコード
今回のソースコードはこちら。MongoDBをローカルで建ててない人向けにDockerも作っています。
このように並列処理を書くのだ
沢山のPromiseがあるときは、一旦全部を配列に入れてあげて、Promise.allを使ってあげると便利に良さそう。
【TypeScript】Puppeteerで言語を固定できるみたい
こんばんは、葛の葉です。
今回は疲れているので省エネです。
起こったこと
PuppeteerでE2Eテストをやっててヘッドレスだとテストが通らないのに、ヘッドレスをやめるとテストが通るという現象がおきました。そしてそれはタイトルの文字が正しく表示されるかどうかをテストするものでした。
ヘッドレスとそうでないのの違いがよくわからず、しばらく詰んでおりました。
原因は
原因はPuppeteerの言語を固定していなかったからでした👀ブラウザに設定された言語によって英語と日本語とが変わる仕様にしていたので、正とする日本語のタイトル文が表示されてなくてパスしていなかったわけですね。
ヘッドレスモードをオフにすると、不思議なことに日本語で固定されてました。osに依存したりするのかな?
こうした
言語を日本語で固定しました。
import { launch } from "puppeteer"; (async () => { const browser = await launch({ args: ['--lang=ja'] }); // 処理が続く })();
これはPuppeteerの書き方というよりchromiumの起動オプションらしいです。それと、英語ページは実際にまだ使ってないのでテストしないようにしました。
参考
【TypeScript】URLパラメータからJSを実行する
こんばんは、葛の葉です。
今回はURLからjsを起動してみます。悪用厳禁ってやつですね。
こんなイメージ
hogehogeというパラメータに渡したデータをhtml上に表示するようなWebサーバーを作ります。今回もnodeの基礎的なライブラリであるhttpを使います。また、htmlパラメータを出力するために、urlモジュールのparseも使用します。
import http from 'http'; import { parse } from 'url' const server = http.createServer(); server.on('request', (req:http.IncomingMessage, res:http.ServerResponse) => { const hogehoge = parse(req.url || "", true); if (hogehoge.query["hogehoge"]){ res.end(hogehoge.query["hogehoge"]); } else { res.end('Insert hogehoge params'); } return; }); server.listen(9000, ()=>{ console.log('listen http://127.0.0.1:9000\n'); });
素直にやるとこうなるっちゃね。
JavaScriptを入れてみると。。。
http://127.0.0.1:9000/?hogehoge=<script>alert('aaa')</script>
といった感じにhogehogeパラメータの中にJSを入れてみる。そうすると以下のようにアラートが表示できる。
エスケープをしましょうというお話。
<
や &
(html特殊文字コード)といったhtmlの操作が出来るような文字はエスケープしてしまいましょう。
参考
【TypeScript】Mongodbが持つ数字を正規表現で取ろうとしたら出来なかった
こんばんは葛の葉です
mongodbのフィールドにある数字を取り出そうと正規表現を使ったのですが、全くヒットせず、正規表現の書き方が間違ってるのかなんなのかがわからず、暫く格闘を続けるというアホみたいなことをしていました。
MongoDBの公式について
Provides regular expression capabilities for pattern matching strings in queries. MongoDB uses Perl compatible regular expressions (i.e. “PCRE” ) version 8.42 with UTF-8 support.
やりたかったこと
{ name: 'kuzunoha', status: 200 }, { name: 'pontaro', status: 230 }, { name: 'daigoro', status: 309 }
こういうデータに対してstatusの3桁目が2のデータを取得してきたかった。
import { MongoClient } from "mongodb"; const uri = 'mongodb://127.0.0.1:27017'; const dbName = '__test__'; const collectionName = 'test'; (async()=>{ const client = await MongoClient.connect(uri, { useUnifiedTopology: true }); const __test__ = client.db(dbName); const cursor = __test__.collection(collectionName).find({ status: /2[0-9]./ }); while(await cursor.hasNext()){ // 検索がヒットしないのでwhileが動作しない const data = await cursor.next(); console.log(data.name); }; await __test__.collection(collectionName).drop(); await client.close() })();
StackOverFlowには
ちょうど同じところで躓いている人がいたみたい。文字列は正規表現出来るけど、数字は正規表現で広くことが出来ないみたい。aggregate
を使うことで文字列となるフィールドを追加して、そのフィールドに正規表現を使うようにします。
こうするみたい
- const cursor = __test__.collection(collectionName).find({ - status: /2[0-9]./ - }); + const cursor = __test__.collection(collectionName).aggregate([ + {$addFields: {statusStr: {$toString: '$status'}}}, + {$match:{statusStr:/2[0-9]./}} + ]);
今回のソースコード
【Vue.js】ちょっとVueを触っている
こんばんは、葛の葉です。
Vue.jsの本を読んでちょっと勉強中です。結構楽しいですね。個人開発のWebアプリケーションを作ろうと思っているのですが、Vueでこさえることが出来たらなぁと思ってます。
ちょっとしたFizzBuzzのゲームを作る。
ランダムで出力される3つの数字の合計がFizzBuzzで言うところの何に当たるかを当てるゲームをつくります。1時間で完成。
算出プロパティ
Vueを勉強していてちょっと聞き慣れない言葉だと思ったのが算出プロパティというものです。
## script.js 8 - 12 lines computed: { result: function () { return this.questions.one + this.questions.two + this.questions.three; } },
computed
というVueのプロパティにresultという関数を渡しています。一方で似たようなプロパティのmethod
というものも存在していて、一見ではどう違うのかがよくわからないと思います。算出プロパティについて、公式では以下に書かれています。
算出プロパティは依存するデータが存在していて、その依存するデータの値が変われば、出力したい値も変わるような、動的な値を使用したい場合に使うことが多いみたいです。キャッシュされるので、算出プロパティのほうが都度処理を行うmethodよりコストが低くなります。今回の出力している3つの数字の合計値なんかは算出プロパティに相当するようなものではないでしょうか。
【TypeScript】CookieとSessionとSessionIDを使ったログイン情報の保持
こんばんは、葛の葉です。
今回はCookieとSessionとSessionIDを使ったログイン情報の保持を実施します。
前述
普段我々が使用しているWebページでは、ログインを必要とするページが多いかと思います。ログインに一回成功するとその成功したログイン状態というステータスになり、ちょっとした時間は改めてログインをする必要がありません。しかし、Httpはステートレスであり、状態を保持するということが出来ません。そのため、どこかにその情報を保存しておくのですが、今回はそのうち、SessionIDを使った方法を記載してみます。
今回のソースコード
Cookieとは
WebサーバーがWebブラウザにデータを送り、ブラウザがその値を保持します。それをCookieといいます。CookieはWebブラウザの機能の一部であり、Cookieを使えないWebブラウザもあることでしょう(現行のWebブラウザはほとんど持っている)。CookieはNameとValueからなります。複数のNameを持つことが出来ます。また、Cookieには様々な設定を施すことができ、例えば保持する時間や、そのCookieを参照できるドメインの指定、Https通信の時のみ保持させる、と言ったものがあります。
Sessionとは
SessionとはWebサーバーがデータベースなどをもちいて、クライアントとサーバー間で保持しておくべきデータを一時的の保管するならわしをいいます。つまり、WebサーバーやWebブラウザにSessionという機能があるのではありません。Webサーバーがプログラムやミドルウェアを通じてデータベースにデータを保持するという点がCookieとの最大の違いになります。また、Sessionは基本的にログイン状態かどうかというステータスを保持するために使用され、長時間は保存されません。
SessionIDとは
SessionIDとはSessionで保存されたデータを取り出すためのKeyとなるIDのことであり、これはCookieとして保存されます。Cookieに保存されたSessionIDはWebブラウザに保存され、都度、Webサーバーから要求を受けるたびに送信します。WebサーバーはそのSessionIDがデータベース等に存在するかどうかを都度確認をし、存在すればログインは正常であるとして、ログイン状態である旨をクライアントに示します。SessionIDが漏洩したり、あるいは推測されてしまうような単純な値であると、悪意を持った人が、一般ユーザーのログイン状態であるステータスを盗むことが出来、それを使って悪事を働くことが出来てしまうため、留意する必要があります。
WebサーバーからCookieを保持させる。
今回もTypeScriptのWebサーバーhttp
を使ってCookieを保持させてみます。
import http from 'http'; const server = http.createServer(); server.on('request', (req:http.IncomingMessage, res:http.ServerResponse) => { res.writeHead(200, {'Set-Cookie': 'myCookie=hogehoge;'}); res.end('200 OK\n'); return; }); server.listen(9000, ()=>{ console.log('listen http://127.0.0.1:9000\n'); });
4行目res.writeHead(200, {'Content-Type':'text/plain', 'Set-Cookie': 'myCookie=hogehoge;'});
こちらがHttpResponseとして返却するHeaderを記載している部分になります。このヘッダーを受け取ったWebブラウザはその情報をCookieに保存します。検証モードがついているWebブラウザならすぐに確認できます。Set-Cookieというレスポンスを受けたブラウザは、その値に書かれた<Name>=<Value>
としてブラウザに保存します。先に、Cookieを保存する期限を設けることが出来ると記載しましたが、そのオプション指定が特別存在しない場合はブラウザが閉じられるまでの間となります。
SessionIDの発行とCookieに保存。
Cookieの保存方法は先の記載のとおりなので、今回はSessionIDを保存させます。SessionIDはログイン状態を保持する重要なものなので、推測されないような値が正しいです。が今回は適当に作ります。
server.on('request', (req:http.IncomingMessage, res:http.ServerResponse) => { const sid = createSessionId(); res.writeHead(200, {'Set-Cookie': `sessionId=${sid};`}); res.end('200 OK\n'); return; }); const createSessionId:() => string = () => { const newSessionId =`hogehoge_${new Date().toISOString()}`; nodeCache.set(newSessionId, "", 60); return newSessionId; }
今回はNodeCacheというライブラリを使います。KVSのようなもののようです。KeyにSessionIDを渡しておき、中身は空の値にしておきます。
10行目nodeCache.set(newSessionId, "", 60);
のset関数
は以下になってます。
## 第一引数がkey ## 第二引数がvalue ## 第三引数がオプションで保存秒数
SessionIDはNodeCache内とCookie内に保存されました。これをログインが成功したタイミングで渡すようにしてあげます。また、ログインが必要なページにはCookieからSessionIDを取得し、NodeCache内に検索をかけます。SessionIDがNodeCache内に存在していなければログイン状態が保持されていないことになるので、ログインを求めるような表示をします。
import http from 'http'; import NodeCache from 'node-cache'; const server = http.createServer(); const nodeCache = new NodeCache(); server.on('request', (req:http.IncomingMessage, res:http.ServerResponse) => { switch (req.url){ case '/': const cookies = cookieParser(req.headers.cookie); const sessionId = cookies.sessionId; if (nodeCache.has(sessionId)){ res.writeHead(200); res.end('200 Loggined\n'); } else { res.writeHead(200); res.end('200 Not Loggined\n'); } break; case '/login': const sid = createSessionId(); res.writeHead(200, {'Set-Cookie': `sessionId=${sid};`}); res.end('200 Cookie Set\n'); break; default: res.writeHead(404); res.end('404 Not Found\n'); break; } return; }); interface Cookies { sessionId:string } const cookieParser:(str:string|undefined) => Cookies = (str)=> { if(!str) return {sessionId:''}; const cookies = {sessionId:''}; str.split(';').forEach((cookie)=>{ let parts = cookie.split('='); let key = parts[0]; let value = parts[1]; let part = {[key]: value} Object.assign(cookies, part); }); return cookies; } const createSessionId:() => string = () => { const newSessionId =`hogehoge_${new Date().toISOString()}`; nodeCache.set(newSessionId, "", 60); return newSessionId; } server.listen(9000, ()=>{ console.log('listen http://127.0.0.1:9000\n'); });
http://127.0.0.1:9000
にアクセスすると200 Not Loggined
と表示されます。http://127.0.0.1:9000/login
にアクセスするとCookieにSessionIDがセットされます。再びhttp://127.0.0.1:9000
にアクセスすると200 Loggined
と表示されます。60秒放置した状態で3度、http://127.0.0.1:9000
にアクセスすると200 Not Loggined
となります。
まず、http://127.0.0.1:9000
にアクセスするとCookie内のSessionIDを確認します。SessionIDがない場合やSessionIDがNodeCacheに存在しない場合は200 Not Loggined
と表示しています。SessionIDが存在する場合は200 Loggined
と表示されます。http://127.0.0.1:9000/login
にアクセスするとCookieとNodeCacheにSessionIDを設定します。
あとはユーザー情報とパスワード情報をデータベースから引っ張ってきて、正しい情報ならSessionIDをセットするようにして、そうでなければ認証に失敗した旨を返せば良いです。
ちょっとやってみておもしろかった
今回作成したものは結構単純なものだけど、かなり大変だった。こういったものはライブラリに任せるのが一番楽だと思う。一方で、中身がよくわかってない状態でライブラリを使うのは良くないと思ってた。本当はもう少し複雑なんだと思うけど、まずはこんな感じで終わりにしようと思う。
【TypeScript】TypeScriptでBasic認証をやってみる
こんばんは、葛の葉です。
今回はBasic認証をやってみます。
Basic認証とは
Basic認証とはHttpに搭載されている認証方法の一つになります。Httpヘッダーに認証のためのユーザー情報とパスワード情報をBase64にて暗号化して送付します。その値をサーバーが受け取って認証をすることが出来ます。
サーバーは対象となるページのヘッダーにWWW-Authenticateをつけることで、Basic認証にすることができます。クライアントは該当するページにアクセスした時、サーバーは401のステータスコードで返します。認証が成功すれば成功したページをステータスコード200で返します。401のステータスを受け取った時、クライアントは認証用の入力フォームを出力し、ユーザー名とパスワードをユーザーに入力させます。入力フォームを出力する機能はブラウザの機能です(ブラウザによっては出来ないものもあるということになります。)。その値はユーザー名:パスワード
という一つの文字列になり、Base64にて暗号化され、HttpRequestのヘッダーAuthorization
の値としてサーバーに送信します。サーバーは受け取ったその値を使って認証を行い、成功不成功にあわせて返答します。このAuthorizationはブラウザを閉じるまで継続します。
ソースコード
事前に必要なものは以下のコマンドになります。
$ node -v v12.13.1 $ npm init -y $ npm i -D typescript @types/node $ npx tsc --init
今回はあえてexpressを使わずにnodeの標準でついているhttpモジュールでHttpサーバーを建ててみます。
4行目const server = http.createServer();
はserver変数にServerクラスのインスタンスを作成します。
6行目server.on('request', callback)
はHttpRequestを受け取った際にcallbackを起動します。callbackの関数には(req:IncomingMessage, res:ServerResponse)の2つの引数を渡します。IncomingMessageはHttpRequestで、ServerResponseはサーバーとして返すHttpResponseになります。
7行目if(req.headers.authorization)
はHttpRequestのヘッダーAuthorization
が存在するかしないかの条件分岐になります。ヘッダーが存在しない場合はundefinedとなるため、この条件は満たされません。条件が満たされた以降はreq.headers.authorization
を加工しているだけになります。
$ curl 127.0.0.1:9000 401 not authenticated $ curl -H 'Authorization:Basic a3V6dW5vaGE6aG9nZWhvZ2U=' http://127.0.0.1:9000 success
このように認証を行うことが出来ます。一方で、認証情報をBase64で暗号化するだけなので、簡単に暗号を解くことが出来ます。そのため、ほぼ平文での送信となり、盗聴された場合の安全性は足りないと言われています。そのため、Httpsとの併用が望ましいとことです。