Quantcast
Channel: Node.jsタグが付けられた新着記事 - Qiita
Viewing all 9050 articles
Browse latest View live

とりあえずMacでVueの環境を整えてみた

$
0
0

スタンドアロン版のVue.js devtoolsをmacに入れていくメモ。
なおNode.JSのインストールまでは
MacにNode.jsをインストール
を参考にさせていただいています。

必要なこと

1.Homebrewのインストール(済)
2.nodebrewのインストール
3.Node.jsのインストール
4.Vue.js devtoolsのインストール

Homebrew

HomebrewはMacでのソフトウェアや拡張機能の管理を行うパッケージマネージャです。便利です。

Homebrewのインストールについては
Homebrewのインストール
を参照してます。

環境

開始段階の環境として
・mac OS Catalina 10.15.7
・Homebrew 2.7.1
はインストール済みです。

nodebrewのインストール

・インストール

ターミナルを開いてnodebrewをインストールしていきます。

ターミナル コマンド
brewinstallnodebrew

・確認

インストールできたか確認してみます。

ターミナル コマンド
nodebrew-v
ターミナル 結果
nodebrew1.0.1Usage:nodebrewhelpShowthismessagenodebrewinstall<version>Downloadandinstall<version>(frombinary)nodebrewcompile<version>Downloadandinstall<version>(fromsource)nodebrewinstall-binary<version>Aliasof`install`(Forbackwardcompatibility)nodebrewuninstall<version>Uninstall<version>nodebrewuse<version>Use<version>nodebrewlistListinstalledversionsnodebrewlsAliasfor`list`nodebrewls-remoteListremoteversionsnodebrewls-allListremoteandinstalledversionsnodebrewalias<key><value>Setaliasnodebrewunalias<key>Removealiasnodebrewclean<version>|allRemovesourcefilenodebrewselfupdateUpdatenodebrewnodebrewmigrate-package<version>InstallglobalNPMpackagescontainedin<version>tocurrentversionnodebrewexec<version>--<command>Execute<command>usingspecified<version>Example:#installnodebrewinstallv8.9.4#useaspecificversionnumbernodebrewusev8.9.4

こんな感じでツラツラと出てきたらOKです。

Node.jsのインストール

・インストール

今回はとりあえず最新版をインストールしようと思います。

ターミナル コマンド
nodebrewinstall-binarylatest
ターミナル 結果
Fetching:https://nodejs.org/dist/v15.5.0/node-v15.5.0-darwin-x64.tar.gz########################################################################100.0%Installedsuccessfully

・有効化

最新版をそのまま有効化していきます。

ターミナル コマンド
nodebrewuselatest
ターミナル 結果
usev15.5.0

今回はインストールされている最新版がv15.5.0だったので、これでOKです。

・パスを通す

ターミナルから起動できるようにパスを通します。

ターミナル コマンド
echo'exportPATH=$HOME/.nodebrew/current/bin:$PATH'>>~/.bash_profile

確認のためターミナルで以下のコマンドを実行します。

ターミナル コマンド
node-v
ターミナル 結果
v15.5.0

先程有効化したv15.5.0が表示されました。
これでNode.jsのインストールは完了です。

Vue.js devtoolsのインストール

・インストール

最後に本命のVue.js devtoolsをインストールしていきます。
引き続きターミナルで以下のコマンドを実行します。

ターミナル コマンド
npminstall-g@vue/devtools

なんやかんや出てきます。

ターミナル 結果
added201packages,andaudited202packagesin21s6packagesarelookingforfundingrun`npmfund`fordetailsfound0vulnerabilities

・起動

インストールできたようなので、動作確認していきます。

ターミナル コマンド
vue-devtools

・結果

別ウインドウが開いて以下の画面が出れば起動成功です。
お疲れ様でした!
スクリーンショット 2021-01-01 16.50.48.png


よく知られたNode.js フレームワークの特徴、インストール方法、ハロー・ワールドまで

$
0
0

StatusCode.NOT_FOUND(404)の処理など、ゼロからコーディングするよりフレームワークを使用するのが普通と思いますが、Express.js以外にも、Node.jsには様々なものが存在します。

GitHub上でスターが多いフレームワークについて、特徴、インストール、Hello, worldを出すところまでをまとめてみました(Hello, worldはできるもののみ)。

GhostはフレームワークというよりCMSですが、自分でホスティングする場合はフリーです。一応調べたのもあって、最後に追記しました。

Node.jsって何?」とか初歩的な使用法を知りたい方は以下の書籍が丁寧で分かりやすいです(解説されてるフレームワークはExpress.jsですが)。

  • Jonathan Wexler著 吉川邦夫監訳 『入門 Node.js プログラミング』 翔泳社、2019

GitHubスター数

  • Express (51,393)
  • koa.js (30,482)
  • Sails.js (21.769)
  • fastify (16,842)
  • hapi (12,954)
  • Adonis (9,248)
  • total.js (4,095)
  • Ghost (35,867)

*2020/01/02時点

Express.js

expressjs.png

特徴

  • 言わずと知られたNode.jsのMVCフレームワーク
  • GitHubのスターは群を抜いて多い(2021/1時点)
  • 最もよく使用されており情報源も多くコミュニティも活発
  • 日本語の情報源も多い(書籍もある)

インストール

yarn add express --save

Hello, world

constexpress=require('express')constapp=express()constport=3000app.get('/',(req,res)=>{res.send('Hello World!')})app.listen(port,()=>{console.log(`Example app listening at http://localhost:${port}`)})

リンク

Koa.js

koa.js.png

特徴

  • Express.jsの開発者らで設計・開発
  • より小型(smaller)で表現力が高く(expressive)堅牢(robust)な基盤を志向
  • Express.jsで実装されていないメソッド・ライブラリに重点1

インストール

yarn global add koa

Hello world

constKoa=require('koa');constapp=newKoa();// responseapp.use(ctx=>{ctx.body='Hello Koa';});app.listen(3000);

Fastify

Fastify.png

特徴

  • パフォーマンスに重点を置いており高速性に特徴

インストール

yarn add fastify --save

Hello, world (async/await)

// Require the framework and instantiate itconstfastify=require('fastify')({logger:true})// Declare a routefastify.get('/',async(request,reply)=>{return{hello:'world'}})// Run the server!conststart=async()=>{try{awaitfastify.listen(3000)fastify.log.info(`server listening on ${fastify.server.address().port}`)}catch(err){fastify.log.error(err)process.exit(1)}}start()

hapi

hapi.png

特徴

  • 理念はシンプル、セキュア
  • 自由な発想(out-of-the-box)による開発も売り
  • 拡張にはmiddlewareではなくPluginを使用
  • Express.jsと同様のアーキテクチャ、かつコーディング量の削減に重点1

インストール

yarn add hapi --save

Hello, world

constHapi=require('hapi');constinit=async()=>{constserver=Hapi.server({port:3000,host:'localhost'});server.route({method:'GET',path:'/',handler:(request,h)=>{return'Hello World!';}});awaitserver.start();console.log('Server running on %s',server.info.uri);};process.on('unhandledRejection',(err)=>{console.log(err);process.exit(1);});init();

リンク

Adonis

Adonis.png

特徴

  • Laravelライクなフレームワーク

インストール

$ yarn global add @adonisjs/cli
$ adnis new demo
$ cd demo
$ adonis serve --dev

リンク

公式サイト

Total.js

totaljs.png

特徴

  • コアHTTPモジュールを土台とした高速なリクエストとレスポンス1
  • 性能とスケーラビリティで評判がよい1
  • ライブラリ・UIコンポーネントが豊富

インストール

Express.jsやKoa.jsほど簡単ではないのでGitHubから空のプロジェクトをダウンロードする(Exampleが多数公開されている)

$ git clone https://github.com/totaljs/emptyproject.git
$ cd emptyproject
$ yarn
$ node index.js

リンク

Sails.js

Sails.js.png

特徴

  • Express.jsが土台で、構造・ライブラリが豊富でありカスタマイズの省力化が可能1
  • Ruby on Railsライク
  • ORMをバンドルしており様々なDBとの連携が容易

インストール

$ yarn global add sails
$ sails new test-project

Web AppかEmptyを聞いてくる。今回は1を選択。

Choose a template for your new Sails app:
1. Web App  ·  Extensible project with auth, login, & password recovery
2. Empty    ·  An empty Sails app, yours to configure
(type"?"for help, or <CTRL+C> to cancel)
?
$ cd test-project
$ sails lift

こんな、おしゃれなメッセージが出てきます。

sailsjs2.png

デフォルトポート番号が他と違うので、localhost:1337にアクセスする。

リンク

公式サイト

ghost

ghost.png

特徴

  • マネージドサービス(Pro)は有料
  • セルフホスティングはフリー
  • セルフホスティングのためのサポートもある(Valet)
  • ググるとWordPressオルタナティブとして頻出
  • テーマも提供されている
  • フレームワークというよりはNode.jsベースのCMS

インストール

$ yarn global add ghost-cli
$ mkdir myapp
$ cd myapp
$ ghost install local

これでインストールされるので、http://localhost:2368/ghostを開く

リンク

公式サイト
GitHub

他の参考となるリンク


  1. Jonathan Wexler著 吉川邦夫監訳 『入門 Node.js プログラミング』 翔泳社、2019(p.92) 

expressのmiddleware使ってみた!

$
0
0

前書き

expressを使うとき何となくmiddlewareを使っていたので、middlewareについて調べていました。
※自分用の備忘録として記事を作成しています

middleareとexpressの関係性

前提として

expressでは基本的に一連のmiddlware関数呼び出す。

(個人的にはここが肝)

⇒ expressアプリケーションを起動することで必要なmiddlewareが読み出されるわけですね!!!
  上手く使えるようになりたいものです笑

middlewareができること

middlewareは下記の4つのことができます


  • 任意のコードを実行する。

  • リクエストオブジェクトとレスポンスオブジェクトを変更する。

  • リクエストレスポンスサイクルを終了する。

  • スタック内の次のミドルウェア関数を呼び出す。


具体的に見ていきましょう!


image.png

今回はapp.jsの33,34行目でマウント・パスを指定しないミドルウェア関数を設定しました。
この関数は、アプリケーションがリクエストを受け取るたびに実行されます。

localhost:3000にアクセスしてみましょう!

image.png

/ に対してgetリクエストを送信する過程において"hey yuta"とTime:~ が確認できましたね!

次に先ほど設定したmiddlewareの処理の一部を yuta という定数に格納し再度middlewareで表示しみましょう!

image.png
先ほどと同様の結果がみられました!
image.png

現在は全てのリクエストに対してconst yuta(定数) の処理が走るようになっています。
今度はconst yuta(定数)が走る範囲を限定してみましょう!

localhost:3000/yuta

のpathでアクセスした場合(GET)にのみconst yuta が走るようにしましょう!

image.png

image.png

確認できましたね!
このようにmiddlewareはどのリクエストに対してリクエストに合わせたmiddlewareを設定できます!

オンラインオセロを作ってHerokuにデプロイしてみた

