かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

JavaScript 今更の async / await

async function は Promose を返す

  • returnすると返される値がPromise.resolveされる
  • throwすると値や例外がPromise.rejectされる
  • 何も返さないと関数終了時にundefinedreturn(resolove)される
// return = Promise.resolve()
async function resolveFunc() {
  return 'this resolve';
}

resolveFunc()
.then((res) => console.log(res))
.catch((err)=> console.log(`ERROR ${err}`));
// => this resolve


// throw = Promise.reject()
async function rejectErrorFunc() {
  throw new Error('throw Error');
}

rejectErrorFunc()
.then((res) => console.log(res))
.catch((err)=> console.log(`ERROR ${err}`));
// => ERROR Error: throw Error

// 値をthrow しても reject 扱い
async function throwValueFunc() {
  throw 'throw Value';
}

throwValueFunc()
.then((res) => console.log(res))
.catch((err)=> console.log(`ERROR ${err}`));
// => ERROR throw Value

// return なら返す値が Error オブジェクトでも resolve
async function returnErrorFunc() {
  return new Error('retun Error');
}

returnErrorFunc()
.then((res) => console.log(`RESOLVE ${res}`))
.catch((err)=> console.log(`ERROR ${err}`));
// => RESOLVE Error: retun Error

// 何も返さない関数
// 関数終了時に undefined が resolve される
async function noReturnFunc() {
  // do't return anything
}

noReturnFunc()
.then((res) => console.log(`RESOLVE ${res}`))
.catch((err)=> console.log(`ERROR ${err}`));
// => RESOLVE undefined

await は何をしているか

  • awaitキーワードはasync function内でしか使用できない
  • awaitキーワードで呼び出した関数からPromiseが返されるまで、自身のあるasync functionの処理を止めて待機する
function waitSquareFunc(x) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(x * x);
    }, 500);
  });
}

// awaitキーワードはasync内でしか使用できない
async function getSquareAndAdd(x, y) {
  // awaitキーワードがあると値が帰ってくるまで処理が止まる
  const res = await waitSquareFunc(x);
  // Promise.resolve
  return res + y;
}

// async function を呼び出しているので Promise が返される
getSquareAndAdd(2, 1)
  .then((res) => console.log(res))
  .catch((err) => console.log(err));
// => 5

// async / await で何も返さない場合も関数が終了時の undefined が resolve される
async function awaitNoReturnFunc() {
  // ここでPromiseが返されるまで待機して
  const res = await waitSquareFunc(2);
  // return undefined される
}

awaitNoReturnFunc()
.then((res) => console.log(`RESOLVE ${res}`))
.catch((err)=> console.log(`ERROR ${err}`));
// => RESOLVE undefined

Promiseで書くとこんな感じ

function waitSquareFunc(x) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(x * x);
    }, 500);
  });
}

function getSquareAndAddByPromise(x, y) {
  return waitSquareFunc(x)
    .then((res) => {return res + y})
    .catch((err) => {throw err});
}

getSquareAndAddByPromise(2, 1)
  .then((res) => console.log(res))
  .catch((err) => console.log(err));
// => 5

エラーハンドリング

try-catchでエラーハンドリング

// isError ... true -> Promise.reject / false -> throw new Error
function throwErrorFunc(isError) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      try {
        if( isError ) {
          reject('Reject ERROR');
        } else {
          // throw new Error されると catch 節に処理が流れる
          throw new Error('Throw new ERROR');
          console.log('ココは実行されない');
          resolve('NO ERROR');
        }
      } catch(e) {
        console.log('> ERROR CATCH');
        reject(e.message);
      }
    }, 500);
  });
}

async function errorHandlingTry_catch(isError) {
  try {
    const res = await throwErrorFunc(isError);
    console.log('>> NOT IN CATCH');
    return res;
  } catch(err) {
    console.log('>> TRY-CATCH ERROR');
    throw err;
  }
}

// throwErrorFunc で reject error
errorHandlingTry_catch(true)
  .then((res) => console.log(res, 'then'))
  .catch((err) => console.log(err, 'catch'));
