tkhrsskの日記

技術ネタなど

Node.jsでCORS回避する中継Webサーバを建てる

ローカルのhtmlでAPIを叩いて結果を見れるツールを作りたいときがたまにある。 できればjavascriptでブラウザで見れるようにしたい。

Node.jsの簡素なパッケージだけで実現してみた。

サンプルなので自分自身にスタブ用意しているけど、リクエスト先をCORS設定されているような公開APIに変更すればいい。

やりたいことはシンプルなのに、つまずきポイントがちょいちょい出てくるので残しておくことにする。

特にCORSの回避周りだったり、bodyの非同期受信の処理とか。

ググると使えない古い情報で溢れていたりするので、 できればnodejsとjqueryの非推奨呼び出しとなった歴史も整理していきたい。

web.js

let http = require("http");
let https = require("https");
let server = http.createServer();

server.on("request", function (req, res) {
  console.log(
    "HTTP",
    req.httpVersion,
    req.method,
    req.url,
    req.headers["user-agent"]
  );
  console.log("header: ", req.headers);

  // CORS全許可
  res.setHeader("Access-Control-Allow-Origin", "*");
  res.setHeader("Access-Control-Allow-Methods", "GET, POST, HEAD, OPTIONS");
  res.setHeader("Access-Control-Allow-Headers", "*");

  // ignore url
  if (req.url.indexOf("/favicon.ico") === 0) {
    let response = {};
    res.writeHead(404, { "Content-Type": "application/json" });
    res.write(JSON.stringify(response));
    res.end();
    return;
  }

  // OPTIONSメソッドは無条件で正常応答(エラー応答時のCORSエラーを防ぐため)
  if (req.method === "OPTIONS") {
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end();
    return;
  }

  body = "";
  req.on("data", (chunk) => {
    body += chunk;
  });
  req.on("end", () => {
    console.log("body: ", body);

    // スタブ応答
    if (req.url.indexOf("/api") === 0) {
      console.log("[API] Called ", req.url);
      let response = {};
      res.writeHead(200, { "Content-Type": "application/json" });
      res.write(JSON.stringify(response));
      res.end();
      return;
    }
    host = "127.0.0.1";
    port = 3000;
    method = req.method;
    path = "/api/";
    content = "json";
    protocol = port == 443 ? https : http;

    let options = {
      host: host,
      port: port,
      method: method,
      path: path,
    };

    let payload = body ? body : null;
    options.headers = {
      "Content-Type": "application/json",
    };

    options.headers["user-agent"] = "API Test Client";
    if (req.headers["authorization"]) {
      options.headers["authorization"] = req.headers["authorization"];
    }

    const reqApi = protocol.request(options, function (resApi) {
      var body = "";
      resApi.on("data", (chunk) => {
        body += chunk;
      });
      resApi.on("end", () => {
        console.log("StatusCode: ", resApi.statusCode);
        console.log("Response Headers: ", resApi.headers);
        console.log("Response Body: ", body);
        res.writeHead(resApi.statusCode, resApi.headers);
        res.write(body);
        res.end();
      });
    });
    reqApi.on("error", function (e) {
      console.log("[ERROR][API]" + e.message);
      let response = {};
      res.writeHead(500, { "Content-Type": "application/json" });
      res.write(JSON.stringify(response));
      res.end();
    });
    if (payload) {
      reqApi.write(payload);
    }
    reqApi.end();
  });
});

console.log("http://127.0.0.1:3000/");
server.listen(3000, "127.0.0.1");

index.html

<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>index</title>
    <style>
        #result {
            white-space: pre-wrap;
        }
    </style>
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"
        integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
</head>

