typeOf 'aki_mana'

August 2014


Node.js 用に昔作ってたんだけど、ソースを読んだらスパゲッティソースで放置してた。
WebMessage によるクロスドメイン間のネタを投下したばかりで、これも公開すべきかなと思ったので。

WebMessage のネタ。気づいていたけど書かなかった文言が、「バックドア技術でもある」ということ。
WebMessage を「安全に」使うなら、CORS技術とCSP技術を修得して併用する「べき」なんですよねぇ。

WebMessage のコードを仕込まれたときに変な挙動を起こさないようにする防護策としても必須になってくると思う。CORS技術やCSP技術を一切使ってないサイトは攻撃対象として踏み台にされる可能性が高まる。
キーロガー型のユーザ関数を受理・実行されるようにもなったら、フォームに入力を促すアカウント情報が抜かれることも想定される。意図しないオリジンからの WebMessage を受け付けないようにする為に、CSP技術を使ってホワイトリストを明確化しておくのがベターです。

JavaScriptの有効化を前提としたサイトはブラウザ側の自己防衛機能を有効化させるために、両技術を使うべき時代に突入し、素人が趣味で…という10年前とは環境も大きく変わってますよね。

さて、モダンブラウザでは整備されてるけど、イマイチとっつきにくくて使ってるサイトも簡単には見つかない、CORS技術やCSP技術。このエントリは後者の CSP について。


で、Content Secure Policy (バージョン 1.0)についてのライブラリ。

