ReverseHTTPを使ってみる

2ヶ月ぶりだろうか、エントリーを追加するのは。
その間、本業の方が洒落にならないくらい忙しく、趣味プログラミングは殆どできなかった。

(サンプルコードを見れば想像できると思うけど)本業はプログラマー/SEではないので、
開発環境には殆ど触れない生活ばかり。

そんな中お盆休みに入り、多少時間にゆとりができたので、
ドラクエ9に没頭する相方を尻目に、なにやらちょっとした騒ぎがあるReverseHTTPをテーマに自宅で少しプログラムをしてみた。

以前のエントリーでぶち上げたウィジェットマネージャは、αレベルくらいなものは出来上がってるんだけど、
思いついたら機能を追加、という形だったので、設計がひど過ぎて、脳内再設計に入りかけたところで、
本業多忙になってしまい、殆ど手付かずなんだけどねw

で、ReverseHTTP

気分転換にイロイロWebを見てると、こういうところで知ったのがきっかけ。


Javascriptでできるんだから、AIRでもできるべ』と安直に考え、ザックリ実装したのがこちら。
バグも少々<(_ _)>


HTTPDeamon.as

package net
{
    import flash.events.Event;
    import flash.events.EventDispatcher;
    import flash.events.HTTPStatusEvent;
    import flash.events.IOErrorEvent;
    import flash.events.SecurityErrorEvent;
    import flash.net.URLLoader;
    import flash.net.URLLoaderDataFormat;
    import flash.net.URLRequest;
    import flash.net.URLRequestMethod;
    import flash.net.URLVariables;
    import flash.system.System;
    import flash.utils.ByteArray;
    import flash.utils.Dictionary;
    
    import mx.core.IMXMLObject;
    
    import events.HTTPDeamonEvent;
    
    [Event(name="listenComplete", type = "org.castor.events.HTTPDeamonEvent")]
    [Event(name="stopComplete", type = "org.castor.events.HTTPDeamonEvent")]
    [Event(name="receiveRequest", type = "org.castor.events.HTTPDeamonEvent")]
    [Event(name="listenError", type = "org.castor.events.HTTPDeamonEvent")]

    public class HTTPDeamon extends EventDispatcher implements IMXMLObject{
        
        // Protected Variables
        protected var _request:URLRequest = null;
        protected var _loader:URLLoader = null;
        protected var _nextUrl:String = "";
        protected var _responseMap:Dictionary = new Dictionary();
        protected var _requestMap:Dictionary = new Dictionary();

        protected var _endpoint:String = "http://www.reversehttp.net/reversehttp";
        protected var _serverType:String = "AIRHTTPD";
        protected var _token:String = "-";
        protected var _label:String = "";
        protected var _isRunning:Boolean = false;

        protected var _responseStatusMap:Object = {
            100:"Continue",
            101:"Switching Protocols", 
            102:"Processing", 
            200:"OK",
            201:"Created",
            202:"Accepted",
            203:"Non-Authoritative Information",
            204:"No Content",
            205:"Reset Content",
            206:"Partial Content",
            207:"Multi-Status",
            226:"IM Used",
            300:"Multiple Choices",
            301:"Moved Permanently",
            302:"Found",
            303:"See Other",
            304:"Not Modified",
            305:"Use Proxy",
            306:"(Unused)",
            307:"Temporary Redirect",
            350:"",
            400:"Bad Request",
            401:"Unauthorized",
            402:"Payment Required",
            403:"Forbidden",
            404:"Not Found",
            405:"Method Not Allowed",
            406:"Not Acceptable",
            407:"Proxy Authentication Required",
            408:"Request Timeout",
            409:"Conflict",
            410:"Gone",
            411:"Length Required",
            412:"Precondition Failed",
            413:"Request Entity Too Large",
            414:"Request-URI Too Long",
            415:"Unsupported Media Type",
            416:"Requested Range Not Satisfiable",
            417:"Expectation Failed",
            418:"",
            420:"",
            421:"",
            422:"Unprocessable Entity",
            423:"Locked",
            424:"Failed Dependency",
            425:"",
            426:"Upgrade Required",
            500:"Internal Server Error",
            501:"Not Implemented",
            502:"Bad Gateway",
            503:"Service Unavailable",
            504:"Gateway Timeout",
            505:"HTTP Version Not Supported",
            506:"Variant Also Negotiates",
            507:"Insufficient Storage",
            510:"Not Extended"
        };
        // Constructor 

        public function HTTPDeamon() {
            _request = new URLRequest();
        }

        // Public Methods

        public function listen():void {
            _nextUrl = "";
            serve(true);
        }

        public function respond(httpDeamonEvent:HTTPDeamonEvent, responseState:int, contentType:String, responseHeaders:Array, responseBody:String):void {
            var url:String = _requestMap[httpDeamonEvent].toString();
            var responseStateString:String = _responseStatusMap[responseState].toString();
            var responseURLRequest:URLRequest;
            var responseURLLoader:URLLoader;
            var responseData:Array = [];
            if(url != null && url != "" && responseStateString != null && responseStateString != "") {
                responseURLRequest = new URLRequest();
                responseURLLoader = new URLLoader();

                responseURLRequest.method = URLRequestMethod.POST;
                responseData.push("HTTP/1.1 " + responseState.toString() + " " + responseStateString);
                responseData.push("Server: " + _serverType);
                for each(var header:Object in responseHeaders) {
                    if(header.name != null && header.value != null) {
                        responseData.push(header.name.toString() + ": " + header.value.toString());
                    }
                }
                responseData.push("Content-Type: " + contentType);
                responseData.push("Content-Length: " + responseBody.length);
                responseData.push("");

                responseURLRequest.data =  responseData.join("\r\n") + "\r\n" + responseBody;
                responseURLRequest.contentType = "message/http";
                responseURLRequest.url = url;
                responseURLLoader.load(responseURLRequest);
                delete _requestMap[httpDeamonEvent];
            }
        }

        public function stop():void {
            _loader.close();
            _isRunning = false;
        }

        // Public Overrided Methods
        public function initialized(document:Object, id:String):void {
        }

        // Public Getters/Setters

        public function get isRunning():Boolean {
            return _isRunning;
        }

        public function get label():String {
            return _label;
        }

        public function set label(value:String):void {
            _label = value;
        }

        public function get token():String {
            return _token;
        }

        public function set token(value:String):void {
            _token = value;
        }

        public function get endpoint():String {
            return _endpoint;
        }

        public function set endpoint(value:String):void {
            _endpoint = value;
        }

        public function get serverType():String {
            return _serverType;
        }

        public function set serverType(value:String):void {
            _serverType = value;
        }

        // Protected Methods

        protected function serve(newLoader:Boolean=false):void {
            var variables:URLVariables = null;
            if(newLoader) {
                _loader = new URLLoader();
    
                _loader.addEventListener(HTTPStatusEvent.HTTP_RESPONSE_STATUS, loaderHTTPResponseStatusHandler);
                _loader.addEventListener(Event.COMPLETE, loaderCompleteHandler);
                _loader.addEventListener(IOErrorEvent.IO_ERROR, loaderIOErrorHandler);
                _loader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, loaderSecurityErrorHandler);
                _loader.dataFormat = URLLoaderDataFormat.BINARY;
            }

            if(_nextUrl == "") {
                variables = new URLVariables();
                variables.name = _label;
                variables.token = _token;
                _request.data = variables;
                _request.url = _endpoint;
                _request.method = URLRequestMethod.POST;
            } else {
                _request.url = _nextUrl;
                _request.method = URLRequestMethod.GET;
            }

            _loader.load(_request);
            System.gc();
        }

        // Protected Overrided Methods

        // Protected Event Handlers
        
        protected function loaderHTTPResponseStatusHandler(event:HTTPStatusEvent):void {
            trace("loaderHTTPResponseStatusHandler");
            var header:Object;
            var headerName:String;
            var headerValue:String;
            _nextUrl = "";
            for each(header in event.responseHeaders) {
                headerName = header.name;
                headerValue = header.value;
                if(headerName == "Link") {
                    _nextUrl = headerValue.substring(headerValue.indexOf("<")+1, headerValue.indexOf(">;"));
                }
            }

            var httpDeamonEvent:HTTPDeamonEvent;
            if(_nextUrl != "") {
                _responseMap[event.currentTarget] = {
                    status:     event.status,
                    url:        event.responseURL,
                    headers:    event.responseHeaders
                };
            } else {
                httpDeamonEvent = new HTTPDeamonEvent(HTTPDeamonEvent.LISTEN_ERROR);
                this.dispatchEvent(httpDeamonEvent);
            }
        }

        protected function loaderCompleteHandler(event:Event):void {
            var loader:URLLoader = event.currentTarget as URLLoader;
            var response:Object = _responseMap[loader]; 

            if(response == null) return;

            if(response.url == _endpoint) {
                // listen()直後のResponse
                httpDeamonEvent = new HTTPDeamonEvent(HTTPDeamonEvent.LISTEN_COMPLETE);
                _isRunning = true;
                this.dispatchEvent(httpDeamonEvent);
                serve(false);
            } else if(response.status == 204) {
                // serve()がTimeout
                serve(false);
            } else if(response.status == 200) {
                // serve()後にRequestアリ
                serve(true);

                var requestData:ByteArray = loader.data;
                var dataAsStr:String = requestData.toString();
    
                var body:ByteArray = new ByteArray();
    
                var httpSeparator:String = "\r\n\r\n";
                var sepPos:int = dataAsStr.indexOf(httpSeparator);
                requestData.position = sepPos + httpSeparator.length;
                requestData.readBytes(body, 0);
                body.position = 0;

                var headers:Array = dataAsStr.substring(0,sepPos).split("\r\n");
                var headerFirstLine:String = headers.shift().toString();
                var methodLength:int = headerFirstLine.indexOf(" ");
                var method:String = headerFirstLine.substring(0, methodLength);
                var versionPos:int = headerFirstLine.lastIndexOf("HTTP");
                var path:String = headerFirstLine.substring(methodLength+1, versionPos-1);
                var httpVersion:String = headerFirstLine.substring(versionPos);
                var headerLine:String;
                for (var i:int=0;i<headers.length;i++) {
                    headerLine = headers[i];
                    if(headerLine == "") {
                        headers.splice(i, headers.length-i);
                        break;
                    }
                }
    
                var request:HTTPRequestObject = new HTTPRequestObject(method, path, httpVersion, headers, body);
    
                var httpDeamonEvent:HTTPDeamonEvent;
                httpDeamonEvent = new HTTPDeamonEvent(HTTPDeamonEvent.RECEIVE_REQUEST, request);
                _requestMap[httpDeamonEvent] = _responseMap[loader].url;
                delete _responseMap[loader];
                this.dispatchEvent(httpDeamonEvent);
            } else {
                httpDeamonEvent = new HTTPDeamonEvent(HTTPDeamonEvent.LISTEN_ERROR);
                this.dispatchEvent(httpDeamonEvent);
            }

        }
        protected function loaderIOErrorHandler(event:IOErrorEvent):void {
            trace("loaderIOErrorHandler");

        }

        protected function loaderSecurityErrorHandler(event:SecurityErrorEvent):void {
            trace("loaderSecurityErrorHandler");
            
        }
    }
}