$
0
0

環境

バックエンド:Node.js(Express)
ソケット通信:socket.io
フロントエンド:javascript
データベース: Redis
描画:HTML5 Canvas
デプロイ環境: Heroku

作ったもの

https://hazama-online-othello-game.herokuapp.com/

感想

初めて扱うものばかりで苦戦しましたが、下記の参考記事を読むことで、オンラインで遊べるゲームを実装でき、公開まで行うことができました。勉強になったので、別のオンラインゲームも作って、公開してみようと思います。

参考

Yet Another な Apple Silicon (M1) + Homebrew のベストプラクティス

$
0
0

ベストプラクティス多すぎ?

参考にさせていただきました。

はじめに

  • 基本的にはフロントエンド開発環境( Node.js や Yarn、Git、GnuPG など)の構築を目指します
  • ターミナル.app のRosetta を使用して開くオフにして、arm64 環境をデフォルトとして利用します
  • zsh シェルを使います

エイリアスの作成

x64もしくは a64でアーキテクチャを行ったり来たりできるように ~/.zshrcでエイリアスを設定します。

.zshrc
if(($+commands[arch] ));then
    alias x64='exec arch -arch x86_64 "$SHELL"'alias a64='exec arch -arch arm64e "$SHELL"'fi

Homebrew arm64 のインストール

arm64環境でターミナルが開かれていることを確認し、Homebrew のインストーラを起動します。

zsh
% arch
arm64

% /bin/bash -c"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

インストーラが M1 Mac に対応したため、自動的に /opt/homebrew/以下にインストールされます。

パッケージのインストール(一例)

% which brew
/opt/homebrew/bin/brew

% brew install-v node yarn git gnupg

(2021/01/02) 現在、上記4つのパッケージ(とそれが依存するパッケージ)はすべてバイナリでインストールすることが可能です。

ただし、NodeJSnode@15のみ対応済みです。node@14node@12はビルドできません。

Homebrew x86_64 のインストール

ターミナルを x64へ切り替えます。

zsh
% a64
% uname-m
x86_64

インストーラは arm64版と同じです。従前通り /usr/local/以下へインストールされます。

zsh
% /bin/bash -c"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

せっかくなので、こちらでは LTS 版の node@14 をインストールします。

zsh
% which brew
/usr/local/bin/brew

% brew install-v node@14 yarn

環境ごとに PATH などのシェル変数を切り換える

arm64 では /opt/homebrew/binを優先し、x64 では node@14 のために /usr/local/opt/node@14を優先し、LDFLAGSCPPFLAGSの2つのシェル変数も設定します。

.zshrc
if[[$(uname-m)="x86_64"]];then
  typeset-U path PATH
  path=($HOME/bin(N-/)
    /usr/local/opt/node\@14/bin(N-/)
    /usr/local/bin(N-/)
    /usr/local/sbin(N-/)$path)export LDFLAGS="-L/usr/local/opt/node@14/lib"export CPPFLAGS="-I/usr/local/opt/node@14/include"else
  typeset-U path PATH
  path=($HOME/bin(N-/)
    /opt/homebrew/bin(N-/)
    /opt/homebrew/sbin(N-/)
    /usr/local/bin(N-/)
    /usr/local/sbin(N-/)$path)export LDFLAGS=""export CPPFLAGS=""fi

いまのところ、うまく行っている感じです。

スクリーンショット 2021-01-02 17.05.33.png

スクリーンショット 2021-01-02 17.06.05.png

Express Tour #1 Express

$
0
0

Expressの公式チュートリアルに沿ってexpressを体験してみる。
Node.jsはインストール済の状態で臨みましょう。

Installing

アプリケーション用のディレクトリを作成しましょう。

$ mkdir myapp
$ cd myapp

次にnpm initコマンドでpackage.jsonを作成します。と、案内されていますがとりあえず全てデフォルトでよいと思いますが、
メインファイルの名前はapp.jsにしておきましょう。

$ npm init

次にExpressをインストールします。

$ npm install express --save

Hello world example

下記のコードは最も単純なアプリの例です。
このアプリはサーバーを起動し、ポート3000で接続できます。 アプリは「Hello World!」で応答します。

Node.js
constexpress=require('express')constapp=express()constport=3000app.get('/',(req,res)=>{res.send('Hello World!')})app.listen(port,()=>{console.log(`Example app listening at http://localhost:${port}`)})

ローカルでの実行方法

myappディレクトリに、app.jsという名前のファイルを作成し、上記の例のコードをコピーします。

次のコマンドでアプリを起動します。

$ node app.js

ブラウザで以下にアクセスすることで出力結果を見ることができます。

http://localhost:3000

参考

http://expressjs.com/en/starter/installing.html

【Robocode】楽しくお勉強できるJavaScriptの戦車ゲームをWindows環境で動作させる。

$
0
0

Robocodeとは

『Robocode』は、オープンソースの教育ゲームである。JavaやVer1.7.2以降の.NET Frameworkの熟達に役立つ。 単純なロボットはわずか数分で作成できるが、本格的に完成させる場合は数ヶ月かかることがある。
出典:https://ja.wikipedia.org/wiki/Robocode

学生時代 Javaが全くかけなかった自分は、RobocodeにハマったのをきっかけにJavaにのめり込むようになりました。

image.png

・・・のJavaScript版を発見したので、手軽に動作させる手順をメモ書きで書いておきます。

前提となる環境

OS:
 Windows10

他:
 node.js(筆者環境はV14)

JavaScript版の導入

githubよりソースコードの入手

githubにアクセスします

https://github.com/youchenlee/robocode-js

gh-pagesブランチに移動します

Windows環境だとコンパイルが大変なので、gh-pagesブランチからコンパイル済のファイルを入手します
image.png

cloneするかzipを取得します

image.png

環境の整備を行います。

npmコマンドで動作に必要なファイルを取得し、ファイルを整理します

コマンドプロンプトを立ち上げて
cloneしたフォルダ若しくは解凍したフォルダに移動して、npm installコマンドを投げます。
※expressはWindows環境でブラウザに依存せずにアプリを動かすために用います

npm install
npm install express

ファイルの整理を行います。

mkdir public
move  /y .\img .\public\
move  /y .\robot .\public\
move  /y .\robot .\public\
move  /y .\base-robot.js .\public\
move  /y .\base-robot.ls .\public\
move  /y .\*.js .\public\
move  /y .\*.ls .\public\
move  /y .\*.html .\public\
move  /y .\Makefile .\public\
echo nul > server.js

ブラウザに依存せず動作させるための準備を行います。

server.jsを開いて下記のコードを貼り付けます

constexpress=require('express')constapp=express()app.use(express.static('public'))constport=3000app.get('/',(req,res)=>{res.send('index.html')})app.listen(port,()=>{console.log(`listening at http://localhost:${port}`)})

動作確認してみます。


コマンドプロンプトで下記のコマンドを実行します。

node server.js

ブラウザを開いて、http://localhost:3000/index.htmlにアクセスします。
下記ような画面が出てきたら確認完了です。

次回はRobocodeの遊び方について解説します。

image.png

[Node.js] macで環境構築をしよう

$
0
0

環境

macOS Catalina バージョン 10.15.7

nodebrewをインストール

$ curl -L git.io/nodebrew | perl - setup

上記のコマンドを実行するとターミナル上に以下のように表示されます。

========================================
Export a path to nodebrew:

export PATH=$HOME/.nodebrew/current/bin:$PATH
========================================

ターミナル上の export PATH=$HOME/.nodebrew/current/bin:$PATHのコマンドをコピーして、もう一度貼り付けてエンターキーで実行してください。

$ export PATH=$HOME/.nodebrew/current/bin:$PATH

インストールに成功しているかを確認しましょう。

$ nodebrew -v

nodebrew use [バージョン名]と表示されたら成功です。

Node.jsのインストール

$ node -v

コマンドを実行し、バージョンが表示された場合はすでにNode.jsがインストールされています。
command not found: nodeと表示された場合はまだNode.jsがインストールされていませんのでインストールしていきます。

インストールする際のコマンドはいくつかありますが、今回は もっとも新しいバージョンかつ安定版を意味する stable のバージョンをインストールしていきます。

$ nodebrew install-binary stable

次にインストールしたバージョンを使えるようにするコマンドを実行します。
nodebrew use 任意のバージョン名を実行します。今回はstable のバージョンが v14.15.3なので以下のようになります。

$ nodebrew use v14.15.3

インストールに成功しているかを確認しましょう。

$ nodebrew ls

コマンドを実行して以下のように使いたいバージョンが current: 任意のバージョン名と表示されていれば成功です。

v14.15.3
v15.5.0

current: v14.15.3

puppeteerでドットインストールの総学習時間を取得する

$
0
0

オンライン学習サイトのドットインストールには学習時間を確認できる機能がある。
image.png
……がこの時間がどうも正しくない。実際の学習時間よりだいぶ少ない。おそらく回線が不安定な環境で動画を視聴しても視聴時間に加算されない。→ 自動取得した合計と視聴時間が一致していたので、ドットインストールの視聴時間は正しかった。

正しい学習時間が欲しいのでブラウザ自動操作ツールであるpuppeteerを利用して学習時間の合計を自動で求める。

注意

自動操作ツールでのアクセスは頻度が高すぎるとサービスへ負荷を与えてアクセスブロック等の対象となるので、適切にwaitを入れてあげると良い。

戦略

①puppeteerでヘッドレスブラウザ(目に見えないブラウザ)を起動する。
②ドットインストールのユーザ認証を行う。
③ドットインストールのプロフィールページへ遷移し、受講した講座のURL一覧を取得する。

④講座ページへ遷移し、受講完了した動画の視聴時間の合計を求める。(これを③で取得したURL分繰り返す)

⑤集めたデータを以下の形で出力する。

[{lessonName:'C#入門',lessonUrl:'https://dotinstall.com/lessons/basic_csharp',completeTime:'01:49:23',incompleteTime:'00:21:01'},{......}]

実装

①npmプロジェクトを作成し、puppeteerとログイン情報入力に使用するreadline-syncをインストールする。

$ npm init -y$ npm i -S puppeteer readline-sync

②puppeteerの動作確認で、googleにアクセスしてみる。

main.js
constpuppeteer=require('puppeteer');// awaitを使うためasync関数を定義する。asyncfunctionmain(){// puppeteerのブラウザを起動する。constbrowser=awaitpuppeteer.launch();// ブラウザの新しいタブを開く。constpage=awaitbrowser.newPage();// googleにアクセスする。awaitpage.goto('https://google.com');// スクショを撮る。awaitpage.screenshot({path:'test.png'});// ブラウザを閉じる。awaitbrowser.close();}main();

上記を実行してpuppeteerからgoogleにアクセスできることを確認した。

②ログイン情報を入力させる。

ドットインストールにログインするためのログイン情報をユーザに入力させる。パスワード入力時は入力内容を出力させず履歴に残させないことがミソだ。