(function(global, undefined){

	"use strict";

	var RE_SINGLEQUOTED, RE_CSP_KEYWORDS, RE_CSP_DIRECTIVE, RE_CSP_HOST, RE_CSP_HOST_EXPR;

	RE_SINGLEQUOTED = /^(\')([^\1]+)\1$/;
	RE_CSP_KEYWORDS = /^(none|self|(?:unsafe\-)?inline|(?:unsafe\-)?eval)$/
	RE_CSP_DIRECTIVE = /.*(\-(?:src|uri))?$/i;
	RE_CSP_HOST_EXPR = /((?:https?\:\/\/)?(?:(?:\*|[\d\w\-]+)(?:\.(?:\*|[\d\w\-]+))*)(?:\:(?:\*|\d+))?)/i;

	function hasAnyValue(v){return Boolean(v)};
	function trim(s){return s.trim()};
	function validHostExpr(expr){return RE_CSP_HOST_EXPR.test(expr)};



	function CSPSourceExpression(targetDirective){
		this.directive = targetDirective;
		this.schemeSources = [];
		this.hostSources = [];
		this.keywordSources = [];
	};

	CSPSourceExpression.prototype.setHost = function(hostExpr){
		if(!hostExpr) {
			this.hostSources.length = 0;
			return;
		}

		if(hostExpr === '*'){
			this.hostSources = ["*"];
			return;
		}
		if( !validHostExpr(hostExpr) ) return;

		if(this.hostSources[1] === '*') this.hostSources.length = 0;
		this.hostSources.push(hostExpr);
		return true;
	};
	CSPSourceExpression.prototype.getHostExpr = function(){
		if(this.hostSources.length) return this.hostSources.filter(hasAnyValue).join(' ');
	};


	CSPSourceExpression.prototype.setScheme = function(scheme){
		if(!scheme) {
			this.schemeSources.length=0;
			return;
		}
		// Version "1.0" では data スキームのみ。
		if(!/data\:?$/.test(scheme)) return;

		if(scheme.substr(-1) !== ':') scheme+=':';
		this.schemeSources.push(scheme);
		return true;
	};
	CSPSourceExpression.prototype.getScheme = function(){
		if(this.schemeSources.length) return this.schemeSources.filter(hasAnyValue).join(' ');
	};


	CSPSourceExpression.prototype.setKeyword = function(key){
		var m, k, v, u;

		if(!(k=key)) {
			this.keywordSources.length = 0;
			return;
		}

		if(m = RE_SINGLEQUOTED.exec(k)){ v = m[0]; k = m[2]; }
		if(!RE_CSP_KEYWORDS.test(k)) return;
		
		if(/^(inline|eval)$/.test(k)) k = 'unsafe-' + k;
		v = "'" + k + "'";
		// unsafe-keyword is default-src, script-src, style-src
		if(!/^(default|script|style)/.test(this.directive) && /^unsafe/.test(k) ){
			return;
		}
		u = "'none'";
		if(k==='none') {
			this.keywordSources = [u];
		}
		else {
			if(this.keywordSources[0] === u) this.keywordSources.length=0;
			this.keywordSources.push(v);
		}
		return true;
	};
	CSPSourceExpression.prototype.getKeywords = function(){
		if(this.keywordSources.length) return this.keywordSources.filter(hasAnyValue).join(' ');
	};

	
	CSPSourceExpression.prototype.setExpression = function( value ){
		return this.setScheme( value ) || this.setKeyword( value ) || this.setHost(value);
	};
	CSPSourceExpression.prototype.getExpression = function(){
		// check set policy
		return (this.schemeSources.length || this.hostSources.length || this.keywordSources.length ) && 
			([
				this.directive,
				this.getKeywords(),
				this.getHostExpr(),
				this.getScheme()
			].filter(hasAnyValue).join(' '));
	};




	function CSP(ver){
		this.version = ver || CSP.defaultVersion;
		this.exprs = {};
	};
	CSP.defaultVersion = "1.0";
	CSP.headers = {
		"1.0": 'Content-Security-Policy'
	};
	CSP.directives = {
		"1.0": {
			default: 'default-src',
			script: 'script-src',
			object: 'object-src',
			img: 'img-src',
			media: 'media-src',
			frame: 'frame-src',
			font: 'font-src',
			connect: 'connect-src',
			style: 'style-src',
			report: 'report-uri'
		}
	};

	CSP.prototype.setPolicy = function(directive, value){
		var directives;

		if(!RE_CSP_DIRECTIVE.test(directive+'')) return;

		directives = CSP.directives[this.version];
		directive = directive.replace(RE_CSP_DIRECTIVE, RegExp.leftContext).toLowerCase();

		if(directive === 'report'){
//			isURL() 関数は未実装。
//			return isURL(value) ? (this.exprs[directive]=value): void 0;
			this.exprs[directive] || (this.exprs[directive]=value);
			return;
		}

		this.exprs[directive] || (this.exprs[directive]=new CSPSourceExpression(directives[directive]));
		if(!this.exprs[directive].setExpression(value)) {
			console.log(' cant set csp-policy ');
		}
	};
	CSP.prototype.getHeaderField = function(){
		return CSP.headers[this.version] || CSP.headers[CSP.defaultVersion];
	};
	CSP.prototype.getPolicyToken = 
	CSP.prototype.getHeaderValue = function(){
		var directives;

		directives = CSP.directives[this.version];
		return Object.keys(directives).map(function(key){
			if(!this.exprs[key]) return;
			return key === 'report'
				? directives[key] + ':' +this.exprs[key]
				: this.exprs[key].getExpression();

		}, this).filter(hasAnyValue).join('; ')
	}

	// factory & utilities
	global['CSP'] = function(ver){
		return new CSP(ver);
	};

	// サーバー側でJSONから一括設定するための実装
	global['CSP'].fromJSON = function(obj){
		var csp;
		obj || (obj={})

		csp = new CSP(obj.version);
		
		Object.keys(obj).forEach(function( directive ){
			if(!RE_CSP_DIRECTIVE.test(directive+'')) return;

			var setValues = obj[directive];
			if(!Array.isArray(setValues)) setValues = [setValues];

			setValues.forEach(function(val){
				csp.setPolicy(directive, val);
			});
		});
		return csp.getHeaderValue();
	};

	// クライアント側で XMLHttpRequest() のヘッダ取得時に解析するための実装
	global['CSP'].toJSON = function( headerValue ){
		var rslt;

		(headerValue.split(/\s*\;\s*/)).forEach(function(source){
			var vals, directive;

			vals = source.split(/\s+/);
			directive = trim(vals.shift()).replace(RE_CSP_DIRECTIVE, RegExp.leftContext).toLowerCase();
			this[directive] = vals.map(trim);
		}, rslt = {});

		return rslt;
	};

}(this, void 0));


CSP とは、サーバー応答時にヘッダフィールド Content-Security-Policy を含めることで、ブラウザの挙動を制限できる仕様。

バージョン 1.1 が検討される現在、Firefox なんかではバージョン 1.0 が普通に機能します。

で、ヘッダ「Content-Security-Policy」に与える値(ポリシー・トークン)は複雑で、コードに埋め込もうとするとTYPO も多いので、ライブラリで適切な ポリシートークンを自動生成しちゃうのが開発の目的でした。
ここで公開してるのは、AJAXでヘッダを受け取った時に解析しやすいよう、ポリシー・トークンをJSON化するユーティリティ関数も追加。

*-src とか *-uri とか、ディレクティブの正式名称がチトうざい。
キーワードはシングルクォートで囲う必要があるとか、スキームは最後に コロン記号(:)を必要とするとかもチトうざい。

なので、そういう面倒な部位文を端折って設定できる実装にしてます。
(1.1の仕様では端折りづらいディレクティブとして、frame-ancestor, frame-src があるので、拡張しづらいけど)

ちなみに、exports に代入していないのは、ブラウザの開発者コンソールで確認した段階のソース。もし使うという方は、適宜、サーバーに対応させてください。

あ、ライブリ化した理由は、「応答するリソースによって内容を変える」のが目的です。

取り敢えず、自分のドメイン以外のアクセス禁止したいだけなら、
apache なら .htaccess に書いておくとか スタティックに設定しちゃえばいい。

Header set X-Content-Security-Policy "default-src 'self'"
Header set X-WebKit-CSP "default-src 'self'"
Header set Content-Security-Policy "default-src 'self'"


サーバーアプリらしく動作させたいなら、リソース毎にインテリジェンスなCSP応答したい。



JavaScript も OOPできるので演算用のクラスを作っちゃえばいいんじゃないか?

LAN環境内のオリジンを評価するのが目的。



(function(global, undefined){
	
	// IPv4 Calculate
	var RE_IPv4ADDRESS, xFF, x00;

	RE_IPv4ADDRESS = /^(\d+).(\d+).(\d+).(\d+)(?:\/(?:(\d+)|(\d+).(\d+).(\d+).(\d+)))?$/;
	xFF = 0xff;
	x00 = 0x0;

	function isByteNumber(n){
		return x00<=n&&n<=xFF
	}

	function parseIPv4(str){
		var rslt, m, ip, cidr, mask;
		if(m = str.match(RE_IPv4ADDRESS)){
			ip = [
				parseInt(m[1]),
				parseInt(m[2]),
				parseInt(m[3]),
				parseInt(m[4])
			];
			if(m[5]) cidr = parseInt(m[5])
			if(m[6]) mask = [
				parseInt(m[6]),
				parseInt(m[7]),
				parseInt(m[8]),
				parseInt(m[9]),
			]
		}

		if((ip = ip.filter(isByteNumber)).length===4) {
			rslt = { IP: ip };
			if(isFinite(cidr)&&0<=cidr&&cidr<=32) {
				rslt.CIDR = cidr;
				rslt.MASK = calcCIDR2MASK(cidr);
			}
			if(mask&&validMask(mask)) {
				rslt.CIDR = calcMASK2CIDR(mask);
				rslt.MASK = mask;
			}
		}
		return rslt
	}

	function validMask(mask){
		var cidr, isFF, is00, state;

		cidr = [128, 192, 224, 240, 248, 252, 254];
		isFF = is00 = state = 0;

		return mask.filter(function(n){
			switch(state){
			case 0: if(n===xFF) return true; else state++;
			case 1: state++; if(isByteNumber(n)) return ~(cidr.indexOf(n));
			case 2: if(n===x00) return true; else state++;
			}
		}).length===4;
	}
	function calcCIDR2MASK (cidr){
		var n1, n2, rslt, i;

		n1 = cidr>>>3;
		n2 = cidr & 7;
		i=0;
		rslt = [];

		for(; i++<n1; rslt.push(xFF)) ;
		if(n2) rslt.push( (xFF<<(8-n2))&xFF );
		for( ;rslt.length<4; ) rslt.push(0);
		return rslt;
	};

	function cidrBits(n){
		var cidr=0, i, tmp=n;
		for( i=0; i<8; i++ ){
			if(tmp&1)cidr++;
			tmp=tmp>>>1;
		}
		return cidr;
	};
	function calcMASK2CIDR (mask){
		var cidr;

		cidr = 0;
		mask.forEach(function(n){
			return cidr += cidrBits(n);
		})
		return cidr;
	}





	function CalcIPv4( ip ){
		this.IP = [127, 0, 0, 1];
		this.CIDR = 24;
		this.MASK = [255, 255, 255, 0];
		if( ip ) this.setIP( ip );
	}
	CalcIPv4.prototype.setIP = function(str){
		var info;
		if( info = parseIPv4(str) ){
			if(info.IP) this.IP = info.IP;
			if(info.CIDR) this.CIDR = info.CIDR;
			if(info.MASK) this.MASK = info.MASK;
		}
		else throw new Error('Invalid IP ADDRESS')
	}
	CalcIPv4.prototype.getNetworkAddress = function(isBit){
		var rslt;
		rslt = this.MASK ?
			this.MASK.map(function(byte, n){
				return byte & this.IP[n];
			}, this)
			: void 0;
		return isBit
			? rslt.map(function(n){return ('0000000'+ n.toString(2)).substr(-8)}).join('')
			: rslt.join('.');
	};
	CalcIPv4.prototype.getBroadcastAddress = function(isBit){
		var rslt;
		rslt = this.MASK ?
			this.MASK.map(function(byte, n){
				if(byte===xFF) return this.IP[n];
				if(byte===x00) return xFF;
				
				return (this.IP[n] & this.MASK[n]) | (this.IP[n] & (~this.MASK[n]));
			}, this)
			: void 0;
		return isBit
			? rslt.map(function(n){return ('0000000'+ n.toString(2)).substr(-8)}).join('')
			: rslt.join('.');
	};
	CalcIPv4.prototype.isNetworkNode = function(ip){
		var addr;
		if((addr=parseIPv4(ip))&&addr.IP&&(!addr.MASK)){
			return this.MASK.map(function(n, i){
				return (n & addr.IP[i])===this.MASK[i];
			}, this).length === 4;
		}
	};
	CalcIPv4.prototype.getNetworkRange = function (){
		if( this.CIDR ) return Math.pow(2, (32-this.CIDR));
	};
	CalcIPv4.prototype.getHosts = function(){
		var re, nw, bc, min, max;

		re = RE_IPv4ADDRESS;
		nw = this.getNetworkAddress();
		bc = this.getBroadcastAddress();

		min = nw.replace(re, function($0, $1, $2, $3, $4){return [$1, $2, $3, parseInt($4)+1]});
		max = bc.replace(re, function($0, $1, $2, $3, $4){return [$1, $2, $3, parseInt($4)-1]});
		return {
			min: min,
			max: max
		}
	};

	CalcIPv4.prototype.toString = function (){
		var rslt;

		rslt = this.IP.join('.');
		if(this.CIDR) rslt += '/' + this.CIDR;
		return rslt;
	}

	global['CalcIPv4'] = function(ip){
		return new CalcIPv4(ip);
	}

}(this, void 0));


参考)IPアドレス計算・サブネットマスク計算ツール