// => >> TRY-CATCH ERROR
// => Reject ERROR catch

// throwErrorFunc で throw new Error
errorHandlingTry_catch(false)
  .then((res) => console.log(res, 'then'))
  .catch((err) => console.log(err, 'catch'));
// => > ERROR CATCH
// => >> TRY-CATCH ERROR
// => Throw new ERROR catch

async function 内ではtry-catchしなくてもawaitで呼び出されている関数でPromise.rejectされると、そのままasync functionの呼び出し元にthrowされる

async function errorHandling(isError) {
  const res = await throwErrorFunc(isError);
  console.log('NOT IN CATCH');
  return res;
}

// throwErrorFunc で reject error
errorHandling(true)
  .then((res) => console.log(res, 'then'))
  .catch((err) => console.log(err, 'catch'));
// => Reject ERROR catch

// throwErrorFunc で throw new Error
errorHandling(false)
  .then((res) => console.log(res, 'then'))
  .catch((err) => console.log(err, 'catch'));
// => > ERROR CATCH
// => Throw new ERROR catch

async / await で Fetch してみる

// fetch でエラーになるとPromise.rejectになるのでtry-catchは省略可
const getDataByFetchAPI = async () => {
  const res = await fetch(api);
  // fetchAPIは res.json() で Promiseを返すのでawaitを2重にする必要がある
  const data = await res.json();
  return data;
};

getDataByFetchAPI()
  .then((res) => console.log(res))
  .catch((err) => console.log(err.message));

まとめ

