20210501のNode.jsに関する記事は5件です。

僕たちは英語ができない

はじめに 日の本の国のプログラマーにとって英単語のスペルミスというのは日常的になります。 最初のうちに検出されたスペルミスは「てへぺろ(・ω<)」ですんで簡単に修正できますが、終盤に見つかったスペルミスは安易に修正できず未来永劫、そのスペルミスが遺産として残ることになります。 いくつかのエディタにはスペルコードを検知する仕組みがあったりします。 例えば、VSCodeにおいてはCode Spell Checkerというプラグインが存在し、英語ができない僕たちのために間違った英単語を教えてくれます。 さて、新しいコードを作成するだけの場合、多くはこのプラグインの導入で解決するでしょう。 しかし残念ながら、僕たちは過去の遺産には縛られますし、警告を無視する勇気ある人間は必ず存在します。 このため、継続的インテグレーションの途中などで、フォルダを指定して一括でスペルチェックをする必要があります。 今回はその方法を考えてみます。 cSpellによるスペルチェック cSpellはコマンドラインでスペルチェックを行ってくれます。いくつかのプログラミング言語固有の辞書のサポートや、キャメルケース、パスカルケースの考慮を行います。 cSpellは元々、VSCodeのCode Spell Checkerプラグインとして作成されたものになります。 なお、Node.jsが動作する環境で使用することが可能です。 今回は以下の環境で実験しています。 v12.20.1 macOS 10.15.7 インストール方法 npm install -g cspell あるいはNode.jsのプロジェクトに組み込む場合は以下の通り npm install --save-dev cspell 使用方法 cspell ファイル名 or cspell lint ファイル名 ファイル名にはワイルドカードを使用することができます。 cspell "src/**/*.js" cspell "src/**/*.{js,vue}" また、規定ではカレントディレクトリ以下に検査対象のソースコードにあることが前提になります。 別のディレクトリを選択する場合は -r, --rootを指定する必要があります。 cspell check -r ルートディレクトリ "src/**/*.js" その他、使用方法についてはhelpで確認してください。 cspell --help # 各コマンドのヘルプは以下で確認できる cspell lint --help cspell check --help cspell trace --help cspell link --help 出力結果 コマンドラインで実行すると以下のような結果を出力します。 HTMLでレポートするような機能は存在しないようです。 https://github.com/streetsidesoftware/cspell/issues/21 jsonで警告の一覧を保存する方法 まず以下のような標準入力を変換するスクリプトを作成します。 makejson.js process.stdin.resume(); process.stdin.setEncoding('utf8'); let fragment = ''; const result = [] process.stdin.on('data', function(chunk){ if (chunk == '') { return ;} var lines = chunk.split('\n'); lines[0] = fragment + lines[0]; fragment = lines.pop(); lines.forEach(function(line){ const item = { path: '', line: 0, column: 0, word: '' }; item.path = line.substr(0, line.indexOf(':')); line = line.substr(line.indexOf(':')+1) const locations = line.substr(0, line.indexOf(' - ')).split(':') item.line = Number(locations[0]); item.column = Number(locations[1]); line = line.substr(line.indexOf(' - ')+1) item.word = line.match(/\((.+)\)/)[1] result.push(item) }); }); process.stdin.on('end', function(){ console.log(JSON.stringify(result, null, 2)) }); 次にパイプで渡せば結果をjsonファイルに保存できます。 cspell src/* | node makejson.js > ret.json 認識できない単語の一覧を取得する 認識できなかった単語の一覧を取得するには以下のようなオプションを指定して実行すると単語の一覧が標準出力されます。 cspell src/* --unique --wordsOnly --no-progress --no-summary この例では以下のような挙動をします。 --wordsOnlyで認識できない単語のみを出力する --uniqueで認識できない単語の重複を排除してます。 --no-progressで経過情報を出力しない --no-summaryでサマリの出力をしない 警告の解決方法 使用しているとわかるのですが、警告として検出される単語が実際は正しい単語の場合もあります。 これを解決するには2通りの方法があります。 設定ファイルに有効な単語として登録する方法 チェックをしないようにソースコードに書き込む 設定ファイルに有効な単語として登録する方法 英単語を登録するにはjsonファイルを記述する必要があります。 cSpell.json { // Version of the setting file. Always 0.1 "version": "0.1", // language - current active spelling language "language": "en", // words - list of words to be always considered correct "words": [ "vuex", "Vetur" ], // flagWords - list of words to be always considered incorrect // This is useful for offensive words and common spelling errors. // For example "hte" should be "the" "flagWords": [ "hte" ] } 作成した設定ファイルを指定して実行するには-cを使用します。 cspell -c ./cSpell.json "src/**/*.{js,vue}" この例では「vuex」,「Vetur」を有効な単語とし、「hte」を誤った単語としてみなします。 なお、設定ファイルの詳細については以下に説明があります。 https://github.com/streetsidesoftware/cspell/tree/master/packages/cspell#customization チェックをしないようにソースコードに書き込む コメントによりスペルチェックの有効・無効を制御できます。 const strChack = '検知される' // cSpell:disable スペルチェックをしない strChack = '検知されない' strChack = '検知されない' /* cSpell:enable スペルチェックを有効に */ strChack = '検知される' // cspell:disable-next-line 次の行を無視する strChack = '検知されない' strChack = '検知される' あるいはコメントを使用して無視する単語を登録することもできます。 // cSpell:ignore strChack const strChack = '検知されない' コメントを利用したcspellの制御については以下を参照してください。 https://github.com/streetsidesoftware/cspell/tree/master/packages/cspell#enable--disable-checking-sections-of-code まとめ cspellを使用することで、英語ができない我々でも単純なスペルミスを検出して修正することが可能です。 これを継続的インテグレーションに組み込むことで、プロジェクトの初期段階においてスペルミスを検知して、謎の英単語をプロジェクトルールとして認めるということをしなくて済むようになります。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SAMLによるシングルサインオンサンプル(node-expressとsaml-idp利用)