postMessage で、HTML-IFRAME 間のメッセージ通信するネタは、ググればたくさん出てくる状況なんだけど、
1つのJavaScriptライブラリを読ませるだけで通信できる方法を考えた。

結論を先に書くと、postMessage のアスタリスク・メッセージングは、仕様上、非推奨ということだけど、たぶんこれは WebWorker 環境も考慮しての表現。popupやiframeでのアスタリスク・メッセージングはダメ。絶対にダメ。



まず、HTML(test_PostMessage.html)。
先に特徴;
ブラウザの window のフレームで top に表示されることが条件。
IFRAMEは動的生成するので、IFRAMEに表示させたいHTMLとそのURLだけ、きっちり押さえます。
ちなみに、CSPで禁止できますので、確実に動作するわけではないこともポイント。


<!doctype html>
<html>
<head>
	<meta charset="UTF-8" />
	<title>Web Messaging HOST</title>
	<script src="http://localhost/test/web-messaging.js"></script>
	<script>
	var m = Messaging(function(messaged){
		console.log('MESSAGED by=%s data=%s\n %o', messaged.by, messaged.data, messaged);
	});
	m.connectToGuest('http://192.168.XXX.XXX/test/test_PostMessage_iframe.html');
	m.send('あ、オレオレ。', 'http://192.168.XXX.XXX');
	</script>