遥か昔のCallback hellなExpressプロジェクトを見ていたので、JavaScriptの非同期処理すごく見やすく書けるようになったんだね。ほえーって感じです。(Promiseでも十分見やすいですよ...

asyncは必ずしもawaitを使う必要はなく、Promiseを返す関数。
awaitを使えばコールバックなネストになった文でなく非同期処理の待機を書けるから完結でとても便利だけど、処理を止めるという意識がないと不用意にループの中で使って処理時間が長くなってしまうとかありそうなので、awaitが何をしているのか理解して並行処理したほうが良いのかとか設計するのが良さそうって思いました。

Promiseの記事を書いた時に次はasync/awaitだ!って言ったままだったので、書けてよかった。 (Promise.resolve


[参考]

Node.jsでFetchAPIを使いたい

node v11.4.0

fetchを使ったJavaScriptファイルをnodeの実行しようとしたらfetchなんて無いよってエラーになりました。
node.jsには現状デフォルトでFetchAPIが入っていないようです。

node-fetch を使う

$ yarn add node-fetch

JavaScriptファイルでnode-fetchを読み込んで後はブラウザと同じ使い方

const fetch = require('node-fetch');

fetch(url, {method: 'GET'})
  .then((res) => console.log(res))
  .catch((err) => console.error(err));

importを使う場合は拡張子を.mjsにする

どのみちインストールが必要なんでfetchにこだわらないなら、axiosとかでも良さそうです。


[参考]

WEB+DB PRESS Vol.108

WEB+DB PRESS Vol.108

  • 作者: 中野暁人,山本浩平,大和田純,曽根壮大,ZOZOTOWNリプレースチーム,権守健嗣,茨木暢仁,松井菜穂子,新多真琴,laiso,豊田啓介,藤原俊一郎,牧大輔,向井咲人,大島一将,上川慶,末永恭正,久保田祐史,星北斗,池田拓司,竹馬光太郎,粕谷大輔,WEB+DB PRESS編集部
  • 出版社/メーカー: 技術評論社
  • 発売日: 2018/12/22
  • メディア: 単行本
  • この商品を含むブログを見る

[WIP] Firebase Cloud Firestore 接続情報が漏れるとパケ死しないか気になっていたので調べてるメモ

最近Compass漁ってNuxt+Firebase とかReactNative(Expo)+FirebaseとかのHands-onに色々と参加していました。

フレームワークで生成した静的ファイル(HTML+CSS+JavaScript)をFirebaseやNetlifyにデプロイすれば簡単にFirebaseと連動したWEBサービス・アプリがが作れてマジヤバイ (語彙力 という感じです。

しかしながら僕のような古(いにしえ)のほうむぺぃじ作ってきたマンとしては、firebaseとか従量課金だと設定漏れて悪用されたらパケ死しない???という不安もあり、生成されたものがブラックボックスなままでは

  • 静的ファイルならどうやってFirebaseと接続してるの?
  • 接続に必要な設定ファイルはどういう扱いになっているんだろう?
  • JavaScriptで読み込むとローカル側に設定ファイルDLするよね?

というような疑問が残ったままになっていました。

Nuxt generate で生成された静的ファイルの中身

Nuxtが吐き出した静的ファイルのディレクトdist内でagコマンド(The Silver Searcher)を使ってfirebase apiKeyを検索。
-lオプションを付けるとマッチしたファイル名だけ返してくれます。

$ cd dist
$ ag -l <firebase apiKey>
_nuxt/cf55170e35703f7a131c.js

nuxt generateで生成されたJavaScriptファイルに.envに書いてたFirebaseの接続情報がバンドルされているようです。

firebase serve コマンドを実行すると、Firebaseのdeploy先に指定していたディレクトリをルートにしたローカルサーバーで確認ができるようです。
これで先程のfirebase apiKeyが含まれているファイルが使用されているか見てみます。

$ firebase serve
i  hosting: Serving hosting files from: dist
✔  hosting: Local server: http://localhost:5000

ブラウザhttp://localhost:5000にアクセスして、devtoolを開いて確認します。
f:id:kikiki-kiki:20190122075825p:plain
読み込まれていました。

やはり静的化されたものでは直接設定ファイルを読み込んでFirebaseにアクセスしているようです。

Firebase Hosting だと接続情報はそもそも見ることが出来る

Firebase Hostingを使っているとhttps://{Hosting Domain}/__/firebase/init.jsonで接続情報が見れるみたいです。

Firebase Hosting は、/__ で始まるサイトの URL を予約します。この予約された名前空間により、Firebase Hosting と他の Firebase 機能を簡単に同時利用できます。予約済みの URL は、デプロイ時だけでなく、firebase serve をローカルで実行したときにも利用されます。
SDK の自動構成
SDK 自体をホスティングするだけでなく、予約済みの名前空間では、Firebase Hosting サイトに関連するプロジェクトの SDK の初期化に必要な構成も提供されます。これは、直接追加できるスクリプトとして提供されます。

<!-- load Firebase SDK before loading this file -->
<script src="/__/firebase/init.js"></script>

出典: 予約された URL  |  Firebase

アプリケーション側からCloud Firestoreに新しいコレクションを作成することが出来るのか?

接続情報は誰でも見れる状態なので、テストモードの誰でも読み書きできるデータベース設定・ドメインホワイトリストを設定していない状態で、アプリケーション側からCloud Firestoreに新しいコレクションを作成できるのか調べてみました。

https://{Hosting Domain}/__/firebase/init.jsonで取得できた接続情報を元に次のような存在しないコレクションevil-tableにデータを追加しようとするスクリプトのあるHTMLを作成しました。


<html>
<body>
<script src="https://www.gstatic.com/firebasejs/5.8.0/firebase.js"></script>
<script src="https://www.gstatic.com/firebasejs/4.12.1/firebase-firestore.js"></script>
<script>
// Initialize Firebase
var config = {
  apiKey: "<init.jsonにあるapiKey>",
  databaseURL: "<init.jsonにあるdatabaseURL>",
  storageBucket: "<init.jsonにあるstorageBucket>",
  authDomain: "<init.jsonにあるauthDomain>",
  messagingSenderId: "<init.jsonにあるmessagingSenderId>",
  projectId: "<init.jsonにあるprojectId>"
};
firebase.initializeApp(config);

const db = firebase.firestore();
// 存在しないコレクションにデータを追加してみる
db.collection("evil-table").add({
  name: 'new item'
})
.then((res) => console.log("Document written with ID: ", res.id))
.catch((err) => console.error("Error adding document: ", err))
</script>
</body>
</html>

これを適当なドメインを割り当てているサーバにおいてアクセスしてみます。
コンソールをみてみると...

> Document written with ID:  QHHn09EqgvNvZxSifBdU

データ作れてるっぽい。
FirebaseコンソールにアクセスしてDatabaseの項目を見てみます。
f:id:kikiki-kiki:20190122130932p:plain
データ作られちゃってますね... (゚∀゚)アヒャャャャャャ

だでれも読み書きできるテストモードのままだと、誰でも新しいDBのテーブルが作れるちゃうので他人のFirebaseアカウントに寄生したアプリとか作れちゃうのでは🤔

Firebase の接続情報より権限設定の方が重要っぽい

Firebase リリース チェックリスト
WEB: 不正使用を防ぐためのドメインホワイトリストを追加します。 - Google Developer Console のブラウザ API キーとクライアント ID の運用環境ドメインホワイトリストに登録 - Firebase コンソール パネルの [Auth] タブにある運用環境ドメインホワイトリストに登録
Cloud Firestore
意図しないデータアクセスを防ぐためにセキュリティ ルールを構成します。
出典: Firebase Launch Checklist  |  Firebase

cf.

The apiKey essentially just identifies your Firebase project on the Google servers. It is not a security risk for someone to know it. In fact, it is necessary for them to know it, in order for them to interact with your Firebase project.
In that sense it is very similar to the database URL that Firebase has historically been used to identify the back-end: https://<app-id>.firebaseio.com. See this question on why this is not a security risk: How to restrict Firebase data modification?, including the use of Firebase's server side security rules to ensure only authorized users can access the backend services.
出典: javascript - Is it safe to expose Firebase apiKey to the public? - Stack Overflow

まとめ

Hands-onで静的ファイルの場合は設定ファイルをサーバーに上げて直接アクセスはできなくする。FirebaseにHostingする場合はFirebase側に設定があるから不要。Nuxtの場合はプロジェクト生成時に.envdist/*が自動的に.gitignoreされるのでFirebaseの設定ファイルがGitHubに上がったりすることはないので安心と聞いていたのですが、そもそもFirebaseの設定は見れても問題がなくて、むしろCloud Firestoreのルール設定や不正使用を防ぐためのドメインホワイトリストの設定の方が重要な印象を受けました。接続情報のモーダルがそもそも、scriptタグに囲まれたのをHTMLに貼り付けてくださいって出てきてますものね...

なのでHands-onなどでDatabaseはテストモードのままサクッと作ったアプリを雑に公開していていたり、接続情報が入ったファイルをGitHubにpushしていたりして、悪意のある誰かに見つかってしまうと接続情報からDBに寄生したアプリに利用されたり、遊び半分に大量のデータを投稿するボット作られちゃったりで、パケ死する可能性がなくもないのでは?と感じました。

サクッとアプリが作れるのはすごいのですが、Cloud Firestoreのルール設定やFirebaseの請求のアラート設定などは、公開をゴールにするHands-onならしっかり説明しておいて欲しいなって思いました。
Firebaseセキュリティ設定のHands-onぜひ開催してください!!


[参考]

firebaseの設定関連の参考サイト

WEB+DB PRESS Vol.105

WEB+DB PRESS Vol.105

  • 作者: 小笠原みつき,西村公宏,柳佳音,志甫侑紀,池田友洋,木村涼平,?橋優介,大塚雅和,飯塚直,吉川竜太,末永恭正,久保田祐史,浜田真成,穴井宏幸,大島一将,桑原仁雄,牧大輔,池田拓司,はまちや2,竹原,WEB+DB PRESS編集部
  • 出版社/メーカー: 技術評論社
  • 発売日: 2018/06/23
  • メディア: 単行本
  • この商品を含むブログを見る

実践Expo React NativeとFirebaseで、SNSアプリを最速ストアリリース! (NextPublishing)

実践Expo React NativeとFirebaseで、SNSアプリを最速ストアリリース! (NextPublishing)