目的 express-generatorでひな形作成 利用モジュールのインストール TypeScript化 テスト用ページ作成 passport-samlの設定 テスト用IdPの設定 動作確認 1:目的と概要 目的 ローカルで完結するSAML-IdP認証サンプルプログラムが見つからなかったため作成しました。 とりあえず動くことを目的としてます。 ※SAML認証とは?参考リンク * SAML認証に関する自分なりのまとめ * SAML2.0でのシングルサインオン実装と戦うあなたに(.NET編) 概要 機能概要 テスト用IdPをローカルで起動し、saml認証動作確認プログラムを動かします。 トップページは認証不要、認証が必要ページに遷移する場合にログイン(IdP側が表示)します。 ログイン、ログアウト機能を持ちます。 ソースはgithubのsimple-saml-authにあります 実装上の特徴 express-generatorでひな形を作成 ts-nodeを利用してtypescript化(ビルド不要) 2:express-generatorでひな形作成 express express-generator でカレントフォルダにひな形を生成します npx express-generator --view=ejs --git ./ 下記ファイルが生成されます / │ app.js │ package.json │ .gitignore ├─bin │ www ├─public │ ├─images │ ├─javascripts │ └─stylesheets │ style.css ├─routes │ index.js │ users.js └─views error.ejs index.ejs package.jsonに記載されたモジュールをインストールします。 npm install 動作確認のため、ターミナルで下記を実行し「localhost:3000」を開きます。 ブラウザで開き「Welcome to Express」と表示されたら成功です。 npm run start 3:利用モジュールのインストール TypeScriptと型定義があった方が、後々役立つので合わせてインストールします。 saml認証で必要なモジュール(と型定義)をインストールします npm i -D typescript nodemon @types/cookie-parser @types/express npm i ts-node express-session passportとpassport-samlを追加(nodeの認証用モジュール) npm i passport passport-saml npm i -D @types/passport インストール後のpackage.json (概ねこのようなファイルになっていると思います) { "name": "simple-saml-auth", "version": "0.0.0", "private": true, "scripts": { "start": "node ./bin/www" }, "dependencies": { "cookie-parser": "~1.4.4", "debug": "~2.6.9", "ejs": "~2.6.1", "express": "~4.16.1", "express-session": "^1.17.1", "http-errors": "~1.6.3", "morgan": "~1.9.1", "passport": "^0.4.1", "passport-saml": "^2.2.0", "ts-node": "^9.1.1" }, "devDependencies": { "@types/cookie-parser": "^1.4.2", "@types/express": "^4.17.11", "@types/passport": "^1.0.6", "nodemon": "^2.0.7", "saml-idp": "^1.2.1", "typescript": "^4.2.4" } } 4:TypeScript化 ts-nodeを使うため、事前コンパイルやトランスパイル後のdistフォルダなど面倒な設定は不要です。 Typescriptの設定ファイルを作成します(tsconfig.json) npx tsc --init 生成されたjsファイルの拡張子を全て「ts」に変更します mv app.js app.ts mv ./routes/index.js ./routes/index.ts mv ./routes/users.js ./routes/users.ts mv ./bin/www ./bin/www.ts ./bin フォルダに「www.js」を追加し下記の内容を追記します。 ここがミソです。.jsファイル内でrequire('ts-node')することで、tsファイルを読み込んで利用することができるようになります。 touch ./bin/www.js echo -e "require('ts-node').register({transpileOnly: true});\nrequire('./www.ts');" > ./bin/www.js 「{transpileOnly: true}」は、起動を高速化するため(型チェックを行いません)  ⇒型チェックはエディタ側に任せて、トランスパイルに専念させます。 tsconfig.json 下記行のコメントを1行外しfalseに変更します。 (元がjsのため型指定のない変数を許可しないとコンパイルエラーとなるためです。 trueに戻して適切に型を付けると安全性が向上します) "noImplicitAny": false, typescript化したexpressアプリケーションが起動することを確認します。 npm run start 現時点(require()での読み込み)では、型推論が行われません。必要時に応じ、Importに変更することで型チェック、オートコンプリートが行われるようになります。 require() ⇒ import module.exports ⇒ export default 変更例(index.ts) // var express = require('express'); // comment out import express from 'express'; // importに変更 var router = express.Router(); /* GET home page. */ router.get('/', function(req, res, next) { res.render('index', { title: 'Express' }); }); // importでもrequire()でも読み込めるようにmodule.exportを残しておきます module.exports = router; export default router; 5:テスト用ページ作成 viewsフォルダのファイルを追加、修正します。 認証不要のトップページ「index.ejs」と、認証が必要な「page1.ejs」を用意します。 トップページ(認証不要) 「index.ejs」 を修正。 ログイン時、ユーザ名、ログアウト認証が必要なページ(page1)への表示 未ログイン時、ログイン、認証が必要なページ(page1)へのリンク <!DOCTYPE html> <html> <head> <title>トップページ(認証不要)</title> <link rel='stylesheet' href='/stylesheets/style.css' /> </head> <body> <div> トップページ(認証不要) <p> <% if (uid) { %> ユーザ名[<%= uid %>] <a href='/logout'>ログアウト</a> <% } else { %> <a href='/login'>ログイン</a> <% } %> </p> </div> <div> <p><a href='/page1'>ログインが必要なページ</a></p> </div> </body> </html> 認証が必要なページ 「page1.ejs」 を新規追加。 ユーザ名の表示、トップページへのリンク <!DOCTYPE html> <html> <head> <title>認証が必要なテストページ</title> <link rel='stylesheet' href='/stylesheets/style.css' /> </head> <body> <div> 認証が必要なテストページ <p>ユーザ名[<%= uid %>]</p> </div> <div> <p><a href='/'>トップページへ戻る</a></p> </div> </body> </html> 6:passport-samlの設定 auth.ts追加 touch ./routes/auth.ts ユーザーのシリアライズ、デシリアライズ処理 // sessionへのシリアライズ、デシリアライズ処理 // saml認証で受け取った値をそのままセットしている // idだけをセッションに保存し、デシリアライズ時にDBから復元するなどの処理を行う passport.serializeUser<any>((user, done) => { done(null, user); }); passport.deserializeUser<any>((user, done) => { done(null, user); }); saml認証用のStrategy設定 // saml認証用の設定 const samlStrategy = new Strategy( { // URL that goes from the Identity Provider -> Service Provider callbackUrl: 'http://localhost:3000/login/callback', // URL that goes from the Service Provider -> Identity Provider entryPoint: 'http://localhost:7000/saml/sso', issuer: 'saml_test_issuer', identifierFormat: undefined, // urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress // Identity Providerのサーバ証明書 cert: fs.readFileSync('idp-public-cert.pem', 'utf8'), validateInResponseTo: false, disableRequestedAuthnContext: true, }, (profile, done) => done(null, profile) ); passport.use(samlStrategy); ログイン処理 router.get('/login', authModule, (req, res) => { res.redirect('/'); }); ログイン処理 /** * idpで認証後のコールバックURL * ・この時点で、認証されたユーザ情報が「req.user」にセットされる * ・リクエスト時のURLにリダイレクトする */ router.post('/login/callback', authModule, (req, res) => { console.log('/login/callback', req.user); if ((req as any).session) { res.redirect((req as any).session.requestUrl); delete (req as any).session.requestUrl; } else { res.redirect('/'); } }); ログイン失敗時の処理 router.get('/login/fail', (req, res) => { res.status(401).send('Login failed'); }); ログアウト /** * ログアウト * ・'/'にアクセスしても、認証情報がないため再度認証画面へ飛ばされる。 */ router.get('/logout', (req, res) => { req.logout(); res.redirect('/'); }); 認証チェック // 認証無しで許可するパス(チェックは手抜きです。適切に書き換えてください) const allowPaths = ['/stylesheets', '/images', '/javascript', '/favicon.ico']; /** * 認証チェック * ・全てのReact側からの通信に対して、認証チェックを行う * ⇒認証されていない場合は、saml認証を行う */ router.all(['/*'], (req, res, next) => { if (req.isAuthenticated()) { console.log(`Authenticated:${JSON.stringify(req.user)}`); return next(); } if (req.url === '/' ) { // topページは認証不要 return next(); } if (allowPaths.some((path) => req.url.startsWith(path))) { // 許可するパス return next(); } console.log(`${req.url} Not authenticated. Redirect to /login`); // リクエストされたurlをセッションに保存してから、idpへ認証を依頼 (req as any).session.requestUrl = req.url; return authModule(req, res, next); }); app.tsに組み込み ページ表示時、認証が先に行われるようにするため「認証モジュールの組み込み」を先に行います。 // samlによる認証処理 app.use(session({secret: 'paosiduf'})); app.use(samlPassport.initialize()); app.use(samlPassport.session()); app.use(samlAuth); // 認証モジュールの後にルートを追加する(先に認証チェックを行うため) app.use('/', indexRouter); app.use('/users', usersRouter); app.use('/page1', page1); 7:テスト用IdP(saml-idp)の設定 saml-idpをpackage.jsonへ追加 (https://www.npmjs.com/package/saml-idp) コマンドラインから起動できるテスト用のIdP(Identity Provider)です。 npm i -D saml-idp IdP用証明書ファイル作成 作成したファイルをプロジェクトルートに配置します。(ルートディレクトリでコマンドを実行すれば、コピーする必要はありません) openssl req -x509 -new -newkey rsa:2048 -nodes -keyout idp-private-key.pem -out idp-public-cert.pem -days 7300 Generating a RSA private key Country Name(国名), State or Province Name(県名), Locality Name(都市名)などは、テスト用途なので適当に入力してください。 出力するファイル名(idp-public-cert.pem)は、saml-idpのデフォルト名を指定しています。変更する場合は、起動時のコマンドライン指定を修正する必要があります。 作成コマンドサンプル $ openssl req -x509 -new -newkey rsa:2048 -nodes -keyout idp-private-key.pem -out idp-public-cert.pem -days 7300 Generating a RSA private key .....+++++ ......+++++ writing new private key to 'idp-private-key.pem' ----- You are about to be asked to enter information that will be incorporated into your certificate request. What you are about to enter is what is called a Distinguished Name or a DN. There are quite a few fields but you can leave some blank For some fields there will be a default value, If you enter '.', the field will be left blank. ----- Country Name (2 letter code) [AU]:JP State or Province Name (full name) [Some-State]:Aichi Locality Name (eg, city) []:Nagoya Organization Name (eg, company) [Internet Widgits Pty Ltd]: Organizational Unit Name (eg, section) []: Common Name (e.g. server FQDN or YOUR name) []:Test Identity Provider Email Address []:tkyk.niimura@gmail.com 起動用スクリプト登録 package.jsonの"scripts"に、テスト用IdP起動スクリプトを追加します。 "saml-idp": "saml-idp --acs http://localhost:7000/auth/saml --aud mock-audience" 8:動作確認 テスト用Idpサーバ(saml-idp)を起動します $ npm run saml-idp > simple-saml-auth@0.0.0 saml-idp C:\Users\t_nii\Documents\git\auth\simple-saml-auth > saml-idp --acs http://localhost:7000/auth/saml --aud mock-audience Listener Port: localhost:7000 HTTPS Enabled: false [Identity Provider] Issuer URI: urn:example:idp Sign Response Message: true Encrypt Assertion: false Authentication Context Class Reference: urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport Authentication Context Declaration: None Default RelayState: None [Service Provider] Issuer URI: None Audience URI: mock-audience ACS URL: http://localhost:7000/auth/saml SLO URL: None Trust ACS URL in Request: true Starting IdP server on port localhost:7000... IdP Metadata URL: http://localhost:7000/metadata SSO Bindings: urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST => http://localhost:7000/saml/sso urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect => http://localhost:7000/saml/sso IdP server ready at http://localhost:7000 プログラムを起動します npm run start 動作確認 トップページを表示(localhost:3000) 認証不要のため、ログイン画面は表示されません ログインが必要なページを表示(localhost:3000/page1) ログイン画面が表示される。「Sign in」を押下すると「/page1」にダイレクトし、ユーザ名が表示される。 一度ログインした後はログイン画面が表示されない。(トップページにユーザ名とログアウトリンクが表示される) トップページから「ログアウト(localhost:3000/logout)」すると、再度ログインが必要となる。 ソース一式
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ローカル環境のみで完結するSAML SSOサンプル(node, express, saml-idp利用)