</head> 
<body>
	<h1>Web-Messaging (MESSAGING HOST)</h1>
</body>
</html>


次に、IFRAME用HTML(test_PostMessage_iframe.html
クロスドメインにあるものとします。


<!doctype html>
<html>
<head>
	<meta charset="UTF-8" />
	<title>Web Messaging GUEST</title>
	<script src="http://localhost/test/web-messaging.js"></script>
	<script>
		MessagingClient.send('もしもし~')
	</script>
</head> 
<body>
	<h1>Web-Messaging (MESSAGING GUEST)</h1>
</body>
</html>


共通のJavaScriptコード(web-messaging.js)
200行程度。
セキュリティ上の理由で、postMessage() 実行にアスタリスク・メッセージング(第二引数に '*')をしない実装(ガンバッタ)。

encodeMessage(), decodeMessage() を用意していますが、コード内では利用していません。
なので、メッセージには文字列化したユーザ関数をもやり取りできます。
メッセージング・ホストで設定したコールバックをメッセージング・ゲストでも使う実装です。

こういう特殊な使い方をしないのであれば、JSON.parse() をラップしたこれらの関数を用いて
ユーザ関数のやり取りを禁止したほうが安全ですね。



// 2014-08-28 : Referrer を送信しない事例( HTTPS で接続中はヘッダに含めないブラウザが多い )に対応するパッチ
(function(global, undefined){

	"use strict";

	var DEBUG;
	var noop, RE_PARSE_URL, propsParseURL, isHost;

	noop = function(){};
	RE_PARSE_URL = /^(?:(https?:)?(?:\/\/(([^\/:]+)(?::([0-9]+))?)))?(\/?[^?#]*)(\??[^?#]*)(#?.*)/;
	propsParseURL = 'protocol host hostname port pathname search hash'.split(' ');
	isHost = self === top;

	function parseURL( url ){
		var rslt, m, isRelative, pathInfo;
		
		rslt = {};
		if( m = (''+url).match( RE_PARSE_URL ) ) {
			propsParseURL.forEach(function( prop, idx ){
				this[prop] = m[(idx+1)]? m[(idx+1)]: null;
			}, rslt);
		};
		if( isRelative = !(/^\./.test(rslt.pathname)) ){
			propsParseURL.forEach(function( prop ){
				if( this[prop]==='' ) this[prop] = location[prop];
			}, rslt);
		}
		return rslt;
	};
	function getOrigin(url){
		var uri =parseURL(url);
		if( uri.host ) return uri.protocol + '//' + uri.host;
	};
	function createTag(tagName, attrs){
		var elm, k;
		elm = document.createElement(tagName);
		if(attrs) for(k in attrs) elm.setAttribute( k, attrs[k] );
		return elm;
	};
	function ready( fn ){
		if(document.readyState === 'complete') fn;
		else addEventListener('load', fn );
	};
	function handle(context, events, listener){
		var i,l, t, addType;
		for(addType=!!context.addEventListener, i=0,l=events.length; i<l; i++) {
			t = events[i];
			addType
				? context.addEventListener(t, listener)
				: context['on'+t] = listener;
		}
		return context;
	};
	function encodeMessage( oj ){
		return this.msgPrefix + (typeof oj==='string'? oj : JSON.stringify(oj));
	};
	function decodeMessage( msg ){
		return JSON.parse(msg.substr(this.msgPrefix.length));
	};



	function Messaging(callback){
		this.msgPrefix = 'MESSAGE:';
		this.target = !isHost ? null: {};
		this._messaged = callback || noop;
		this.queue = []; // connection が確立するまでに send() されたメッセージはキューに保持される。
		this.startMessageRecieve()
	}
	Messaging.origin = getOrigin(self.location.href);
	Messaging.prototype = {

		appendQueue: function(msg, origin){
			if(!(isHost ?  this.target[origin]: this.target)) {
				this.queue.push({origin: origin, msg: msg});
				return true;
			}
		},
		dequeue: function(){
			var me, q, info;
			me = this;

			if(!(q = me.queue.length)) return;

			if(isHost) {
				if(me.queue.filter(function(i){return !!me.target[i.origin]}).length!==q) return;
			}
			else {if(!me.target) return;}

			for(; me.queue.length;){
				info = me.queue.shift();
				me.send(info.msg, info.origin);
			}
		},

		startMessageRecieve: function(){
			var me, re, recieveMessageHandler, guestOpenHandler;

			me = this;
			re = new RegExp('^'+this.msgPrefix+'(.*)');

			function messagingFromGuest(event){
				var m, d, q, o;

				if( (m = /^LOADED:(.*)/.exec(d = event.data)) && (o = m[1]) ){
					me.target[o] || (me.target[o]=event.source);
					me.send(Messaging.origin, o, 'CONNECTED:');
					me.send(me._messaged.toString(), o, 'CALLBACK:');
					me.dequeue();
					return;
				}
				if(!(m = re.exec(d))) return;

				me._messaged({ by:'GUEST', data:m[1] });
			};
			function messagingFromHost(event){
				var m, d;

				if( m = /^CONNECTED:(.*)/.exec(d = event.data) ){
					me.hostOrigin = m[1];
					me.target || (me.target=event.source);
					me.dequeue();
					return;
				}
				if( m=/^CALLBACK:(.*)/m.exec(d) ){
					eval('me._messaged='+d.substr(9));
					return;
				}
				if(!(m = re.exec(d))) return;

				me._messaged({ by:'HOST', data:m[1] });
			};

			recieveMessageHandler = (isHost ? messagingFromGuest: messagingFromHost)
			handle(self, ['message'], recieveMessageHandler);

			if(isHost) return;

			guestOpenHandler = function(event){
-				top.postMessage('LOADED:'+Messaging.origin, getOrigin(event.target.referrer));
+				top.postMessage('LOADED:'+Messaging.origin, event.target.location.hash.substr(1));
				self.removeEventListener('load', guestOpenHandler);
			}
			handle(self, ['load'], guestOpenHandler);
		},


		connectToGuest: function( url ){
			var msg, origin;

			if(!isHost) return;
			if(!url) return;

			msg = this;
			origin = getOrigin(url);

			if(Messaging.origin===origin) return;
			if(msg.target&&msg.target[origin]) return;

			function addGuestFrame() {
				document.querySelector('body').appendChild(createTag('iframe', {
					width:0, height:0, style:'margin:0;padding:0;border:0',
-					src: url
+					src: url + '#' + Messaging.origin
				}));
			}

			// create IFrame
			ready( addGuestFrame );
		},

		// order
		send: function(msg, clientURL, prefix){
			var target, origin;

			if(!msg) throw new Error('required any message in first argument.');
			if(isHost && !clientURL){
				throw new Error('Messaging#send() : required client-url in Host mode')
			}

			if(isHost){
				origin = getOrigin(clientURL);
				target = this.target[origin];
			}
			else {
				target = this.target;
				origin = this.hostOrigin;
			}
			if(this.appendQueue(msg, origin)) return;

			target.postMessage( (prefix||this.msgPrefix) + msg, origin );
		}

	};

	if(isHost) global['Messaging'] = function(cb){ return new Messaging(cb); }
	if(!isHost) global['MessagingClient'] = new Messaging();

}(this, void 0))


表題で「ヤバイね。」と書いたのは、ユーザ関数(任意のスクリプトコード)をやりとりできる部分。
「諸刃の剣」って感じ。



コードの解説:

電話でいう、掛ける側、掛けられる側の関係性を HTML, IFRAME内HTMLとする。
便宜上、メッセージング・ホスト、メッセージング・ゲストという。

メッセージング・ホストのconnectToGuest() でメッセージング・ゲストとの接続を行う。
引数には IFRAME用HTMLのURLを記述(クロスドメイン前提)

双方がメッセージ交換できる状態になるのを待つ必要があり、「もしもし~」の機能を埋め込んだ。

未接続の場合は、キューに送信したい内容を保持。接続した瞬間にマシンガントークを展開する。
あ、トークの基本はsend()ね。この関数で任意のメッセージを送る。

メッセージング・ゲストは MessageClient というオブジェクトが生成されてるので、それを使う。
詳細はコードを読んでw

開発者コンソールで確認すると

MESSAGED by=HOST data=あ、オレオレ。
  {
    by: 'HOST',
    data: 'あ、オレオレ。'
  }
MESSAGED by=GUEST data=もしもし~
  {
    by: 'GUEST',
    data: 'もしもし~'
  }


が、確認できるはず。


HOSTPAGEのSCRIPTをこんな風に書き換えると、面白いことに。


var m = Messaging(function(messaged){
	console.log('MESSAGED by=%s data=%s\n %o', messaged.by, messaged.data, messaged);
	switch(messaged.by){
	case 'HOST':// HOSTからのメッセージ == GUEST モード
		var siteData;
		// サイトデータにアクセスしてデータ取得

		MessagingClient.send('RE:'+ messaged.data+';SITEDATA='+siteData ); 
		break;
	case 'GUEST':// HOSTモード
		if(/RE:(.*)/.test(messaged.data)){
			// レスポンスされたデータを処理!
		}
		break;
	}
	
});
m.connectToGuest('http://192.168.XXX.XXX/test/test_PostMessage_iframe.html');
m.send('anyOrder', 'http://192.168.XXX.XXX');



これでクロスドメインのサイトデータにアクセスできる

インストーラのネタは何度も書いたので、クロスドメインのサイトデータに対するアクセスは必須条件です。
複数のドメインから リソースを取得する場合、各ドメインに紐づけられたサイトデータに分散して格納したいですからねぇ。こうした有益な使い方がある反面、クロスドメインのメッセージ交換が簡単だからと、むやみにアスタリスク・メッセージングを使うのはやめたほうがいい。

とにかく、WebMessage におけるアスタリスク・メッセージングがいかに危険かを考えさせられます。






今更なんだけど、UDP を使って syslog に対する ロギングを試みた。

NodeSyslogUDP なるパッケージが npmjs の中で紹介されてて、


npm install syslogudp
使ってみると、動かない。
仕方なく、node_modules/syslogudp/lib/NodeSyslogUDP.js を開くと、
ガッカリした。

synopsis に書かれてるコードを実行すると、
syslog サーバーに メッセージをsendする前に、せっかく開いたソケットをクローズするので、ログは取れない。

理由は
logger#close() メソッドが強制的にソケットを閉じる実装で、
logger#log() メソッド内で createSocket().send() のコールバックが発生する前にソケットをクローズしてる。



結局書き直す羽目に。

module.exports = SyslogClient;

var dgram, os,
	hostname,
	noop,
	k, m,
	
	facilityNames,
	severityLevelNames
;

dgram=require('dgram');
os = require('os');

hostname = os.hostname();
noop = function(){};

function defaultNoLogFilter(severity, options){
	switch(options.facility){
	case SyslogClient.FACILITY_USER:
	case SyslogClient.FACILITY_LOCAL0:
	case SyslogClient.FACILITY_LOCAL1:
	case SyslogClient.FACILITY_LOCAL2:
	case SyslogClient.FACILITY_LOCAL3:
	case SyslogClient.FACILITY_LOCAL4:
	case SyslogClient.FACILITY_LOCAL5:
	case SyslogClient.FACILITY_LOCAL6:
	case SyslogClient.FACILITY_LOCAL7:
		switch(severity){
		case SyslogClient.LOG_DEBUG:
			return !Boolean(options.filter); // options.filter が true の時にロギングする。
		default:
			return false; // オプションに関わらず ロギングする。
		}
	default:// 他のファシリティはログを取らない。
		return true;
	}
}

function SyslogClient(port, host, options) {
	this.port = port || 514;
	this.host = host || 'localhost';
	this.options = options || {};

	// 実行状態を維持するためのフラグ
	this.loggingOrders = 0;
	this.isWorking = false;
	this.callbackOnclose = noop;

	for (var k in SyslogClient.DEFAULT_OPTIONS) {
	    if (this.options[k] === undefined) { this.options[k] = SyslogClient.DEFAULT_OPTIONS[k] }
	}

	this.noLogging = (!!this.options.nolog && typeof this.options.nolog === 'function')
		? this.options.nolog
		: defaultNoLogFilter;

	// We need to set this option here, incase the module is loaded before `process.title` is set.
	if (!this.options.name) { this.options.name = process.title || process.argv.join(' ') }

	this.socket = null;
};

facilityNames = [
		'KERN','USER','MAIL','DAEMON','AUTH','SYSLOG','LPR','NEWS',
		'UUCP','CRON','AUTHPR','FTP','NTP','AUDIT','ALEART','CRON',
		'LOCAL0','LOCAL1','LOCAL2','LOCAL3','LOCAL4','LOCAL5','LOCAL6','LOCAL7'
	]
	.map(function(name, idx){
		SyslogClient['FACILITY_'+name] = idx;
		return name.toLowerCase()
	});

// Message severity levels
severityLevelNames = [
		'EMERG','ALERT','CRIT','ERROR','WARNING','NOTICE','INFO','DEBUG'
	]
	.map(function(name, idx){
		var n, l;
		n = name.toLowerCase();
		l = 'LOG_'+name;
		SyslogClient[l] = idx;
		SyslogClient.prototype[n] = function (msg, callback) {
		    this.log(name + ' ' + msg, SyslogClient[l], callback);
		};
		return n;
	});


// TIMESTAMP of RFC3164 - 4.1.2
function getTimeStamp(){
	var t, d, h, m, s;
	return [
		['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'][(t=new Date()).getMonth()],
		(((d=t.getDate())<10) ? ' ': '') + d,
		[
			(((h=t.getHours())<10) ? '0': '') + h,
			(((m=t.getMinutes())<10) ? '0': '') + m,
			(((s=t.getSeconds())<10) ? '0': '') + s
		].join(':')
	].join(' ');
}

SyslogClient.DEFAULT_OPTIONS = {
	facility: SyslogClient.FACILITY_USER,
	name: null,
	debug: false
};

SyslogClient.createClient = function (port, host, options) {
	return new SyslogClient(port, host, options);
};

SyslogClient.prototype._openUDPSocket = function(){
	var logger;

	this.loggingOrders++;
	if(this.socket) return;

	logger = this;
	function onErrorCreatingSocket(err) {
		console.log('Error creating UDP socket.', err);
	};

	(this.socket = dgram.createSocket("udp4", onErrorCreatingSocket))
		.on('close',function onCloseSocket(){
			console.log('Socket closing');
			logger.callbackOnclose();
		})
		.on('error',function onErrorSendMessage(err){
			console.log('Socket error');
			console.log(err);
			throw err;
		});
	this.isWorking = true;
};

SyslogClient.prototype._closeUDPSocket = function(noDeclement){
	if(!noDeclement) this.loggingOrders--;
	if(!this.isWorking && this.loggingOrders===0) {
		this.socket.close();
	}
};

SyslogClient.prototype.log = function (msg, severity, callback) {
	var logger, entry, buf;

	callback || (callback = noop);
	severity = isFinite(severity) ? severity : SyslogClient.LOG_INFO;

	this._openUDPSocket();

	severity = severity !== undefined ? severity : SyslogClient.LOG_INFO;

	if(this.noLogging(severity, this.options)){
		//console.log(' not logging by filter : ', severity, this.options.debug );
		this._closeUDPSocket();
		return;
	}

	// Message 生成は RFC 以外のフォーマットにも対応したい

	// SYSLOG PACKET: PRI + HEADER + MSG
	//   PRI : '<' + ((facility * 8) + severity) + '>'
	//   HEADER : TIMESTAMP + HOSTNAME
	//   MSG : ASCII ONLY
	//
	entry = [
		'<' + ((this.options.facility * 8) + severity) + '>' + 
		getTimeStamp(),
		hostname,
		// MSG part
		this.options.name + '[' + process.pid + ']:',
		msg.trim()
	].join(' ') + '\n';

	buf = new Buffer(entry,"utf-8");
	logger = this;
	this.socket.send(buf, 0, buf.length, this.port, this.host, function(err, bytes){
		logger._closeUDPSocket();
	});
}

SyslogClient.prototype.close = function(callback) {
	this.isWorking = false;
	if(typeof callback === 'function') this.callbackOnclose = callback;
	this._closeUDPSocket(1);
}



Windows 8.1 上で、syslog サーバーを実行したかったので、 Vector から pSyslog をダウンロードして利用することに。
うまくログが取れるようになった。

他の方が UDP を用いた syslog 系のソースを git で公開してる様子なので、そちらを使ってもいいかも。


追記)2014-08-16 18:45
pSyslog は 作者が IPV6 でもログ集積できる手軽なコレクタとして開発したもの。
また、メールで通知できるといいね。と、PMailServer (同作者)に対するリレー機能を有する。
syslog サーバーとはいえ、開発者自身もRFCの仕様に即していないことを表明してるので、
収集されたログが syslog と若干異なっても気にしてはいけない。

一応、RFC3164 を読んでログ生成する方向に直した。


var logger = require('path/to/syslogudp').createClient(514, hostname, options);

logger.log('message', severity, callback);
// severity には logger.LOG_* を与える。
// ( * 部分 は'EMERG','ALERT','CRIT','ERROR','WARNING','NOTICE','INFO','DEBUG')
// オプショナルの callback は function(err, msg){} で、UDP送信したら呼ばれる。

// severityName('message', callback) の関数は log() を呼ぶ。第二引数は関数名に合わせて自動指定
logger.emerg('emergency message')
logger.alert('alert message')
logger.crit('critical message')
logger.error('error message')
logger.warning('warning message')
logger.notice('notice message')
logger.info('information message')
logger.debug('debug message')

// ロギング関数をすべて終えたときにソケットを閉じる実装だが、全終了時に実行する関数を指定できる。
logger.close(callback)

facilityNames とか、severityLevelNames は、ログのフォーマットを Apache のログのようにカスタマイズできるといいな。ってことで メッセージ生成関数もオプション指定できるように検討してたので作った配列変数。この機能は未実装。




NodeJS + OpenSSL で 「自己証明(オレオレ証明)」し、サイトデータ(IndexedDB)を使ってみた。

格納するデータはテキストメディア(画像をDataURL形式に変換)。

ホスト名が同一でもプロトコルが違うと二重に保存するだろうなぁ。と思ってたら、やっぱりそうだった。

devTestSSLandSiteData




// NodeJS による WebServer はこんな感じ。

var sslOpts = null;
(function(){
  var fileKey, fileCrt;
  try {
    // DEPLOY_DIR + '/public' だけ公開してる前提。
    fileKey = fs.readFileSync(DEPLOY_DIR + '/csr/server.key');
    fileCrt = fs.readFileSync(DEPLOY_DIR + '/csr/server.crt'); // 自己証明:モダンブラウザは信頼してくれない
    sslOpts = { key  : fileKey, cert : fileCrt };
  } catch(e) {};
}());
var app = connect(/* omitted */);

http.createServer(app).listen(80);
if( sslOpts ) https.createServer(sslOpts, app).listen(443);


OpenSSLについては、脆弱性が報告されて改善されたバージョンを入れなおします。
公式サイトでも、脆弱性の認められたファイルはDLできなくなってる様子。






WebSQLDatabase を検討する必要性は Safari に対応するため、また、IndexedDBはサイトデータを扱う場合に最も高速処理されるため。

で、表題の通り。
Blobの格納については見合わせることにした。

まず、WebSQLDatabase はBlobを格納すると…
Chrome は String で "[object Blob]" なデータが格納される。Chromeの場合、IndexedDBを使う方向で処理しちゃえばいいので、まぁ、無問題。

けど、iOS Safari を考えると、開発が停止されていると評判の Windows用Safari 5.1.7 に合わせておくのが、古いiPhone にも対応できると思い、確認。開発者コンソールのリソースを表示すると、データ格納に失敗している。

次に、IndexedDB について。 put() メソッドが Blob, 格納したいレコードのKeyの順に指定する必要があるっぽいのだけど、ObjectStore に格納しようとすると、一筋縄ではいかなかった。

詳細は未確認だけど、以下推測(要確認事項)。
1) Arrayなプロパティの配列要素として格納する
2) レコードの構造を { primaryKey: Blob } とする必要がある。(key以外のプロパティを持たせる場合は1の方法)

推測にとどまっているのは、Chrome が objectStore.put( blob, key ) を受け付けず、調査を中断したから。
throw されたエラーを読む限り、「ChromeのIndexedDBはBlobに未対応」らしい。
てことで、IEとOperaも調査は中断…。

Chromeにだけ実装されてる、サンドボックスな FileSystem では試してみたんだけど、こっちは、他のブラウザが追随するまでは放置かなぁ。


Blobを格納したい場合、素直に DataURL 形式の文字列で処理したほうが無難


このページのトップヘ