HTTPRequestObject.as

package net
{
    import flash.utils.ByteArray;
    
    import mx.utils.ObjectUtil;
    
    public class HTTPRequestObject {

        protected var _method:String = "";
        protected var _path:String = "";
        protected var _httpVersion:String = "";
        protected var _headers:Array = [];
        protected var _body:ByteArray = null;

        public function HTTPRequestObject(method:String, path:String="/", httpVersion:String="HTTP/1.1", headers:Array=null, body:ByteArray=null) {
            _method = method;
            _path = path;
            _httpVersion = httpVersion;
            _headers = headers;
            _body = ObjectUtil.copy(body) as ByteArray;
            _body.position = 0;
        }

        public function get requestMethod():String {
            return _method;
        }

        public function get requestPath():String {
            return _path;
        }

        public function get requestHttpVersion():String {
            return _httpVersion;
        }

        public function get requestHeaders():Array {
            return _headers;
        }

        public function get requestBody():ByteArray {
            return _body;
        }
    }
}


HTTPDeamonEvent.as

package events
{
    import flash.events.Event;
    
    import net.HTTPRequestObject;

    public class HTTPDeamonEvent extends Event {

        public static const LISTEN_COMPLETE:String = "listenComplete";
        public static const LISTEN_ERROR:String = "listenError";
        public static const STOP_COMPLETE:String = "stopComplete";
        public static const RECEIVE_REQUEST:String = "receiveRequest";

        protected var _request:HTTPRequestObject = null;
        public function HTTPDeamonEvent(type:String, request:HTTPRequestObject=null, bubbles:Boolean=false, cancelable:Boolean=false) {
            super(type, bubbles, cancelable);
            _request = request;
        }

        public function get request():HTTPRequestObject {
            return _request;
        }
    }
}

