Kuzunoha-NEのブログ

プログラミングなどの勉強をしてます

【TypeScript】Promise.allで頑張る

こんばんは。今回はPromise.allを使ってみます。MongoDBも使います。

developer.mozilla.org

記載の通り、配列に入った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も作っています。

github.com

このように並列処理を書くのだ

沢山のPromiseがあるときは、一旦全部を配列に入れてあげて、Promise.allを使ってあげると便利に良さそう。

【TypeScript】Puppeteerで言語を固定できるみたい

こんばんは、葛の葉です。

今回は疲れているので省エネです。

起こったこと

PuppeteerでE2Eテストをやっててヘッドレスだとテストが通らないのに、ヘッドレスをやめるとテストが通るという現象がおきました。そしてそれはタイトルの文字が正しく表示されるかどうかをテストするものでした。

ヘッドレスとそうでないのの違いがよくわからず、しばらく詰んでおりました。

原因は

原因はPuppeteerの言語を固定していなかったからでした👀ブラウザに設定された言語によって英語と日本語とが変わる仕様にしていたので、正とする日本語のタイトル文が表示されてなくてパスしていなかったわけですね。

ヘッドレスモードをオフにすると、不思議なことに日本語で固定されてました。osに依存したりするのかな?

こうした

言語を日本語で固定しました。

import { launch } from "puppeteer";

(async () => {
    const browser = await launch({
        args: ['--lang=ja']
    });
    // 処理が続く
})();

これはPuppeteerの書き方というよりchromiumの起動オプションらしいです。それと、英語ページは実際にまだ使ってないのでテストしないようにしました。

参考

stackoverflow.com

【TypeScript】URLパラメータからJSを実行する

こんばんは、葛の葉です。

今回はURLからjsを起動してみます。悪用厳禁ってやつですね。

こんなイメージ

hogehogeというパラメータに渡したデータをhtml上に表示するようなWebサーバーを作ります。今回もnodeの基礎的なライブラリであるhttpを使います。また、htmlパラメータを出力するために、urlモジュールのparseも使用します。

nodejs.org

nodejs.org

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');
});

f:id:Kuzunoha-NE:20200201004505g:plain
hogehogeパラメータに入れた値が画面に表示される動画

素直にやるとこうなるっちゃね。

JavaScriptを入れてみると。。。

http://127.0.0.1:9000/?hogehoge=<script>alert('aaa')</script>といった感じにhogehogeパラメータの中にJSを入れてみる。そうすると以下のようにアラートが表示できる。

f:id:Kuzunoha-NE:20200201004831g:plain
アラートが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.

docs.mongodb.com

やりたかったこと

{ 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を使うことで文字列となるフィールドを追加して、そのフィールドに正規表現を使うようにします。

stackoverflow.com

こうするみたい

- 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]./}}
+ ]);

今回のソースコード

github.com

【Vue.js】ちょっとVueを触っている

こんばんは、葛の葉です。

Vue.jsの本を読んでちょっと勉強中です。結構楽しいですね。個人開発のWebアプリケーションを作ろうと思っているのですが、Vueでこさえることが出来たらなぁと思ってます。

ちょっとしたFizzBuzzのゲームを作る。

ランダムで出力される3つの数字の合計がFizzBuzzで言うところの何に当たるかを当てるゲームをつくります。1時間で完成。

github.com

f:id:Kuzunoha-NE:20200118011617g:plain

算出プロパティ

Vueを勉強していてちょっと聞き慣れない言葉だと思ったのが算出プロパティというものです。

## script.js 8 - 12 lines
    computed: {
        result: function () {
            return this.questions.one + this.questions.two + this.questions.three;
        }
    },

computedというVueのプロパティにresultという関数を渡しています。一方で似たようなプロパティのmethodというものも存在していて、一見ではどう違うのかがよくわからないと思います。算出プロパティについて、公式では以下に書かれています。

jp.vuejs.org

算出プロパティは依存するデータが存在していて、その依存するデータの値が変われば、出力したい値も変わるような、動的な値を使用したい場合に使うことが多いみたいです。キャッシュされるので、算出プロパティのほうが都度処理を行うmethodよりコストが低くなります。今回の出力している3つの数字の合計値なんかは算出プロパティに相当するようなものではないでしょうか。

【TypeScript】CookieとSessionとSessionIDを使ったログイン情報の保持

こんばんは、葛の葉です。

今回はCookieとSessionとSessionIDを使ったログイン情報の保持を実施します。

前述

普段我々が使用しているWebページでは、ログインを必要とするページが多いかと思います。ログインに一回成功するとその成功したログイン状態というステータスになり、ちょっとした時間は改めてログインをする必要がありません。しかし、Httpはステートレスであり、状態を保持するということが出来ません。そのため、どこかにその情報を保存しておくのですが、今回はそのうち、SessionIDを使った方法を記載してみます。

今回のソースコード

github.com

Cookieとは

WebサーバーがWebブラウザにデータを送り、ブラウザがその値を保持します。それをCookieといいます。CookieWebブラウザの機能の一部であり、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を保持させてみます。

nodejs.org

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を保存する期限を設けることが出来ると記載しましたが、そのオプション指定が特別存在しない場合はブラウザが閉じられるまでの間となります。

f:id:Kuzunoha-NE:20200104121737p:plain
myCookieというNameとhogehogeという値を持つ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
## 第三引数がオプションで保存秒数

www.npmjs.com

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となります。

f:id:Kuzunoha-NE:20200110214156g:plain
擬似的にログインを再現する

まず、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はブラウザを閉じるまで継続します。

f:id:Kuzunoha-NE:20200103201805g:plain

ソースコード

事前に必要なものは以下のコマンドになります。

$ node -v
v12.13.1
$ npm init -y
$ npm i -D typescript @types/node
$ npx tsc --init

httpでBasicAuth

今回はあえてexpressを使わずにnodeの標準でついているhttpモジュールでHttpサーバーを建ててみます。

nodejs.org

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との併用が望ましいとことです。