001package gu.dtalk.engine;
002
003import static com.google.common.base.Preconditions.checkArgument;
004import static com.google.common.base.Preconditions.checkNotNull;
005import static com.google.common.base.Preconditions.checkState;
006import static gu.dtalk.CommonConstant.DEFAULT_IDLE_TIME_MILLS;
007import static gu.dtalk.engine.DeviceUtils.DEVINFO_PROVIDER;
008
009import java.io.ByteArrayInputStream;
010import java.io.IOException;
011import java.net.URL;
012import java.util.Arrays;
013import java.util.Collections;
014import java.util.Date;
015import java.util.Map;
016import java.util.Set;
017import java.util.Map.Entry;
018import java.util.Timer;
019import java.util.TimerTask;
020import java.util.concurrent.atomic.AtomicReference;
021import org.slf4j.Logger;
022import org.slf4j.LoggerFactory;
023
024import com.alibaba.fastjson.JSONObject;
025import com.alibaba.fastjson.TypeReference;
026import com.google.common.base.Function;
027import com.google.common.base.Joiner;
028import com.google.common.base.MoreObjects;
029import com.google.common.base.Objects;
030import com.google.common.base.Strings;
031import com.google.common.base.Supplier;
032import com.google.common.collect.ImmutableMap;
033import com.google.common.collect.ImmutableSet;
034import com.google.common.collect.Maps;
035import fi.iki.elonen.NanoHTTPD;
036import fi.iki.elonen.NanoHTTPD.Response.Status;
037import fi.iki.elonen.NanoWSD.WebSocketFrame.CloseCode;
038import fi.iki.elonen.NanoWSD;
039import gu.dtalk.Ack;
040import gu.dtalk.MenuItem;
041import gu.simplemq.exceptions.SmqUnsubscribeException;
042import gu.simplemq.json.BaseJsonEncoder;
043import net.gdface.utils.BinaryUtils;
044
045import static gu.dtalk.CommonConstant.*;
046import static gu.dtalk.Version.*;
047/**
048 * dtalk http 服务
049 * @author guyadong
050 *
051 */
052public class DtalkHttpServer extends NanoWSD {
053        private static final Logger logger = LoggerFactory.getLogger(DtalkHttpServer.class);
054
055        /**
056     * Standard HTTP header names.
057     */
058    public static final class HeaderNames {
059        /**
060         * {@code "Accept"}
061         */
062        public static final String ACCEPT = "Accept";
063        /**
064         * {@code "Accept-Charset"}
065         */
066        public static final String ACCEPT_CHARSET = "Accept-Charset";
067        /**
068         * {@code "Accept-Encoding"}
069         */
070        public static final String ACCEPT_ENCODING = "Accept-Encoding";
071        /**
072         * {@code "Accept-Language"}
073         */
074        public static final String ACCEPT_LANGUAGE = "Accept-Language";
075        /**
076         * {@code "Accept-Ranges"}
077         */
078        public static final String ACCEPT_RANGES = "Accept-Ranges";
079        /**
080         * {@code "Accept-Patch"}
081         */
082        public static final String ACCEPT_PATCH = "Accept-Patch";
083        /**
084         * {@code "Access-Control-Allow-Credentials"}
085         */
086        public static final String ACCESS_CONTROL_ALLOW_CREDENTIALS = "Access-Control-Allow-Credentials";
087        /**
088         * {@code "Access-Control-Allow-Headers"}
089         */
090        public static final String ACCESS_CONTROL_ALLOW_HEADERS = "Access-Control-Allow-Headers";
091        /**
092         * {@code "Access-Control-Allow-Methods"}
093         */
094        public static final String ACCESS_CONTROL_ALLOW_METHODS = "Access-Control-Allow-Methods";
095        /**
096         * {@code "Access-Control-Allow-Origin"}
097         */
098        public static final String ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin";
099        /**
100         * {@code "Access-Control-Expose-Headers"}
101         */
102        public static final String ACCESS_CONTROL_EXPOSE_HEADERS = "Access-Control-Expose-Headers";
103        /**
104         * {@code "Access-Control-Max-Age"}
105         */
106        public static final String ACCESS_CONTROL_MAX_AGE = "Access-Control-Max-Age";
107        /**
108         * {@code "Access-Control-Request-Headers"}
109         */
110        public static final String ACCESS_CONTROL_REQUEST_HEADERS = "Access-Control-Request-Headers";
111        /**
112         * {@code "Access-Control-Request-Method"}
113         */
114        public static final String ACCESS_CONTROL_REQUEST_METHOD = "Access-Control-Request-Method";
115        /**
116         * {@code "Age"}
117         */
118        public static final String AGE = "Age";
119        /**
120         * {@code "Allow"}
121         */
122        public static final String ALLOW = "Allow";
123        /**
124         * {@code "Authorization"}
125         */
126        public static final String AUTHORIZATION = "Authorization";
127        /**
128         * {@code "Cache-Control"}
129         */
130        public static final String CACHE_CONTROL = "Cache-Control";
131        /**
132         * {@code "Connection"}
133         */
134        public static final String CONNECTION = "Connection";
135        /**
136         * {@code "Content-Base"}
137         */
138        public static final String CONTENT_BASE = "Content-Base";
139        /**
140         * {@code "Content-Encoding"}
141         */
142        public static final String CONTENT_ENCODING = "Content-Encoding";
143        /**
144         * {@code "Content-Language"}
145         */
146        public static final String CONTENT_LANGUAGE = "Content-Language";
147        /**
148         * {@code "Content-Length"}
149         */
150        public static final String CONTENT_LENGTH = "Content-Length";
151        /**
152         * {@code "Content-Location"}
153         */
154        public static final String CONTENT_LOCATION = "Content-Location";
155        /**
156         * {@code "Content-Transfer-Encoding"}
157         */
158        public static final String CONTENT_TRANSFER_ENCODING = "Content-Transfer-Encoding";
159        /**
160         * {@code "Content-MD5"}
161         */
162        public static final String CONTENT_MD5 = "Content-MD5";
163        /**
164         * {@code "Content-Range"}
165         */
166        public static final String CONTENT_RANGE = "Content-Range";
167        /**
168         * {@code "Content-Type"}
169         */
170        public static final String CONTENT_TYPE = "Content-Type";
171        /**
172         * {@code "Cookie"}
173         */
174        public static final String COOKIE = "Cookie";
175        /**
176         * {@code "Date"}
177         */
178        public static final String DATE = "Date";
179        /**
180         * {@code "ETag"}
181         */
182        public static final String ETAG = "ETag";
183        /**
184         * {@code "Expect"}
185         */
186        public static final String EXPECT = "Expect";
187        /**
188         * {@code "Expires"}
189         */
190        public static final String EXPIRES = "Expires";
191        /**
192         * {@code "From"}
193         */
194        public static final String FROM = "From";
195        /**
196         * {@code "Host"}
197         */
198        public static final String HOST = "Host";
199        /**
200         * {@code "If-Match"}
201         */
202        public static final String IF_MATCH = "If-Match";
203        /**
204         * {@code "If-Modified-Since"}
205         */
206        public static final String IF_MODIFIED_SINCE = "If-Modified-Since";
207        /**
208         * {@code "If-None-Match"}
209         */
210        public static final String IF_NONE_MATCH = "If-None-Match";
211        /**
212         * {@code "If-Range"}
213         */
214        public static final String IF_RANGE = "If-Range";
215        /**
216         * {@code "If-Unmodified-Since"}
217         */
218        public static final String IF_UNMODIFIED_SINCE = "If-Unmodified-Since";
219        /**
220         * {@code "Last-Modified"}
221         */
222        public static final String LAST_MODIFIED = "Last-Modified";
223        /**
224         * {@code "Location"}
225         */
226        public static final String LOCATION = "Location";
227        /**
228         * {@code "Max-Forwards"}
229         */
230        public static final String MAX_FORWARDS = "Max-Forwards";
231        /**
232         * {@code "Origin"}
233         */
234        public static final String ORIGIN = "Origin";
235        /**
236         * {@code "Pragma"}
237         */
238        public static final String PRAGMA = "Pragma";
239        /**
240         * {@code "Proxy-Authenticate"}
241         */
242        public static final String PROXY_AUTHENTICATE = "Proxy-Authenticate";
243        /**
244         * {@code "Proxy-Authorization"}
245         */
246        public static final String PROXY_AUTHORIZATION = "Proxy-Authorization";
247        /**
248         * {@code "Range"}
249         */
250        public static final String RANGE = "Range";
251        /**
252         * {@code "Referer"}
253         */
254        public static final String REFERER = "Referer";
255        /**
256         * {@code "Retry-After"}
257         */
258        public static final String RETRY_AFTER = "Retry-After";
259        /**
260         * {@code "Sec-WebSocket-Key1"}
261         */
262        public static final String SEC_WEBSOCKET_KEY1 = "Sec-WebSocket-Key1";
263        /**
264         * {@code "Sec-WebSocket-Key2"}
265         */
266        public static final String SEC_WEBSOCKET_KEY2 = "Sec-WebSocket-Key2";
267        /**
268         * {@code "Sec-WebSocket-Location"}
269         */
270        public static final String SEC_WEBSOCKET_LOCATION = "Sec-WebSocket-Location";
271        /**
272         * {@code "Sec-WebSocket-Origin"}
273         */
274        public static final String SEC_WEBSOCKET_ORIGIN = "Sec-WebSocket-Origin";
275        /**
276         * {@code "Sec-WebSocket-Protocol"}
277         */
278        public static final String SEC_WEBSOCKET_PROTOCOL = "Sec-WebSocket-Protocol";
279        /**
280         * {@code "Sec-WebSocket-Version"}
281         */
282        public static final String SEC_WEBSOCKET_VERSION = "Sec-WebSocket-Version";
283        /**
284         * {@code "Sec-WebSocket-Key"}
285         */
286        public static final String SEC_WEBSOCKET_KEY = "Sec-WebSocket-Key";
287        /**
288         * {@code "Sec-WebSocket-Accept"}
289         */
290        public static final String SEC_WEBSOCKET_ACCEPT = "Sec-WebSocket-Accept";
291        /**
292         * {@code "Server"}
293         */
294        public static final String SERVER = "Server";
295        /**
296         * {@code "Set-Cookie"}
297         */
298        public static final String SET_COOKIE = "Set-Cookie";
299        /**
300         * {@code "Set-Cookie2"}
301         */
302        public static final String SET_COOKIE2 = "Set-Cookie2";
303        /**
304         * {@code "TE"}
305         */
306        public static final String TE = "TE";
307        /**
308         * {@code "Trailer"}
309         */
310        public static final String TRAILER = "Trailer";
311        /**
312         * {@code "Transfer-Encoding"}
313         */
314        public static final String TRANSFER_ENCODING = "Transfer-Encoding";
315        /**
316         * {@code "Upgrade"}
317         */
318        public static final String UPGRADE = "Upgrade";
319        /**
320         * {@code "User-Agent"}
321         */
322        public static final String USER_AGENT = "User-Agent";
323        /**
324         * {@code "Vary"}
325         */
326        public static final String VARY = "Vary";
327        /**
328         * {@code "Via"}
329         */
330        public static final String VIA = "Via";
331        /**
332         * {@code "Warning"}
333         */
334        public static final String WARNING = "Warning";
335        /**
336         * {@code "WebSocket-Location"}
337         */
338        public static final String WEBSOCKET_LOCATION = "WebSocket-Location";
339        /**
340         * {@code "WebSocket-Origin"}
341         */
342        public static final String WEBSOCKET_ORIGIN = "WebSocket-Origin";
343        /**
344         * {@code "WebSocket-Protocol"}
345         */
346        public static final String WEBSOCKET_PROTOCOL = "WebSocket-Protocol";
347        /**
348         * {@code "WWW-Authenticate"}
349         */
350        public static final String WWW_AUTHENTICATE = "WWW-Authenticate";
351
352        private HeaderNames() {
353        }
354    }
355
356    /**
357     * Standard HTTP header values.
358     */
359    public static final class HeaderValues {
360        /**
361         * {@code "application/x-www-form-urlencoded"}
362         */
363        public static final String APPLICATION_X_WWW_FORM_URLENCODED = "application/x-www-form-urlencoded";
364        /**
365         * {@code "base64"}
366         */
367        public static final String BASE64 = "base64";
368        /**
369         * {@code "binary"}
370         */
371        public static final String BINARY = "binary";
372        /**
373         * {@code "boundary"}
374         */
375        public static final String BOUNDARY = "boundary";
376        /**
377         * {@code "bytes"}
378         */
379        public static final String BYTES = "bytes";
380        /**
381         * {@code "charset"}
382         */
383        public static final String CHARSET = "charset";
384        /**
385         * {@code "chunked"}
386         */
387        public static final String CHUNKED = "chunked";
388        /**
389         * {@code "close"}
390         */
391        public static final String CLOSE = "close";
392        /**
393         * {@code "compress"}
394         */
395        public static final String COMPRESS = "compress";
396        /**
397         * {@code "100-continue"}
398         */
399        public static final String CONTINUE =  "100-continue";
400        /**
401         * {@code "deflate"}
402         */
403        public static final String DEFLATE = "deflate";
404        /**
405         * {@code "gzip"}
406         */
407        public static final String GZIP = "gzip";
408        /**
409         * {@code "identity"}
410         */
411        public static final String IDENTITY = "identity";
412        /**
413         * {@code "keep-alive"}
414         */
415        public static final String KEEP_ALIVE = "keep-alive";
416        /**
417         * {@code "max-age"}
418         */
419        public static final String MAX_AGE = "max-age";
420        /**
421         * {@code "max-stale"}
422         */
423        public static final String MAX_STALE = "max-stale";
424        /**
425         * {@code "min-fresh"}
426         */
427        public static final String MIN_FRESH = "min-fresh";
428        /**
429         * {@code "multipart/form-data"}
430         */
431        public static final String MULTIPART_FORM_DATA = "multipart/form-data";
432        /**
433         * {@code "must-revalidate"}
434         */
435        public static final String MUST_REVALIDATE = "must-revalidate";
436        /**
437         * {@code "no-cache"}
438         */
439        public static final String NO_CACHE = "no-cache";
440        /**
441         * {@code "no-store"}
442         */
443        public static final String NO_STORE = "no-store";
444        /**
445         * {@code "no-transform"}
446         */
447        public static final String NO_TRANSFORM = "no-transform";
448        /**
449         * {@code "none"}
450         */
451        public static final String NONE = "none";
452        /**
453         * {@code "only-if-cached"}
454         */
455        public static final String ONLY_IF_CACHED = "only-if-cached";
456        /**
457         * {@code "private"}
458         */
459        public static final String PRIVATE = "private";
460        /**
461         * {@code "proxy-revalidate"}
462         */
463        public static final String PROXY_REVALIDATE = "proxy-revalidate";
464        /**
465         * {@code "public"}
466         */
467        public static final String PUBLIC = "public";
468        /**
469         * {@code "quoted-printable"}
470         */
471        public static final String QUOTED_PRINTABLE = "quoted-printable";
472        /**
473         * {@code "s-maxage"}
474         */
475        public static final String S_MAXAGE = "s-maxage";
476        /**
477         * {@code "trailers"}
478         */
479        public static final String TRAILERS = "trailers";
480        /**
481         * {@code "Upgrade"}
482         */
483        public static final String UPGRADE = "Upgrade";
484        /**
485         * {@code "WebSocket"}
486         */
487        public static final String WEBSOCKET = "WebSocket";
488
489        private HeaderValues() {
490        }
491    }
492
493        private static final String DTALK_SESSION="dtalk-session"; 
494        public static final String APPICATION_JSON="application/json";
495        private static final String UNAUTH_SESSION="UNAUTHORIZATION SESSION";
496        private static final String AUTH_OK="AUTHORIZATION OK";
497        private static final String CLIENT_LOCKED="ANOTHER CLIENT LOCKED";
498        private static final String INVALID_PWD="INVALID REQUEST PASSWORD";
499        private static final String POST_DATA="postData";
500        private static final String DTALK_PREFIX="/dtalk";
501        private static final String STATIC_PAGE_PREFIX="/web";
502        private static final String ALLOW_METHODS = Joiner.on(',').join(Arrays.asList(Method.POST,Method.GET,Method.PUT,Method.DELETE));
503        private static final String ALLOW_METHODS_CORS = ALLOW_METHODS + "," + Method.OPTIONS ;
504        private static final String DEFAULT_ALLOW_HEADERS = Joiner.on(',').join(Arrays.asList(HeaderNames.CONTENT_TYPE));
505        public static final URL DEFAULT_HOME_PAGE = DtalkHttpServer.class.getResource(STATIC_PAGE_PREFIX + "/index.html");
506        private static final Map<String,String>MIME_OF_SUFFIX = ImmutableMap.<String,String>builder()
507                        .put(".jpeg", "image/jpeg")
508                        .put(".jpg", "image/jpeg")
509                        .put(".png", "image/png")
510                        .put(".gif", "image/gif")
511                        .put(".htm","text/html")
512                        .put(".html","text/html")
513                        .put(".txt","text/plain")
514                        .put(".css","text/css")
515                        .put(".csv","text/csv")
516                        .put(".json","application/json")
517                        .put(".js","application/javascript")
518                        .put(".xml","application/xml")
519                        .put(".zip","application/zip")
520                        .put(".pdf","application/pdf")
521                        .put(".sql","application/sql")
522                        .put(".doc","application/msword")               
523                        .build();
524        private static final Set<String> SUPPORTED_MIME = ImmutableSet.copyOf(MIME_OF_SUFFIX.values());
525        private static class SingletonTimer{
526                private static final Timer instnace = new Timer(true);
527        }
528
529        private Timer timer;
530        private Timer getTimer(){
531                // 懒加载
532                if(timer == null){
533                        timer = SingletonTimer.instnace;
534                }
535                return timer;
536        }
537        private long idleTimeLimit = DEFAULT_IDLE_TIME_MILLS;
538        private long timerPeriod = 2000;
539
540        private String selfMac;
541        
542        /**
543         * 当前对话ID
544         */
545        private String dtalkSession;
546        /**
547         * 当前websocket 连接
548         */
549        private final AtomicReference<WebSocket> wsReference = new AtomicReference<>();
550        private final Supplier<AtomicReference<WebSocket>> webSocketSupplier = new Supplier<AtomicReference<WebSocket>>() {
551
552                @Override
553                public AtomicReference<WebSocket> get() {
554                        return wsReference;
555                }
556        };
557        private ItemEngineHttpImpl engine = new ItemEngineHttpImpl().setSupplier(webSocketSupplier);
558        private boolean debug = false;
559        /**
560         * 不做安全验证
561         */
562        private boolean noAuth = false;
563        /**
564         * 不支持跨域请求(CORS)
565         */
566        private boolean noCORS = false;
567        /**
568         * 首页内容
569         */
570        private String homePageContent;
571        /**
572         * 处理扩展http请求的实例
573         */
574        private final Map<String,Function<IHTTPSession, Response>> extServes = 
575                        Collections.synchronizedMap(Maps.<String,Function<IHTTPSession, Response>>newLinkedHashMap());
576        public DtalkHttpServer()  {
577                this(DEFAULT_HTTP_PORT);
578        }
579    public DtalkHttpServer(int port)  {
580        super(port);
581        this.selfMac = BinaryUtils.toHex(DeviceUtils.DEVINFO_PROVIDER.getMac());
582                // 定时检查引擎工作状态,当空闲超时,则中止连接
583                getTimer().schedule(new TimerTask() {
584
585                        @Override
586                        public void run() {
587                                try{
588                                        if(null != dtalkSession && DtalkHttpServer.this.isAlive()){
589                                                long lasthit = engine.lastHitTime();
590                                                if(System.currentTimeMillis() - lasthit > idleTimeLimit){
591                                                        resetSession();
592                                                }
593                                        }
594                                }catch (Exception e) {
595                                        logger.error(e.getMessage());
596                                }
597                        }
598                }, 0, timerPeriod);
599                try {
600                        setHomePage(DEFAULT_HOME_PAGE);
601                } catch (IOException e) {
602                        throw new RuntimeException(e);
603                }
604    }
605    private boolean isAuthorizationSession(IHTTPSession session){
606        return dtalkSession != null && dtalkSession.equals(session.getCookies().read(DTALK_SESSION));
607    }
608    private void checkAuthorizationSession(IHTTPSession session) throws ResponseException{
609        if(noAuth  || isAuthorizationSession(session)){
610                return;
611        }
612        throw new ResponseException(Status.UNAUTHORIZED, ackError(UNAUTH_SESSION));
613    }
614    private <T> Response responseAck(Ack<T> ack){
615        String json=BaseJsonEncoder.getEncoder().toJsonString(ack);
616        return newFixedLengthResponse(
617                        Ack.Status.OK.equals(ack.getStatus()) ? Status.OK: Status.INTERNAL_ERROR, 
618                        APPICATION_JSON, 
619                        json);
620    }
621    private <T> String ackMessage(Ack.Status status,String message){
622        Ack<Object> ack = new Ack<Object>().setStatus(status).setStatusMessage(message);
623        String json=BaseJsonEncoder.getEncoder().toJsonString(ack);
624        return  json;
625    }
626    private <T> String ackError(String message){
627        return  ackMessage(Ack.Status.ERROR,message);
628    }
629
630        private void resetSession(){
631                synchronized (wsReference) {
632                        dtalkSession = null;
633                        WebSocket wsSocket = wsReference.get();
634                        if(null != wsSocket){
635                        try {
636                                        wsSocket.close(CloseCode.NormalClosure, "", false);
637                                } catch (IOException e) {
638                                        e.printStackTrace();
639                                }
640                        wsSocket=null;
641                        }
642                }
643        }
644        /**
645         * 根据提供的路径返回静态资源响应对象
646         * @param uri 请求路径
647         * @return 响应对象,资源没找到返回{@code null}
648         */
649        private Response responseStaticResource(String uri){            
650                URL res = getClass().getResource(STATIC_PAGE_PREFIX + uri);
651                if(null == res){
652                        return null;
653                }
654
655                try {
656                        byte[] content = BinaryUtils.getBytes(res);                             
657                        String suffix = uri.substring(uri.lastIndexOf('.'));
658                        return makeResponse(Status.OK,suffix, content);
659                } catch (IOException e) {
660                        return newFixedLengthResponse(
661                                Status.INTERNAL_ERROR, 
662                                NanoHTTPD.MIME_PLAINTEXT, 
663                                String.format("IO ERROR %s", uri));     
664                } 
665        
666        }
667        /**
668         * 判断是否为CORS 预检请求请求(Preflight)
669         * @param session
670         * @return
671         */
672        private static boolean isPreflightRequest(IHTTPSession session) {
673                Map<String, String> headers = session.getHeaders();
674                return Method.OPTIONS.equals(session.getMethod()) 
675                                && headers.containsKey(HeaderNames.ORIGIN.toLowerCase()) 
676                                && headers.containsKey(HeaderNames.ACCESS_CONTROL_REQUEST_METHOD.toLowerCase()) 
677                                && headers.containsKey(HeaderNames.ACCESS_CONTROL_REQUEST_HEADERS.toLowerCase());
678        }
679        /**
680         * 封装响应包
681         * @param session http请求
682         * @param resp 响应包
683         * @return resp
684         */
685        private Response wrapResponse(IHTTPSession session,Response resp) {
686                if(null != resp){
687                        Map<String, String> headers = session.getHeaders();
688                        resp.addHeader(HeaderNames.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
689                        // 如果请求头中包含'Origin',则响应头中'Access-Control-Allow-Origin'使用此值否则为'*'
690                        // nanohttd将所有请求头的名称强制转为了小写
691                        String origin = MoreObjects.firstNonNull(headers.get(HeaderNames.ORIGIN.toLowerCase()), "*");
692                        resp.addHeader(HeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN, origin);
693                        
694                        String  requestHeaders = headers.get(HeaderNames.ACCESS_CONTROL_REQUEST_HEADERS.toLowerCase());
695                        if(requestHeaders != null){
696                                resp.addHeader(HeaderNames.ACCESS_CONTROL_ALLOW_HEADERS, requestHeaders);
697                        }
698                }
699                return resp;
700        }
701
702        /**
703         * 向响应包中添加CORS包头数据
704         * @param session
705         * @return
706         */
707        private Response responseCORS(IHTTPSession session) {
708                Response resp = wrapResponse(session,newFixedLengthResponse(""));
709                Map<String, String> headers = session.getHeaders();
710                resp.addHeader(HeaderNames.ACCESS_CONTROL_ALLOW_METHODS, 
711                                noCORS ? ALLOW_METHODS : ALLOW_METHODS_CORS);
712
713                String requestHeaders = headers.get(HeaderNames.ACCESS_CONTROL_REQUEST_HEADERS.toLowerCase());
714                String allowHeaders = MoreObjects.firstNonNull(requestHeaders, DEFAULT_ALLOW_HEADERS);
715                resp.addHeader(HeaderNames.ACCESS_CONTROL_ALLOW_HEADERS, allowHeaders);
716                //resp.addHeader(HeaderNames.ACCESS_CONTROL_MAX_AGE, "86400");
717                resp.addHeader(HeaderNames.ACCESS_CONTROL_MAX_AGE, "0");
718                return resp;
719        }
720        
721        private Response runExtServs(IHTTPSession session){
722                String path = session.getUri();
723                for(Entry<String, Function<IHTTPSession, Response>> entry:extServes.entrySet()){
724                        if(path.startsWith(entry.getKey())){
725                                return entry.getValue().apply(session);
726                        }
727                }
728                return null; 
729        }
730        /**
731         * http响应Body数据对象
732         * @author guyadong
733         *
734         */
735        public static final class Body{
736                public final Status status;
737                public final String mimeType;
738                public final byte[] content;
739                /**
740                 * @param status HTTP response status code
741                 * @param mimeType mine type,such as 'image/jpeg','text/html'
742                 * @param content content of HTTP response body 
743                 */
744                public Body(Status status,String mimeType, byte[] content) {
745                        checkArgument(!Strings.isNullOrEmpty(mimeType),"mimeType is null");
746                        this.status = MoreObjects.firstNonNull(status, Status.OK);
747                        this.mimeType = mimeType;
748                        this.content = MoreObjects.firstNonNull(content, new byte[]{});
749                }
750                /**
751                 * @param status HTTP response status code
752                 * @param mimeType mine type,such as 'image/jpeg','text/html'
753                 * @param content content of HTTP response body 
754                 */
755                public Body(Status status,String mimeType, String content) {
756                        this(status, content,content == null ? null : content.getBytes());
757                }
758                /**
759                 * @param mimeType mine type,such as 'image/jpeg','text/html'
760                 * @param content content of HTTP response body 
761                 */
762                public Body(String mimeType, byte[] content) {
763                        this(null, mimeType, content);
764                }
765                /**
766                 * @param mimeType mine type,such as 'image/jpeg','text/html'
767                 * @param content content of HTTP response body 
768                 */
769                public Body(String mimeType, String content) {
770                        this(null, mimeType,content);
771                }
772        }
773        public static Response makeResponse(Status status,String mimeType, byte[] content){
774                if(MIME_OF_SUFFIX.containsKey(mimeType)){
775                        return newFixedLengthResponse(
776                                        checkNotNull(status,"status is null"), 
777                                        MIME_OF_SUFFIX.get(mimeType), 
778                                        new ByteArrayInputStream(checkNotNull(content,"content is null")),content.length);
779                } if (SUPPORTED_MIME.contains(mimeType)){
780                        return newFixedLengthResponse(
781                                        checkNotNull(status,"status is null"), 
782                                        mimeType, 
783                                        new ByteArrayInputStream(checkNotNull(content,"content is null")),content.length);
784                }else{
785                        return newFixedLengthResponse(
786                                Status.UNSUPPORTED_MEDIA_TYPE, 
787                                NanoHTTPD.MIME_PLAINTEXT, 
788                                String.format("UNSUPPORTED MEDIA TYPE %s", mimeType));  
789                }
790        }
791        public static Response makeResponse(Body body){
792                return body == null ? null : makeResponse(body.status, body.mimeType, body.content);
793        }
794        @Override
795        public void start(int timeout, boolean daemon) throws IOException {
796                if(!isAlive()){
797                        super.start(timeout, daemon);
798                        // 定时发送PING
799                        getTimer().schedule(new TimerTask() {
800
801                                @Override
802                                public void run() {
803                                        
804                                                if(null != dtalkSession && DtalkHttpServer.this.isAlive()){
805                                                        
806                                                        synchronized (wsReference) {
807                                                                try{
808                                                                        WebSocket wsSocket = wsReference.get();
809                                                                        if(dtalkSession != null && wsSocket != null && wsSocket.isOpen()){
810                                                                                wsSocket.ping(new byte[0]);
811                                                                                if(debug){
812                                                                                        wsSocket.send("dtalk wscocket heartbeat " + new Date());
813                                                                                }
814                                                                        }
815                                                                }catch (Exception e) {
816                                                                        logger.error("{}:{}",e.getClass().getName(),e.getMessage());
817                                                                        //logger.error(e.getMessage(),e);
818                                                                }                       
819                                                        }                                                                               
820                                                }
821
822                                }
823                        }, 0, timeout*3/4);
824                }
825        }
826        @Override
827    public Response serve(IHTTPSession session) {
828        if (isWebsocketRequested(session) && ! isAuthorizationSession(session)) {
829                return newFixedLengthResponse(
830                                Status.UNAUTHORIZED, 
831                                                NanoHTTPD.MIME_PLAINTEXT, 
832                                                UNAUTH_SESSION);
833        }
834                return super.serve(session);            
835    }
836    @Override
837    public Response serveHttp(IHTTPSession session) {
838        if(isPreflightRequest(session)){
839                return responseCORS(session);
840        }
841        Ack<Object> ack = new Ack<Object>().setStatus(Ack.Status.OK).setDeviceMac(selfMac);
842        try{
843                switch(session.getUri()){
844                case "/login":
845                        login(session, ack);
846                        break;
847                case "/logout":
848                        logout(session, ack);
849                        break;
850                case "/":
851                case "/index.html":
852                case "/index.htm":
853                {
854                        return newFixedLengthResponse(Status.OK, NanoHTTPD.MIME_HTML, getHomePageContent());
855                }
856                default:
857                        Response resp = responseStaticResource(session.getUri());
858                        if(null != resp){
859                                return wrapResponse(session,resp);
860                        }
861                                if(session.getUri().startsWith(DTALK_PREFIX )){
862                                checkAuthorizationSession(session);
863                                if(DTALK_PREFIX.equals(session.getUri())){
864                                        checkState(Method.POST.equals(session.getMethod()),"POST method supported only");
865                                }
866                                JSONObject jsonObject = getJSONObject(session);
867                                        if(session.getUri().startsWith(DTALK_PREFIX + '/')){
868                                                String path=session.getUri().substring(DTALK_PREFIX.length());
869                                                jsonObject.put("path", path);
870                                        } 
871                                
872                                try {
873                                        engine.onSubscribe(jsonObject);
874                                        return wrapResponse(session,engine.getResponse());
875                                        } catch (SmqUnsubscribeException e) {
876                                                logout(session, ack);
877                                                break;
878                                        }                               
879                        }else if((resp = runExtServs(session)) != null){
880                                        return wrapResponse(session,resp);
881                        }
882                                return wrapResponse(session,newFixedLengthResponse(
883                                                Status.NOT_FOUND, 
884                                                NanoHTTPD.MIME_PLAINTEXT, 
885                                                String.format("NOT FOUND %s", session.getUri())));      
886                }
887        }catch(ResponseException e){
888                return wrapResponse(session,newFixedLengthResponse(
889                                        e.getStatus(), 
890                                        NanoHTTPD.MIME_PLAINTEXT, 
891                                        e.getMessage()));       
892        }catch (Exception e) {                  
893                ack.setStatus(Ack.Status.ERROR).setException(e.getClass().getName()).setStatusMessage(e.getMessage());
894        }
895        return wrapResponse(session,responseAck(ack));
896    }
897
898        @Override
899        protected boolean useGzipWhenAccepted(Response r) {
900                // 判断是否为websocket握手响应,如果是则调用父类方法返回false,否则按正常的http响应对待,使用 NanoHTTPD的useGzipWhenAccepted方法逻辑判断
901                if (null != r.getHeader(NanoWSD.HEADER_WEBSOCKET_ACCEPT)){
902                        return super.useGzipWhenAccepted(r);
903                }
904                // 对文本响应执行Gzip压缩
905                return r.getMimeType()!= null && r.getMimeType().toLowerCase().matches(".*(text/|/json|/javascript|/xml).*");
906        }
907        /**
908         * @param isMd5 
909         * @return 
910         */
911        private boolean validate(String pwd, boolean isMd5) {
912                checkArgument(pwd != null,"NULL PASSWORD");
913                
914                String admPwd = checkNotNull(DEVINFO_PROVIDER.getPassword(),"admin password for device is null");
915                checkArgument(!Strings.isNullOrEmpty(pwd),"NULL REQUEST PASSWORD");
916                checkArgument(!Strings.isNullOrEmpty(admPwd),"NULL ADMIN PASSWORD");
917                if(isMd5){
918                        String pwdmd5 = BinaryUtils.getMD5String(admPwd.getBytes());
919                        return pwdmd5.equalsIgnoreCase(pwd);
920                }else{
921                        return admPwd.equals(pwd);
922                }
923        }
924        
925        /**
926         * 从HTTP请求body中解析参数返回{@link Map}实例
927         * @param session
928         * @return
929         * @throws IOException
930         * @throws ResponseException
931         */
932        private static  Map<String, String> getParamOfPostBody(IHTTPSession session) throws IOException, ResponseException{
933                Map<String,String> postData = Maps.newHashMap();
934                session.parseBody(postData);
935                String jsonstr = postData.get(POST_DATA);
936                if (null == jsonstr) {
937                        return Maps.newHashMap();
938                }
939                return BaseJsonEncoder.getEncoder().fromJson(jsonstr, 
940                                new TypeReference<Map<String, String>>(){}.getType());          
941        }
942        /**
943         * 从HTTP请求中解析参数返回{@link JSONObject}实例
944         * @param session
945         * @return
946         * @throws IOException
947         * @throws ResponseException
948         */
949        @SuppressWarnings("deprecation")
950        private static JSONObject getJSONObject(IHTTPSession session) throws IOException, ResponseException{
951        String jsonstr;
952        if(Method.POST.equals(session.getMethod())){ 
953                Map<String,String> postData = Maps.newHashMap();
954                session.parseBody(postData);
955                jsonstr = postData.get(POST_DATA);
956        }else{
957                jsonstr = BaseJsonEncoder.getEncoder().toJsonString(session.getParms());
958        }
959                return null == jsonstr ? new  JSONObject():BaseJsonEncoder.getEncoder().fromJson(jsonstr,JSONObject.class);
960        }
961    /**
962     * 从HTTP请求中解析参数返回{@link Map}实例
963     * @param session
964     * @return 参数K-V映射
965     * @throws IOException
966     * @throws ResponseException
967     */
968    @SuppressWarnings("deprecation")
969        private static Map<String, String> getParams(IHTTPSession session) throws IOException, ResponseException{
970        Map<String,String> params = Maps.newHashMap();
971        if(Method.POST.equals(session.getMethod())){                    
972                        params = getParamOfPostBody(session);                   
973        }else{
974                params = session.getParms();
975        }
976        return params;
977    }
978    
979        protected synchronized void login(IHTTPSession session, Ack<Object> ack) throws IOException, ResponseException{
980
981        Map<String,String> parms = getParams(session);
982
983        String sid=parms.get(DTALK_SESSION);
984        if(sid ==null){
985                sid=session.getCookies().read(DTALK_SESSION);
986        }
987        
988        if (dtalkSession == null || sid == null ){
989                if(!validate(parms.get("password"), 
990                                        Boolean.valueOf(MoreObjects.firstNonNull(parms.get("isMd5"), "true")))){
991                        throw new ResponseException(Status.FORBIDDEN, 
992                                        ackError(INVALID_PWD));
993                }
994                        sid = dtalkSession = Long.toHexString(System.nanoTime());
995                session.getCookies().set(DTALK_SESSION, dtalkSession, 1);
996                logger.info("session {} connected",dtalkSession);
997        }
998        if(!Objects.equal(dtalkSession, sid)){
999                throw new ResponseException(Status.FORBIDDEN, 
1000                                ackError(CLIENT_LOCKED));
1001        }
1002        ack.setStatus(Ack.Status.OK).setStatusMessage(AUTH_OK);
1003        engine.setLastHitTime(System.currentTimeMillis());
1004
1005        }
1006        protected synchronized void logout(IHTTPSession session, Ack<Object> ack) throws IOException, ResponseException{
1007        checkAuthorizationSession(session);
1008        logger.info("session {} disconnected",dtalkSession);
1009        resetSession();
1010        ack.setStatus(Ack.Status.OK).setStatusMessage("logout OK");
1011    }
1012
1013        /**
1014         * @return engine
1015         */
1016        public ItemEngineHttpImpl getItemAdapter() {
1017                return engine;
1018        }
1019
1020        /**
1021         * @param engine 要设置的 engine
1022         * @return 当前对象
1023         */
1024        public DtalkHttpServer setItemAdapter(ItemEngineHttpImpl engine) {
1025                this.engine = checkNotNull(engine,"engine is null").setSupplier(webSocketSupplier);
1026                return this;
1027        }
1028        
1029        /**
1030         * @return 当前对象
1031         * @see gu.dtalk.engine.BaseItemEngine#getRoot()
1032         */
1033        public DtalkHttpServer getRoot() {
1034                engine.getRoot();
1035                return this;
1036        }
1037        /**
1038         * @param root 根菜单对象
1039         * @return 当前对象
1040         * @see gu.dtalk.engine.BaseItemEngine#setRoot(gu.dtalk.MenuItem)
1041         */
1042        public DtalkHttpServer setRoot(MenuItem root) {
1043                engine.setRoot(root);
1044                return this;
1045        }
1046        /**
1047         * 设置定义检查连接的任务时间间隔(毫秒)
1048         * @param timerPeriod 时间间隔(毫秒)
1049         * @return 当前对象
1050         */
1051        public DtalkHttpServer setTimerPeriod(long timerPeriod) {
1052                if(timerPeriod > 0){
1053                        this.timerPeriod = timerPeriod;
1054                }
1055                return this;
1056        }
1057        @Override
1058        protected WebSocket openWebSocket(IHTTPSession handshake) {
1059                return new DtalkWebSocket(handshake);
1060        }
1061        private class DtalkWebSocket extends WebSocket {
1062
1063                public DtalkWebSocket(IHTTPSession handshakeRequest) {
1064                        super(handshakeRequest);
1065                }
1066
1067                @Override
1068                protected void onOpen() {
1069                        synchronized (wsReference) {
1070                                wsReference.set(this);
1071                        }                       
1072                }
1073
1074                @Override
1075                protected void onClose(CloseCode code, String reason, boolean initiatedByRemote) {
1076                        if(debug){
1077                    logger.info("C [" + (initiatedByRemote ? "Remote" : "Self") + "] " + (code != null ? code : "UnknownCloseCode[" + code + "]")
1078                            + (reason != null && !reason.isEmpty() ? ": " + reason : ""));
1079                        }
1080                }
1081
1082                @Override
1083                protected void onMessage(WebSocketFrame message) {
1084            try {
1085                if(debug){
1086                        String payload = message.getTextPayload();
1087                        if(payload!= null && !payload.startsWith("ack:")){
1088                                send("ack:" + message.getTextPayload());
1089                        }
1090                }
1091            } catch (IOException e) {
1092                throw new RuntimeException(e);
1093            }
1094                }
1095
1096                @Override
1097                protected void debugFrameReceived(WebSocketFrame frame) {
1098                        if(debug){
1099                                logger.info("frame:{}",frame);
1100                        }
1101                }
1102
1103                @Override
1104                protected void onPong(WebSocketFrame pong) {
1105                }
1106
1107                @Override
1108                protected void onException(IOException exception) {
1109                        
1110                        logger.info("{}:{}",exception.getClass().getName(),exception.getMessage());
1111                }
1112                
1113        }
1114        /**
1115         * 设置 DEBUG 模式,默认false
1116         * @param debug 要设置的 debug
1117         * @return 当前对象
1118         */
1119        public DtalkHttpServer setDebug(boolean debug) {
1120                this.debug = debug;
1121                return this;
1122        }
1123        /**
1124         * 设置是否不验证session合法性,默认false<br>
1125         * 开发模式下可以设置为true,跳过安全验证
1126         * @param noAuth 要设置的 noAuth
1127         * @return 当前对象
1128         */
1129        public DtalkHttpServer setNoAuth(boolean noAuth) {
1130                this.noAuth = noAuth;
1131                return this;
1132        }
1133        /**
1134         * 设置是否不支持跨域请求(CORS),默认false<br>
1135         * @param noCORS 要设置的 noCORS
1136         * @return 当前对象
1137         */
1138        public DtalkHttpServer setNoCORS(boolean noCORS) {
1139                this.noCORS = noCORS;
1140                return this;
1141        }
1142
1143        /**
1144         * 设置http服务的首页文件,默认值为 {@link #DEFAULT_HOME_PAGE}<br>
1145         * 应用层可以用此方法替换默认的设备首页
1146         * @param homePage 要设置的 homePage
1147         * @return 当前对象
1148         * @throws IOException 从homePage中读取内容发生异常
1149         */
1150        public DtalkHttpServer setHomePage(URL homePage) throws IOException {
1151                String content = new String(BinaryUtils.getBytes(checkNotNull(homePage,"homePage is null")),"UTF-8");
1152                return setHomePageContent(content);
1153        }
1154        /**
1155         * 以字符串形式设置http服务的首页内容,默认为'/web/index.html'的内容<br>
1156         * 应用层可以用此方法替换默认的设备首页
1157         * @param homePageContent 要设置的 homePageContent
1158         * @return 当前对象
1159         */
1160        public DtalkHttpServer setHomePageContent(String homePageContent) {
1161                this.homePageContent = checkNotNull(Strings.emptyToNull(homePageContent),"homePageContent is null or empty");
1162                return this;
1163        }
1164        private String getHomePageContent() {
1165                if(null != homePageContent){
1166                        this.homePageContent = homePageContent
1167                                        .replace("{VERSION}", VERSION)
1168                                        .replace("{MAC}", selfMac)
1169                                        .replace("{EXTSERVE}", Joiner.on(",").join(extServes.keySet()));
1170                }
1171                return homePageContent;
1172        }
1173        /**
1174         * 返回name指定扩展的http响应实例
1175         * @return Function 实例,没找到返回{@code null}
1176         */
1177        public Function<IHTTPSession, Response> getExtServe(String name) {
1178                return extServes.get(name);
1179        }
1180        /**
1181         * @return 返回所有的扩展的http响应实例
1182         */
1183        public Map<String, Function<IHTTPSession, Response>> getExtServes(){
1184                return Collections.unmodifiableMap(extServes);
1185        }
1186        /**
1187         * 添加扩展的http响应实例<br>
1188         * 应用层可以通过此方法添加多个扩展实例处理额外的http请求,
1189         * 如果指定路径前缀的实例已经存在则用新实例替换
1190         * http响应实例接口(Function<IHTTPSession, Response>):<br>
1191         * INPUT (IHTTPSession) http请求<br>
1192         * OUTPU (Response) http响应<br>
1193         * @param pathPrefix http请求路径前缀
1194         * @param extServe 要设置的 extServe
1195         * @return 当前对象
1196         */
1197        public DtalkHttpServer addExtServe(String pathPrefix,Function<IHTTPSession, Response> extServe) {
1198                checkArgument(!Strings.isNullOrEmpty(pathPrefix),"path is null");
1199                checkArgument(null != extServe,"extServe is null");
1200                this.extServes.put(pathPrefix,extServe);
1201                return this;
1202        }
1203        /**
1204         * 添加扩展的http响应实例<br>
1205         * 应用层可以通过此方法添加多个扩展实例处理额外的http请求,
1206         * 如果指定路径前缀的实例已经存在则用新实例替换
1207         * @param resTfulServe
1208         * @return 当前对象
1209         */
1210        public DtalkHttpServer addExtServe(RESTfulServe resTfulServe) {
1211                checkArgument(null != resTfulServe,"resTfulServe is null");
1212                return addExtServe(resTfulServe.getPathPrefix(),resTfulServe);
1213        }
1214}