<body>
    <h1>API Request</h1>
    <div>
        <h2>Request</h2>
        Url:<br>
        <select id="requestUrl">
            <option>http://127.0.0.1:3000/</option>
            <option>http://127.0.0.1:3000/api/</option>
            <option>https://www.githubstatus.com/api/v2/summary.json</option>
            <!-- <option>https://www.jma.go.jp/bosai/forecast/data/forecast/140000.json</option> -->
        </select><br>
        Bearer Token:<br>
        <input id="token" type="text" size="64" placeholder="token" value=""><br>
        Payload:<br>
        <textarea id="payload" cols="64" rows="5" placeholder="payload"></textarea></br>
        <button id="run">RUN</button>
        <h2>Response</h2>
        <pre id="status"></pre>
        <pre id="result"></pre>
        <pre id="responsetime"></pre>
    </div>
    <script>
        $('#run').on('click', function () {
            var token = $('#token').val();
            var requestUrl = $('#requestUrl').val();
            var payload = $('#payload').val();
            console.log(payload);
            console.log(token);
            var options = {
                url: requestUrl,
                type: 'get',
                dataType: 'json',
                contentType: 'application/json',
            };
            if (token) {
                options.headers = {
                    'Authorization': 'Bearer ' + token
                };
            }
            if (payload) {
                options.data = payload;
            }
            console.log({ requestUrl, token });
            $('#responsetime').text("");
            $('#status').text("");
            $('#result').text("");
            const startTime = Date.now(); // 開始時間
            $.ajax(options)
                .done(function (data, textStatus, jqXHR ) {
                    const endTime = Date.now(); // 終了時間
                    console.log(JSON.stringify(data));
                    $('#status').text("status: " + jqXHR.status);
                    $('#result').text(JSON.stringify(data));
                    $('#responsetime').text("ResponseTime: " + (endTime - startTime) + " ms");
                })
                .fail(function (jqXHR, textStatus, errorThrown) {
                    const endTime = Date.now(); // 終了時間
                    console.log("fail", jqXHR, textStatus, errorThrown);
                    $('#responsetime').text("ResponseTime: " + (endTime - startTime) + " ms");
                    $('#status').text("status: " + jqXHR.status);
                    $('#result').text("fail");
                    console.log(jqXHR.responseText);
                    $('#result').text(jqXHR.responseText);
                });
        });
    </script>
</body>

</html>

Android/iOSアプリ起動(DeepLinkなど)

はじめに

ブラウザや他のアプリから、アプリを起動する手法についてまとめる

用語

DeepLink

アプリで特定のコンテンツを直接開くためのリンクのこと。 例えば、広告のAmazonの商品を選んで、Amazonアプリが開かれるようにしたとき、その商品画面で起動すること。 DeepLinkは概念であって、具体的な手法ではない。

手法

Firebase App Indexing (Google)

Google App Indexing.

firebase.google.com

Googleの検索結果からアプリを起動する手法としてあるが、 現在は、AppLinksが推奨されている模様。

AndroidiOSも対応

アプリ側:Firebase App Indexingライブラリの組み込み サーバ側:assetlinks.json を配置

AppLinks (Google)

developer.android.com

Androidの機能

https:// で始めるURLでアプリが起動

アプリ側:intent-filterの実装

サーバ側:.well-known/assetlinks.json を配置

intentスキームURI

Chrome専用?

intent:// から始まるURIで、アプリインストール済みであればアプリ起動、アプリ未インストールであればストアに飛ぶ。

developer.chrome.com

Universal Link (Apple)

iOSの機能

サーバ側:apple-app-site-association を配置

Firebase Dynamic Link (Google)

ブラウザ上でDynamicリンク用のハッシュ値を含んだURLを選択したときにアプリを起動 アプリがインストールされていない場合、ストアに移動

準備:Firebaseの管理画面で、リンクを発行

Amzon Linux 2 のDocker環境構築例

Dockerfileを作成

とりあえずamazon corettoのjavaとか、perlとか

FROM amazonlinux:2

