en ja en nb ja en ja ja ja fr ja ja ja ja ja ja ja en ja ja ja ja ja ja ja ja ja ja en fa ja ja ja en ja es ja en ja ja ja ja ja ja ja ja ko ja ja ja en ja en ja ja ja en ja ja ja ja ja ja en ja ja en ja ja en ja en ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja en ja en nl ja ja pl ja nl ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja en nl ja ja en ja ja ja ja ja ja ja pl zh cs ja ja ja ja ja ja en ja ja en ja mk ja ja ja ja zh ja ja ja ja ja ja ja ja ja ja ja pl ja es ja zh ja ja ja ja ja ja ja ja cs ja ja en ja ja ja ja ja ja ja ja en ja ja ja ja ja ja ja ja id ja ja ja sv ja ja ja ja ja fr ja ja ja ja ja ja ja ja ja ja en ja en ja en en ja en ja ja en ja en ja en ja ja en ja ja ja ja ja ja fr ja en ja tr ja ja en ja en ja en ja en ja zh fr en te ja ja zh ja ja en ja en ja en ja es ja ja mk ja ja ja en ja ja ja ja sl ja ja ja ja ja ja ja ja ja ja ja ja fr ja en ja zh ja ja en ja en ja es ja ja ja ja ja es ja ja ja en nl ja ja nb ja ja ja ja ja ja ja en ja en ja ja ja en ja ja ja ja ja ja ja ja ja ja zh ja en ja en ja ja en ja ja ja ja pl nl ja ja nb ja es ja ja ja ja ja en en en ja ja zh ja ja ja ja ja ja ja ja nl ja ja ja en ja de nl ja ja he ja es ja ja ja en en en ja zh ja ja ja ja en ja ja ja ja ja en ja ja en ja ja ja ja nl ja ja de ja en ja de ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja en ja ja ja ja ja nb en ja ko en ja ur en ja ru en zh zh ko zh ja zh ja ja zh kn ja ja zh zh si ja ja ja en ja en ja en ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja en ja ja ja ja ja en ja ja en ja ja en ja ja ja ja ja ja en ja en ja ja en ja ja ja en ja ja ja en ja en ja en ja en ja en ja en ja ja en ja en ja en ja ja ja en ja en ja ja ja ja ja ja ja ja en ja ja ja ja ja ja ja ja ja ja ja pl nl ja ja ja en ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja jbo en en ja nl nb en ko it en it it it en fr it ja ja en la fi ja it it it pt it de en ja en en it fr it en it nb it en ja ja ja ja ja ja ja ja ja
表題の通りです。できない理由を書きます。また、ついでにSame-Origin PolicyとCORS (Cross-Origin Resource Sharing)についても簡単に解説します。
とあるデータ(刻々と追加されていきます。更新はなし)をDynamoDBに溜め込んでいます。そのデータを検索して返すWeb APIをAPI Gateway + Lambdaで実装しました。
以下、API GatwayからLambda (Node.js)のコードを呼び出し、そのまま結果を返すか、またはS3へリダイレクトすることを考えます。また、API Gateway側は「Enable CORS」の設定を済ませていて、さらにLambda Proxy integrationを使うことを前提にします。リクエストはGETを利用します。
cors - Are there any browsers that set the origin header to "null" for privacy-sensitive contexts? - Stack Overflow
Chrome cancels CORS XHR upon HTTP 302 redirect - Stack Overflow
If the response has an HTTP status code of 301, 302, 303, 307, or 308 Apply the cache and network error steps.
API GatewayはLambdaを実行し、上記のコードに従ってHTTP 303 See Otherを返します。
*3:本当は jquery.get() で書けますが比較のために jquery.ajax() で書いています
ただし @channel や @here は使えない
その発言を開始点としてスレッドを作って、
その中で参加者にメンションを飛ばす
とすればいいことになります。
ピン留めされたスニペットとの違い
弊社ではゴルフ場様向けに様々なサービスを開発・提供しており、お客様からのお問い合わせも数多くいただきます。そのインシデント管理はSalesforceを活用しているのですが、担当者だけでは解決しなかった場合、別の詳しいメンバーや開発担当等にエスカレーションし、ヘルプを求めます。それを多数のメンバーでスピーディに行うためにSlackを活用しており、サポートチャンネルにその旨をスニペットで*1書き込んでピン留めし、簡易的な課題管理のような使い方をしています。
ピン留めされたスニペットや画像は、
そのチャンネルに入っている全員が見える
現在未解決のまま残っている問い合わせが一目で分かる*2
コメントをスレッド形式で書き込んでいける
進捗や担当・開発者間のやり取りも当初の問い合わせ内容の下にぶら下げて書ける
というメリットがあります。こんな感じになります。
スレッド表示もこんな感じで使えると良いかと思ったのですが、スレッドに参加する・参加させるというステップが必要なこともあり、現在のところはもっと軽い使い方になりそうです。また、チャンネルに参加している人数の多寡によってもありがたみは変わってきそうだと感じました。数百人といった大きなチャンネルでの使い勝手も聞いてみたいところです。
それでは良いチャットライフを。
*1:一言二言で終わるような問い合わせはないため、これは自然な行動です
*2:インシデント管理は別途Salesforce+Remedyforceで行っているため、これはあくまで現状を簡単に確認するためのものです。また言うまでもありませんが、解決したらピン留めは外します。
nurenezumi 2017-01-20 22:48
石器時代みたいなWebアプリ開発をしていたエンジニアがReact.jsに導かれて2016年にやってきた話
社内向けのシンプルなWebアプリケーションを作成することになりました。 求められているものはサービスそのものであり、実装方式については全て自分で決めていいわけですが、この際なので2016年のモダンな開発方法で書こうと思い立ちました。そこでReact.jsを採用します。今回の記事はその日記のようなものです。
まずReact.jsについて
React.jsってなに
JSX→JavaScriptの変換をしたい
ここでちょっとnpmについて
パッケージ取得・インストール
ES2015? ES2016???
gulp + Babel
Node.jsではどうやっているか
そこでBrowserify
React.jsを使い始めました。そのためにnpmとBabelを入れました。ついでにgulpとBrowerifyを入れました。快適かつモダンな開発環境を手に入れました。
2016年を代表する猫
まずReact.jsについて
React.jsってなに
Facebookの偉大なエンジニアが作ったフロントエンド用のフレームワークです。特徴として、
DOMを全てJavaScriptで構築・操作する
(基本的には)データの流れが一方通行
コードをReact.js独自の文法「JSX」で記述する
HTMLを書くのはやめます。DOMは全てJavaScript側で構築してHTMLに流し込みます。動的に操作するのももちろんJavaScriptです。したがって、HTMLは
こんな風に超シンプル(というより空っぽ)になります。React.jsが動き出すと、この 読み込み中... が丸ごとJavaScript側で生成したDOMに置き換わるわけです。
jQueryを使ったDOM操作でWebアプリケーションを作るとき、たとえば画面の右側(例:アイテムの絞り込み条件)と左側(例:アイテムの一覧表示)で不整合が起きるようなバグに苦しむことがあります。これは画面の要素のあちこちが独自に状態を抱え込み、しかもその要素が複雑に絡み合って情報を受け渡しするため、その管理を正しく行うのが難しくなるからです。
これに対してReact.jsでは、
画面に表示すべき全てのデータを1個のオブジェクトにまとめてしまう
そのオブジェクトを受け取って画面表示する(=DOMに変換する)処理を書く
という基本原則があります。画面を書き換えたい場合は、
オブジェクトの値を更新する
全ての画面要素を一括で描画し直す
で済ませてしまいます。このルールにより、画面表示と内部データは常に一貫性を持っていることが保証されます。jQueryでこんなことをやっていたら重くてまともに操作できなくなりそうですが、React.jsはまず画面更新後のDOMを内部的に算出し、発生した差分のみに対して実際のDOM操作を行うというテクニックを採用しており(これはVirtual DOMと呼ばれています)、処理は高速に行われます。差分がなければDOM操作は発生しません。
まず、React.jsのコードはJSXという文法で記述します。これはJavaScriptを拡張した独自の文法で、JavaScriptに似ていますがJavaScriptではありません(明らかに文法エラーになります)。簡単な例を示します。
function App() { return  hello world! ; } ReactDOM.render( , document.getElementById('app'));
JavaScriptのコード中にHTMLのタグが出現しています。これは、
1行目で独自のAppというタグ(コンポーネントと呼びます)を定義し、
2行目でAppコンポーネントを新しく生成し、HTML側のdivタグに流し込む
という処理です。コンポーネントはいくらでも作ることができます。React.jsでは、Webアプリケーションを構成する要素をこのコンポーネントに分解し、それらを組み立てることでDOMを構築していきます。
JSX→JavaScriptの変換をしたい
JSXはFacebookの偉大なエンジニアが勝手に定義した言語ですので、ブラウザは解釈できません。したがって、JSX→素のJavaScriptへの変換を誰かが行う必要があります。
1つめの方法は手動で変換をかけるものです。オンラインの変換ツールがありますので、ここにJSXをコピペすれば通常のJavaScriptに変換してもらえます。実際にやることはないでしょうが、一度このリンクを開いておいてください。
2つめの方法は毎回動的に変換をかけるものです。HTML側で
と書いておくと(type="text/jsx"に注目しましょう)、JSXで書かれたapp.jsをその場でJSXTransformerがJavaScriptに変換してくれます。これはお手軽なのですが、当然ながらパフォーマンス上の問題が生じますし、JSXTransformerは既にdeprecatedになっています。
最後の方法は、何らかのツールでトランスパイル*1をかける方法です。
ここでちょっとnpmについて
.NETにNuGetがあるように、PHPにPEARやComposerがあるように、またRubyにgemがあるように、JavaScript*2にもnpm (Node.js Package Manager)というパッケージマネージャーがあります。多数の人々により様々なパッケージが提供されているわけですが、そのパッケージをダウンロード・インストールしてくれたり、その際に依存関係を解決してくれたり、また新しいコマンドをインストールしてくれたりする優れものです。
npmはsudo apt install npmで簡単に入ります。すみませんがAmazon LinuxなどFedora系では入れるのが地味に面倒くさいので自分で調べてください。
npmでは、まず最初にプロジェクトを作成する必要があります。空のディレクトリに対して
を実行すると、プロジェクト名などの基本情報を入力するように要求され*3、これが終わるとこのディレクトリはプロジェクトとして管理対象になります。管理対象と言っても、単に管理用ファイルpackage.jsonが新規作成されるだけです。
パッケージ取得・インストール
これができたら次のコマンドでパッケージを取得・インストールすることができます。
npm intall --save   npm intall --save-dev
ここで--saveと書いているのは、package.jsonの中に「このプロジェクトが依存するパッケージ」としてインストールしたパッケージを記録しておくことを示します。開発時にのみ必要なパッケージをインストールする際は--save-devオプションを指定します。他の人は後からpackage.jsonを見ればそのプロジェクトのために必要なパッケージが分かりますし、package.jsonだけ手元にあれば、npm installでその中に記載されたパッケージを一括でダウンロード・インストールすることができます。
さてインストールを行うと、プロジェクトのディレクトリ直下にnode_modulesディレクトリが作成され、ダウンロードしたファイル(多くの場合はJavaScriptのコード)が入ります。これらのコードは、
var hoge = require("MODULE_NAME");
などの文法で参照することができます。これについては後述します。
また、インストールと同時にnode_modules/.bin/以下に実行可能ファイルが置かれるものもあります。これはプロジェクトごとにインストールされるものですが、プロジェクトによらず、OS全体で(グローバルに)利用したいこともあるでしょう。後述のgulpなどがそれにあたります。この場合は、
sudo npm install --global
でインストールができます。OS全体に影響が及ぶのでsudoが必要なことがあります。
さて、上の方で試しに利用したオンライン変換ツールの名前はBabelとありました。そもそもBabelってなんでしょう?
Babelの名前は、(おそらく)聖書のバベルの塔の記述、「全ての地は、同じ言葉と同じ言語を用いていた」から来ています。JavaScript*4にはES5, ES2015(ES6), ES2017(ES7)といくつかのバージョンがあり、また正式採用前ながらも開発者コミュニティによく使われている文法もあり、さらにはJSXのような方言も存在します。そのサポート状況はブラウザによって異なります。先進的なバージョンの文法で書かれたJavaScriptはどれ一つとしてブラウザが対応していないことだって考えられますし、現時点ではJSXを理解できるブラウザは存在しません(これから先もないでしょう)。このようなJavaScriptを古いバージョンのJavaScriptに変換し、どのようなブラウザでも解釈できるようにしてくれるのがBabelです。「同じ言葉と同じ言語」が実現するわけですね。
Babelはオンラインで使うこともできますが、普通はコマンドとして使いたいでしょう。そこでnpmを使います。このような流れで実行します。
空のプロジェクトを作る
このプロジェクトにnpmでBabelをインストールする(babelコマンドも入る)
このプロジェクトにnpmでBabelのreactプリセット(プラグインのようなもの)をインストールする
順に実行します。まず空のディレクトリを作り、そこに空のプロジェクトを作成します。
mkdir helloworld cd helloworld npm init
次にBabelとBabelのプリセットをインストールします。
npm install --save-dev babel-cli babel-preset-react
変換が終わってしまえば稼働時には必要ないので--save-devを指定しています。これで準備は完了です。次のコマンドでJSXのファイルを読み込み、JavaScriptに変換して標準出力に吐き出します。
function App() { return  hello world! ; } ReactDOM.render( , document.getElementById('app'));
neko@ubuntu:/workdir$ node_modules/.bin/babel --presets react app.js function App() { return React.createElement( 'span', null, 'hello world!' ); } ReactDOM.render(React.createElement(App, null), document.getElementById('app'));
先ほどオンラインで試したものと同じコードが出力されました。
ES2015? ES2016???
上の方で出てきた「ES2015」「ES2016」について説明します。
まず、JavaScriptはECMAScriptという規格に沿った原語です。ECMAScriptには、
ECMAScript 5th Edition (ES5)
IE6とかのいにしえの時代はこれ準拠のJavaScriptが使われていた
ECMAScript 2015 (ES2015)
ECMAScript 2016 (ES2016)
というバージョンがあり、更新されるたびに便利な(あるいは複雑な)機能が追加されていっています。テンプレートリテラル(文字列の組み立て)などはもうES5の時代には戻れません。
var name = "neko"; // ES5 console.log("my name is " + name + "."); // ES2015 console.log(`my name is ${name}.`);
主要なブラウザが搭載しているJavaScriptエンジンはES2015に対応していますが、ES2016は新しすぎるためまったくと言っていいほど未対応です。
さて話を戻して、ここではgulpというツールを投入します。
gulpとは「ビルドシステム」で、あらかじめ定義したタスクをまとめてコマンド一発で実行してくれるツールです。たとえばJSXをJavaScriptに変換するだけではなく、ついでに圧縮とかもしたいですよね? それもコマンド一発でやって欲しいですよね? さらにはファイルの更新を監視し、それらの処理を自動で済ませたりしてほしいですよね? gulpがやってくれます。
まず、グローバルでgulp-cliをインストールします。どこのディレクトリで実行しても構いません。
sudo npm install --global gulp-cli
これでどこからでもgulpコマンドが実行できるようになりました。次に、プロジェクトのディレクトリに移動してgulpとgulpが利用するパッケージをインストールします。ここではJavaScriptのコードを圧縮するgulp-minifyを入れてみます。
npm install --save-dev gulp gulp-minify
gulpは「あらかじめ定義したタスク」を自動で行うものだと言いました。この定義はgulpfile.jsというファイルで行います。プロジェクトディレクトリの直下にgulpfile.jsを作成し、以下のように書きます。
var gulp = require('gulp'); var minify = require('gulp-minify'); gulp.task("neko", function() { gulp.src("src/*.js") .pipe(minify()) .pipe(gulp.dest("lib")); }); gulp.task("default", ["neko"]);
ここではnekoという名前のタスクを定義していて、その内容はsrcディレクトリ以下の*.jsを対象にminify()して、結果をlibディレクトリに出力するというものになっています。また、タスク名が指定されなかった場合のdefaultタスクの内容としてはnekoを使ってね、ということも書いてあります。
試しにsrc/app.jsとしてこんなJavaScriptを書いて、
// tama-san is kawaii var name = "tama"; console.log("hello, my name is " + name + " desu.");
コマンドラインでgulpとだけ実行すると(明示的にgulp nekoと実行しても構いません)、libディレクトリ(存在しなければ自動的に作成されます)にオリジナルのapp.js、圧縮済みのapp-min.jsが出力されます。中身を見てみます。
var name="tama";console.log("hello, my name is "+name+".");
確かにコメントや空白が削除されて圧縮されています。
gulp + Babel
これでgulpが何者かは分かりました。gulpからBabelを使えるようにしましょう。まずgulpが利用するパッケージをインストールします。
npm install --save-dev gulp babel-cli babelify browserify vinyl-source-stream babel-preset-react babel-preset-es2015
見知らぬ名前がたくさん出てきました。
gulp : gulp本体
babel-cli : Babel CLI
babelify : Babel
browserify : (後述)
vinyl-source-stream : gulpが利用する
babel-preset-react : BabelのReact.js用プリセット
babel-preset-es2015 : BabelのES2015用プリセット
分かってしまえばどうと言うことはありませんね!*5
gulpfile.jsの内容はこうなります。
var gulp = require("gulp"); var babelify = require("babelify"); var browserify = require("browserify"); var source = require("vinyl-source-stream"); gulp.task("neko", function() { browserify({entries: "src/app.js"}) .transform(babelify) .bundle() .on("error", function (err) { console.log("Error : " + err.message); }) .pipe(source("app.js")) .pipe(gulp.dest("./lib/")) }); gulp.task("default", ["neko"]); gulp.task("watch", function() { gulp.watch("src/*.js", ["neko"]); });
さらにプロジェクトディレクトリの直下に.babelrcというファイルを新規作成します。これはBabelの動作内容を決める設定ファイルです。ReactのJSXとES2015を使いたいのでこのように書きます。
{"presets": ["react", "es2015"]}
これで下準備が全て整いました。srcディレクトリにapp.jsを配置します。
const App = props =>  hello world!!!! ; // ES2015の新文法 ReactDOM.render( , document.getElementById('app'));
これでgulpを実行すると、libディレクトリに変換されたJavaScriptが出力されます。これを次のHTMLで読み込めば動作します。
gulpとだけ実行すると、gulpはタスクを1回だけ行って終了します。gulp watchとすると、gulpはファイルの更新を監視し、更新されたらその場その場でタスクを繰り返し実行してくれます。つまり、もう手動でコマンドを実行する必要はなくなるのです。
次に、上の項で唐突に出てきたBrowserifyの解説をします。
ここでjQuery UIのことを思い出してみましょう。ドキュメントを読むと「jQuery UIはjQueryに依存している」と書いてあります。したがって、HTMLに
のように書かないといけないのでした。このような依存関係は人間がドキュメントを読んだり書いたりして管理しないといけません。また、jQueryはグローバル変数$を利用するため、同じ変数を利用するライブラリとは同時に利用できません*6。これも利用者が気を配ってやる必要があります。
Node.jsではどうやっているか
このような牧歌的な時代は終わりました。現代のJavaScriptではこんなのどかなことを言っていてはいけません。
サーバーサイドJavaScript実行環境であるNode.jsには他モジュールのインポート機能があります。たとえばAWS APIを使いたいのであれば、まずnpmを使ってaws-sdkパッケージをインストールします。
npm install --save aws-sdk
プログラムコード中でaws-sdkをインポートするにはrequire()文を使います。
var AWS = require("aws-sdk"); // aws-sdkという名前のモジュールを読み込む var ec2 = new AWS.EC2(); ec2.startInstances(...); // EC2インスタンスを起動
ブラウザでも同じことがしたいと誰もが思います。しかし(現在の)ブラウザに許されるのは タグでHTTPリクエストを発生させてソースコードを読み込むことだけ。このような仕事はブラウザの領分を超えています。
そこでBrowserify
Browserifyは、npmと組み合わせてこのインポート構文をブラウザ用JavaScriptにも持ち込むためのツールです。このように使います。
npm install --save  で必要なパッケージをインストールする
JavaScriptのコードの中にrequire()文を書く
Browserifyを通すと、require()が消滅し、代わりに参照したパッケージが埋め込まれる
この結果、ユーザーは参照しているライブラリ(パッケージ)が全て結合された1つの大きな.jsファイルを得ます。名前空間の汚染がないような工夫も行われています。
JavaScriptで日次を任意の書式(たとえばYYYY/mm/dd HH:mmとか)で出力したいというとき、整数のフォーマットが面倒でいつも頭を悩ませていました。従来はそのためのjQueryプラグインなどに頼っていたわけです。
我々はもうjQueryと手を切ると決めたので、ここでnpmパッケージを探すとdateformatというものがあります。便利そうなので使わせてもらいましょう。インストールは簡単です。
npm install --save dateformat
あとはrequire()で読み込むだけです。
var dateFormat = require("dateformat"); // これ! var timestamp = dateFormat(new Date(), "yyyy/mm/dd HH:MM"); console.log(timestamp); // "2016/12/21 12:34"
これをgulpでビルドすると、require()部分が消えてdateformatが取り込まれ、単体で動作するJavaScriptのソースコードが出てきます。
最初の方の例では、react.js, react-dom.jsを直接CDNから読み込んでいました。これを統合してみましょう。
まず、npmでreact, react-domを追加します。
npm install --save react react-dom
次にapp.jsを修正します(上2行を追加します)。
var React = require("react"); var ReactDOM = require("react-dom"); const App = props =>  hello world!!!! ; // ES6の文法 ReactDOM.render( , document.getElementById('app'));
そしてgulpでこのapp.jsをビルドすると、生成されたlib/app.jsはライブラリを取り込んで700KBになりました*7。これならもうスクリプトは1個で済みます。
ここまで準備してしまえば、その他のライブラリを導入するのも、別のトランスパイラを利用するのもぐっと障壁が低くなります。特にJavaScriptの開発環境は異様なほど流行廃りが速いのですが、なるべく便利なものをどんどん取り入れて快適に開発を進めていきたいものです。
*1:コンパイルのようなものですが、コンパイルのようにバイナリを吐くわけではないのでこう呼ばれることがあります
*2:正確にはNode.js
*3:全てEnter空打ちでOKですが、プロジェクト名に大文字は含められないため、ディレクトリ名に大文字が入っている場合だけは手で入力する必要があります
*4:正しくはECMAScript
*5:こういうところでは気になってもとりあえず先に進み、しかし後から戻ってきてちゃんと理解するのが惑わずにいるコツです
*6:近年ではあまり名前を見なくなりましたが、prototype.jsが該当します
*7:圧縮すればもっと縮みます
nurenezumi 2016-12-21 12:06
LINEのMessaging API WebhookをAPI Gatewayで受ける
こんにちは、AWS担当のwakです。
最近LINE botがMessaging APIとしてリニューアルしたとのことで、同僚が面白がって触っていたので説明してもらいました。聞くところによると、LINEのbotを友達登録したりメッセージを送ったりアンケートに答えたりすると、その内容がリアルタイムで自分のサーバーに送られてくるWebhookがあるそうです。
そしてLINEのMessaging API WebhookはHTTPSのエンドポイントを要求します。このためだけにサーバーを立てたり証明書を取ったりするのも面倒ですし、AWSのAPI Gateway+Lambda(Node.js)で受けることにしました。今回はその話を書きます。
話はシンプルです。LINEからのメッセージをAPI Gatewayで受け、署名(後述)をチェックし、それ以外のものをどこか(SNS, DynamoDB, アプリケーションサーバーなど)に投げます。こうすることにより、仮に一気にアクセスが集中した場合でも応答ができなくなることは防げますし、API Gateway側で流量制御もできます。また、アプリケーションが直接インターネットに露出する必要がなくなるので気を遣う点が少なくなります。
Node.jsで書きます。コードはこの記事の末尾にあります。先に作成しておいてください。
API Gateway
いつも通りResources→Create Resource→Create MethodとたどってPOSTのエンドポイントを用意します。ここでLambdaを指定するのですが、"Use Lambda Proxy integration"のチェックをONにしてください。
Use Lambda Proxy integrationがONなので、その他の細かい設定は不要です。API Gatewayがアクセスを受け付けると、HTTPヘッダ、ボディ等がまとめてLambdaに渡されます。
問題なければデプロイしてエンドポイントを確定させておきます。
LINE Messaging API Webhook
LINE Business Centerにログインし、「アカウントリスト」タブからMessaging APIを探して設定画面を開きます。ここにある「Channel Secret」を取得し、Lambdaのコード中にコピペして更新しておきます*1。
同じ画面の下の方にWebhookのエンドポイントを登録する場所があります。
ここにAPI Gatewayのエンドポイントを書いて「VERIFY」ボタンを押してみましょう。ここまでの設定が全て正しく行っていれば、API Gateway経由でLambdaが実行され、ログが出力されるはずです。
正しくログが出力されればこれで終わりです。あとはbotを友達登録したりチャットをしたりしてください。
さて、ちょっと別のことを考えます。
LINE(メッセージを送信する側)の立場から見た場合、WebhookのエンドポイントはHTTPS限定ですから、登録したエンドポイントにメッセージを送信すれば確実に私に届くことは保証されています。なりすましに騙されて違う相手にメッセージが届いてしまうこと、送信経路でメッセージが盗聴・改竄を受ける可能性を気にする必要はありません*2。ところが私(メッセージを受ける側)から見た場合は話が変わります。たとえば誰かがLINEのサーバーを装って本物と同じフォーマットで偽のメッセージを送ってくるかもしれません。こういった攻撃は正しく見破って破棄する必要があります。
このように、メッセージの改竄・捏造を検出する手段の一つがHMAC: Hash-based Message Authentication Codeです。HMACでは、事前に共有した秘密鍵とメッセージを混ぜ合わせてハッシュを取り、MAC値と呼ばれる値を算出します。送信側はこのMAC値をメッセージに添えて送ります。受信側はメッセージの内容と秘密鍵*3から同じ手順でMAC値を算出し、添えられたMAC値と比較して、一致すればこのメッセージは本物であると判断します。なぜならば、メッセージに対応した正しいMAC値が分かるのは秘密鍵を知っている2人だけだからです。秘密鍵が分からないのにメッセージをでっち上げようとしてもうまくいきません。
なお、HMACの利点は秘密鍵そのものをメッセージに含める必要がないことで、つまりSSLを通さず平文でメッセージをやり取りすることも可能です(覗き見は避けられませんが、改竄は不可能だということになります)。
ドキュメント | LINE Developers に従ってメッセージのMAC値(をBASE64エンコードした文字列)を算出します*4。
var crypto = require('crypto'); var hmacsha256 = crypto.createHmac("sha256", "deadbeefdeadbeefdeadbeefdeadbeef"); // 第2引数はChannel Secret var body = "..."; // 受信したbody部 var bodyStream = new stream.Readable(); bodyStream.push(body); bodyStream.push(null); bodyStream.on("data", chunk => { hmacsha256.update(chunk); }); bodyStream.on("end", () => { var digest = hmacsha256.digest("base64"); console.log(`digest: ${digest}`); });
このdigestの値と、X-Line-Signatureヘッダの値とを比較すれば検証は終わりです。
var signature = (event.headers || {})["X-Line-Signature"];
aws-sdk, crypto, streamはいずれも標準の環境に含まれているため、特に複雑なことをする必要はありません。このコードではログを出力するだけですが、もちろんなんでも好きなことができます。
'use strict'; var AWS = require("aws-sdk"); var lambda = new AWS.Lambda({region: "ap-northeast-1"}); var crypto = require('crypto'); var stream = require("stream"); exports.handler = function(event, context) { console.log(event); var signature = (event.headers || {})["X-Line-Signature"]; var body = event.body || ""; var bodyStream = new stream.Readable(); bodyStream.push(body); bodyStream.push(null); var hmacsha256 = crypto.createHmac("sha256", "YOUR-CHANNEL-SECRET"); // TODO: 管理画面からChannel Secretを取得して書き換える bodyStream.on('data', chunk => { hmacsha256.update(chunk); }); bodyStream.on('end', () => { var digest = hmacsha256.digest("base64"); if (digest === signature) { console.log("署名検証結果OK"); // TODO: 何かする } else { console.log("署名検証結果NG"); // TODO: 何かする } var response = { statusCode: 200, headers: { "x-custom-header" : "my custom header value" }, body: '{"result":"owari"}' }; context.succeed(response); }); };
*1:もちろん本当はDynamoDBとかに入れておきます
*2:そのためのSSLなので
*3:上述の通り、秘密鍵は事前にこっそり共有します。メッセージには含まれていません
*4:もちろん本来はバイトストリームから直接MAC値を算出すべきですが、API Gatewayでは文字列しか扱えません。LINEがメッセージを送信する際の文字エンコーディングも、Streamのデフォルトの文字エンコーディングもUTF-8であることを前提に処理を書いています
nurenezumi 2016-11-02 15:21
ASP.NET Webフォームプロジェクトでwebフォームの内容をhtmlファイルにしてダウンロードさせる
いまだASP.NET Webフォームを用いているシステムも多々あると思います。
弊社もWebフォームのシステムがあり、そこで普段やらないことをやりましたのでそれをまとめておきます。
こんな処理を書くことはそうそう無いのですが何かの役に立つかもしれません。
.NET Framework 4.5.1
ASP.NET Web Form
他システム(Linux/Apache)にアップロードするためのhtmlファイルをWebフォームを元に作成する。
HTML作成テンプレートとなるWebフォームを作成
HTML作成用テンプレートを呼び出すWebフォームを作成
2で1に渡すパラメータの取得、session経由での引き渡しを行い、1を実行する
結果を加工しダウンロードさせる
Web Formを使うことによりListView等にバインドできたり、編集が比較的楽というメリットがあります。コードも見やすいです。
当然ですが、Web Formの結果であるため不要なタグを端折る必要があります。
テンプレートのコードビハインド
using System; public partial class hogehoge { protected void Page_Load(object sender, EventArgs e) { // セッションからパラメータ取得しバインド var data = (HogeHoge)Session["hogehoge"]; this.DataBind(); Session.Remove("HogeHoge"); } }
HTML作成するWebフォームのコードビハインド
using System; using System.Text; using System.Web; protected void CreateHtml_Click(object sender, EventArgs e) { string url = "/template/hogehoge.aspx"; // セッションに引き渡しパラメータを入れる Session.Add("hogehoge", new HogeHoge()); // 実行・出力 using (System.IO.StringWriter writer = new System.IO.StringWriter()) { StringBuilder outputhtml = new StringBuilder(); // テンプレートファイルを実行する HttpContext.Current.Server.Execute(url, writer, false); string html = writer.ToString().Replace(System.Environment.NewLine, ""); // headタグ取得 Regex regex = new Regex(" .* "); Match match = regex.Match(html); string headerhtml = match.Value; // 出力タグ取得 regex = new Regex(" (? .*?) "); match = regex.Match(html); string bodyhtml = match.Groups["body"].Value; // html組み立て outputhtml.Append(" "); outputhtml.Append(headerhtml); outputhtml.Append(" "); outputhtml.Append(bodyhtml); outputhtml.Append("  "); // ダウンロード Response.ClearContent(); Response.ContentType = "text/html"; Response.AddHeader("Content-Disposition", "attachment;filename=" + url.Substring(url.LastIndexOf('/') + 1).Replace(".aspx", ".html")); Response.Write(outputhtml.ToString()); Response.End(); Response.Flush(); } }
HttpContext.Current.Server.Executeで実行結果を受け取れますので、不要なタグを端折った後、ダウンロードさせています。
ihisa 2016-09-14 10:10
ASP.NET Webフォームプロジェクトでオートコンプリートのドロップダウンを実現する
ASP.NET Webフォームのドロップダウンコントロールでオートコンプリートをやろうというお話です。 ちょっとと面倒だったのでまとめておきます。
.NET Framework 4.5.1
ASP.NET Web Form
jqueryui.com
オートコンプリート化したいDropDownListコントロールを含むページで、お手軽にオートコンプリート化させる
オートコンプリート関数の作成(jquery.autocomplete.js)
;(function ($) { $.fn.autocompleteExtend = function () { return this.each(function() { $.widget("custom.combobox", { _create: function () { this.wrapper = $(" ") .addClass("custom-combobox") .insertAfter(this.element); this.element.hide(); this._createAutocomplete(); this._createShowAllButton(); }, _createAutocomplete: function () { var selected = this.element.children(":selected"), value = selected.val() ? selected.text() : ""; this.input = $(" ") .appendTo(this.wrapper) .val(value) .attr("title", "") .attr("onKeydown", "return InvalidInput(event)") // enterキーの挙動を打ち消す .attr("style", "float:left;") // ブラウザ差異対応 .addClass("custom-combobox-input ui-widget ui-widget-content ui-state-default ui-corner-left autocomplete") .autocomplete({ delay: 0, minLength: 0, source: $.proxy(this, "_source") }) .tooltip({ tooltipClass: "ui-state-highlight" }); this._on(this.input, { autocompleteselect: function (event, ui) { ui.item.option.selected = true; this._trigger("select", event, { item: ui.item.option }); }, autocompletechange: "_removeIfInvalid" }); }, _createShowAllButton: function () { var input = this.input, wasOpen = false; $(" ") .attr("tabIndex", -1) .attr("title", "") .tooltip() .appendTo(this.wrapper) .button({ icons: { primary: "ui-icon-triangle-1-s" }, text: false }) .removeClass("ui-corner-all") .addClass("custom-combobox-toggle ui-corner-right autocomplete") .mousedown(function () { wasOpen = input.autocomplete("widget").is(":visible"); }) .click(function () { input.focus(); // Close if already visible if (wasOpen) { return; } // Pass empty string as value to search for, displaying all results input.autocomplete("search", ""); }); }, _source: function (request, response) { var matcher = new RegExp($.ui.autocomplete.escapeRegex(request.term), "i"); response(this.element.children("option").map(function () { var text = $(this).text(); if (this.value && (!request.term || matcher.test(text))) return { label: text, value: text, option: this }; })); }, _removeIfInvalid: function (event, ui) { // Selected an item, nothing to do if (ui.item) { // 選択値でポストバックさせる setTimeout('__doPostBack(\' + $(this).attr("name") + \' ,\'\')', 0); return; } // Search for a match (case-insensitive) var value = this.input.val(), valueLowerCase = value.toLowerCase(), valid = false; this.element.children("option").each(function () { if ($(this).text().toLowerCase() === valueLowerCase) { this.selected = valid = true; return false; } }); // Found a match, nothing to do if (valid) { return; } // Remove invalid value this.input .val("") .attr("title", "") .tooltip("open"); this.element.val(""); this._delay(function () { this.input.tooltip("close").attr("title", ""); }, 2500); this.input.autocomplete("instance").term = ""; }, _destroy: function () { this.wrapper.remove(); this.element.show(); } }); // 挙動設定 $('#' + $(this).attr("id") ).combobox(); // ドロップダウンのスクロール指定 $('.ui-autocomplete').css('overflow-y', 'auto'); $('.ui-autocomplete').css('overflow-x', 'hidden'); $('.ui-autocomplete').css('height', '200px'); }); } })(jQuery); $(function () { $(".autocomplete").autocompleteExtend(); });
参考先のコードを元に作成。
入力値/選択値がリスト内の要素と完全一致するときのみポストバックさせます。
また動的に生成されるドロップダウンのスクロール指定等を行っています。
最後の処理はautocompleteクラスを持つ要素に対してautocompleteExtendを呼び出すためのものです。
呼び出し元コード(sample.aspx)
.custom-combobox { position: relative; display: inline-block; } .custom-combobox-toggle { position: absolute; top: 0; bottom: 0; margin-left: -1px; padding: 0; } .custom-combobox-input { margin: 0; padding: 5px 10px; }
DropDownListコントロールにautocompleteクラスを指定しているのみです。
呼び出し元コード(sample.aspx.cs)
using System; using System.Collections.Generic; namespace WebApplication1 { public partial class sample : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { if (!IsPostBack) { var list = new Dictionary  { { "", "0" }, { "Aゴルフ場", "1" }, { "Bカントリー倶楽部", "2" }, { "Cゴルフコース", "3" }, { "Dカントリークラブ", "4" }, { "Eゴルフリゾート", "5" } }; this.HogeHogeList.DataSource = list; this.HogeHogeList.DataTextField = "Key"; this.HogeHogeList.DataValueField = "Value"; this.HogeHogeList.DataBind(); } } protected void HogeHogeList_SelectedIndexChanged(object sender, EventArgs e) { // 選択時の処理 } } }
入力すると一致する候補がきっちり表示されます。
リストから選択すればSelectedIndexChangedイベントが発生します。
ihisa 2016-07-25 13:32
grepコマンドとPowerShellのsls (Select-String)の比較
こんにちは、PowerShellが好きなwakです。以前書いたこちらの記事をリライトします。
tech.sanwasystem.com
1件のファイルから検索をする場合
-i : 大文字・小文字を区別したい/区別したくない
-e : 正規表現を使いたい/使いたくない
-v : パターンにマッチしない行を探したい
-A / -B : パターンにマッチした前後の行も出力したい
文字エンコーディングを指定したい
複数のファイルから検索する場合
ワイルドカードを使いたい
特定の複数のファイルを指定して検索したい
ディレクトリを再帰的にたどって検索したい
特定の拡張子のファイルは除外したい
検索対象のファイルを別のテキストファイルから与えたい
出力の書式や出力先を変える
結果だけを画面に出力したい
結果だけを画面に出力したい
マッチした部分だけを出力したい
ファイル名(フルパス)、行数、行の内容をタブ区切りで出力したい
結果をファイルに書き出す
PowerShellの基本
\のエスケープっていらないの?
Windowsにはgrepコマンドがないとはよく言われることですが、Windowsに標準で備わっているPowerShellにはgrepよりも高性能な検索コマンドレット、Select-Stringが実装されています。この記事はgrepとSelect-Stringとを比較し、慣れない人でもすぐに使えるようにすることを目的としています。また、「そもそもこのSelect-Stringコマンドをどうやって実行するの?」といったPowerShellの(超)基本的な使い方は本稿の末尾に記載してあります。
Select-String
-CaseSensitive *1
-SimpleMatch *2
-Context 《行数》
-Encoding 《文字エンコーディング名》
(dir -recurse 《ファイル名パターン》)
以下、順に解説していきます。
1件のファイルから検索をする場合
grepで1件のファイルから指定したフレーズを検索する場合はこのように実行しますが、
grep pattern filename.txt
PowerShellでこちらに相当する処理はこうなります。
Select-String "pattern" filename.txt
Select-Stringにはslsという省略形が用意されています。エイリアスですのでどちらを使ってもまったく同じ結果が得られます。
sls "pattern" filename.txt
本稿では以降もSelect-Stringと表記しますが、もちろん実際に使うときにはslsで構いません。
-i : 大文字・小文字を区別したい/区別したくない
デフォルトでは大文字・小文字は区別されません。区別したい場合は-CaseSensitiveオプションを追加します。
# "Neko"にはマッチするが"neko"にはマッチしない Select-String "Neko" filename.txt -CaseSensitive
-e : 正規表現を使いたい/使いたくない
パターンはデフォルトで正規表現とみなされます。正規表現として扱ってほしくない場合は-SimpleMatchオプションを追加します。
# "SELECT * FROM"にマッチ。-CaseSensitiveも指定しているので小文字ならマッチしない Select-String "SELECT * FROM" filename.txt -SimpleMatch -CaseSensitive
-v : パターンにマッチしない行を探したい
-NotMatchオプションを使います。
# 「A」「a」「B」「b」「C」「c」のいずれも含まない行にマッチ Select-String "[A-C]" filename.txt -NotMatch
-A / -B : パターンにマッチした前後の行も出力したい
-Contextオプションで行数を指定します。残念ながら前後別々に行数を指定することはできません。
# "function", "Function"などがある行と、その上下3行ずつ(計7行)を検索 Select-String "function" HogeClass.cs -Context 3
文字エンコーディングを指定したい
検索対象のファイルがShift-JISだと文字エンコーディングの自動判定ができないので、明示的に指定してあげなければいけません。これは-Encodingオプションを使います。
Select-String "猫" filename.txt -Encoding oem
-Encoding(gの後ろにスペース1個)と入力した後でTABキーを押せば指定可能な文字エンコーディングが順に表示されます。oemがShift-JISだと覚えておけば事足りるでしょう(UTF-8がデフォルトです)。
複数のファイルから検索する場合
ここまでは1件のファイルから検索を行うものでした。次は複数件のファイルを対象として検索を行う方法です。
ワイルドカードを使いたい
まず普通にワイルドカードが使えます。
Select-String "pattern" *.txt
特定の複数のファイルを指定して検索したい
ワイルドカードではなく、ファイル名を書き並べたいならこうなります。カッコとダブルクオートが少々見づらいのですが我慢してください。
Select-String "pattern" ("z:\log\filename1.txt", "c:\data\text\filename2.txt")
ディレクトリを再帰的にたどって検索したい
次のどちらでも好きなものを選んでください。c:\data以下にある*.cs全てを検索対象としています。
Select-String "pattern" (dir -recurse c:\data\*.cs) dir -recurse c:\data\*.cs | Select-String "pattern"
Dir -recurse c:\data\*.csは、c:\data以下から*.csを全部探すという意味になります(1回このコマンドだけを実行してみるといいです)。ディレクトリ名を省略したらカレントディレクトリになります。
特定の拡張子のファイルは除外したい
-Excludeで除外できます。
# カレントディレクトリ以下の全てのファイルから検索、ただし*.exeと*.binは除く Select-String "pattern" (dir -recurse *.* -Exclude *.exe, *.bin) dir -recurse *.* -Exclude *.exe, *.bin | Select-String "pattern"
検索対象のファイルを別のテキストファイルから与えたい
1行に1件ファイル名(フルパス)が書いてあるテキストファイルfilelist.txtがあったとして、そこから検索をする場合です。
Get-Content filelist.txt -Encoding UTF8 | % { Select-String "pattern" $_ }
なお、カレントディレクトリ以下の全てのファイル名(フルパス)をファイルに書き出すにはこのように実行します。ワイルドカードの部分は必要に応じて*.txtなどと書き換えてください。
dir -Recurse -File *.* | % { $_.FullName } | Out-File filelist.txt -Encoding UTF8
出力の書式や出力先を変える
標準の出力の書式では、ファイル名・行数・行の内容(行全体)が出力されます。なぜかというと、この結果1行1行はそれぞれMicrosoft.PowerShell.Commands.MatchInfoクラスのインスタンスであり、このToString()メソッドがそのような書式の文字列を返すようになっているからです。
https://msdn.microsoft.com/ja-jp/library/microsoft.powershell.commands.matchinfo(v=vs.85).aspx
このオブジェクトはFileName, Lineといったプロパティを持っていますから、欲しいものを好きなように並べて結果を出力することができます。
結果だけを画面に出力したい
ファイル名はいらない場合です。
# 末尾に「| Select Line」と書き加える Select-String "pattern" filename | Select Line Select-String "pattern" (dir -recurse *.* -Exclude *.exe, *.bin) | Select Line
以下では例は1行ずつしか示しませんが、どのような検索を行ったかにかかわらず、検索を行ったコマンドの末尾に「| ~」と書き加える形でOKです。
結果だけを画面に出力したい
ファイル名はいらない場合です。
Select-String "pattern" filename | Select Line
少し書式は面倒ですが、こちらの方を使うと色々と融通がききます。
Select-String "pattern" filename | % { $_.Line }
マッチした部分だけを出力したい
Select-String "pattern" filename | % { $_.Matches.Value }
このMatchesは.NETのSystem.Text.RegularExpressions.Matchクラスのインスタンスです。パターンに()を使っていればGroupsプロパティでさらに部分文字列を取り出せたりします。*4
Select-String "^(\d{4})-(\d{2})-(\d{2})" (dir -Recurse *.*) | % { $g = $_.Matches.Groups; $g.Groups[1].Value }
ファイル名(フルパス)、行数、行の内容をタブ区切りで出力したい
Select-String "pattern" filename | % { [string]::Format("{0}`t{1}`t{2}", $_.Path, $_.LineNumber, $_.Line) }
.NETを使っている人にはお馴染みのSystem.String.Format()を使ってみました。`tは他言語の\tと同じタブ文字を意味します。PowerShellらしく書くなら次のどちらかになりそうです。
# 末尾に「|」以降を書き加える Select-String "pattern" filename | % { "{0}`t{1}`t{2}" -f $_.Path, $_.LineNumber, $_.Line } Select-String "pattern" filename | % { ( $_.Path, $_.LineNumber, $_.Line) -join "`t" }
検索結果をまず先に変数に格納しておけば毎回検索する手間が省けるので話が早くなります。
$searchresult = Select-String "pattern" filename
これでまず検索結果が変数に入ります(画面には何も出ません)。
$searchresult
とだけ入力して実行すれば検索結果が出力されますし、
$searchresult | Select Line $searchresult | % { "{0}`t{1}`t{2}" -f $_.Path, $_.LineNumber, $_.Line }
のように色々と試すこともできます。
結果をファイルに書き出す
検索結果は画面に出力するだけではなくファイルに書き出すこともできます。これについてはgrepと同じようにリダイレクトで吐き出すのが一番簡単でしょう。
Select-String "pattern" filename | % { ... } > result.txt $searchresult | Select Line > result.txt
文字エンコーディングはUTF-16になります。これが気に食わない人は、末尾に> ファイル名と書き加えるのではなく、さらにパイプをつなげてOut-Fileコマンドレットに渡します。
Select-String "pattern" filename | % { ... } | Out-File result.txt -Encoding UTF8 $searchresult | Select Line | Out-File result.txt -Encoding UTF8
ここで出力されるファイルはBOMありUTF-8という微妙なフォーマットになるのですが、これは諦めてください(少しコードを書けば解決するのですが)。
PowerShellの基本
まず、何はなくともPowerShellを立ち上げましょう。
スタートメニューからPowerShellを探す
エクスプローラーのアドレスバーに「powershell」と入力してエンターキーを押す
エクスプローラーのメニューからPowerShellを起動する
dir, Select-Stringといったコマンドレットによって入力可能なオプションはあらかじめ決まっています。-まで入力してTABキーを連打すると、指定可能なオプションが列挙されます。たとえば-CaseSensitiveであれば、-cまで入力してTABキーを押せば一発で補完される、といった具合です。
また、ファイル名・ディレクトリ名も同じように補完が効きます。
コマンドレット・オプションの大文字・小文字は区別されません。Select-StringをSELECT-STRINGやselect-stringと書いても問題ありません。
\のエスケープっていらないの?
PowerShellでは\文字は特別な意味を持ちません。AやBや.と同じ普通の文字です。したがって\のエスケープは必要ありません。特に正規表現を書くときにはこれが楽です。
そのかわり、たとえば改行文字はn`、タブ文字は\t`と書くことになっています。バッククオートそのものを検索したいときはバッククオートを2つ連ねて書いてください。