main.js
constpuppeteer=require('puppeteer');constreadlineSync=require('readline-sync');asyncfunctionmain(){// メールアドレスを入力させる。constmail=readlineSync.question('mail: ');// パスワードを入力させる。オプションで入力内容を出力させない。constpassword=readlineSync.question('password: ',{hideEchoBack:true});console.log(mail,password);}main();

③ドットインストールにログインする。

ドットインストールへのログインは以下の流れで行う。

ログインページへの遷移(https://dotinstall.com/login)

ユーザ名とパスワードのinput欄へ自動入力する。

ログインボタンを押下させる。

main.js
constpuppeteer=require('puppeteer');constreadlineSync=require('readline-sync');asyncfunctionmain(){......// ログインページへの遷移awaitpage.goto('https://dotinstall.com/login');// メールアドレスとパスワードの入力awaitpage.evaluate(text=>document.querySelector('#mail').value=text,mail);awaitpage.evaluate(text=>document.querySelector('#password').value=text,password);// ログインボタン押下awaitpage.click('#login_button');......

④受講した講座のURL一覧を求める。

ユーザ名をクリックしプロフィールページへ遷移する。

各講座へのリンクのaタグのhref属性を取得する。

講座のページへ遷移し時間を求める。

asyncfunctionmain(){......awaitPromise.all([page.waitForNavigation({waitUntil:['load','networkidle2']}),page.click('#login_button')]);awaitPromise.all([page.waitForNavigation({waitUntil:['load','networkidle2']}),page.click('a.user-name')]);consturls=awaitpage.evaluate(()=>{consturls=[];constaElements=document.querySelectorAll('.cardBox > h3 > a');for(constaElementofaElements){urls.push(aElement.getAttribute('href'));}returnurls;});constresult=[];for(consturlofurls){constlessonUrl='https://dotinstall.com'+url;awaitpage.goto(lessonUrl,{waitUntil:['load','networkidle2']});// 負荷軽減のため3秒待機するawaitpage.waitForTimeout(3000);constlessonName=awaitpage.$eval('.package-info-title span',element=>element.innerHTML);const[completeTime,incompleteTime]=awaitpage.evaluate(()=>{letcompleteTime=0;letincompleteTime=0;constsectionElements=document.querySelectorAll('#lessons_list > li');for(constsectionElementofsectionElements){consttime=sectionElement.querySelector('.lessons-list-title > span').innerHTML;const[,min,sec]=time.match(/\((\d\d)\:(\d\d)\)/);constseconds=parseInt(min)*60+parseInt(sec);constisCompleted=sectionElement.querySelector('.lesson_complete_button > span').innerHTML==='完了済';if(isCompleted){completeTime+=seconds;}else{incompleteTime+=seconds;}}return[completeTime,incompleteTime];});functionsec2time(sec){return`${parseInt(sec/3600)}:${parseInt((sec/60)%60)}:${sec%60}`;}result.push({lessonName,lessonUrl,completeTime:sec2time(completeTime),incompleteTime:sec2time(incompleteTime)});}console.log(result);......

node.js + express + nodemailer

$
0
0

node.jsでmail機能を実装するには…

node.jsでmail機能を実装するにあたってnpmのnodemailerというパッケージを使っていきます!

$ mkdir myapp
$ cd myapp
$ npm init

ルートフォルダーにcontact.jsファイルを作って下記の記述をします

contact.js
"use strict";constnodemailer=require("nodemailer");asyncfunctionmain(){lettransporter=nodemailer.createTransport({ignoreTLS:true,port:1025,secure:false,// true for 465, false for other ports});letinfo=awaittransporter.sendMail({from:'"Fred Foo 👻" <foo@example.com>',// sender addressto:" s@gmail.com",// list of receiverssubject:"成功!!!!",// Subject linetext:"サンプルテキストです",// plain text body});console.log("送られたメッセージ: %s",info.messageId);}main().catch(console.error);

参考)https://nodemailer.com/about/

メールが送信されているかをチェックするためにmailcatcherを使います
mailcatcherを使うためのパッケージをインストールして起動してみます

$ npm install -g maildev
$ maildev

image.png

$ node contact.js

localhostの1080ポートが起動しているのでhttp://127.0.0.1:1080にアクセスすると

image.png

image.png

ポート1025でメールを受け取ることができました!

あとがき

mail機能を実装するためにはamazon SESでもできるようです
今回nodemailerを使ってみましたが、圧倒的にメールサーバについての理解が乏しいと実感しました(´;ω;`)
mailについて学習し、また記事を書きます!

ElectronアプリでKeycloakと連携(1. keycloakの設定編)

$
0
0

背景

以前、Nuxt.jsでKeycloakと連携したログイン機能を実装してみた。
https://qiita.com/yusuke-ka/items/1beef8d9e0bbeb052e5a

今度はelectronアプリでKeycloakと連携したログイン機能を実装してみようと思う。

その準備として、今回は、まずkeycloakの設定をやってみる。

keycloakのインストール等は以下で実施したので、これを使う。

https://qiita.com/yusuke-ka/items/1beef8d9e0bbeb052e5a#keycloak%E3%81%AE%E3%82%BB%E3%83%83%E3%83%88%E3%82%A2%E3%83%83%E3%83%97

keycloakの設定

keycloakの管理UIにアクセスして、keycloakの設定を実施していく。

レルム追加

最初にレルムを追加する。

image.png

「Name」を入力して、「Create」ボタンを押すだけ。

クライアントの追加

続いて、作成したレルムの「Clients」でclientを新規作成。

image.png

右のほうにある「create」ボタンを押す。

image.png

Clientの追加画面が表示されるので、「Client ID」だけ入力して「Save」ボタンを押すと、詳細設定画面に遷移する。

image.png

「Client Protocol」の設定は「openid-connect」のままにする。

electronアプリの場合、クライアント認証は難しそうなので、「Access Type」もとりあえず「public」のままにしておく。
(「confidencial」にすると、Client Secretや秘密鍵をクライアント側で保持する必要がでてくる。electronアプリのように、エンドユーザーのPCにインストールするようなアプリだと、これらを安全に保持するのはとても難しいと思われる。)

「Standard Flow Enabled」が有効になっていることを確認。

electronアプリから利用することを想定しているので、「Valid Redirect URIs」には、とりあえず、「 http://localhost/callback」を設定しておく。

publicなクライアントの場合、コードの横取りに対処する必要があるとのことなので、PKCE(Proof Key for Code Exchange by OAuth Public Clients)の設定を使うようにしてみる。

image.png

下の方にある「Advanced Settings」を展開すると、「Proof Key for Code Exchange Code Challenge Method」という項目が出てくる。「Plain」と「S256」が選択できるので、今回は「S256」を選択してみた。

「save」ボタンで保存する。

ユーザーの追加

確認用のユーザーも作っておく。やり方は下記を参照(「Keycloakの設定」という項目の最後のほう)。
https://qiita.com/yusuke-ka/items/1beef8d9e0bbeb052e5a#keycloak%E3%81%AE%E8%A8%AD%E5%AE%9A

次回予告

electronアプリでKeycloakと連携したログイン機能を実装するために、今回は、Keycloak側の設定を実施した。

次は、electronアプリを作成する。
https://qiita.com/yusuke-ka/items/a4767c511f03b6083afc

ElectronアプリでKeycloakと連携(2. Electronアプリの作成編)

$
0
0

背景

前回、keycloakの設定まで実施した。今回は認証機能を導入するElectronアプリを実装してみる。
https://qiita.com/yusuke-ka/items/69d4146f344a95aa4662

Auth0のブログで公開されているやり方が安全そうな気がするので、ここを参考にしてやってみようと思う。
https://auth0.com/blog/securing-electron-applications-with-openid-connect-and-oauth-2/

といっても、今回は単純なElectronアプリを作るだけで、認証回りは次回。

準備

まずは、プロジェクトの作成から。

>mkdir electron
>cd electron
>yarn init

initはすべてデフォルトのまま。

続いて依存のインストールを実施する。

>yarn add electron

Electronのインストールだけ。少し時間がかかった。

Electronアプリの作成

最初に、アプリのホームページを作ってみる。

renderersというフォルダを作り、その下に以下のようなファイル(home.html)を作成する。

renderers/home.html
<!DOCTYPE html><htmllang="en"><head><metacharset="UTF-8"/><title>Electron App</title></head><body><p>Home</p><buttonid="logout"class="btn">Logout</button></body></html>

直接ブラウザでhome.htmlを読み込んだ際の見た目はこんな感じ。

image.png

テスト用なので、Homeという文字とログアウトボタンだけを配置したシンプルなホームページ。
もちろん、ログアウトボタンを押しても何も起こらない。

続いて、このページをElectronで表示するようにしてみる。

今度はmainというフォルダを作成し、その下にapp-process.jsというファイルを置く。

main/app-process.js
const{BrowserWindow}=require("electron");functioncreateAppWindow(){letwin=newBrowserWindow({width:1000,height:600,webPreferences:{nodeIntegration:true,enableRemoteModule:true,},});win.loadFile("./renderers/home.html");win.on("closed",()=>{win=null;});}module.exports=createAppWindow;

nodeIntegration: trueは、Node.jsの組み込みを実施するための設定。
enableRemoteModule: trueは、レンダラープロセスがメインプロセスと通信するための設定。

createAppWindowというfunctionをexportして公開している。

続いて、ルートフォルダの下に、最初に呼び出されるファイル(main.js)を作成する。

main.js
const{app}=require('electron');constcreateAppWindow=require('./main/app-process');asyncfunctionshowWindow(){try{returncreateAppWindow();}catch(err){// TODO 認証}}app.on('ready',showWindow);app.on('window-all-closed',()=>{app.quit();});

今はまだ、先ほどのapp-process.jsが公開しているcreateAppWindowを呼び出すだけの実装。

とりあえず、この状態で動かしてみる。

package.jsonに以下を追記。

package.json
"scripts":{"start":"electron ./main.js"}

electronコマンドでmain.jsを呼び出しているだけ。

実行。

>yarn start

image.png

Electronアプリとして、ホームページが表示された。

次回予告

ElectronアプリでKeycloakと連携したログイン機能を実装するために、今回は、確認用のシンプルなElectronアプリの実装をおこなった。

次は、いよいよElectronアプリに認証機能を導入する。
https://qiita.com/yusuke-ka/items/17a5b8fbd544c4c211a3

ElectronアプリでKeycloakと連携(3. ログイン機能導入編)

$
0
0

背景

今回は、前回作成したelectronのアプリに認証機能を追加してみる。
https://qiita.com/yusuke-ka/items/a4767c511f03b6083afc

今回も前回に引き続きAuth0のブログを参考にしてみる。
https://auth0.com/blog/securing-electron-applications-with-openid-connect-and-oauth-2/

※(注意)自学習を目的として書いています。この記事に記載の内容は、あくまで自分(素人)の解釈となります。

electronアプリへの認証機能追加

前回作成したシンプルなelectronアプリにログイン機能、ログアウト機能を追加してみる。

ログイン機能の実装

まずは、単純にkeycloakのログイン画面にリダイレクトして、認証するところまでを実装してみる。

前回作ったmainフォルダの下に今度はauth-process.jsというファイルを置く。

main/auth-process.js
const{BrowserWindow}=require('electron');constcreateAppWindow=require('../main/app-process');letwin=null;functioncreateAuthWindow(){destroyAuthWin();win=newBrowserWindow({width:1000,height:600,webPreferences:{nodeIntegration:false,enableRemoteModule:false}});win.loadURL('http://localhost:8080/auth/realms/electron/protocol/openid-connect/auth?'+'scope=openid profile offline_access&'+'response_type=code&'+'client_id=test&'+'redirect_uri=http://localhost/callback');const{session:{webRequest}}=win.webContents;constfilter={urls:['http://localhost/callback*']};webRequest.onBeforeRequest(filter,async()=>{// TODO トークン取得createAppWindow();returndestroyAuthWin();});win.on('closed',()=>{win=null;});}functiondestroyAuthWin(){if(!win)return;win.close();win=null;}module.exports={createAuthWindow,};

createAuthWindow()は、ログインウインドウを生成するfunctionで、最後にexportすることで公開している。

最初にウインドウを破棄するfunctionであるdestroyAuthWin()を一旦呼び出してから生成を開始している。

ホームページ用のウインドウとは異なり、ログインウインドウはセキュリティリスクを軽減するため以下のように設定している。
nodeIntegration: falseは、Node.jsの組み込みを実施しないための設定。
enableRemoteModule: falseは、レンダラープロセスがメインプロセスと通信しないための設定。

その後、win.loadURL()で呼び出すログインページを指定している。
ここは、前々回にkeycloakで設定したレルムにおける認証用のURLを指定しておく。

webRequest.onBeforeRequest()で特定のURLへのリクエストの実行前に実施する処理を書いている。
filterに'http://localhost/callback*'を指定しているので、keycloakでログインした後のリダイレクトのURLにリクエストを送る際に発動する。

処理としては、最初にトークンを取得して、ホームページ用のウインドウを開き、ログイン用のウインドウを閉じる。
ただ、トークンの取得はまだ実装していないので、現時点ではTODOとしておく。

続いて、main.jsを以下のように修正。

main.js
...constcreateAppWindow=require('./main/app-process');const{createAuthWindow}=require('./main/auth-process');// 追加asyncfunctionshowWindow(){// return createAppWindow(); 一旦コメントアウトreturncreateAuthWindow();}...

createAppWindow()に代えて、createAuthWindow() を呼び出すようにしてみる。

ちなみに、createAuthWindow() の処理の中でcreateAppWindow()が呼び出される。

一旦ここで実行してみる。

>yarn start

image.png

無事にkeycloakのログイン画面が表示された。

ちなみに、今回は組み込みのBrowserWindowにログイン画面を表示している(Auth0のブログも同様)。この場合、セキュリティ的にはあまりよろしくないという意見もあるらしい。

大きなところでは下記の2つの問題点が挙げられる。

  • 正規のサイトかどうかアプリの利用者には判定できないので、利用者に不安を与える。

  • 表示したログイン画面からいろんなところに遷移できるようになっていると、組み込みのBrowserWindow内で、悪さをされる可能性が出てくる。

一方で、標準ブラウザを利用してログインさせるやり方もあるけど、いきなり別のブラウザが開く挙動は、ユーザービリティ的には微妙な気がする(ログイン後に標準ブラウザのタブを閉じる方法も考える必要がある)。

まぁ、今回は認証サービスも自前で作っていて(そもそもテスト用だけど。。)、googleやtwitterのアカウントと連携しているわけではないので、ユーザービリティ優先ということで。keycloakのログイン画面から他のページに遷移することもできなさそうだし。

ということで、用意しておいたユーザーでログインしてみる。

image.png

ちゃんと前回のホームページが表示された。

ログアウト機能の追加

トークン取得処理を先に実装すべきかもしれないが、「logout」ボタンを押しても何も起こらないのは寂しいので、先にログアウトを実装してみる。

auth-process.jsを以下のように編集。

main/auth-process.js
...functioncreateLogoutWindow(){constlogoutWindow=newBrowserWindow({show:false,});logoutWindow.loadURL('http://localhost:8080/auth/realms/electron/protocol/openid-connect/logout');logoutWindow.on('ready-to-show',async()=>{logoutWindow.close();// TODO クライアント側のログアウト処理});}module.exports={createAuthWindow,createLogoutWindow,// 追加};

createLogoutWindow()というfunctionを追加してexportしている。

createLogoutWindow()では、非表示のウインドウを開いて、keycloakのlogout用エンドポイントを呼び出しているだけ。

続いて、renderersフォルダの下に、以下のようなhome.jsを追加する。

renderers/home.js
const{remote}=require("electron");constauthProcess=remote.require("./main/auth-process");document.getElementById("logout").onclick=()=>{authProcess.createLogoutWindow();remote.getCurrentWindow().close();};

ログアウトボタンが押されたときにログアウト処理が実行されるようにしている。

home.htmlでこのスクリプトファイルを読み込むようにする。

renderers/home.html
<htmllang="en">

  ...

  <script src="home.js"></script></html>

実行して確認してみる。

>yarn start

image.png

ログアウトボタンを押すと、Electronアプリが閉じることは確認できた。

ただ、本当にログアウトできたか怪しいので、keycloak側で確認してみる。

image.png

ログアウトボタン押下前と押下後でセッションが減っていることが確認できた。

トークンの取得

ここからが本番。

先に「axios」と「jwt-decode」をインストールしておく。

>yarn add axios
>yarn add jwt-decode

今度はservicesというフォルダを作成し、その下にauth-service.jsというファイルを置いて以下のように実装した。

services/auth-service.js
constjwtDecode=require("jwt-decode");constaxios=require('axios');constcrypto=require('crypto');consturl=require('url');letcodeVerifier=null;letaccessToken=null;letprofile=null;functiongetAccessToken(){returnaccessToken;}functiongetProfile(){returnprofile;}asyncfunctionloadTokens(callbackURL){consturlParts=url.parse(callbackURL,true);constquery=urlParts.query;varparams=newURLSearchParams();params.append('grant_type','authorization_code');params.append('client_id','test');params.append('code',query.code);params.append('redirect_uri','http://localhost/callback');params.append('code_verifier',codeVerifier);try{constresponse=awaitaxios.post('http://localhost:8080/auth/realms/electron/protocol/openid-connect/token',params);accessToken=response.data.access_token;profile=jwtDecode(response.data.id_token);}catch(error){// TODO ログアウトthrowerror;}}functiongetChallenge(){codeVerifier=base64URLEncode(crypto.randomBytes(32));returnbase64URLEncode(sha256(codeVerifier));}functionsha256(buffer){returncrypto.createHash('sha256').update(buffer).digest();}functionbase64URLEncode(str){returnstr.toString('base64').replace(/\+/g,'-').replace(/\//g,'_').replace(/=/g,'');}module.exports={getChallenge,getAccessToken,getProfile,loadTokens,};

loadTokens()がトークン取得のメインロジック。認証後のリダイレクト時に呼び出せるようにするので、このfunctionはexportしておく。

loadTokens()の中で、トークン取得用のエンドポイントにPOSTリクエストを投げる。

リクエストパラメータにはURLSearchParamsを利用する必要があるので注意。また、PKCEを利用するので、code_verifierも指定している。

取得したaccess_token(accessToken)とid_token(profile)は、メモリ上にキャッシュしておき、getAccessToken()とgetProfile()で外から取得できるようにしておく。

getChallenge()では、最初にcode_verifierで利用する値を生成する。この値を利用して、code_challengeを生成する。

先に生成したcode_verifierの値はloadTokens()の中で、リクエストパラメータに指定するのでメモリ上にキャッシュしておく(codeVerifier)。

次に生成したcode_challengeの値は、keycloakのログイン画面呼び出し時に利用するため、getChallenge()の返り値として取得できるようにし、getChallenge()は、exportしてauth-process.jsから呼び出せるようにしておく。

PKCEの流れは下記のような感じ。

  1. ログイン画面のリクエスト時に指定されたcode_challengeの値を、keycloak側が認可コードに紐づけて保存しておく。
  2. 後のトークン取得リクエスト時に送られてきたcode_verifierを利用して、keycloak側でクライアントと同じロジックでチャレンジを生成。
  3. 1と2のチャレンジコードを比較して、同じクライアントから送られたリクエストであることを検証する。

つまり、認証後のリダイレクトURLに含まれる認可コードを何らかの方法で盗んだとしても、正しいcode_verifierの値を知っていなければ、トークン取得に失敗することになるということらしい。

auth-process.js側の修正は以下のような感じ。

main/auth-process.js
const{BrowserWindow}=require('electron');constcreateAppWindow=require('../main/app-process');constauthService=require('../services/auth-service');// 追加...functioncreateAuthWindow(){... win.loadURL('http://localhost:8080/auth/realms/electron/protocol/openid-connect/auth?'+'scope=openid profile offline_access&'+'response_type=code&'+'client_id=test&'+'code_challenge='+authService.getChallenge()+'&'+// 追加'code_challenge_method=S256&'+// 追加'redirect_uri=http://localhost/callback');...webRequest.onBeforeRequest(filter,async({url})=>{// パラメータにurlを追加awaitauthService.loadTokens(url);// 追加createAppWindow();returndestroyAuthWin();});...}...

まずは、keycloakのログイン画面呼び出し時のURLのパラメータに「code_challenge」と「code_challenge_method」を追加している。

「code_challenge」の値は、auth-service.jsのgetChallenge()で取得した値を指定する。

「code_challenge_method」はkeycloak側の設定に合わせて「S256」を指定する。

続いて、keycloakのログイン画面で認証した後のリダイレクト時の事前処理で、auth-service.jsのloadTokens()を呼び出し、keycloakから取得した認可コードを利用してアクセストークンを取得する。

その後の、ホームのウインドウを生成し、ログイン用のウインドウを破棄する流れは変更なし。

せっかくなので、取得した情報をホームの画面で表示できるようにしてみる。home.htmlを以下のように修正。

renderers/home.html
  ...

  <body><p>Home</p><div><!-- ここから追加--><textareaid="token"rows="12"cols="120"></textarea><textareaid="profile"rows="8"cols="120"></textarea></div><!-- ここまで--><buttonid="logout">Logout</button></body>

  ...

</html>

tokenとprofileを表示するテキストエリアを追加しただけ。

続いて、home.jsを編集して、auth-service.jsからアクセストークンとプロファイルの情報を取得して、テキストエリアにセットするようにする。

renderers/home.js
const{remote}=require("electron");constauthProcess=remote.require("./main/auth-process");constauthService=remote.require("./services/auth-service");// 追加constwebContents=remote.getCurrentWebContents();// 追加// 追加webContents.on("dom-ready",()=>{consttoken=authService.getAccessToken();constprofile=authService.getProfile();document.getElementById("token").innerText=token;document.getElementById("profile").innerText=JSON.stringify(profile);});...

実行して確認してみる。

>yarn start

用意したユーザーでログインすると、下記の画面が表示された。

image.png

無事にアクセストークンとプロファイルが取得できていることが確認できた。

リフレッシュトークンの利用

続いて、リフレッシュトークンを利用する実装を導入してみようと思う。

先に「keytar」をインストールしておく。

>yarn add keytar

「keytar」は「システムのキーチェーンでパスワードを取得、追加、置換、削除するためのネイティブモジュール」とのこと。

アクセストークンはメモリ上に保持しているが、リフレッシュトークンは、一度クライアントを落とした後に再起動しても利用できるように、何らかの形で永続化する必要がある。

ということで、比較的安全に保存するために、システムのキーチェーンを利用する。

「keytar」をインストールしたら、auth-service.jsに下記のようにリフレッシュトークンの取得処理を追加する。

services/auth-service.js
constjwtDecode=require("jwt-decode");constaxios=require('axios');constkeytar=require("keytar");// 追加constos=require("os");// 追加constcrypto=require('crypto');consturl=require('url');letcodeVerifier=null;letaccessToken=null;letprofile=null;letrefreshToken=null;// 追加...// function追加asyncfunctionrefreshTokens(){constrefreshToken=awaitkeytar.getPassword('electron-openid-test',os.userInfo().username);if(refreshToken){varparams=newURLSearchParams();params.append('grant_type','refresh_token');params.append('client_id','test');params.append('refresh_token',refreshToken);try{constresponse=awaitaxios.post('http://localhost:8080/auth/realms/electron/protocol/openid-connect/token',params);accessToken=response.data.access_token;profile=jwtDecode(response.data.id_token);}catch(error){// TODO ログアウトthrowerror;}}else{thrownewError("No available refresh token.");}}...asyncfunctionloadTokens(callbackURL){...try{constresponse=......// 追加refreshToken=response.data.refresh_token;if(refreshToken){awaitkeytar.setPassword('electron-openid-test',os.userInfo().username,refreshToken);}}catch(error){...}}...module.exports={...refreshTokens,// 追加};

refreshTokens()のfunctionを追加して、トークンエンドポイントにトークンのリフレッシュを要求する。

refreshTokenが有効であれば、アクセストークンとプロファイル情報を返してくれる。refreshTokenを取得していない場合や、refreshTokenが無効な場合はエラーをあげる。

refreshTokenは、アクセストークンの取得時に一緒に返してくれているので、loadTokens()の中でメモリ上にキャッシュし、システムのキーチェーンにも保存しておく。

続いて、main.jsの最初でリフレッシュトークンの処理を実行するように、showWindow()の中身を変更する。

main.js
...constauthService=require('./services/auth-service');// 追加asyncfunctionshowWindow(){try{awaitauthService.refreshTokens();returncreateAppWindow();}catch(err){createAuthWindow();}}...

最初にrefreshTokens()を呼び出して、アクセストークンをリフレッシュしてから、ホームのウインドウを開く。

リフレッシュトークンが無効になっている場合などには、エラーがあがってくるので、catchしてログイン処理にまわす。

実行して確認してみると、とりあえず以下の動作を確認できた。

  1. 初回起動
    → ログイン画面が表示されて、ログインするとホームの画面が表示される

  2. 一旦アプリを落として再起動
    → ログイン画面が表示されずにホームの画面が表示される

ただ、ログアウト時にリフレッシュトークンを破棄する処理を実装していないので、このままでは再度ログイン画面を表示させるためには、30日間放置するか、keycloakの管理画面で強制的にログアウトさせる必要がある。

※認証時のリクエストで「scope」に「offline_access」を指定しているため、keycloakの「Offline Session Idle」がタイムアウト時間(デフォルト30日)となる。

ログアウト時のトークン破棄

ということで、ログアウト時にトークンを破棄する実装を入れる。

services/auth-service.js
...asyncfunctionrefreshTokens(){...if(refreshToken){...try{...}catch(error){awaitlogout();// 追加throwerror;}}else{...}}asyncfunctionloadTokens(callbackURL){...try{...}catch(error){awaitlogout();// 追加throwerror;}}...// 追加asyncfunctionlogout(){awaitkeytar.deletePassword('electron-openid-test',os.userInfo().username);accessToken=null;profile=null;refreshToken=null;}module.exports={...logout,// 追加};

logout()のfunctionを追加して、各種トークンを削除する処理を入れる。また、各種トークン取得処理の失敗時にこれを呼び出すようにする。

明示的にログアウトが指示された場合にもlogout()が呼び出せるように、exortして公開しておく。

続いて、明示的にログアウトが指示された場合の、呼び出し側のコード修正。

auth-process.jsを修正する。

main/auth-process.js
...functioncreateLogoutWindow(){...logoutWindow.on('ready-to-show',async()=>{logoutWindow.close();awaitauthService.logout();// 追加});}...

ログアウトが指示されたときに、auth-service.jsのlogout()を呼び出すようにするだけ。

実行して確認してみると、以下のよう感じで意図した動作となった。

image.png

おまけ(Signed JWTを利用したクライアント認証の導入)

ここまでで一通りの実装は出来たはず(取得したアクセストークンは利用してないけど。。)なので、ここからはおまけ。

実は、最初は署名付きのJWTを利用して、クライアント認証を実施しようと考えていた。

keycloakはクライアントの設定で「Access Type」を「Confidential」にすることで、クライアント認証の設定ができるようになる。

で、「Credentials」のタブで「Signed JWT」を選択すると、署名付きのJWTを利用したクライアント認証を実施できるようになっている。

image.png

「Generate new keys and certificate」を押すと、キーペアの生成画面が表示されるので、「PKCS12」を選択し、適当にパスワードを設定して、「Generate and Download」ボタンを押下する。

image.png

「credentials」の画面に戻ると同時に「keystore.p12」がダウンロードされる。

image.png

ここまででkeycloak側の設定は完了なんだけれども、この時点で行き詰まってしまった。

クライアント認証するためには、この「keystore.p12」の秘密鍵を使って、クライアントで生成したJWTに署名を付ける必要があるんだけれども、そのためには、クライアント側で「keystore.p12」を保持する必要がある。

electronアプリのようなクライアントはエンドユーザーの端末にインストールされる感じかと思うので、どう足掻いても秘密鍵を盗まれるリスクは排除できないし、electronアプリに秘密鍵を同梱して配布したら、秘密鍵が悪用された場合などに、簡単に差し替え出来なくなる。

ということで、そもそもelectronアプリのようなクライアントの「Access Type」は「Public」を選ぶべきなんだろうと考え、クライアント認証は諦めた。(いろんな記事を見ても、「public」にして、PKCEで認可コード横取りに対処するのが一般的なんだろうと考えた。)

で今に至る。

一応、署名付きJWTのクライアント実装も試してみたので、載せておく。

services/auth-service.js
...asyncfunctionloadTokens(callbackURL){consturlParts=url.parse(callbackURL,true);constquery=urlParts.query;varparams=newURLSearchParams();params.append('grant_type','authorization_code');params.append('client_id','test');params.append('code',query.code);params.append('redirect_uri','http://localhost/callback');params.append('client_assertion_type','urn:ietf:params:oauth:client-assertion-type:jwt-bearer');params.append('client_assertion',generateClientAssertion());try{constresponse=awaitaxios.post('http://localhost:8080/auth/realms/electron/protocol/openid-connect/token',params);accessToken=response.data.access_token;profile=jwt.decode(response.data.id_token);console.log(profile);}catch(error){throwerror;}}functiongenerateClientAssertion(){constnow=newDate();constiatValue=now.getTime();now.setMinutes(now.getMinutes()+1);constexpValue=now.getTime();constpayload={aud:'http://localhost:8080/auth/realms/electron/protocol/openid-connect/token',// トークンエンドポイントのURLexp:expValue,// トークンの有効期限jti:Math.random().toString(32).substring(2), // ユニークな値(今回はランダム文字列を簡易生成)iat:iatValue,// トークンを署名した時刻iss:'http://localhost:3000/',// JWT を署名したクライアントの識別子sub:'test'// keycloakに登録したクライアントID};returnsign(payload);}functionsign(payload){constkeyFile=fs.readFileSync('keys/keystore.p12');// ファイル読み込みconstkeyBase64=keyFile.toString('base64');// Stringで取得constp12Der=forge.util.decode64(keyBase64);// base64からデコードconstp12Asn1=forge.asn1.fromDer(p12Der);// ASN.1オブジェクトを取得constp12=forge.pkcs12.pkcs12FromAsn1(p12Asn1,'test');// p12として読み込みconstprivateKey=p12.getBags({bagType:forge.pki.oids.pkcs8ShroudedKeyBag})[forge.pki.oids.pkcs8ShroudedKeyBag][0].key;// 秘密鍵取得constrsaPrivateKey=forge.pki.privateKeyToAsn1(privateKey);// RSA秘密鍵に変換constprivateKeyInfo=forge.pki.wrapRsaPrivateKey(rsaPrivateKey);// PrivateKeyInfoでラップconstpemPrivate=forge.pki.privateKeyInfoToPem(privateKeyInfo);// PEM形式に変換constsignedJwt=jwt.sign(payload,pemPrivate,{algorithm:'RS256'});// JWTに署名returnsignedJwt;}...

依存として「jsonwebtoken」をインストールし、auth-service.jsのloadTokens()を上記のように書き換える。さらに「keys」というフォルダの下に「keystore.p12」を置く必要がある。

上記実装で、クライアント認証したうえでトークンが取得できた。

今回は確認のために、electronアプリの実装に埋め込んで試してみたが、もしクライアント認証を実施したいなら、別のやり方を考える必要がある。

例えば、トークンに署名するための専用サーバーを立ち上げて、認可コードを受け取って、署名したJWTを返すようなサービスを作る。

秘密鍵はサーバー側にあるので、盗まれるリスクが少なく、後から差し替えも可能になる。

ただ、盗まれた認可コードで署名を要求される可能性があるので、結局、署名用の専用サーバーにPKCEのような仕組みを導入して、認証時のユーザーと同一かどうかを検証する必要がありそう。

非常にめんどくさいし、やらかしそう。

ということで、特別な事情でもない限りは、「Access Type」は「Public」として実装する方がよいと思われる。

さいごに

3つの記事でに分けて、keycloakと連携してelectronのアプリに認証機能を追加するところまで実施してみました。

基本的にはAuth0のブログを参考にして実装しましたが、一気に理解するのは難しそうだったので、少しずつ実装を追加していく形にしました。おかげで、個人的には理解が深まった(気がする)けど、長編記事になってしまいました。なので、もともと詳しい人は、Auth0のブログを直接見た方がきっと分かりやすいんじゃないかと思います。

ただ、Keycloakの場合はトークン取得時のリクエストの形式(content-type)が違うなど、いくつかAuth0とは違う実装が必要だったので、その辺りの実装が参考になれば幸いです。

過去2回の記事は以下です。

ElectronアプリでKeycloakと連携(1. keycloakの設定編)
https://qiita.com/yusuke-ka/items/69d4146f344a95aa4662

ElectronアプリでKeycloakと連携(2. Electronアプリの作成編)
https://qiita.com/yusuke-ka/items/a4767c511f03b6083afc

Node.jsを勉強する① - 開発環境構築

$
0
0

はじめに

ずっと勉強したいと思っていたNode.js
Udemyの講座で勉強してみました。

備忘録として学んだことを自分なりにまとめて記しておきます。
なお、今回は英語の講座でしたが、英語も勉強できて一石二鳥ですね。

今回は開発環境構築を記事にします。

教材

Udemy
The Complete Node.js Developer Course (3rd Edition)
https://www.udemy.com/course/the-complete-nodejs-developer-course-2/

Node.jsのインストール

公式ホームページ(nodejs.org)に行き、最新のNode.jsをダウンロードします
Node-js.png

カスタマイズする必要はないので、インストーラーは"Next"をclickして進め、インストールを完了します。

ダウンロードされているかチェック

コマンドプロンプトを起動して、node -vでインストールされたバージョンを確認

node -v.png

コードのエディタがない場合は、ローカルにインストール

講座では、Visual Studio Code を使用しています。
個人的にatomの方が馴染みがありますが、Visual Studio Codeも使いやすかったです。

ダウンロードはこちらから。(https://code.visualstudio.com)
Visual-Studio-Code-Code-Editing-Redefined.png

コードを書いてみる

まずは任意のフォルダーをデスクトップにつくり、それをVisual Studio Codeで開きます。
左上にフォルダーが表示されます。
Screenshot 2021-01-01 143836.png
さらに一番左のNew Fileボタンを押して、nodeapp.jsという名前のファイルを作って下さい。
Screenshot 2021-01-01 144049.png

作ったファイルの中に、console. logを用いてメッセージが表示されるコードを書きます。

nodeapp.js
console.log("Hello World!")

Terminal >> New Terminalをクリックし、ターミナルを開いて、コードを実行してみます。
Screenshot 2021-01-01 144654.png

ターミナル
node nodeapp.js
Hello World!

nodeapp.jsでconsole.logの中に設定したメッセージが表示されれば、完成です。

Node.jsを勉強する② - テキストファイルの作成方法

$
0
0

はじめに

前回は、Node.jsの環境構築についてまとめました。今回はテキストファイルの作成方法を記事にします。

教材

Udemy
The Complete Node.js Developer Course (3rd Edition)
https://www.udemy.com/course/the-complete-nodejs-developer-course-2/

Jsファイルの作成とファイルモジュールの導入

まずは、コードを書くファイルを作成します。今回はnodeapp.jsと名付けます。
次に、app.jsの中にrequireを用いファイルを操作するモジュールを導入します。
constを用いて変数fsを定義し、モジュールの中身を代入します。

nodeapp.js
constfs=require('fs')

テキストファイルを作成する

次に、"writeFileSync"メソッドを用いて、テキストファイルを作成し、文字も入れます。
カッコの中の一つ目の要素はファイル名、2つ目の要素は、書く文字になります。

nodeapp.js
//ファイルモジュールを導入constfs=require('fs')//ファイルを作成し、文字を書くfs.writeFileSync('notes.txt','Hello, this is the first message!')

実行するにはターミナルにnode ファイル名と入力します。

ターミナル
node nodeapp.js

文字を追加する

文字を追加するには、appendFileSyncメソッドを使います。
カッコの中の一つ目が

nodeapp.js
//ファイルを作成し、文字を追加するfs.appendFileSync('notes.txt','Hello, this is the first message!')

再度実行して、動作を確認しましょう

ターミナル
node nodeapp.js

テキストファイルを確認して、文字が追加されていれば、完成です。


Node.jsのプロファイリングを試してみる

$
0
0

PM2のプロファイリング周りの調査結果とNode.jsでプロファイリングを行ってみた結果です。(PM2のinstancesの値の変更時の確認方法 - Qiitaの続き)

PM2 Pricingについて

PM2PM2PM2 PlusPM2 Enteprise
ゼロダウンタイムリロード
ターミナルベースのモニタリング(pm2 monit)
............
CPUプロファイリング

PM2 - Pricing

PM2 PlusでもCPUプロファイリング可能?

PM2 PlusでもCPUプロファイリングができるみたいなドキュメントが存在しますが、Enterprise限定のようです。

Memory & CPU Profiling | Guide | PM2 Plus Documentation · Issue #212 · keymetrics/doc-pm2 · GitHub

Node.jsでプロファイリングを行う

PlusでCPUプロファイリングできるなら課金しようと思いましたが、Enterpriseの契約はめんどくさそうなので今回はPM2を使わずにプロファイリングを行います。

今回使用したコードはこちら
app.js
constexpress=require("express");constcrypto=require("crypto");constapp=express();server=app.listen(3000,function(){console.log("Node.js is listening to PORT:"+server.address().port);});letusers=[];app.get('/newUser',(req,res)=>{letusername=req.query.username||'';constpassword=req.query.password||'';username=username.replace(/[!@#$%^&*]/g,'');if(!username||!password||users.username){returnres.sendStatus(400);}constsalt=crypto.randomBytes(128).toString('base64');consthash=crypto.pbkdf2Sync(password,salt,10000,512,'sha512');users[username]={salt,hash};res.sendStatus(200);});app.get('/auth',(req,res)=>{letusername=req.query.username||'';constpassword=req.query.password||'';username=username.replace(/[!@#$%^&*]/g,'');if(!username||!password||!users[username]){returnres.sendStatus(400);}const{salt,hash}=users[username];constencryptHash=crypto.pbkdf2Sync(password,salt,10000,512,'sha512');if(crypto.timingSafeEqual(hash,encryptHash)){res.sendStatus(200);}else{res.sendStatus(401);}});app.get('/auth2',(req,res)=>{letusername=req.query.username||'';constpassword=req.query.password||'';username=username.replace(/[!@#$%^&*]/g,'');if(!username||!password||!users[username]){returnres.sendStatus(400);}crypto.pbkdf2(password,users[username].salt,10000,512,'sha512',(err,hash)=>{if(users[username].hash.toString()===hash.toString()){res.sendStatus(200);}else{res.sendStatus(401);}});});

参考:Node.js アプリケーションの簡単なプロファイリング | Node.js

# ApacheBenchを利用するためhttpdをインストール
yum install httpd -y
# 今回PM2は使わないことにしたのでstopしておく
pm2 stop test-app
node --prof app.js
# ユーザー作成
curl -X GET "http://localhost:3000/newUser?username=matt&password=password"
# ベンチマークテスト
ab -k -c 20 -n 250 "http://localhost:3000/auth?username=matt&password=password"

ベンチマーク結果
This is ApacheBench, Version 2.3 <$Revision: 1879490 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking localhost (be patient)
Completed 100 requests
Completed 200 requests
Finished 250 requests


Server Software:        
Server Hostname:        localhost
Server Port:            3000

Document Path:          /auth?username=matt&password=password
Document Length:        2 bytes

Concurrency Level:      20
Time taken for tests:   20.130 seconds
Complete requests:      250
Failed requests:        0
Keep-Alive requests:    250
Total transferred:      57250 bytes
HTML transferred:       500 bytes
Requests per second:    12.42 [#/sec] (mean)
Time per request:       1610.378 [ms] (mean)
Time per request:       80.519 [ms] (mean, across all concurrent requests)
Transfer rate:          2.78 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.1      0       1
Processing:    81 1543 263.8   1610    1697
Waiting:       78 1543 263.9   1610    1697
Total:         81 1543 263.7   1610    1697

Percentage of the requests served within a certain time (ms)
  50%   1610
  66%   1612
  75%   1612
  80%   1613
  90%   1614
  95%   1614
  98%   1615
  99%   1615
 100%   1697 (longest request)

# 人が読みやすいファイルに変換
node --prof-process isolate-0x3d49da0-16098-v8.log > processed.txt
cat processed.txt

同期バージョンのpbkdf2関数を使用した後のプロファイリング結果。

processed.txt
...

 [Bottom up (heavy) profile]:
  Note: percentage shows a share of a particular caller in the total
  amount of its parent calls.
  Callers occupying less than 1.0% are not shown.

   ticks parent  name
  56011   75.3%  __GI_epoll_pwait

  18023   24.2%  node::crypto::PBKDF2(v8::FunctionCallbackInfo<v8::Value> const&)
  18023  100.0%    v8::internal::Builtin_HandleApiCall(int, unsigned long*, v8::internal::Isolate*)
  18023  100.0%      LazyCompile: ~pbkdf2Sync internal/crypto/pbkdf2.js:45:20
  17951   99.6%        LazyCompile: ~<anonymous> /var/www/test/app.js:29:18
  17951  100.0%          LazyCompile: ~handle /var/www/test/node_modules/express/lib/router/layer.js:86:49
  17951  100.0%            LazyCompile: ~next /var/www/test/node_modules/express/lib/router/route.js:114:16

コード改善後のプロファイリング結果

ベンチマーク結果は差がありませんでした。(Nodeのバージョンがv12.20.0


変更後のベンチマーク結果
This is ApacheBench, Version 2.3 <$Revision: 1879490 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking localhost (be patient)
Completed 100 requests
Completed 200 requests
Finished 250 requests


Server Software:        
Server Hostname:        localhost
Server Port:            3000

Document Path:          /auth?username=matt&password=password
Document Length:        2 bytes

Concurrency Level:      20
Time taken for tests:   20.287 seconds
Complete requests:      250
Failed requests:        0
Keep-Alive requests:    250
Total transferred:      57250 bytes
HTML transferred:       500 bytes
Requests per second:    12.32 [#/sec] (mean)
Time per request:       1622.975 [ms] (mean)
Time per request:       81.149 [ms] (mean, across all concurrent requests)
Transfer rate:          2.76 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.1      0       0
Processing:    86 1564 237.9   1619    1657
Waiting:       82 1564 238.0   1619    1657
Total:         86 1564 237.8   1619    1657

Percentage of the requests served within a certain time (ms)
  50%   1619
  66%   1629
  75%   1634
  80%   1635
  90%   1643
  95%   1647
  98%   1656
  99%   1657
 100%   1657 (longest request)


processed.txt
...
 [Bottom up (heavy) profile]:
  Note: percentage shows a share of a particular caller in the total
  amount of its parent calls.
  Callers occupying less than 1.0% are not shown.

   ticks parent  name
  94151   98.9%  __GI_epoll_pwait

Apache Benchの引数を変更

# 改善前のコード、同時接続200、合計250リクエスト
 ab -k -c 200 -n 250 "http://localhost:3000/auth?username=matt&password=password"
> apr_socket_recv: Connection reset by peer (104)
# 改善後のコード、同時接続200、合計250リクエスト
 ab -k -c 200 -n 250 "http://localhost:3000/auth2?username=matt&password=password"

同時接続20であればパフォーマンスに影響はありませんでしたが、200にしたところ改善前のコードではapr_socket_recv: Connection reset by peer (104)が返ってきました。また、同条件でpm2で起動した場合は改善前のコードでも正常に実行されました。(PM2のプロセス数を見直す際にはApache Benchでテストしてみる)

参考:Apache Benchでサクッと性能テスト - Qiita

javascriptによる並列処理

$
0
0

概要

普段javascriptを書いているとよく使う非同期処理(Promise)だが、言語仕様上(
シングルスレッドのため)非同期処理は処理の順番を変えているだけで厳密には同期処理のようだった。

ざっくり図

同期処理
実行→結果→実行→結果...

非同期処理
実行→実行→結果→結果...

並列処理
実行→結果...
実行→結果...

よくよく調べているとWorkerオブジェクトを使用すれば並列処理ができるみたいなので試してみた

Document
- Docs Worker

ブラウサでの使用

※メインスレッド以外では、domの更新などができない

index.js
varworker=newWorker("./greeting-worker.js");// データが送られてきたら発火worker.onmessage=function(message){console.log(message)// workerの停止this.terminate();};// Worker作成時かWorker実行中でエラーとなった場合に発火worker.onerror=function(err){console.error("onerror",err.message);};// データ送信worker.postMessage("Hi");
greeting-worker.js
// worker.postMessageが呼ばれたら発火self.onmessage=function(message){// 送られてきたデータconsole.log(message);// workerにデータを送るself.postMessage("I'm good.");};

バックエンド(Node.js)での使用

index.js
var{Worker}=require("worker_threads");varworker=newWorker("./greeting-worker.js");// データが送られてきたら発火worker.on("message",function(message){console.log(message);// workerの停止worker.terminate();});// Worker作成時かWorker実行中でエラーとなった場合に発火worker.on("error",function(err){console.error("onerror",err.message);// workerの停止worker.terminate();});// workerが停止したら発火worker.on("exit",function(){console.log("Bye!!");});// データ送信worker.postMessage("Hi, how are you?");
greeting-worker.js
// worker.postMessageが呼ばれたら発火self.onmessage=function(message){// 送られてきたデータconsole.log(message);// workerにデータを送るself.postMessage("I'm good.");};
// スレッドを増やしたい場合varworker1=newWorker("./worker1.js")varworker2=newWorker("./worker2.js")varworker3=newWorker("./worker3.js")...

感想

あまり並列にすることもない処理でしたが、意外と簡単に扱うことができました。
使い所としては、処理が重い計算処理などやindexedDB(ブラウザ)があるそうです。
またワーカースレッドでデータをやりとりできるMessageChannelというのもありました。(他ArrayBufferやFileHandleというものもあった)

まとめ

  • javascriptは非同期処理の他に並列処理もできる
  • ブラウザ/Node.jsともにAPIが用意されている.(使用感もほぼ同じ)
  • メインスレッド以外では、domの更新などができない
  • ワーカースレッドを増やしたい場合は複数Workerを呼び出す
  • ワーカースレッド間でのデータのやりとりはMessageChannelを使用して行うことができる。 

【Robocode】JavaScriptを勉強しながら戦車ゲームを攻略していく その1

$
0
0

はじめに

前回記事で構築したRobocodeを遊びながら攻略していきます。
https://qiita.com/abemaki/items/54712e50e4a4a25c229b

ゲームを起動すると以下のような画面が出てきます。
初期段階でもステージ0であれば、五分五分の戦いなので
今回は軽く触りながら、ステージ0での勝率を高めていきたいと思います。

 
画面イメージと画面の見方
image.png

ソースコードを理解する

公式の説明を読む前にちょっとだけソースコードを見ていきます

ソースコードを見ていくと
boss-.jsといたファイルとmyrobot.jsといったファイルがあるのがわかります。
myrobot.jsは自機で、boss-
.jsは敵機を表しているものであることがファイル名からわかると思います。
ファイルを開いてみると、何やらファンクションがずらりと並んでいるのがわかりますがこのままだと詳細がよくわかりません。
image.png

公式の説明を読んでみる

https://github.com/youchenlee/robocode-js

公式のロボットHOWTOを見てみると以下のことがわかります

入手可能な情報:

自己情報
・ me.id
・ me.x
・ me.y
・ me.hp
・ me.angle-現在の角度(タンク角度+タレット角度)
・ me.tank_angle
・ me.turret_angle

敵情報
・ enemy-spot [N] .id
・ enemy-spot [N] .hp
・ enemy-spot [N] .angle-敵に対する角度(方向)

シーケンシャルアクション:
・turn_left(角度)
・turn_right(角度)
・move_forwards(距離)
・move_backwards(距離)
・move_opposide(distance)-このアクションはOnWallCollide()でのみ使用できます

並列アクション:
・turn_turret_left(角度)
・turn_turret_right(角度)
・シュート()
・yell(メッセージ)
 
イベント:
OnIdle()-アイドル時にトリガーされます(実装する必要があります)
OnWallCollide()-タンクが壁に衝突したとき
OnHit()-弾丸が当たったとき
OnEnemySpot()-砲塔が敵に直接面している場合(発砲しない理由はないようです!)

試しに自機と敵機のセリフを変えてみます。

 
セリフはyell(メッセージ)で定義します。 ファイルを変更したら保存して、ブラウザを更新しましょう
image.png

 

・・・超賑やかになった。
robo.gif

ファイルに変更を加えるとしっかりと結果に反映されるのがわかります。

 

現状の勝率が何故5割程度なのか整理してみる

両者同じような動きをしているので、五分五分の戦いをしていることがわかります。

・自分の動き
 右に動き回りながら相手を探して見つけたら弾を発射するだけ

・相手の動き
 クルクル砲台を回転させながら相手を探して見つけたら弾を発射するだけ

 
 
 

敵に対面した後の動作を変更してみる

そこで小刻みに動き回って、相手を見つけたらできるだけ
近い場所で相手を探して攻撃をしかけるようにソースを修正してみようかと思います。
 
  

小刻みに敵を探して、敵を発見したらできるだけ近い場所を探して敵を攻撃するように修正してみます。
 
対面フラグを追加してアイドル時の処理を以下のように分岐させます。

■アイドル時
対面フラグ=敵を未発見(false)の際の処理:
 小刻みに右回りで敵を探す

対面フラグ=敵を発見(true)の際の処理:
 小刻みに左回りで敵を探す
 この際に敵を発見できなかった場合に対面フラグを対面フラグを敵を未発見(false)に変更します

■敵発見時
 攻撃をしかけて
 対面フラグを敵を発見(true)に変更します

 
 

ソースコード

// 対面フラグdiscoveryFired=false;// アイドル時にトリガーされます(実装する必要があります)prototype.onIdle=function(){if(this.discoveryFired){// 対面後の動作 小刻みに左に動いで相手を探す// 前進(距離)this.move_forwards(10);// 砲塔を左に向ける(角度)this.turn_turret_left(10);// 左に曲がる(角度)this.turn_left(10);// 対面フラグリセットthis.discoveryFired=false;}else{// 対面前の動作 小刻みに右に動いで相手を探す// 前進(距離)this.move_forwards(10);// 砲塔を右に向ける(角度)this.turn_turret_right(10);// 右に曲がる(角度)this.turn_right(10);}};// タンクが壁に衝突したときprototype.onWallCollide=function(){// 反対に移動?たぶん誤字?(距離)this.move_opposide(10);// 左に曲がる(角度)this.turn_left(90);};// 弾丸が当たったときprototype.onHit=function(){// セリフthis.yell("痛いっ");};// 砲塔が敵に直接面している場合(発砲しない理由はないようです!)prototype.onEnemySpot=function(){// セリフthis.yell("当たれ~っ");// 発射this.shoot();// 対面フラグを立てるthis.discoveryFired=true;};

改良した戦車で闘ってみる

圧勝です。 このステージに関しては勝率100%です。

なんと! 遊んでいるだけなのにちょっとエンジニアの基本っぽいことが学べているじゃないですか!

・既存ソースコードを見て把握する
・公式ドキュメントを読んで理解を深める
・既存ソースコードに公式ドキュメントで深めた理解をソースコードに落とし込んでみる
・現状の動きから課題点を洗い出す
・課題点を改善してみる
・動作確認して自己超満足

robo2.gif

[Node.js] 新規アプリケーションの製作方法

$
0
0

環境構築をする

  1. nodebrewをインストールする
  2. Node.jsをインストールする

詳しくは以下でまとめています。
https://qiita.com/momo1010/items/dab9c70bfe84a78f23e2

好きな場所にアプリケーション用のディレクトリを作成する

アプリケーション用のディレクトリの作成と移動のコマンドを実行します。

$ mkdir sample
$ cd sample

エディタを起動して先程作成したフォルダを開く

今回エディタは Visual Studio Codeを使用します。
Open Folder...から先程作成したフォルダを選択します。

パッケージをインストールする

以下のコマンドを実行すると、 npmの設定ファイルである package.jsonが生成されます。package.jsonには、 npmパッケージの設定情報などが書き込まれています。

$ npm init --yes

次に、npmパッケージのインストールをします。今回は、 expressejsをインストールします。

$ npm install express ejs

nodemonをインストールする

jsファイルの変更を反映するには、毎回サーバーを再起動する必要があります。その作業を省くために nodemonという、ファイル更新時に自動でサーバーが再起動するようになるnpmパッケージをインストールします。

$ npm install -g nodemon

サーバーを起動してページを表示する

Visual Studio Codeで必要なファイルを作成します。
node_modulesフォルダと package-lock.jsonpackage.jsonファイルがnpmパッケージのインストールをしたときに既に生成されています。

app.jsファイルを作成する

追加で app.jsファイルを作成します。 app.jsファイルに以下のコードをコピーして、貼り付けてください。

const express = require('express');
const app = express();

//CSSや画像ファイルを置くフォルダ(public)を指定するコード
app.use(express.static('public'));

app.get('/', (req, res) => {
  res.render('hello.ejs');
});

app.listen(3000);

viewsフォルダを作成する

viewsフォルダを作成し、 hello.ejsファイルに以下のコードをコピーして、貼り付けてください。

<h1>Hello World</h1>

publicフォルダを作成する

publicフォルダの中に cssフォルダや imagesフォルダを作成します。

サーバーを起動する

nodemonを使ってサーバーを起動していきます。

$ nodemon app.js

ブラウザを開いて localhost:3000というURLにアクセスして Hello Worldと表示されれば成功です。

Raspberry Pi用node.jsでゲームパッド入力をキーボード/マウス入力に変換する

$
0
0

概要

Raspberry Piでゲームパッドの入力をキーボードやマウスの入力に変換するソフトは今の所無いようなので、node.jsでさくっと作ってみた。

(Intel/AMD製CPU用の)Linux・Windows・MacにはGUIで設定できる同様のソフトが存在するので素直にそっちを使ったほうがいいです。

インストール

node.js及び、joystick・robotjsモジュールをインストールする必要があります。
また、robotjsをインストールするためにlibx11-devとlibxtst-devもインストールする必要があるようです(参考)

sudo apt install npm libx11-dev libxtst-dev
npm install joystick robotjs --save-dev

スクリプト

joystickモジュールでゲームパッドの入力を受け取ってその結果を元にrobotjsモジュールを利用してキー・マウス操作する感じになります。

接続されるまでポーリングで待機したり、キー・マウス入力への変換をオブジェクトで定義したりできるようにしてみました。

joykey.js
constfs=require('fs');constjoystick=require('joystick');constrobot=require('robotjs');varjoys=newjoystick(jid);//コンソールにジョイスティックイベントの内容を表示//var consolelog = true;varconsolelog=false;//ジョイスティックのID(通常は0)varjid=0;//キーやスティックが押されたときの動作を定義/*
ジョイスティックのボタン押下(button)
"0", "1", "2"....

ジョイスティックのスティック操作(axis)
"a0", "a1", "a2"...


type: "enable"    キー変換の有効・無効の切り替え(無効でもenableだけは有効)

type: "key"       キークリック
    key:      クリックするキー文字もしくはキー文字列

type: "text"      文字列送信
    str:      送信する文字列

type: "mouse"     マウスボタン操作
    mtype:    マウスボタンの操作タイプ("ckick", "toggle", "double")
    button:   クリックするマウスボタン("left", "middle", "right")

type: "move"      マウス移動
    mx:       x軸移動量
    my:       y軸移動量

type: "scroll"    スクロール
    mx:       x軸スクロール量
    my:       y軸スクロール量

type: "updown"    上下カーソルキー
    mv:       0以上:そのまま 0未満:反転 

type: "leftright" 左右カーソルキー
    mv:       0以上:そのまま 0未満:反転 

*/varjoytable={"0":{type:"mouse",mtype:"click",button:"right"},"1":{type:"mouse",mtype:"click",button:"left"},"2":{type:"toggle",key:"left"},"a0":{type:"move",mx:10,my:0},"a1":{type:"move",mx:0,my:10},"a3":{type:"scroll",mx:0,my:5},"a4":{type:"leftright",mv:0},"a5":{type:"updown",mv:0},};//toggle用にマウスボタンの状態を記憶(trueが押されている状態)varmousestate={"left":false,"right":false,"middle":false};//キーマウス動作が有効かどうかvarenable=true;//定義を元にキーやマウスの動作を実行varsendkey=function(dat,mul){if(dat==null||dat.type==null)return;mul=mul<0?-1:1;if(dat.type==="enable"){//キー変換有効無効の切り替えenable=enable?false:true;}elseif(enable&&dat.type==="key"){//キー送信if(dat.key==null)return;robot.keyTap(dat.key);}elseif(enable&&dat.type==="text"){//文字列送信if(dat.str==null)return;robot.typeString(dat.str);}elseif(enable&&dat.type==="mouse"){if(dat.mtype==="click"){//マウスボタンクリックif(dat.button==null)return;robot.mouseClick(dat.button);}elseif(dat.mtype==="double"){//マウスボタンダブルクリックif(dat.button==null)return;robot.mouseClick(dat.button,true);}elseif(dat.mtype==="toggle"){//マウスボタン押下状態切り替えif(dat.button==null||mousestate[dat.button]==null)return;robot.mouseToggle(mousestate[dat.button]?"up":"down",dat.button);mousestate[dat.button]=mousestate[dat.button]?false:true;}}elseif(enable&&dat.type==="move"){//マウスポインタ移動if(dat.mx==null||dat.my==null)return;varpos=robot.getMousePos();robot.moveMouseSmooth(pos.x+(dat.mx*mul),pos.y+(dat.my*mul));}elseif(enable&&dat.type==="scroll"){//スクロールif(dat.mx==null||dat.my==null)return;robot.scrollMouse(dat.mx*mul,dat.my*mul);}elseif(enable&&dat.type==="updown"){//上下カーソルキー送信if(dat.mv==null)return;dat.mv=dat.mv<0?-1:1;robot.keyTap(dat.mv*mul<0?"up":"down");}elseif(enable&&dat.type==="leftright"){//左右カーソルキー送信if(dat.mv==null)return;dat.mv=dat.mv<0?-1:1;robot.keyTap(dat.mv*mul<0?"left":"right");}};//ジョイスティック接続待ち用のsleepvarsleep=function(waitSec){returnnewPromise(function(resolve){setTimeout(function(){resolve()},waitSec);});};//joystickイベントの定義varinit=function(joy){//ボタンクリックイベントjoy.on('button',button=>{if(consolelog)console.log({button});if(button.value!=0){sendkey(joytable[button.number],0);}});//axis移動イベントjoy.on('axis',axis=>{if(consolelog)console.log({axis});if(axis.value!=0){sendkey(joytable["a"+axis.number],axis.value);}});//エラーイベントjoy.on('error',async(err)=>{if(consolelog)console.log({err});//エラーが発生したら/dev/input/js?の存在を1秒ごとに確認して存在すれば再度joystickオブジェクトを作成while(1){if(fs.existsSync('/dev/input/js'+jid))break;awaitsleep(1000);}joys=newjoystick(jid);init(joys)});};//開始init(joys);

キーリピート対応版

このまんまだとキーリピートができないのでちょっと使いにくいが、対応しようと思うと根本的に作り直す感じになるっぽいので保留・・・
こっちの方もさくっと作ってみた。

joykeyrepeat.js
constfs=require('fs');constjoystick=require('joystick');constrobot=require('robotjs');varjoys=newjoystick(jid);//コンソールにジョイスティックイベントの内容を表示//var consolelog = true;varconsolelog=false;//ジョイスティックのID(通常は0)varjid=0;//キーやスティックが押されたときの動作を定義/*
ジョイスティックのボタン押下(button)
"0", "1", "2"....

ジョイスティックのスティック操作(axis)
"a0", "a1", "a2"...


type: "enable"    キー変換の有効・無効の切り替え(無効でもenableだけは有効)

type: "key"       キークリック
    key:      クリックするキー文字もしくはキー文字列
    repeat:   キーリピート(true:有効, false:無効)

type: "text"      文字列送信
    str:      送信する文字列
    repeat:   キーリピート(true:有効, false:無効)

type: "mouse"     マウスボタン操作
    mtype:    マウスボタンの操作タイプ("ckick", "toggle", "double")
    button:   クリックするマウスボタン("left", "middle", "right")
    repeat:   キーリピート(true:有効, false:無効)

type: "move"      マウス移動
    mx:       x軸移動量
    my:       y軸移動量
    repeat:   キーリピート(true:有効, false:無効)

type: "scroll"    スクロール
    mx:       x軸スクロール量
    my:       y軸スクロール量
    repeat:   キーリピート(true:有効, false:無効)

type: "updown"    上下カーソルキー
    mv:       0以上:そのまま 0未満:反転 
    repeat:   キーリピート(true:有効, false:無効)

type: "leftright" 左右カーソルキー
    mv:       0以上:そのまま 0未満:反転 
    repeat:   キーリピート(true:有効, false:無効)

*/varjoytable={"0":{type:"mouse",mtype:"click",button:"right",repeat:false},"1":{type:"mouse",mtype:"click",button:"left",repeat:false},"2":{type:"toggle",key:"left",repeat:false},"a0":{type:"move",mx:10,my:0,repeat:true},"a1":{type:"move",mx:0,my:10,repeat:true},"a3":{type:"scroll",mx:0,my:5,repeat:true},"a4":{type:"leftright",mv:0,repeat:true},"a5":{type:"updown",mv:0,repeat:true},};//ジョイスティックの状態を記憶varjoystate={"0":0,"1":0,"2":0,"3":0,"4":0,"5":0,"6":0,"7":0,"8":0,"9":0,"10":0,"11":0,"12":0,"13":0,"14":0,"15":0,"16":0,"17":0,"18":0,"19":0,"a0":0,"a1":0,"a2":0,"a3":0,"a4":0,"a5":0,"a6":0,"a7":0,"a8":0,"a9":0,};varjoyenable=false;//toggle用にマウスボタンの状態を記憶(trueが押されている状態)varmousestate={"left":false,"right":false,"middle":false};//キーマウス動作が有効かどうかvarenable=true;//定義を元にキーやマウスの動作を実行varsendkey=function(dat,mul){if(dat==null||dat.type==null)return;mul=mul<0?-1:1;if(dat.type==="enable"){//キー変換有効無効の切り替えenable=enable?false:true;}elseif(enable&&dat.type==="key"){//キー送信if(dat.key==null)return;robot.keyTap(dat.key);}elseif(enable&&dat.type==="text"){//文字列送信if(dat.str==null)return;robot.typeString(dat.str);}elseif(enable&&dat.type==="mouse"){if(dat.mtype==="click"){//マウスボタンクリックif(dat.button==null)return;robot.mouseClick(dat.button);}elseif(dat.mtype==="double"){//マウスボタンダブルクリックif(dat.button==null)return;robot.mouseClick(dat.button,true);}elseif(dat.mtype==="toggle"){//マウスボタン押下状態切り替えif(dat.button==null||mousestate[dat.button]==null)return;robot.mouseToggle(mousestate[dat.button]?"up":"down",dat.button);mousestate[dat.button]=mousestate[dat.button]?false:true;}}elseif(enable&&dat.type==="move"){//マウスポインタ移動if(dat.mx==null||dat.my==null)return;varpos=robot.getMousePos();robot.moveMouseSmooth(pos.x+(dat.mx*mul),pos.y+(dat.my*mul));}elseif(enable&&dat.type==="scroll"){//スクロールif(dat.mx==null||dat.my==null)return;robot.scrollMouse(dat.mx*mul,dat.my*mul);}elseif(enable&&dat.type==="updown"){//上下カーソルキー送信if(dat.mv==null)return;dat.mv=dat.mv<0?-1:1;robot.keyTap(dat.mv*mul<0?"up":"down");}elseif(enable&&dat.type==="leftright"){//左右カーソルキー送信if(dat.mv==null)return;dat.mv=dat.mv<0?-1:1;robot.keyTap(dat.mv*mul<0?"left":"right");}};//ジョイスティック接続待ち、ループ用のsleepvarsleep=function(waitSec){returnnewPromise(function(resolve){setTimeout(function(){resolve()},waitSec);});};//joystickイベントの定義varinit=function(joy){//ボタンクリックイベントjoy.on('button',button=>{if(consolelog)console.log({button});joystate[button.number]=button.value;});//axis移動イベントjoy.on('axis',axis=>{if(consolelog)console.log({axis});joystate['a'+axis.number]=axis.value;});//エラーイベントjoy.on('error',async(err)=>{if(consolelog)console.log({err});//エラーが発生したら/dev/input/js?の存在を1秒ごとに確認して存在すれば再度joystickオブジェクトを作成joyenable=false;while(1){if(fs.existsSync('/dev/input/js'+jid))break;awaitsleep(1000);}joys=newjoystick(jid);init(joys)});joyenable=true;};//ループvarstart=asyncfunction(){while(1){if(joyenable){for(varkeyinjoystate){if(joystate[key]!=0){sendkey(joytable[key],joystate[key]);if(!joytable[key].repeat)joystate[key]=0;}}}awaitsleep(16);}}//開始init(joys);start();
Viewing all 9050 articles
Browse latest View live