まだプログラマーですが何か?

プログラマーネタ中心。たまに作成したウェブサービス関連の話も https://twitter.com/dotnsf

タグ:cors

REST API やフロントエンドフレームワークを使ったウェブアプリケーションを作っていると、CORS (Cross-Origin Resource Sharing) に悩まされることが珍しくありません:
cors.001


CORS とはウェブブラウザ(フロントエンド)の JavaScript におけるセキュリティ制約の1つで、AJAX などを使って HTTP リクエストを行う場合、原則的には同一オリジンからのリクエストだけが許可され、異なるオリジンからのリクエストは CORS 制約によって失敗するようになっています。バックエンドからの HTTP リクエストに関しては一切制約はありませんが、フロントエンドからのリクエストに対してのみ適用されるものです。詳しくはこちらもどうぞ:
https://developer.mozilla.org/ja/docs/Web/HTTP/CORS


ただ REST API とフロントエンド・ウェブアプリケーションを作っている人が同一であったり、そういったリクエストが来るとわかっている前提で REST API を用意する場合など、API 提供側からもこの CORS 制約を無視したくなることがあります。そういった場合のために「REST API 側で特定のオリジンからの HTTP リクエストについては許可する」設定も可能です。具体的には許可するオリジン(https://allowed-origin.com)を指定して、
 Access-Control-Allow-Origin: https://allowed-origin.com
という HTTP レスポンスヘッダを返します。リクエスト元ではこのヘッダの内容を自分自身のオリジンと比較して、一致している場合のみ正常処理とみなす(一致していない場合はエラーとする)という処理が行われます。なお「全てのオリジンからのリクエストを許可する」という指定も可能で、その場合は例外的に
 Access-Control-Allow-Origin: *
という HTTP レスポンスヘッダを返すことで実現できるようになっています。

さて、実際にフロントエンド側を開発していると、以下のような問題に直面することがあります:
(1)開発中やテスト中は http://localhost:3000 で動かす
(2)開発終了後は https://xxxxx.com で実運用する
(3)REST API は(1)とも(2)とも異なる環境で稼働している(つまり CORS 設定しないと動かない)

開発中は自分の PC でコーディングするので http://localhost:3000 のようなローカルホストを使った動作確認となり、実運用中は運用のために用意した https://xxxxx.com というインターネット上のホストを使ってアクセスする、というケースです(特別に珍しいケースではないと思います)。開発(テスト)環境でも実運用環境でも REST API を動かす必要があるため、REST API 側ではいずれのケースでも動作するような CORS 設定ができると楽です(http://localhost:3000 からのリクエストを許可する是非はともかく)。しかし CORS の Access-Control-Allow-Origin ヘッダには1つのオリジン(それも * 以外の正規表現とかは使えずに)しか記述できない、という制約があります。つまり「http://localhost:3000 または https://xxxxx.com からのリクエストを許可する」という Access-Control-Allow-Origin の指定はできないのですが、これをうまく回避・実現する方法はないでしょうか?

1つの考えられる方法として「開発中だけは "Access-Control-Allow-Origin: *" を指定」して全てのオリジンからのリクエストを許可する、という方法も考えられます。が、これはあまりに無防備な設定でもあります。

そこで考えたのが以下の方法です。あらかじめ許可するオリジンを配列として用意した上で、リクエスト元が許可オリジン配列の中に含まれていた場合はそのオリジンを Access-Control-Allow-Origin ヘッダに動的に指定して返す、という方法です。Node.js + Express 環境向けに具体的に作るとこんな感じ:
// app.js
var express = require( 'express' ),
    app = express();

app.use( express.Router() );

var settings_cors = 'CORS' in process.env ? process.env.CORS : '';
app.all( '/*', function( req, res, next ){
  if( settings_cors ){
    var origin = req.headers.origin;
    if( origin ){
      var cors = settings_cors.split( " " ).join( "" ).split( "," );

      //. cors = [ "*" ] への対応が必要
      if( cors.indexOf( '*' ) > -1 ){
        res.setHeader( 'Access-Control-Allow-Origin', '*' );
        res.setHeader( 'Vary', 'Origin' );
      }else{
        if( cors.indexOf( origin ) > -1 ){
          res.setHeader( 'Access-Control-Allow-Origin', origin );
          res.setHeader( 'Vary', 'Origin' );
        }
      }
    }
  }
  next();
});

app.get( '/ping', function( req, res ){
  res.contentType( 'application/json; charset=utf-8' );

  res.write( JSON.stringify( { status: true, message: 'PONG' }, null, 2 ) );
  res.end();
});

var port = process.env.PORT || 8080;
app.listen( port );
console.log( "server starting on " + port + " ..." );


起動時の環境変数 CORS として許可オリジンの配列(以下の例では "http://localhost:3000" と "https://xxxxx.herokuapp.com" の2つ)をカンマ区切りで指定します:
(例)$ CORS=http://localhost:3000,https://xxxxx.herokuapp.com node app

このサンプル(8080 番ポートで起動)では GET /ping というリクエストに { status: true, message: 'PONG' } というレスポンスを返す REST API が定義されていますが、リクエスト元のオリジンが http://localhost:3000 か https://xxxxx.herokuapp.com のいずれかの場合は Access-Control-Allow-Origin ヘッダによって CORS の制約を回避して実行できます。

これでテスト環境、ステージング環境、本番環境などで CORS の設定を変えずに運用できる API サーバーが用意できそうです。


上記サンプルソースコードはこちらに用意しました:
https://github.com/dotnsf/multicors


まず、今回紹介するのは Node.js + Express で作った API を CORS 対応にする、という、これ自体はシンプルな内容なのですが、この話を考えるに至った経緯を最初にまとめておきます。


【背景】
普段からウェブアプリケーション・サービスを開発しています。詳しいアーキテクチャはともかく、ユーザーのアクセス先となるフロントエンドのアプリケーション・サーバーとバックエンド(データベースなど)があって、クラウドっぽくバックエンドには API サーバー経由でアクセスする、という形態を多く採用しています。

この形態を採用している時に限らない話ですが、「サービスの安定運用」を考えると「いかにフロントエンドを安定させるか」を考慮する必要があります。ネットワークやバックエンド含めたサービス全体のどこかに不具合が生じた場合であっても、ユーザーが最初にアクセスするフロントエンドが動いていれば「画面に障害発生メッセージを出す」ことができるようになります。逆にフロントエンドにアクセス過多を含めた不具合が発生してしまうと、「メッセージで利用者に不具合が発生していることを知らせる」ことすらもできなくなってしまいます。

このフロントエンドサーバーを安定稼働させるための技術として、最近は(docker などの)コンテナ技術であったり、(k8s や OpenShift などの)コンテナのオーケストレーション技術が流行っています。ここまでは「いわゆる一般論」的な話です。

一般論を理解した所で、技術者としての「一般論ではない話」も考えます。自分は業務でもプログラミングや作ったりサービスの運用を行ったりしていますが、業務外でも(つまり個人でも)プログラミングしたり、作ったサービスを公開して運用したりします。2つの異なる立場を持っているわけです(決して珍しくないと思ってます)。前者ではある程度の予算の中でクラウドのサービスを契約したりして、必要であればベンダーが提供するコンテナやコンテナ・オーケストレーションも使って構築することになります。 一方後者では、これらのインフラ構築部分も自腹になるわけです。まあ「これも授業代」と太っ腹に考える人は立派だと思いますが、コンテナ・オーケストレーションまで使おうとするとそれなりに懐も痛む価格だったりします。


要するに「個人開発者としての自分はケチ」なわけで(繰り返しますが、決して珍しくないと思ってますw)、「ケチはケチなりに知恵と作業でなんとかしたい」と考えるわけです。コンテナ技術やコンテナ・オーケストレーション技術を否定するつもりは全くありませんが、「より安価」に「フロントエンドを安定稼働」させる方法はないものか、と:
2021032001


#「コンテナ・オーケストレーションまで自分で構築すればいい」と考える人もいると思うので一応コメントを。技術的にはそのとおりなんですが、目的はあくまで「安価なフロントエンドの安定稼働」です。そう考えると例えば1ノードで構築した場合に目的を達成しているといえるか・・・ では複数ノードを構築する場合、今度は安価といえるのか・・・ となってしまうと考えました。


そんな自分にひらめいた1つの案が「フロントエンドを GitHub Pages にする」方法です。GitHub Pages は GitHub の無料アカウントがあれば使うことのできる「静的ウェブコンテンツの公開サービス」です。GitHub の一部として考えると、容量は(ほぼ)無制限で、アップロード直後にバックアップされ、世界中のウェブサービスの中でも指折りの安定稼働を誇っています。「フロントエンドを安定させたい」という目的だけを考えると、「かなり使える案」だと思っています:
2021032002
(↑フロントエンドを GitHub Pages にして、バックエンドを IBM Cloud にする場合)


もちろんこの方法は万能ではありません。まず GitHub Pages で公開できるコンテンツはウェブアプリケーションではなく「静的ページ」、つまり HTML ページに限られます(この時点で i18n などを考慮するアプリケーションページの公開は難しくなります)。表示データは REST API で取得すれば良いのですが、フロントエンドが静的ページである以上、通信は AJAX に限られてしまいます。その結果、API 側は(クロスオリジン通信をすることになるため)CORS を考慮した設計が必須となります:
2021032003


フロントエンドはウェブアプリケーションではないので、ウェブページをテンプレートから作る、といった便利な手法が使えず、AJAX を駆使したレンダリングを実装しないといけないことも不利な点となりますが、バックエンド側も考慮点の影響が大きな方法ではあると思っています。ただ「安価にフロントエンドを安定稼働」させる面ではイケそうな方法にも感じています。


・・・といった背景がありました。この前提だと REST API 部分も便利なサービスを有償契約して使うのではなく、安価なアプリケーション・サーバーを使って、自前で API サーバーを構築する必要があります(最悪、ここにトラブルがあってもフロントエンド側ではその旨を伝えることができる構成)。というわけで、無料のライトプランが使える IBM Cloud で「Node.js + Express で作った API を CORS 対応にする」ための方法を理解しておく必要がありました。


【Node.js + Express の REST API を CORS 対応する】
こちらはサンプルを用意しておきました:
https://github.com/dotnsf/express-cors


まず settings.js の exports.cors 配列変数内にクロスオリジン通信を許可するオリジン(ドメイン)を登録しておきます。デフォルトでは以下のようになっていて、'http://localhost:8080' と 'https://dotnsf.github.io' からの AJAX 通信を許可するよう設定しています。前者はローカルでの動作確認用ですが、実際に Github Pages で運用を始めた後は削除しておくべきです。後者は僕の Github Pages のドメインです。実際にみなさんが Github Pages でこの API を使う場合は自分自身の Github Pages ドメインに変更してください:
//. settings for CORS
exports.cors = [ 'http://localhost:8080', 'https://dotnsf.github.io' ];

本体とも言える app.js の内部は以下です:
//. app.js
var express = require( 'express' ),
    app = express();

var settings = require( './settings' );

app.use( express.static( __dirname + '/public' ) );

//. CORS
if( settings && settings.cors && settings.cors.length && settings.cors[0] ){
  var cors = require( 'cors' );
  var option = {
    origin: settings.cors,
    optionSuccessStatus: 200
  };
  app.use( cors( option ) );
}

app.get( '/ping', function( req, res ){
  res.contentType( 'application/json; charset=utf-8' );
  res.write( JSON.stringify( { status: 'OK' } ) );
  res.end();
});


var port = process.env.PORT || 8080;
app.listen( port );
console.log( "server starting on " + port + " ..." );

赤字以外の部分はごく普通の Node.js + Express を使った REST API アプリケーションです。この例では GET /ping という動作確認用のシンプルな API を1つだけ定義しています(サーバー側に障害等が起きていなければ、返り値は常に { status: 'OK' } という JSON です)。

肝心の CORS の対応をしているのが赤字部分です。settings.js の内容を確認し、exports.cors 配列に有効な値が1つでも含まれていると判断した場合は、cors ライブラリを使って設定されているオリジンを対象に CORS を有効にします。ちなみに settings.js の exports.cors が例えば null とか [](空配列)とかに設定されている場合は CORS の処理が行われないので、CORS の処理としてはデフォルトの「全てのクロスオリジンからの AJAX アクセスを許可しない(同一オリジンからの AJAX アクセスのみ許可する)」ことになります。

あとは必要に応じてベーシック認証を加えるとか、トークン認証を加えるとか(フロントエンドが静的コンテンツだとトークンの管理が面倒そうだけど)するなどして CORS 対応の REST API を構築することができます。ここは手順がわかってしまえば簡単そうですね。


【運用時】
こんな感じでデータベースへの読み書き検索などが REST API 化され、CORS 対応までできていれば Github Pages の HTML からも利用できるので、かなり安定したフロントエンドコンテンツを実現できそうです。この形で REST API が実現できていると、例えばウェブアプリを作るハンズオンでもあらかじめ用意した REST API を Github Pages から呼び出す形で実現できるので、フロントエンドの運用サーバーは無料で(しかもかなり安定したものを)用意できます。更にフロントエンド部分の開発時にはこんなフォルダ構成の Node.js + Express のアプリケーションにしておくと docs/ 以下の静的コンテンツを対象に作って、ローカルで動作確認もして、コードごと Github にあげて docs/ 以下を Github Pages で公開する、といった便利な開発・運用も可能になります:
//. app.js
var express = require( 'express' ),
    app = express();

app.use( express.static( __dirname + '/docs' ) );

var port = process.env.PORT || 8080;
app.listen( port );
console.log( "server starting on " + port + " ..." );
(↑メインファイル。静的コンテンツのフォルダを /docs に指定している以外はごく普通のシンプルな Express アプリ)


2021032004
(↑このプロジェクトを Github に上げる)


2021032005
(↑Github Pages の設定で /docs フォルダ以下を公開するよう指定する)


フロントエンドコンテンツは全て AJAX 対応させる必要があり、昨今の流行りとは違う形での実装は必要になります。それはそれで大変だし、普通のウェブアプリケーションで使える便利な技術も使えなくて不便な面も出てくるのですが、"Poorman's stable web contents" みたいに割り切って使うネタを準備しています。まだアイデア先行な面もあって実証実験が必要な段階だと思っていますが、運用していて気づくことがあったらまたこのページに追記します。


IBM Cloud から提供されている NoSQL のマネージドデータベースである Cloudant (実体は Apache CouchDB)を便利に使っています。最大の特徴の一つが「無料でも一定条件下で使える」点ですが、今回は無料である以外の便利な使い方を紹介します。

この記事の中で紹介するのはこのような使いかたです:
(1) Cloudant の CORS を有効に設定する(特定ドメインのページからの REST API リクエストを受け付けて読み書きできるようにする)
(2) Github ページに用意したウェブページから (1) で有効に設定した Cloudant のデータを読み書きする


2021022401


最終的に実現したいのは、世の中の SaaS(といっていいのかな)の中でもかなり可用性高く、安定して動いている印象の Github ページを使ったウェブアプリケーションを作ることです。ただ静的ページそのものは安定して稼働していてもデータの読み書きが出来なければウェブアプリケーションとしては不十分で、そこを REST API で読み書きできる IBM Cloud の Cloudant データベースで賄えないか、と考えました。ただそのままでは CORS の制約もあって API が実行できないのですが、Cloudant 側の設定で CORS を正しく設定して Github ページからの読み書きを可能な状態にする、そのための Cloudant の設定手順を確認する、というのが目標です。

以下では Github のアカウントを持っている方を対象に、Github ページを使って作成するウェブページから Cloudant にアクセスするサンプルを紹介します。Github ページでなくても(静的ページのホスティング環境があれば)同様に可能なはずですが、その場合は適宜読み直してください(特に CORS 設定時のドメインなど)。


【CORS】
"Cross-Origin Resource Sharing" 、つまり「オリジン間リソース共有」の略語です。ここでの「オリジン」は「ドメイン」と読み替えたほうが理解しやすいかもしれまえん。

例えば A というウェブサーバーが https://aaa.com/ 上で、また B というデータベース・サーバーが https://bbb.net/ 上で動いていると仮定します(つまり全く別々に管理されている2つのサーバーです。この状態を Cross-Origin と呼びます)。A 上のウェブページで表示される画面の中で B から取得したデータを表示しようと思っています。

例えば A が Java などのプログラミング言語によって作られたアプリケーションサーバーであった場合、A は B からのデータ取得に Java を使うことになります。A の中で Java が実行され、B にアクセスしてデータを取り出し、その結果を使って A のページの画面を作成することができます。A の内部でプログラミング言語が使われている場合は、Cross-Origin であってもこのような方法でデータを取得することができます。

ところが A がアプリケーションサーバーではなく、ただの(既存の HTML ページを表示するだけの)ウェブサーバーであった場合、A から B にアクセスしてデータを取得する手段はかなり限られてしまいます。その限られた手段の1つが AJAX などに代表される JavaScript 処理です。A のサーバーにプログラミング言語が用意されていなくても、ウェブブラウザ自体が持つ JavaScript 実行環境を使って B サーバーにアクセスしてデータを取得する、という考え方です。ただし Cross-Origin の場合、A にアクセスしたウェブブラウザから B のサーバーに JavaScript で HTTP アクセスすることは原則できないことになっています(Cross-Origin でなければ、つまり A = B であれば可能です)。これが CORS の制約です。

CORS の制約はウェブブラウザが持っている制約なので、ウェブブラウザを使わない HTTP 通信(上述の Java 言語によるプログラミングによる通信など)には関係ありませんが、(ウェブブラウザの)利用者側でこの制約を解除する方法はありません。唯一の方法がリソース提供側(この場合だと B サーバー)による許可のみです。

今回のブログエントリは IBM Cloud のデータベースの1つである Cloudant の CORS をうまく設定することで、代表的な静的ウェブページの1つである Github ページから(Cloudant 内の)外部データを読み書きする方法について紹介する、というものです。


【Cloudant の用意】
まず IBM Cloud のアカウントを取得します。IBM Cloud アカウントを新規に取得した時点では「ライトアカウント」と呼ばれる無料の制約のあるアカウントとなります。今回紹介する作業を実行するだけであれば(ライトアカウントだとデータは 1GB までの格納となりますが、この容量などに問題なければ)ライトアカウントのまま実行いただいても構いません。

改めて取得したアカウントで IBM Cloud にログインし、「リソースの作成」ボタンクリック後に「Cloudant」を選択します:
2021022501

2021022502


インスタンス作成前にいくつか設定箇所があります。主なものは以下になりますが、"Authentification Method" だけはデフォルトの "IAM" ではなく "IAM and legacy credentials" を選択してください:
・Available Regions: インスタンスを作成するエリア、お好きな場所でいいが「東京」あたりが無難
・Authentification Method: 今回は必ず "IAM and legacy credentials" を選択
・Plan: 料金プラン。「Lite」であれば無料

最後に "Create" ボタンで作成します:
2021022503


Authentification Method について補足しておくと、デフォルトの "IAM" だといわゆる API キーを使った認証/認可を行います。ただ今回はアプリケーションサーバーからではなく(GitHub ページの)静的なページから Basic 認証を使ってデータベースの REST API を使うことを想定しています。この場合は "IAM" ではなく "IAM and legacy credentials" (API キーまたは Basic 認証を使う)を選択しておく必要があるためです。


少し待つと Cloudant インスタンスが起動済みになります。インスタンスを開いて「サービス資格情報」タブを選び、「新規資格情報」ボタンを押して資格情報を作成します。作成後に作成した資格情報(「サービス資格情報-1」のような名前になっていると思います)を選択・展開して、中身を確認します:
2021022504


資格情報の中身は以下のような内容の JSON 文字列になっているはずです(この中身は他人に見せないように注意してください):
{
  "apikey": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "host": "xxxxxxxxxx-bluemix.cloudantnosqldb.appdomain.cloud",
  "iam_apikey_description": "Auto-generated for key xxxxxxxxxxxxxxxxxxx",
  "iam_apikey_name": "サービス資格情報-1",
  "iam_role_crn": "crn:v1:bluemix:public:iam::::serviceRole:Manager",
  "iam_serviceid_crn": "crn:v1:bluemix:public:iam-identity::a/xxxxxxxxxxxxxxxxxxxx::serviceid:ServiceId-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "password": "パスワード",
  "port": 443,
  "url": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-bluemix.cloudantnosqldb.appdomain.cloud",
  "username": "ユーザー名"
}

この情報のうち、後で必要になるのが以下の(赤字の)4つです。メモするか、コピペできるようにしておいてください:
キー意味補足
hostnameホスト名*******-bluemix.cloudantnosqldb.appdomain.cloud といった感じの文字列になっています(******* 部分が可変)
portポート番号443 で固定のはずです
usernameユーザー名ログイン時にこの2つの値が必要です
passwordパスワード


認証情報を確認したら、実際に Cloudant データベースのダッシュボードを見てみましょう。画面左のメニューから「管理」タブを選択し、右上の「Launch Dashboard」ボタンをクリックします:
2021022505


(初期状態では空の)ダッシュボード画面が表示されます。最初にアクセスした時点ではまだ何のデータベースも作成されていないため、下図のような画面になります。後述の作業のためのデータベースをここで1つ作成しておくことにします。画面右上の「Create Database」ボタンをクリックします:
2021022506


データベースを作成するメニューが画面右に表示されます。ここでは Database name に「mydb(異なる名前を指定しても構いませんが、その場合は後述のコマンド等を作成した名前に読み替えて実行してください)」、Partitioning は「Non-partitioned」を選択し、「Create」ボタンをクリックします:
2021022507


作成した mydb データベースが出来上がりました。このデータベースもまだデータが1件もないので、このような画面になります。一旦元の画面に戻るため、画面左上の "<" マークをクリックします:
2021022508


一つ前のデータベース一覧画面に戻りました。先程は空でしたが、作成したデータベースが1つ追加された画面になっているはずです:
2021022509


Cloudant 側の準備はいったんここまでできていればOKとします。 次に CORS のための設定を行います。


【Cloudant に CORS を設定する】
次に Cloudant に CORS の設定をして外部からのアクセスを許可します。ダッシュボードのメニューから「アカウント」(下から3番め)を選び、「CORS」タブを選択します。以下のような画面(「CORS は有効になっていて、外部のどこからのリクエストも受け付けない」という内容)になるはずです:
2021022501


画面下部の「Restrict to specific domains」欄にアクセスを許可するドメインの URL を指定して「Add Domain」ボタンで追加します。ここでは "https://(github のアカウント名).github.io/" を指定することで自分の Github ページからのアクセスを許可することになるので、自分の github アカウントに合わせて設定してください:
2021022502


このような画面になれば自分の github ページ用の CORS 設定ができたことになり、自分の github ページからは JavaScript で Cloudant データを読み書きできることになります:
2021022503


【Cloudant に初期データを入力する】
今回の紹介では Github ページから Cloudant のデータを REST API 経由で表示する(CORS の設定をしていれば取得できるはず)、ということを試してみます。そのための「表示するデータ」をあらかじめ Cloudant に入力しておきます。

もちろん(ダッシュボードから)自分で好きにデータを入力いただいても構いませんが、比較的簡単に動作を確認できるサンプルを用意しており、そこで対象とするデータを curl 一発でまとめて入力できるように準備しているのでこちらの方法を紹介します。

まず以下の URL からサンプルデータをダウンロード(表示後に右クリック→名前を付けて保存で "prefs.json" という名前で保存)してください:
https://raw.githubusercontent.com/dotnsf/cloudant_cors/main/prefs.json

2021022701


このファイルの中身は以下のようなフォーマットになっており、各都道府県庁所在地と、その位置情報が格納されています:
{
  "docs": [
    { "code": 1, "prefecture": "北海道", "capital": "札幌市", "geometry": { "type": "Point", "coordinates": [ 141.34694, 43.06417 ] } },
    { "code": 2, "prefecture": "青森県", "capital": "青森市", "geometry": { "type": "Point", "coordinates": [ 140.74, 40.82444 ] } },
    { "code": 3, "prefecture": "岩手県", "capital": "盛岡市", "geometry": { "type": "Point", "coordinates": [ 141.1525, 39.70361 ] } },
    { "code": 4, "prefecture": "宮城県", "capital": "仙台市", "geometry": { "type": "Point", "coordinates": [ 140.87194, 38.26889 ] } },
    { "code": 5, "prefecture": "秋田県", "capital": "秋田市", "geometry": { "type": "Point", "coordinates": [ 140.1025, 39.71861 ] } },
    { "code": 6, "prefecture": "山形県", "capital": "山形市", "geometry": { "type": "Point", "coordinates": [ 140.36333, 38.24056 ] } },
    { "code": 7, "prefecture": "福島県", "capital": "福島市", "geometry": { "type": "Point", "coordinates": [ 140.46778, 37.75 ] } },
    { "code": 8, "prefecture": "茨城県", "capital": "水戸市", "geometry": { "type": "Point", "coordinates": [ 140.44667, 36.34139 ] } },
    { "code": 9, "prefecture": "栃木県", "capital": "宇都宮市", "geometry": { "type": "Point", "coordinates": [ 139.88361, 36.56583 ] } },
    { "code": 10, "prefecture": "群馬県", "capital": "前橋市", "geometry": { "type": "Point", "coordinates": [ 139.06083, 36.39111 ] } },
    { "code": 11, "prefecture": "埼玉県", "capital": "さいたま市", "geometry": { "type": "Point", "coordinates": [ 139.64889, 35.85694 ] } },
    { "code": 12, "prefecture": "千葉県", "capital": "千葉市", "geometry": { "type": "Point", "coordinates": [ 140.12333, 35.60472 ] } },
    { "code": 13, "prefecture": "東京都", "capital": "新宿区", "geometry": { "type": "Point", "coordinates": [ 139.69167, 35.68944 ] } },
    { "code": 14, "prefecture": "神奈川県", "capital": "横浜市", "geometry": { "type": "Point", "coordinates": [ 139.6425, 35.44778 ] } },
      :
      :
  ]
}

このデータを先程作成した Cloudant 内の mydb データベースに格納します。curl コマンドを使うと以下のコマンド(※)で格納できます:
$ curl -u "username:password" -XPOST "https://host/mydb/_bulk_docs" -H "Content-Type: application/json" -d @prefs.json

username, password, host の値は上述で確認した以下の値を指定して入力してください:
 username: 上述の資格情報で確認した username の値
 password: 上述の資格情報で確認した password の値
 host: 上述の資格情報で確認した host の値


このコマンドが成功すると Cloudant の mydb データベースに47都道府県のデータが挿入されます。ダッシュボードから mydb データベースを選択すると以下のように 47 件のデータが表示されるようになります:
2021022702


試しにどれか1つ選択してみると、個々の内容を確認することも可能です(下図は北海道の例、"Cancel" で一覧に戻ります):
2021022703


ここまでの作業で Cloudant に Github ページからアクセスするための CORS を設定し、表示用のデータも格納できました。ではこのデータを Github ページから REST API 経由で取得して表示してみましょう。


【Github ページを用意する】
では実際に Github ページからクロスオリジンな Cloudant データベースに REST API 経由でアクセスできることを確認してみます。

まず Github にログインし、その後で以下のページにアクセスし、画面右上の "Fork" をクリックしてください:
https://github.com/dotnsf/cloudant_cors

2021022706


Fork 処理が成功すると、
 https://github.com/(あなたの github アカウント名)/cloudant_cors
というリポジトリができるはずです。

このリポジトリの Github ページを有効にします。リポジトリページの右上にある "Settings" をクリックします:
2021022707


画面を下に "GitHub Pages" と書かれている箇所までスクロールします。"Source" に "main" ブランチを指定して保存すると、"main" ブランチの内容がそのまま Github ページとして公開され、この中にある CORS の動作確認用に作った index.html を参照するための URL(https://(あなたの github アカウント名).github.io/cloudant_cors/)とそのリンクが表示されます(リンク先が有効になるまで1分程度かかります):
2021022708


少し待ってからリンク先にアクセスします。以下のような画面が表示されるはずです(Github ページなので、****.github.io というドメインのページになっていることを再確認してください):
2021022704


改めて資格情報から取得した値を画面内の各該当箇所に入力します。上にある横幅の大きなフィールドには host の値、その下は左から作成した DB 名(mydb)、username、password の値を入力します。最後に "Refresh" ボタンを押すと入力された情報を使ってこのページから AJAX で Cloudant へ REST API を実行して文書一覧を取得し、表形式で出力します。成功すると以下のように47都道府県の情報が(クロスオリジンの制約を乗り越えて)表示されます:
2021022705


以上、正しく設定することでクロスオリジンの壁を超えて Github ページから Cloudant のデータを利用することができました。Github も Cloudant も(一定制限の中で)無料で利用できるので、安定したウェブページを Github で運用しつつ、表示データを Cloudant から提供する、といった形で利用することができそうです。


なお、Cloudant(CouchDB) の REST API についての詳しくは、こちらのリファレンスを参照してください:
https://docs.couchdb.org/en/stable/api/index.html


なお、今回使った「特定データベース内の全文書を取得する」 REST API はこちらです:
GET /{db}/_all_docs

また準備段階で都道府県データをバルクインサートしましたが、その REST API はこちらで紹介されています:
POST /{db}/_bulk_docs


ほぼ備忘録目的のブログエントリです。

HTML で <canvas> を使うと画面に単純な画像だけでなく、グラフィックコンテキストを使って自由度の高い図形や関数曲線を描くことができるようになります。個人的にもよく使っています。

特筆すべきはピクセル単位での画素情報を取得することもできる点です。ここでの画素情報とは RGB 値や輝度の値といった情報です。例えば <canvas id="mycanvas"></canvas> 内に描かれている内容のピクセル単位での画素情報を動的に取得するには以下のような JavaScript コードを実装すれば可能です:
  var canvas = document.getElementById( 'mycanvas' );
  if( !canvas || !canvas.getContext ){
    return false;
  }
  var ctx = canvas.getContext( '2d' );

  var imagedata = ctx.getImageData( 0, 0, ctx.canvas.width, ctx.canvas.height );
  for( var i = 0; i < imagedata.height; i ++ ){
    for( var j = 0; j < imagedata.width; j ++ ){
      var idx = ( j + i * imagedata.width ) * 4;

      var r = imagedata.data[idx];    //. R(0-255)
      var g = imagedata.data[idx+1];  //. G(0-255)
      var b = imagedata.data[idx+2];  //. B(0-255)
      var a = imagedata.data[idx+3];  //. 輝度(0-255)

        :
    }
  }

この中で画素情報を取得する際に使っているのが getImageData() 関数です。これは canvas 内の開始点座標と幅、高さを指定し、その指定範囲内の画素情報を配列で取り出すことができます。また上述例のように1つのピクセルの情報は配列内の4つの値(R, G, B, 輝度)として格納されており、1つずつ取り出して参照することも可能です。画像を独自に減色したりする際等に必要な情報を取り出すことができて、個人的にも便利に使っています。


このように便利な canvas と getImageData() 関数ですが、この getImageData() 実行時に以下のようなエラーが発生して、画素情報を取得できないケースがありました:
'The canvas has been tainted by cross-origin data.'

ん? "Cross-Origin" ?? どういうこと???


これが発生した時の <canvas> 内は、事前に以下のような処理を行って画像が表示されていました:
  var img = new Image();
  img.src = 'https://www.xxx.com/images/abc.png';
  img.addEventListener( 'load', function(){
    var canvas = document.getElementById( 'mycanvas' );
    if( !canvas || !canvas.getContext ){
      return false;
    }
    var ctx = canvas.getContext( '2d' );
    ctx.drawImage( img, 0, 0, canvas.width, canvas.height );
  }, false );

要するに <canvas> 内には https://www.xxx.com/images/abc.png という(架空の) URL で表現される画像ファイルが表示されている状態でした。単に画像を表示するだけなら <img src="https://www.xxx.com/images/abc.png"/> みたいに記述すればいいのですが、後で前述のような画素情報を取り出す処理を実行したかったので、<img> ではなく <canvas> を使って表示していたのでした、という事情がありました。が、このケースだと getImageData() 実行時に上述のようなクロスオリジン関連のエラーが発生して情報を取得することができなくなっていました。これはいったい・・?


エラーメッセージからなんとなく想像はできたのですが、この getImageData() 関数はクロスオリジンの画像が表示されている場合は CORS の制約がかかるようです。つまりこの例であれば www.xxx.com というサーバー上にある https://www.xxx.com/images/abc.png という画像を表示しています(上のコード例では drawImage() 関数によって実際に表示されます)。この drawImage() 関数はクロスオリジンに関係なく実行することができるので、(自サーバーではない)www.xxx.com というサーバー上にある画像に対して実行してもエラーが発生することなく実行され、画像は <canvas> 上に表示されます。

しかし getImageData() 関数についてはクロスオリジン制約があるようです。したがって自サーバー上ではない URL を指定して表示された画像に対して実行すると、今回のようなエラーが発生してしまう、という制約があるようでした。


このエラーを回避しようとすると、www.xxx.com 側で CORS の制約を緩和するような設定を行うか、それが難しい場合は目的の画像をクロスオリジンにならない形の URL で指定できるようにする必要があります(そのため静的な HTML や JavaScript だけだと実現が難しいです)。具体的にはサーバーサイドの処理で、例えば GET /getimage のような REST API を用意し、この API が目的の画像バイナリと Content-Type を返すようにする、といった対応が必要になります。


現にこういう方法で回避できちゃう CORS 制約は、処理が面倒くさくなるだけであまり意味ないと思っちゃうんだけどな。。


 

Node.js / Express を使って API を作成する場合は、こんな感じの記述で実装することになります(3000 番ポートで listen する POST /mypost を実装する場合の例):
var express = require( 'express' );
var app = express();

  :

app.post( '/mypost', function( req, res ){
   :
   :
});

  :

app.listen( 3000, function(){
  console.log( 'server running on port 3000...' );
});

これで POST /mypost を 3000 番ポートで待ち受ける API ができました。同一アプリケーション内の HTML などからであれば、こんな感じでリクエストを受けることができるようになります:
(jQuery を使って AJAX でポストする例)
  :
$.ajax({
  type: 'POST',
  url: '/mypost',
  data: { a: 1, b: 2, c: 3 }
}).done( function( result ){
  console.log( result );
});
  :

さて、ではこの POST /mypost を外部アプリケーションの HTML からリクエストするにはどうすればいいでしょうか?深く考えずに URL にサーバー名やポート番号も指定して、
(jQuery を使って AJAX でポストする例)
  :
$.ajax({
  type: 'POST',
  url: 'http://servername:3000/mypost',
  data: { a: 1, b: 2, c: 3 }
}).done( function( result ){
  console.log( result );
});
  :

こんな感じ↑にすればいいかというと、たしかに AJAX 部分はこれでいいのですが、結論としてはまだ不充分です。これを実行すると、ブラウザのコンソールには以下のようなメッセージが表示され、期待通りの挙動にはなりません:
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://servername:3000/mypost. (Reason: CORS header 'Access-Control-Allow-Origin' missing).

CORS(Cross Origin Resource Sharing) というメカニズムによって、このリクエストはブロックされている、というエラーメッセージです。つまり「 servername:3000 上で動いている POST /mypost という API を外部から呼ぶことはポリシーに反しているのでできない」ということになります。

この外部リクエストを許可する方法はないでしょうか? CORS の設定の問題なので理論上はできるはずですが、その中でも比較的簡単な方法が cors ライブラリを使う方法です:
2017112401


この cors ライブラリを使うと CORS のデフォルトポリシーを変更して、柔軟に API のクロスオリジン対応が可能になります。以下に例を紹介しますが、まずは cors をインストールしておきます:
$ npm install cors

実際にクロスオリジンリクエストを許可するには以下のようにします。まずは全ての API をクロスオリジンで許可する場合です。API の定義を行う前に app.use() で cors を指定します:
var express = require( 'express' );
var app = express();
var cors = require( 'cors' );

app.use( cors() );   //. ここから下に定義する全ての API にクロスオリジン実行を許可する

  :

app.post( '/mypost', function( req, res ){  //. クロスオリジン実行が許可される
   :
   :
});

  :

app.listen( 3000, function(){
  console.log( 'server running on port 3000...' );
});

また、特定の API に対して個別にクロスオリジン実行を許可する場合は、以下のように指定します:
var express = require( 'express' );
var app = express();
var cors = require( 'cors' );

  :

app.post( '/mypost', cors(), function( req, res ){  //. POST /mypost のクロスオリジン実行は許可される
   :
   :
});

  :

app.listen( 3000, function(){
  console.log( 'server running on port 3000...' );
});




 

このページのトップヘ