目的 express-generatorでひな形作成 利用モジュールのインストール TypeScript化 テスト用ページ作成 passport-samlの設定 テスト用IdPの設定 動作確認 1:目的と概要 目的 ローカルで完結するSAML-IdP認証サンプルプログラムが見つからなかったため作成しました。 とりあえず動くことを目的としてます。 ※SAML認証とは?参考リンク * SAML認証に関する自分なりのまとめ * SAML2.0でのシングルサインオン実装と戦うあなたに(.NET編) 概要 機能概要 テスト用IdPをローカルで起動し、saml認証動作確認プログラムを動かします。 トップページは認証不要、認証が必要ページに遷移する場合にログイン(IdP側が表示)します。 ログイン、ログアウト機能を持ちます。 ソースはgithubのsimple-saml-authにあります 実装上の特徴 express-generatorでひな形を作成 ts-nodeを利用してtypescript化(ビルド不要) 2:express-generatorでひな形作成 express express-generator でカレントフォルダにひな形を生成します npx express-generator --view=ejs --git ./ 下記ファイルが生成されます / │ app.js │ package.json │ .gitignore ├─bin │ www ├─public │ ├─images │ ├─javascripts │ └─stylesheets │ style.css ├─routes │ index.js │ users.js └─views error.ejs index.ejs package.jsonに記載されたモジュールをインストールします。 npm install 動作確認のため、ターミナルで下記を実行し「localhost:3000」を開きます。 ブラウザで開き「Welcome to Express」と表示されたら成功です。 npm run start 3:利用モジュールのインストール TypeScriptと型定義があった方が、後々役立つので合わせてインストールします。 saml認証で必要なモジュール(と型定義)をインストールします npm i -D typescript nodemon @types/cookie-parser @types/express npm i ts-node express-session passportとpassport-samlを追加(nodeの認証用モジュール) npm i passport passport-saml npm i -D @types/passport インストール後のpackage.json (概ねこのようなファイルになっていると思います) { "name": "simple-saml-auth", "version": "0.0.0", "private": true, "scripts": { "start": "node ./bin/www" }, "dependencies": { "cookie-parser": "~1.4.4", "debug": "~2.6.9", "ejs": "~2.6.1", "express": "~4.16.1", "express-session": "^1.17.1", "http-errors": "~1.6.3", "morgan": "~1.9.1", "passport": "^0.4.1", "passport-saml": "^2.2.0", "ts-node": "^9.1.1" }, "devDependencies": { "@types/cookie-parser": "^1.4.2", "@types/express": "^4.17.11", "@types/passport": "^1.0.6", "nodemon": "^2.0.7", "saml-idp": "^1.2.1", "typescript": "^4.2.4" } } 4:TypeScript化 ts-nodeを使うため、事前コンパイルやトランスパイル後のdistフォルダなど面倒な設定は不要です。 Typescriptの設定ファイルを作成します(tsconfig.json) npx tsc --init 生成されたjsファイルの拡張子を全て「ts」に変更します mv app.js app.ts mv ./routes/index.js ./routes/index.ts mv ./routes/users.js ./routes/users.ts mv ./bin/www ./bin/www.ts ./bin フォルダに「www.js」を追加し下記の内容を追記します。 ここがミソです。.jsファイル内でrequire('ts-node')することで、tsファイルを読み込んで利用することができるようになります。 touch ./bin/www.js echo -e "require('ts-node').register({transpileOnly: true});\nrequire('./www.ts');" > ./bin/www.js 「{transpileOnly: true}」は、起動を高速化するため(型チェックを行いません)  ⇒型チェックはエディタ側に任せて、トランスパイルに専念させます。 tsconfig.json 下記行のコメントを1行外しfalseに変更します。 元々のソースがjsで型指定は一切ありません。any変数を許可してコンパイルエラーを回避します。 trueに戻して適切に型を付けると安全性が向上します(がこの記事の目的とは異なるため行いません。 "noImplicitAny": false, typescript化したexpressアプリケーションが起動することを確認します。 npm run start 現時点(require()での読み込み)では、型推論が行われません。必要時に応じ、Importに変更することで型チェック、オートコンプリートが行われるようになります。 require() ⇒ import module.exports ⇒ export default 変更例(index.ts) // var express = require('express'); // comment out import express from 'express'; // importに変更 var router = express.Router(); /* GET home page. */ router.get('/', function(req, res, next) { res.render('index', { title: 'Express' }); }); // importでもrequire()でも読み込めるようにmodule.exportを残しておきます module.exports = router; export default router; 5:テスト用ページ作成 viewsフォルダのファイルを追加、修正します。 認証不要のトップページ「index.ejs」と、認証が必要な「page1.ejs」を用意します。 トップページ(認証不要) 「index.ejs」 を修正。 ログイン時、ユーザ名、ログアウト認証が必要なページ(page1)への表示 未ログイン時、ログイン、認証が必要なページ(page1)へのリンク <!DOCTYPE html> <html> <head> <title>トップページ(認証不要)</title> <link rel='stylesheet' href='/stylesheets/style.css' /> </head> <body> <div> トップページ(認証不要) <p> <% if (uid) { %> ユーザ名[<%= uid %>] <a href='/logout'>ログアウト</a> <% } else { %> <a href='/login'>ログイン</a> <% } %> </p> </div> <div> <p><a href='/page1'>ログインが必要なページ</a></p> </div> </body> </html> 認証が必要なページ 「page1.ejs」 を新規追加。 ユーザ名の表示、トップページへのリンク <!DOCTYPE html> <html> <head> <title>認証が必要なテストページ</title> <link rel='stylesheet' href='/stylesheets/style.css' /> </head> <body> <div> 認証が必要なテストページ <p>ユーザ名[<%= uid %>]</p> </div> <div> <p><a href='/'>トップページへ戻る</a></p> </div> </body> </html> 6:passport-samlの設定 auth.ts追加 touch ./routes/auth.ts ユーザーのシリアライズ、デシリアライズ処理 // sessionへのシリアライズ、デシリアライズ処理 // saml認証で受け取った値をそのままセットしている // idだけをセッションに保存し、デシリアライズ時にDBから復元するなどの処理を行う passport.serializeUser<any>((user, done) => { done(null, user); }); passport.deserializeUser<any>((user, done) => { done(null, user); }); saml認証用のStrategy設定 // saml認証用の設定 const samlStrategy = new Strategy( { // URL that goes from the Identity Provider -> Service Provider callbackUrl: 'http://localhost:3000/login/callback', // URL that goes from the Service Provider -> Identity Provider entryPoint: 'http://localhost:7000/saml/sso', issuer: 'saml_test_issuer', identifierFormat: undefined, // urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress // Identity Providerのサーバ証明書 cert: fs.readFileSync('idp-public-cert.pem', 'utf8'), validateInResponseTo: false, disableRequestedAuthnContext: true, }, (profile, done) => done(null, profile) ); passport.use(samlStrategy); ログイン処理 router.get('/login', authModule, (req, res) => { res.redirect('/'); }); ログイン処理 /** * idpで認証後のコールバックURL * ・この時点で、認証されたユーザ情報が「req.user」にセットされる * ・リクエスト時のURLにリダイレクトする */ router.post('/login/callback', authModule, (req, res) => { console.log('/login/callback', req.user); if ((req as any).session) { res.redirect((req as any).session.requestUrl); delete (req as any).session.requestUrl; } else { res.redirect('/'); } }); ログイン失敗時の処理 router.get('/login/fail', (req, res) => { res.status(401).send('Login failed'); }); ログアウト /** * ログアウト * ・'/'にアクセスしても、認証情報がないため再度認証画面へ飛ばされる。 */ router.get('/logout', (req, res) => { req.logout(); res.redirect('/'); }); 認証チェック // 認証無しで許可するパス(チェックは手抜きです。適切に書き換えてください) const allowPaths = ['/stylesheets', '/images', '/javascript', '/favicon.ico']; /** * 認証チェック * ・全てのReact側からの通信に対して、認証チェックを行う * ⇒認証されていない場合は、saml認証を行う */ router.all(['/*'], (req, res, next) => { if (req.isAuthenticated()) { console.log(`Authenticated:${JSON.stringify(req.user)}`); return next(); } if (req.url === '/' ) { // topページは認証不要 return next(); } if (allowPaths.some((path) => req.url.startsWith(path))) { // 許可するパス return next(); } console.log(`${req.url} Not authenticated. Redirect to /login`); // リクエストされたurlをセッションに保存してから、idpへ認証を依頼 (req as any).session.requestUrl = req.url; return authModule(req, res, next); }); app.tsに組み込み ページ表示時、認証が先に行われるようにするため「認証モジュールの組み込み」を先に行います。 // samlによる認証処理 app.use(session({secret: 'paosiduf'})); app.use(samlPassport.initialize()); app.use(samlPassport.session()); app.use(samlAuth); // 認証モジュールの後にルートを追加する(先に認証チェックを行うため) app.use('/', indexRouter); app.use('/users', usersRouter); app.use('/page1', page1); 7:テスト用IdP(saml-idp)の設定 saml-idpをpackage.jsonへ追加 (https://www.npmjs.com/package/saml-idp) コマンドラインから起動できるテスト用のIdP(Identity Provider)です。 npm i -D saml-idp IdP用証明書ファイル作成 作成したファイルをプロジェクトルートに配置します。(ルートディレクトリでコマンドを実行すれば、コピーする必要はありません) openssl req -x509 -new -newkey rsa:2048 -nodes -keyout idp-private-key.pem -out idp-public-cert.pem -days 7300 Generating a RSA private key Country Name(国名), State or Province Name(県名), Locality Name(都市名)などは、テスト用途なので適当に入力してください。 出力するファイル名(idp-public-cert.pem)は、saml-idpのデフォルト名を指定しています。変更する場合は、起動時のコマンドライン指定を修正する必要があります。 作成コマンドサンプル $ openssl req -x509 -new -newkey rsa:2048 -nodes -keyout idp-private-key.pem -out idp-public-cert.pem -days 7300 Generating a RSA private key .....+++++ ......+++++ writing new private key to 'idp-private-key.pem' ----- You are about to be asked to enter information that will be incorporated into your certificate request. What you are about to enter is what is called a Distinguished Name or a DN. There are quite a few fields but you can leave some blank For some fields there will be a default value, If you enter '.', the field will be left blank. ----- Country Name (2 letter code) [AU]:JP State or Province Name (full name) [Some-State]:Aichi Locality Name (eg, city) []:Nagoya Organization Name (eg, company) [Internet Widgits Pty Ltd]: Organizational Unit Name (eg, section) []: Common Name (e.g. server FQDN or YOUR name) []:Test Identity Provider Email Address []:tkyk.niimura@gmail.com 起動用スクリプト登録 package.jsonの"scripts"に、テスト用IdP起動スクリプトを追加します。 "saml-idp": "saml-idp --acs http://localhost:7000/auth/saml --aud mock-audience" 8:動作確認 テスト用Idpサーバ(saml-idp)を起動します $ npm run saml-idp > simple-saml-auth@0.0.0 saml-idp C:\Users\t_nii\Documents\git\auth\simple-saml-auth > saml-idp --acs http://localhost:7000/auth/saml --aud mock-audience Listener Port: localhost:7000 HTTPS Enabled: false [Identity Provider] Issuer URI: urn:example:idp Sign Response Message: true Encrypt Assertion: false Authentication Context Class Reference: urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport Authentication Context Declaration: None Default RelayState: None [Service Provider] Issuer URI: None Audience URI: mock-audience ACS URL: http://localhost:7000/auth/saml SLO URL: None Trust ACS URL in Request: true Starting IdP server on port localhost:7000... IdP Metadata URL: http://localhost:7000/metadata SSO Bindings: urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST => http://localhost:7000/saml/sso urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect => http://localhost:7000/saml/sso IdP server ready at http://localhost:7000 プログラムを起動します npm run start 動作確認 トップページを表示(localhost:3000) 認証不要のため、ログイン画面は表示されません ログインが必要なページを表示(localhost:3000/page1) ログイン画面が表示される。「Sign in」を押下すると「/page1」にダイレクトし、ユーザ名が表示される。 一度ログインした後はログイン画面が表示されない。(トップページにユーザ名とログアウトリンクが表示される) トップページから「ログアウト(localhost:3000/logout)」すると、再度ログインが必要となる。 ソース一式
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ニコニコ実況(生放送)のコメントをNodejsで取得してみた

前書き的なもの PX-S1UD-1を手に入れたのでRaspberryPi4を有効活用してみようと視聴環境を構築したものの、物足りなさにニコニコ実況を実装しようと思いその事前調査した記録。 何番煎じだろうかw 出先でアニメ視聴用に地デジチューナーの低遅延HLS配信を多重Proxyと端末認証で垂れ流していた際はリソース余りまくりのラックサーバーでページの動画領域取得して無理矢理クロマキーしたりしていたが、スマートじゃないなと思いコメントのSocket取得を簡単にできるようにしてみようと思ったのも動機の一つ。 コメントの取得に関わる通信 以下のページとその参照元、引用元を覗いてみるとWebSocketで動画情報、コメントを配信しているらしい。 ニコ生新配信の放送をアプリで再生するための覚書き - Qiita ニコ生のコメント送受信をWebSocket+JSONでやる方法ざっくり解説 - Qiita これらを大雑把にまとめると以下の流れでコメント取得できそう。 Get Comment 動画ページ ==> Socket用アドレス取得 ↓ コンテンツ用Socketのセッション確立 ↓ コンテンツ用Socketでコンテンツ取得要求 ==> 動画再生の為の情報が送られてくる ↓ コメント用Socketのアドレスと必要な情報の抜き出し ↓ コメント用Socketのセッション確立 ↓ コメント用Socketにコンテンツ用Socketで発行されたIDを用いてコメント取得要求 ↓ コメントが送られてくる これをNodejs辺りで行えば簡単にコメント取得できそう。 Nodejsでコメント取得 Chromeブラウザのデベロッパツールとjavascriptで実際にコメント取得の手順を説明している記事を発見。 ニコニコ生放送のコメントを取得して色々するための第一歩(前編:JavaScript版) - Qiita 超分かり易かったのでこれを参考にさせてもらってNodejsで組んでみた。 使ったパッケージは「websocket」と「puppeteer-core」にLinux環境はブラウザとして「chromium」 Get Comment const puppeteer = require('puppeteer-core') let dir_Brwsr = ''; let url_page = (process.argv[2] || 'https://live.nicovideo.jp/watch/ch2646485'); let channel_name = ""; let socket_view = ''; const message_system_1 = '{"type":"startWatching","data":{"stream":{"quality":"abr","protocol":"hls","latency":"low","chasePlay":false},"room":{"protocol":"webSocket","commentable":true},"reconnect":false}}'; const message_system_2 ='{"type":"getAkashic","data":{"chasePlay":false}}' let uri_comment let threadID //Browser Directory (WinはEdge、LinuxはChromiumの判別) if(process.platform==='win32') dir_Brwsr = 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe'; else if(process.platform==='darwin') dir_Brwsr = ''; else if(process.platform==='linux') dir_Brwsr = '/usr/bin/chromium'; //Browser Controle async function getLatestDate(page, url){ await page.goto(url) // Open URL Page // Browser JavaScript channel_name = await page.evaluate(() => JSON.parse(document.getElementById("embedded-data").getAttribute("data-props")).socialGroup.name); return await page.evaluate(() => JSON.parse(document.getElementById("embedded-data").getAttribute("data-props")).site.relive.webSocketUrl); //ヘッドレスブラウザで開いてsocketアドレス取得 } !(async() => { try { const browser = await puppeteer.launch({args: ['--no-sandbox', '--disable-setuid-sandbox'],executablePath: (dir_Brwsr),ignoreDefaultArgs: ['--disable-extensions']}); const page = await browser.newPage(); const url_view = await getLatestDate(page, url_page); console.log(channel_name); console.log("WebSocket Connection ==> " + url_view); client.connect(url_view, null, null, {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36'}, null); browser.close() } catch(e) { console.error(e) } })() //View Session: WebSocket Connection let WebSocketClient = require('websocket').client; let client = new WebSocketClient(); client.on('connectFailed', function(error) { console.log('View Session Connect Error: ' + error.toString()); }); client.on('connect', function(connection) { console.log('WebSocket Client Connected[View Session]'); socket_view = connection; //コメントSocketから閉じられるようにコネクション格納 connection.sendUTF(message_system_1); //コンテンツ情報要求 connection.sendUTF(message_system_2); connection.on('error', function(error) { console.log("View Session Connection Error: " + error.toString()); }); connection.on('close', function() { console.log('WebSocket Client Closed[View Session]'); }); connection.on('message', function(message) { if (message.type === 'utf8') { // Get Comment WWS Addres & Option Data if(message.utf8Data.indexOf("room")>0) { //色々データ抜いてコメントsocketに接続 evt_data_json = JSON.parse(message.utf8Data); uri_comment = evt_data_json.data.messageServer.uri threadID = evt_data_json.data.threadId message_comment = '[{"ping":{"content":"rs:0"}},{"ping":{"content":"ps:0"}},{"thread":{"thread":"'+threadID+'","version":"20061206","user_id":"guest","res_from":-150,"with_global":1,"scores":1,"nicoru":0}},{"ping":{"content":"pf:0"}},{"ping":{"content":"rf:0"}}]' console.log("WebSocket Connection ==> " + uri_comment); // Comment WebSocket Connection comclient.connect(uri_comment, 'niconama', { headers: { 'Sec-WebSocket-Extensions': 'permessage-deflate; client_max_window_bits', 'Sec-WebSocket-Protocol': 'msg.nicovideo.jp#json', }, }); } // Keep View Session if(message.utf8Data.indexOf("ping")>0) { //pingに応答 connection.sendUTF('{"type":"pong"}'); connection.sendUTF('{"type":"keepSeat"}'); } } }); }); // Comment Session: WebSocket Connection let comclient = new WebSocketClient(); comclient.on('connectFailed', function(comerror) { console.log('Comment Session Connect Error: ' + comerror.toString()); }); comclient.on('connect', function(connection) { console.log('WebSocket Client Connected[Comment Session]'); connection.sendUTF(message_comment); // Comment Session Keep Alive setInterval((connection)=>{connection.sendUTF("");}, 60000, connection); //コメントSocketの生存確認送信 connection.on('error', function(error) { console.log("Comment Session Connection Error: " + error.toString()); }); connection.on('close', function() { console.log('WebSocket Client Closed[Comment Session]'); socket_view.close(); //コメントSocket終了時、コンテンツsocketも終了 }); connection.on('message', function(message) { if (message.type === 'utf8') { if (message.utf8Data.indexOf("chat")>0){ //コメント以外スルー let baff = JSON.parse(message.utf8Data); if (baff.chat.content.indexOf('spi')<=0 && baff.chat.content.indexOf('nicoad')<=0){ //広告コメントスルー //console.log('Received:' + message.utf8Data); //コメントのjson(コメントの色、位置などのコマンドあり)をコンソール出力 console.log('Received Coment: ' + baff.chat.content); //コメント文字のみをコンソール出力 } } } }); }); ポイントとしては他の記事でも書いてある通りコンテンツ用Socket開通時に適当なブラウザのユーザーエージェントをヘッダーに付与してブラウザからのアクセスを装う事と、コンテンツ用Socketで「Ping」に応答しつつ「keepSeat」も一緒に送信して動画を視聴しているような状態にしてセッションを確立し続ける事。 さらにコメントセッション確立後はコメント送信しない場合、約1分おきにコメントSocketでメッセージを送信して通信が生きていることを示す事。 ちなみにコメント用Socketはコンテンツ用SocketとIDで紐づけられており、コンテンツSocketが閉じるとコメントSocketも閉じるようになっている。 逆に放送が終了していないのにコメント用Socketが何らかの影響で閉じてしまってもコンテンツ用Socketは閉じずに生きているので、上記のプログラムではコンテンツ用Socketのコネクションを変数に格納してコメント用Socketが終了した際にコンテンツ用Socketも終了するようにしている。 一応、使い方説明 使い方は「nodejs」インストール後、今回使用している必要なパッケージもnpmインストールして実行。 今回WebページからSocketアドレスを取得するのに「puppeteer-core」を使用しているが、これはchrome系のブラウザを使用するので「Chrome」か「Chromium」もインストールする。 Windows環境では標準インストールの「Edge」を使用するのでブラウザインストール不要。 Mac環境はChrome系ブラウザインストール後、実行ファイルのパスをプログラムに要記述。 nodejs & Browser install $ curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash - $ apt update $ sudo apt install -y nodejs Chromium $ sudo npm install -g websocket puppeteer-core 適当な名前のjsファイルで保存してnodejsで実行。 実行時にニコニコ実況、生放送のURLを指定するとそこからコメントをリアルタイムに取得する。 URLの指定がない場合は初期値として設定されているニコニコ実況の「TOKYO MX」のコメントをリアルタイム取得する。 node nico_comment Run $ nodejs nico_comment.js https://live.nicovideo.jp/watch/ch2646485 後書き的なもの 今回のプログラムではとりあえずコンソールにコメントを出力するようにしているので必要な方は個々人で取得したコメントの処理を書いてください。 あと、nodejs単体で実行した場合はニコニコ実況だと朝4時に一度放送が終了して切り替わる際にSocketも閉じてプログラムも終了します。 うちの場合はサーバーのDockerコンテナでReStart="Always"設定で運用しているのでプログラム終了してもコンテナがRestartして再接続するので再接続処理書いてません。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Node.js+ExpressでWebアプリケーション開発 第2回

前の記事 前回はHelloWorldまででした。 今回はGETとPOSTを勉強してきたのでまとめていきます。 GET index.ejsに、自分自身にGetパラメータで"name"と"age"を渡し、再度読み込まれたときに"name"と"age"を表示するようなイメージで作りにしました。 views\index.ejs <div> <a href="?name=TEST&age=17">GETのテスト!!</a> <p>Name:<%= name %></p> <p>Age:<%= age %></p> </div> サーバサイドのroutes\index.jsの処理です。 router.get関数でGetパラメータの"name"と"age"を受け取り、JSON形式のオブジェクトにセットし、自分自身に返しています。各Getパラメータは"request.query"で取得できます。 routes\index.js const express = require('express'); const router = express.Router(); // ここを追加 var resObj = { title: 'Sample NodeExpTest', message: 'Hello World!', name: "", age: "", sports: "" }; router.get('/', function (request, response) { // response.render('index', { title: 'Sample NodeExpTest', message: 'Hello World!' }); // ここを追加 const query = request.query; resObj.name = query.name; resObj.age = query.age; response.render('index', resObj); }); 動作確認 初期表示時 リンククリック後 POST index.ejsに入力フォームを作成し、自分自身にPOSTすると、サーバサイドでフォームのデータを受け取り、JSON形式のオブジェクトにセットし、自分自身に返しています。各フォームデータは"request.body"で取得できます。 views\index.ejs <div> <form action="/" method="POST"> <p>好きなスポーツは何ですか?</p> <input type="text" name="sports"> <input type="submit" value="POSTのテスト!!"></input> </form> <p>好きなスポーツは・・・<%= sports %>です</p> </div> routes\index.js router.post("/", function (request, response) { resObj.sports = request.body.sports; response.render('index', resObj); }); このままでは"request.body"はundefinedとなり、フォームデータを受け取ることができませんでした。server.jsのappの直下に以下のようなおまじないを追加する必要があるようです。 server.js // express の実態 Application を生成 const app = express(); // POSTのクエリーを良い感じに処理する app.use(express.urlencoded({extended: true})); 動作確認 初期表示時 好きなスポーツを入力します。 "POSTのテスト!!"ボタンをクリックすると、入力したスポーツが画面に表示されました。 今回はここまで。 次回は画面遷移について勉強していきたいと思います。 ソースコードはこちら。 https://github.com/pocota5260/NodeExpTest
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む