簡単な使い方。このアプリケーションを起動したあとに、
ブラウザ等でhttp://hogefuga.www.reversehttp.net/にアクセスすると、リクエストヘッダを返してくれる。
ただそれだけ。

<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" xmlns:net="net.*" creationComplete="onCreationCompleteHandler(event)">
    <mx:Script>
        <![CDATA[
            import events.HTTPDeamonEvent;
            import mx.events.FlexEvent;
            protected function onCreationCompleteHandler(event:FlexEvent):void {
                httpd.addEventListener(HTTPDeamonEvent.LISTEN_COMPLETE, httpdListenCompleteHandler);
                httpd.addEventListener(HTTPDeamonEvent.LISTEN_ERROR, httpdListenErrorHandler);
                httpd.addEventListener(HTTPDeamonEvent.STOP_COMPLETE, httpdStopCompleteHandler);
                httpd.addEventListener(HTTPDeamonEvent.RECEIVE_REQUEST, httpdReceiveRequestHandler);
                httpd.listen();
            }
            protected function httpdListenCompleteHandler(event:HTTPDeamonEvent):void {
                trace("httpdListenCompleteHandler");
                btn.enabled = true;
            }
            protected function httpdReceiveRequestHandler(event:HTTPDeamonEvent):void {
                trace("httpdReceiveRequestHandler");
                var body:Array = [];
                body.push(event.request.requestMethod + " " + event.request.requestPath + " " +event.request.requestHttpVersion);
                for each(var header:String in event.request.requestHeaders) {
                    body.push(header);
                }
                
                httpd.respond(event, 200, "text/plain", [], "\r\n" + body.join("\r\n"));
                trace(event.request.requestBody.readBoolean());
            }
            protected function httpdStopCompleteHandler(event:HTTPDeamonEvent):void {
                trace("httpdStopCompleteHandler");
            }

            protected function httpdListenErrorHandler(event:HTTPDeamonEvent):void {
                trace("httpdListenErrorHandler");
            }
        ]]>
    </mx:Script>
    <net:HTTPDeamon id="httpd" label="hogefuga"/>
</mx:WindowedApplication>


ReverseHTTP、考え方としては面白いけど、
いろんなサービスがWebHookに対応してくれないと、使い方に困るかもなぁ。