RUN yum install -y which wget perl \
  && amazon-linux-extras enable corretto8 \
  && yum install -y java-1.8.0-amazon-corretto \
  && rm -rf /var/cache/yum/* \
  && yum clean all

ENV TZ Asia/Tokyo

IMAGE作成

$ docker build -t amzn2 . 

IMAGEからコンテナ起動

$ docker run -it --rm amzn2 /bin/bash

簡易Webサーバ(APIサーバのスタブ) Node.js版

勢いでサンプルを書いておく。

let http = require('http');
let server = http.createServer();

let id = 0;
server.on('request', function(req, res)
{
    console.log("HTTP", req.httpVersion, req.method, req.url, req.headers);

    let response = {};
    id += 1;
    response.id = id;
    response.status = 200;
    response.message = 'Hello, world!';
    console.log(JSON.stringify(response));
    res.setHeader('Access-Control-Allow-Origin', '*')
    res.setHeader('Access-Control-Allow-Methods', 'GET, POST, HEAD, OPTIONS')
    res.setHeader('Access-Control-Allow-Headers', '*')
    res.writeHead(200, {"Content-Type": "application/json"})
    res.write(JSON.stringify(response));
    res.end();

    console.log(res.statusCode, res.statusMessage);
});

server.listen(3000, '127.0.0.1');

これで、下記のようなレスポンスが返ってくる。

{"id":1,"status":200,"message":"Hello, world!"}

Pythonだと下記などが参考になりそう

Python http通信のサンプル | ITSakura

Dockerで簡易proxyサーバ構築

スマホ端末のhosts設定をいじりたいが、AndroidiOSの/etc/hosts を変えるのはそれなりにハードルが高い。
もっと簡単にできないか探していたところ Macproxyサーバを立てる方法を見つけた。

www.yoheim.net

apache使うくらいなら、Docker で環境依存しない手順を確立したいないと思い、この記事を書くことにした。

ベースは、公式イメージを使えばいい。

https://hub.docker.com/_/httpd

confのいじり方も書いていたので、基本はそれに従うだけ。

まず、イメージの中身からconfを取り出す

$ docker run --rm httpd:2.4 cat /usr/local/apache2/conf/httpd.conf > my-httpd.con

編集してproxyサーバとして動くようにする
以下、変更した箇所。

@@ -139,10 +139,10 @@
 LoadModule setenvif_module modules/mod_setenvif.so
 LoadModule version_module modules/mod_version.so
 #LoadModule remoteip_module modules/mod_remoteip.so
-#LoadModule proxy_module modules/mod_proxy.so
-#LoadModule proxy_connect_module modules/mod_proxy_connect.so
+LoadModule proxy_module modules/mod_proxy.so
+LoadModule proxy_connect_module modules/mod_proxy_connect.so
 #LoadModule proxy_ftp_module modules/mod_proxy_ftp.so
-#LoadModule proxy_http_module modules/mod_proxy_http.so
+LoadModule proxy_http_module modules/mod_proxy_http.so
 #LoadModule proxy_fcgi_module modules/mod_proxy_fcgi.so
 #LoadModule proxy_scgi_module modules/mod_proxy_scgi.so
 #LoadModule proxy_uwsgi_module modules/mod_proxy_uwsgi.so
@@ -549,3 +549,13 @@
 SSLRandomSeed connect builtin
 </IfModule>
 
+<IfModule mod_proxy.c>
+    ProxyRequests On
+    ProxyVia On
+    <Proxy *>
+        Order deny,allow
+        Deny from all
+        Allow from all
+    </Proxy>
+</IfModule>

次に Dockerfileを用意する

FROM httpd:2.4
COPY ./my-httpd.conf /usr/local/apache2/conf/httpd.conf

続いてDocekrイメージを作成

$ docker build -t my-httpd .

そして Dockerコンテナ起動

docker run -it -d --rm --name my-httpd -p 8080:80 my-httpd

あとはスマホ側のプロキシ設定で、Docker起動したPCのIPアドレスを指定すればOK

  • プロキシのホスト名:Docker起動したPCのIPアドレス
  • プロキシポート:8080

これで、スマホからインターネットに出るときに、Docker起動したPCのhosts設定が参照されることになる。