All Downloads are FREE. Search and download functionalities are using the official Maven repository.

gu.dtalk.engine.DtalkHttpServer Maven / Gradle / Ivy

package gu.dtalk.engine;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static gu.dtalk.CommonConstant.DEFAULT_IDLE_TIME_MILLS;
import static gu.dtalk.engine.DeviceUtils.DEVINFO_PROVIDER;
import static org.nanohttpd.protocols.http.response.Response.newFixedLengthResponse;


import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.URL;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicReference;

import org.nanohttpd.protocols.http.IHTTPSession;
import org.nanohttpd.protocols.http.NanoHTTPD;
import org.nanohttpd.protocols.http.request.Method;
import org.nanohttpd.protocols.http.response.Response;
import org.nanohttpd.protocols.http.response.Status;
import org.nanohttpd.protocols.websockets.CloseCode;
import org.nanohttpd.protocols.websockets.NanoWSD;
import org.nanohttpd.protocols.websockets.WebSocket;
import org.nanohttpd.protocols.websockets.WebSocketFrame;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.TypeReference;
import com.gitee.l0km.com4j.base.BinaryUtils;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import com.google.common.base.Strings;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import gu.dtalk.Ack;
import gu.dtalk.MenuItem;
import gu.simplemq.exceptions.SmqUnsubscribeException;
import gu.simplemq.json.BaseJsonEncoder;

import static gu.dtalk.CommonConstant.*;
import static gu.dtalk.Version.*;
/**
 * dtalk http 服务
 * @author guyadong
 *
 */
public class DtalkHttpServer extends NanoWSD {
	private static final Logger logger = LoggerFactory.getLogger(DtalkHttpServer.class);

	/**
     * Standard HTTP header names.
     */
    public static final class HeaderNames {
        /**
         * {@code "Accept"}
         */
        public static final String ACCEPT = "Accept";
        /**
         * {@code "Accept-Charset"}
         */
        public static final String ACCEPT_CHARSET = "Accept-Charset";
        /**
         * {@code "Accept-Encoding"}
         */
        public static final String ACCEPT_ENCODING = "Accept-Encoding";
        /**
         * {@code "Accept-Language"}
         */
        public static final String ACCEPT_LANGUAGE = "Accept-Language";
        /**
         * {@code "Accept-Ranges"}
         */
        public static final String ACCEPT_RANGES = "Accept-Ranges";
        /**
         * {@code "Accept-Patch"}
         */
        public static final String ACCEPT_PATCH = "Accept-Patch";
        /**
         * {@code "Access-Control-Allow-Credentials"}
         */
        public static final String ACCESS_CONTROL_ALLOW_CREDENTIALS = "Access-Control-Allow-Credentials";
        /**
         * {@code "Access-Control-Allow-Headers"}
         */
        public static final String ACCESS_CONTROL_ALLOW_HEADERS = "Access-Control-Allow-Headers";
        /**
         * {@code "Access-Control-Allow-Methods"}
         */
        public static final String ACCESS_CONTROL_ALLOW_METHODS = "Access-Control-Allow-Methods";
        /**
         * {@code "Access-Control-Allow-Origin"}
         */
        public static final String ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin";
        /**
         * {@code "Access-Control-Expose-Headers"}
         */
        public static final String ACCESS_CONTROL_EXPOSE_HEADERS = "Access-Control-Expose-Headers";
        /**
         * {@code "Access-Control-Max-Age"}
         */
        public static final String ACCESS_CONTROL_MAX_AGE = "Access-Control-Max-Age";
        /**
         * {@code "Access-Control-Request-Headers"}
         */
        public static final String ACCESS_CONTROL_REQUEST_HEADERS = "Access-Control-Request-Headers";
        /**
         * {@code "Access-Control-Request-Method"}
         */
        public static final String ACCESS_CONTROL_REQUEST_METHOD = "Access-Control-Request-Method";
        /**
         * {@code "Age"}
         */
        public static final String AGE = "Age";
        /**
         * {@code "Allow"}
         */
        public static final String ALLOW = "Allow";
        /**
         * {@code "Authorization"}
         */
        public static final String AUTHORIZATION = "Authorization";
        /**
         * {@code "Cache-Control"}
         */
        public static final String CACHE_CONTROL = "Cache-Control";
        /**
         * {@code "Connection"}
         */
        public static final String CONNECTION = "Connection";
        /**
         * {@code "Content-Base"}
         */
        public static final String CONTENT_BASE = "Content-Base";
        /**
         * {@code "Content-Encoding"}
         */
        public static final String CONTENT_ENCODING = "Content-Encoding";
        /**
         * {@code "Content-Language"}
         */
        public static final String CONTENT_LANGUAGE = "Content-Language";
        /**
         * {@code "Content-Length"}
         */
        public static final String CONTENT_LENGTH = "Content-Length";
        /**
         * {@code "Content-Location"}
         */
        public static final String CONTENT_LOCATION = "Content-Location";
        /**
         * {@code "Content-Transfer-Encoding"}
         */
        public static final String CONTENT_TRANSFER_ENCODING = "Content-Transfer-Encoding";
        /**
         * {@code "Content-MD5"}
         */
        public static final String CONTENT_MD5 = "Content-MD5";
        /**
         * {@code "Content-Range"}
         */
        public static final String CONTENT_RANGE = "Content-Range";
        /**
         * {@code "Content-Type"}
         */
        public static final String CONTENT_TYPE = "Content-Type";
        /**
         * {@code "Cookie"}
         */
        public static final String COOKIE = "Cookie";
        /**
         * {@code "Date"}
         */
        public static final String DATE = "Date";
        /**
         * {@code "ETag"}
         */
        public static final String ETAG = "ETag";
        /**
         * {@code "Expect"}
         */
        public static final String EXPECT = "Expect";
        /**
         * {@code "Expires"}
         */
        public static final String EXPIRES = "Expires";
        /**
         * {@code "From"}
         */
        public static final String FROM = "From";
        /**
         * {@code "Host"}
         */
        public static final String HOST = "Host";
        /**
         * {@code "If-Match"}
         */
        public static final String IF_MATCH = "If-Match";
        /**
         * {@code "If-Modified-Since"}
         */
        public static final String IF_MODIFIED_SINCE = "If-Modified-Since";
        /**
         * {@code "If-None-Match"}
         */
        public static final String IF_NONE_MATCH = "If-None-Match";
        /**
         * {@code "If-Range"}
         */
        public static final String IF_RANGE = "If-Range";
        /**
         * {@code "If-Unmodified-Since"}
         */
        public static final String IF_UNMODIFIED_SINCE = "If-Unmodified-Since";
        /**
         * {@code "Last-Modified"}
         */
        public static final String LAST_MODIFIED = "Last-Modified";
        /**
         * {@code "Location"}
         */
        public static final String LOCATION = "Location";
        /**
         * {@code "Max-Forwards"}
         */
        public static final String MAX_FORWARDS = "Max-Forwards";
        /**
         * {@code "Origin"}
         */
        public static final String ORIGIN = "Origin";
        /**
         * {@code "Pragma"}
         */
        public static final String PRAGMA = "Pragma";
        /**
         * {@code "Proxy-Authenticate"}
         */
        public static final String PROXY_AUTHENTICATE = "Proxy-Authenticate";
        /**
         * {@code "Proxy-Authorization"}
         */
        public static final String PROXY_AUTHORIZATION = "Proxy-Authorization";
        /**
         * {@code "Range"}
         */
        public static final String RANGE = "Range";
        /**
         * {@code "Referer"}
         */
        public static final String REFERER = "Referer";
        /**
         * {@code "Retry-After"}
         */
        public static final String RETRY_AFTER = "Retry-After";
        /**
         * {@code "Sec-WebSocket-Key1"}
         */
        public static final String SEC_WEBSOCKET_KEY1 = "Sec-WebSocket-Key1";
        /**
         * {@code "Sec-WebSocket-Key2"}
         */
        public static final String SEC_WEBSOCKET_KEY2 = "Sec-WebSocket-Key2";
        /**
         * {@code "Sec-WebSocket-Location"}
         */
        public static final String SEC_WEBSOCKET_LOCATION = "Sec-WebSocket-Location";
        /**
         * {@code "Sec-WebSocket-Origin"}
         */
        public static final String SEC_WEBSOCKET_ORIGIN = "Sec-WebSocket-Origin";
        /**
         * {@code "Sec-WebSocket-Protocol"}
         */
        public static final String SEC_WEBSOCKET_PROTOCOL = "Sec-WebSocket-Protocol";
        /**
         * {@code "Sec-WebSocket-Version"}
         */
        public static final String SEC_WEBSOCKET_VERSION = "Sec-WebSocket-Version";
        /**
         * {@code "Sec-WebSocket-Key"}
         */
        public static final String SEC_WEBSOCKET_KEY = "Sec-WebSocket-Key";
        /**
         * {@code "Sec-WebSocket-Accept"}
         */
        public static final String SEC_WEBSOCKET_ACCEPT = "Sec-WebSocket-Accept";
        /**
         * {@code "Server"}
         */
        public static final String SERVER = "Server";
        /**
         * {@code "Set-Cookie"}
         */
        public static final String SET_COOKIE = "Set-Cookie";
        /**
         * {@code "Set-Cookie2"}
         */
        public static final String SET_COOKIE2 = "Set-Cookie2";
        /**
         * {@code "TE"}
         */
        public static final String TE = "TE";
        /**
         * {@code "Trailer"}
         */
        public static final String TRAILER = "Trailer";
        /**
         * {@code "Transfer-Encoding"}
         */
        public static final String TRANSFER_ENCODING = "Transfer-Encoding";
        /**
         * {@code "Upgrade"}
         */
        public static final String UPGRADE = "Upgrade";
        /**
         * {@code "User-Agent"}
         */
        public static final String USER_AGENT = "User-Agent";
        /**
         * {@code "Vary"}
         */
        public static final String VARY = "Vary";
        /**
         * {@code "Via"}
         */
        public static final String VIA = "Via";
        /**
         * {@code "Warning"}
         */
        public static final String WARNING = "Warning";
        /**
         * {@code "WebSocket-Location"}
         */
        public static final String WEBSOCKET_LOCATION = "WebSocket-Location";
        /**
         * {@code "WebSocket-Origin"}
         */
        public static final String WEBSOCKET_ORIGIN = "WebSocket-Origin";
        /**
         * {@code "WebSocket-Protocol"}
         */
        public static final String WEBSOCKET_PROTOCOL = "WebSocket-Protocol";
        /**
         * {@code "WWW-Authenticate"}
         */
        public static final String WWW_AUTHENTICATE = "WWW-Authenticate";

        private HeaderNames() {
        }
    }

    /**
     * Standard HTTP header values.
     */
    public static final class HeaderValues {
        /**
         * {@code "application/x-www-form-urlencoded"}
         */
        public static final String APPLICATION_X_WWW_FORM_URLENCODED = "application/x-www-form-urlencoded";
        /**
         * {@code "base64"}
         */
        public static final String BASE64 = "base64";
        /**
         * {@code "binary"}
         */
        public static final String BINARY = "binary";
        /**
         * {@code "boundary"}
         */
        public static final String BOUNDARY = "boundary";
        /**
         * {@code "bytes"}
         */
        public static final String BYTES = "bytes";
        /**
         * {@code "charset"}
         */
        public static final String CHARSET = "charset";
        /**
         * {@code "chunked"}
         */
        public static final String CHUNKED = "chunked";
        /**
         * {@code "close"}
         */
        public static final String CLOSE = "close";
        /**
         * {@code "compress"}
         */
        public static final String COMPRESS = "compress";
        /**
         * {@code "100-continue"}
         */
        public static final String CONTINUE =  "100-continue";
        /**
         * {@code "deflate"}
         */
        public static final String DEFLATE = "deflate";
        /**
         * {@code "gzip"}
         */
        public static final String GZIP = "gzip";
        /**
         * {@code "identity"}
         */
        public static final String IDENTITY = "identity";
        /**
         * {@code "keep-alive"}
         */
        public static final String KEEP_ALIVE = "keep-alive";
        /**
         * {@code "max-age"}
         */
        public static final String MAX_AGE = "max-age";
        /**
         * {@code "max-stale"}
         */
        public static final String MAX_STALE = "max-stale";
        /**
         * {@code "min-fresh"}
         */
        public static final String MIN_FRESH = "min-fresh";
        /**
         * {@code "multipart/form-data"}
         */
        public static final String MULTIPART_FORM_DATA = "multipart/form-data";
        /**
         * {@code "must-revalidate"}
         */
        public static final String MUST_REVALIDATE = "must-revalidate";
        /**
         * {@code "no-cache"}
         */
        public static final String NO_CACHE = "no-cache";
        /**
         * {@code "no-store"}
         */
        public static final String NO_STORE = "no-store";
        /**
         * {@code "no-transform"}
         */
        public static final String NO_TRANSFORM = "no-transform";
        /**
         * {@code "none"}
         */
        public static final String NONE = "none";
        /**
         * {@code "only-if-cached"}
         */
        public static final String ONLY_IF_CACHED = "only-if-cached";
        /**
         * {@code "private"}
         */
        public static final String PRIVATE = "private";
        /**
         * {@code "proxy-revalidate"}
         */
        public static final String PROXY_REVALIDATE = "proxy-revalidate";
        /**
         * {@code "public"}
         */
        public static final String PUBLIC = "public";
        /**
         * {@code "quoted-printable"}
         */
        public static final String QUOTED_PRINTABLE = "quoted-printable";
        /**
         * {@code "s-maxage"}
         */
        public static final String S_MAXAGE = "s-maxage";
        /**
         * {@code "trailers"}
         */
        public static final String TRAILERS = "trailers";
        /**
         * {@code "Upgrade"}
         */
        public static final String UPGRADE = "Upgrade";
        /**
         * {@code "WebSocket"}
         */
        public static final String WEBSOCKET = "WebSocket";

        private HeaderValues() {
        }
    }

	private static final String DTALK_SESSION="dtalk-session"; 
	public static final String APPICATION_JSON="application/json";
	private static final String UNAUTH_SESSION="UNAUTHORIZATION SESSION";
	private static final String AUTH_OK="AUTHORIZATION OK";
	private static final String CLIENT_LOCKED="ANOTHER CLIENT LOCKED";
	private static final String INVALID_PWD="INVALID REQUEST PASSWORD";
	private static final String POST_DATA="postData";
	private static final String DTALK_PREFIX="/dtalk";
	private static final String STATIC_PAGE_PREFIX="/web";
	private static final String ALLOW_METHODS = Joiner.on(',').join(Arrays.asList(Method.POST,Method.GET,Method.PUT,Method.DELETE));
	private static final String ALLOW_METHODS_CORS = ALLOW_METHODS + "," + Method.OPTIONS ;
	private static final String DEFAULT_ALLOW_HEADERS = Joiner.on(',').join(Arrays.asList(HeaderNames.CONTENT_TYPE));
	public static final URL DEFAULT_HOME_PAGE = DtalkHttpServer.class.getResource(STATIC_PAGE_PREFIX + "/index.html");
	private static final MapMIME_OF_SUFFIX = ImmutableMap.builder()
			.put(".jpeg", "image/jpeg")
			.put(".jpg", "image/jpeg")
			.put(".png", "image/png")
			.put(".gif", "image/gif")
			.put(".htm","text/html")
			.put(".html","text/html")
			.put(".txt","text/plain")
			.put(".css","text/css")
			.put(".csv","text/csv")
			.put(".json","application/json")
			.put(".js","application/javascript")
			.put(".xml","application/xml")
			.put(".zip","application/zip")
			.put(".pdf","application/pdf")
			.put(".sql","application/sql")
			.put(".doc","application/msword")    		
			.build();
	private static final Set SUPPORTED_MIME = ImmutableSet.copyOf(MIME_OF_SUFFIX.values());
	private static class SingletonTimer{
		private static final Timer instnace = new Timer(true);
	}

	private Timer timer;
	private Timer getTimer(){
		// 懒加载
		if(timer == null){
			timer = SingletonTimer.instnace;
		}
		return timer;
	}
	private long idleTimeLimit = DEFAULT_IDLE_TIME_MILLS;
	private long timerPeriod = 2000;

	private String selfMac;
	
	/**
	 * 当前对话ID
	 */
	private String dtalkSession;
	/**
	 * 当前websocket 连接
	 */
	private final AtomicReference wsReference = new AtomicReference<>();
	private final Supplier> webSocketSupplier = new Supplier>() {

		@Override
		public AtomicReference get() {
			return wsReference;
		}
	};
	private ItemEngineHttpImpl engine = new ItemEngineHttpImpl().setSupplier(webSocketSupplier);
	private boolean debug = false;
	/**
	 * 不做安全验证
	 */
	private boolean noAuth = false;
	/**
	 * 不支持跨域请求(CORS)
	 */
	private boolean noCORS = false;
	/**
	 * 首页内容
	 */
	private String homePageContent;
	/**
	 * 处理扩展http请求的实例
	 */
	private final Map> extServes = 
			Collections.synchronizedMap(Maps.>newLinkedHashMap());
	public DtalkHttpServer()  {
		this(DEFAULT_HTTP_PORT);
	}
    public DtalkHttpServer(int port)  {
        super(port);
        this.selfMac = BinaryUtils.toHex(DeviceUtils.DEVINFO_PROVIDER.getMac());
		// 定时检查引擎工作状态,当空闲超时,则中止连接
		getTimer().schedule(new TimerTask() {

			@Override
			public void run() {
				try{
					if(null != dtalkSession && DtalkHttpServer.this.isAlive()){
						long lasthit = engine.lastHitTime();
						if(System.currentTimeMillis() - lasthit > idleTimeLimit){
							resetSession();
						}
					}
				}catch (Throwable e) {
					logger.error(e.getMessage());
				}
			}
		}, 0, timerPeriod);
		try {
			setHomePage(DEFAULT_HOME_PAGE);
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
    }
    private boolean isAuthorizationSession(IHTTPSession session){
    	return dtalkSession != null && dtalkSession.equals(session.getCookies().read(DTALK_SESSION));
    }
    private void checkAuthorizationSession(IHTTPSession session) throws ResponseException{
    	if(noAuth  || isAuthorizationSession(session)){
    		return;
    	}
    	throw new ResponseException(Status.UNAUTHORIZED, ackError(UNAUTH_SESSION));
    }
    private  Response responseAck(Ack ack){
    	String json=BaseJsonEncoder.getEncoder().toJsonString(ack);
    	return newFixedLengthResponse(
    			Ack.Status.OK.equals(ack.getStatus()) ? Status.OK: Status.INTERNAL_ERROR, 
    			APPICATION_JSON, 
    			json);
    }
    private  String ackMessage(Ack.Status status,String message){
    	Ack ack = new Ack().setStatus(status).setStatusMessage(message);
    	String json=BaseJsonEncoder.getEncoder().toJsonString(ack);
    	return  json;
    }
    private  String ackError(String message){
    	return  ackMessage(Ack.Status.ERROR,message);
    }

	private void resetSession(){
		synchronized (wsReference) {
			dtalkSession = null;
			WebSocket wsSocket = wsReference.get();
			if(null != wsSocket){
	    		try {
					wsSocket.close(CloseCode.NormalClosure, "", false);
				} catch (IOException e) {
					e.printStackTrace();
				}
	    		wsSocket=null;
			}
		}
	}
	/**
	 * 根据提供的路径返回静态资源响应对象
	 * @param uri 请求路径
	 * @return 响应对象,资源没找到返回{@code null}
	 */
	private Response responseStaticResource(String uri){    	
		URL res = getClass().getResource(STATIC_PAGE_PREFIX + uri);
		if(null == res){
			return null;
		}

		try {
			byte[] content = BinaryUtils.getBytes(res);				
			String suffix = uri.substring(uri.lastIndexOf('.'));
			return makeResponse(Status.OK,suffix, content);
		} catch (IOException e) {
			return newFixedLengthResponse(
	    			Status.INTERNAL_ERROR, 
	    			NanoHTTPD.MIME_PLAINTEXT, 
	    			String.format("IO ERROR %s", uri));	
		} 
	
	}
	/**
	 * 判断是否为CORS 预检请求请求(Preflight)
	 * @param session
	 * @return
	 */
	private static boolean isPreflightRequest(IHTTPSession session) {
		Map headers = session.getHeaders();
		return Method.OPTIONS.equals(session.getMethod()) 
				&& headers.containsKey(HeaderNames.ORIGIN.toLowerCase()) 
				&& headers.containsKey(HeaderNames.ACCESS_CONTROL_REQUEST_METHOD.toLowerCase()) 
				&& headers.containsKey(HeaderNames.ACCESS_CONTROL_REQUEST_HEADERS.toLowerCase());
	}
	/**
	 * 封装响应包
	 * @param session http请求
	 * @param resp 响应包
	 * @return resp
	 */
	private Response wrapResponse(IHTTPSession session,Response resp) {
		if(null != resp){
			Map headers = session.getHeaders();
			resp.addHeader(HeaderNames.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
			// 如果请求头中包含'Origin',则响应头中'Access-Control-Allow-Origin'使用此值否则为'*'
			// nanohttd将所有请求头的名称强制转为了小写
			String origin = MoreObjects.firstNonNull(headers.get(HeaderNames.ORIGIN.toLowerCase()), "*");
			resp.addHeader(HeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN, origin);
			
			String  requestHeaders = headers.get(HeaderNames.ACCESS_CONTROL_REQUEST_HEADERS.toLowerCase());
			if(requestHeaders != null){
				resp.addHeader(HeaderNames.ACCESS_CONTROL_ALLOW_HEADERS, requestHeaders);
			}
		}
		return resp;
	}

	/**
	 * 向响应包中添加CORS包头数据
	 * @param session
	 * @return
	 */
	private Response responseCORS(IHTTPSession session) {
		Response resp = wrapResponse(session,newFixedLengthResponse(""));
		Map headers = session.getHeaders();
		resp.addHeader(HeaderNames.ACCESS_CONTROL_ALLOW_METHODS, 
				noCORS ? ALLOW_METHODS : ALLOW_METHODS_CORS);

		String requestHeaders = headers.get(HeaderNames.ACCESS_CONTROL_REQUEST_HEADERS.toLowerCase());
		String allowHeaders = MoreObjects.firstNonNull(requestHeaders, DEFAULT_ALLOW_HEADERS);
		resp.addHeader(HeaderNames.ACCESS_CONTROL_ALLOW_HEADERS, allowHeaders);
		//resp.addHeader(HeaderNames.ACCESS_CONTROL_MAX_AGE, "86400");
		resp.addHeader(HeaderNames.ACCESS_CONTROL_MAX_AGE, "0");
		return resp;
	}
	
	private Response runExtServs(IHTTPSession session){
		String path = session.getUri();
		for(Entry> entry:extServes.entrySet()){
			if(path.startsWith(entry.getKey())){
				return entry.getValue().apply(session);
			}
		}
		return null; 
	}
	/**
	 * http响应Body数据对象
	 * @author guyadong
	 *
	 */
	public static final class Body{
		public final Status status;
		public final String mimeType;
		public final byte[] content;
		/**
		 * @param status HTTP response status code
		 * @param mimeType mine type,such as 'image/jpeg','text/html'
		 * @param content content of HTTP response body 
		 */
		public Body(Status status,String mimeType, byte[] content) {
			checkArgument(!Strings.isNullOrEmpty(mimeType),"mimeType is null");
			this.status = MoreObjects.firstNonNull(status, Status.OK);
			this.mimeType = mimeType;
			this.content = MoreObjects.firstNonNull(content, new byte[]{});
		}
		/**
		 * @param status HTTP response status code
		 * @param mimeType mine type,such as 'image/jpeg','text/html'
		 * @param content content of HTTP response body 
		 */
		public Body(Status status,String mimeType, String content) {
			this(status, content,content == null ? null : content.getBytes());
		}
		/**
		 * @param mimeType mine type,such as 'image/jpeg','text/html'
		 * @param content content of HTTP response body 
		 */
		public Body(String mimeType, byte[] content) {
			this(null, mimeType, content);
		}
		/**
		 * @param mimeType mine type,such as 'image/jpeg','text/html'
		 * @param content content of HTTP response body 
		 */
		public Body(String mimeType, String content) {
			this(null, mimeType,content);
		}
	}
	public static Response makeResponse(Status status,String mimeType, byte[] content){
		if(MIME_OF_SUFFIX.containsKey(mimeType)){
			return newFixedLengthResponse(
					checkNotNull(status,"status is null"), 
					MIME_OF_SUFFIX.get(mimeType), 
					new ByteArrayInputStream(checkNotNull(content,"content is null")),content.length);
		} if (SUPPORTED_MIME.contains(mimeType)){
			return newFixedLengthResponse(
					checkNotNull(status,"status is null"), 
					mimeType, 
					new ByteArrayInputStream(checkNotNull(content,"content is null")),content.length);
		}else{
			return newFixedLengthResponse(
	    			Status.UNSUPPORTED_MEDIA_TYPE, 
	    			NanoHTTPD.MIME_PLAINTEXT, 
	    			String.format("UNSUPPORTED MEDIA TYPE %s", mimeType));	
		}
	}
	public static Response makeResponse(Body body){
		return body == null ? null : makeResponse(body.status, body.mimeType, body.content);
	}
	@Override
	public void start(int timeout, boolean daemon) throws IOException {
		if(!isAlive()){
			super.start(timeout, daemon);
			// 定时发送PING
			getTimer().schedule(new TimerTask() {

				@Override
				public void run() {
					
						if(null != dtalkSession && DtalkHttpServer.this.isAlive()){
							
							synchronized (wsReference) {
								try{
									WebSocket wsSocket = wsReference.get();
									if(dtalkSession != null && wsSocket != null && wsSocket.isOpen()){
										wsSocket.ping(new byte[0]);
										if(debug){
											wsSocket.send("dtalk wscocket heartbeat " + new Date());
										}
									}
								}catch (Throwable e) {
									logger.error("{}:{}",e.getClass().getName(),e.getMessage());
									//logger.error(e.getMessage(),e);
								}			
							}										
						}

				}
			}, 0, timeout*3/4);
		}
	}
	@SuppressWarnings("deprecation")
	@Override
    public Response serve(IHTTPSession session) {
    	if (isWebsocketRequested(session) && ! isAuthorizationSession(session)) {
    		return newFixedLengthResponse(
        			Status.UNAUTHORIZED, 
        					NanoHTTPD.MIME_PLAINTEXT, 
        					UNAUTH_SESSION);
    	}
		return super.serve(session);    	
    }
    @Override
    public Response handleWebSocket(IHTTPSession session) {
    	if(isPreflightRequest(session)){
    		return responseCORS(session);
    	}
    	Ack ack = new Ack().setStatus(Ack.Status.OK).setDeviceMac(selfMac);
    	try{
    		switch(session.getUri()){
    		case "/login":
    			login(session, ack);
    			break;
    		case "/logout":
    			logout(session, ack);
    			break;
    		case "/":
    		case "/index.html":
    		case "/index.htm":
    		{
    			return newFixedLengthResponse(Status.OK, NanoHTTPD.MIME_HTML, getHomePageContent());
    		}
    		// 返回当前设备MAC地址
    		case "/mac":
				return wrapResponse(session,newFixedLengthResponse(
						Status.OK, 
						NanoHTTPD.MIME_PLAINTEXT, 
						selfMac));
    		default:
    			Response resp = responseStaticResource(session.getUri());
    			if(null != resp){
    				return wrapResponse(session,resp);
    			}
				if(session.getUri().startsWith(DTALK_PREFIX )){
    				checkAuthorizationSession(session);
    				if(DTALK_PREFIX.equals(session.getUri())){
    					checkState(Method.POST.equals(session.getMethod()),"POST method supported only");
    				}
    				JSONObject jsonObject = getJSONObject(session);
					if(session.getUri().startsWith(DTALK_PREFIX + '/')){
						String path=session.getUri().substring(DTALK_PREFIX.length());
						jsonObject.put("path", path);
					} 
				
    				try {
    					engine.onSubscribe(jsonObject);
    					return wrapResponse(session,engine.getResponse());
					} catch (SmqUnsubscribeException e) {
						logout(session, ack);
						break;
					}     				
    			}else if((resp = runExtServs(session)) != null){
    					return wrapResponse(session,resp);
    			}
				return wrapResponse(session,newFixedLengthResponse(
						Status.NOT_FOUND, 
						NanoHTTPD.MIME_PLAINTEXT, 
						String.format("NOT FOUND %s", session.getUri())));	
    		}
    	}catch(ResponseException e){
    		return wrapResponse(session,newFixedLengthResponse(
					e.getStatus(), 
					NanoHTTPD.MIME_PLAINTEXT, 
					e.getMessage()));	
    	}catch (Throwable e) {    		
    		ack.setStatus(Ack.Status.ERROR).setException(e.getClass().getName()).setStatusMessage(e.getMessage());
    	}
    	return wrapResponse(session,responseAck(ack));
    }

	/**
	 * @param isMd5 
	 * @return 
	 */
	private boolean validate(String pwd, boolean isMd5) {
		checkArgument(pwd != null,"NULL PASSWORD");
		
		String admPwd = checkNotNull(DEVINFO_PROVIDER.getPassword(),"admin password for device is null");
		checkArgument(!Strings.isNullOrEmpty(pwd),"NULL REQUEST PASSWORD");
		checkArgument(!Strings.isNullOrEmpty(admPwd),"NULL ADMIN PASSWORD");
		if(isMd5){
			String pwdmd5 = BinaryUtils.getMD5String(admPwd.getBytes());
			return pwdmd5.equalsIgnoreCase(pwd);
		}else{
			return admPwd.equals(pwd);
		}
	}
	
	/**
	 * 从HTTP请求body中解析参数返回{@link Map}实例
	 * @param session
	 * @return
	 * @throws IOException
	 * @throws ResponseException
	 */
	private static  Map getParamOfPostBody(IHTTPSession session) throws IOException, ResponseException{
		Map postData = Maps.newHashMap();
		session.parseBody(postData);
		String jsonstr = postData.get(POST_DATA);
		if (null == jsonstr) {
			return Maps.newHashMap();
		}
		return BaseJsonEncoder.getEncoder().fromJson(jsonstr, 
				new TypeReference>(){}.getType());		
	}
	/**
	 * 从HTTP请求中解析参数返回{@link JSONObject}实例
	 * @param session
	 * @return
	 * @throws IOException
	 * @throws ResponseException
	 */
	@SuppressWarnings("deprecation")
	private static JSONObject getJSONObject(IHTTPSession session) throws IOException, ResponseException{
    	String jsonstr;
    	if(Method.POST.equals(session.getMethod())){ 
    		Map postData = Maps.newHashMap();
    		session.parseBody(postData);
    		jsonstr = postData.get(POST_DATA);
    	}else{
    		jsonstr = BaseJsonEncoder.getEncoder().toJsonString(session.getParms());
    	}
		return null == jsonstr ? new  JSONObject():BaseJsonEncoder.getEncoder().fromJson(jsonstr,JSONObject.class);
	}
    /**
     * 从HTTP请求中解析参数返回{@link Map}实例
     * @param session
     * @return 参数K-V映射
     * @throws IOException
     * @throws ResponseException
     */
    @SuppressWarnings("deprecation")
	private static Map getParams(IHTTPSession session) throws IOException, ResponseException{
    	Map params = Maps.newHashMap();
    	if(Method.POST.equals(session.getMethod())){    		
			params = getParamOfPostBody(session);			
    	}else{
    		params = session.getParms();
    	}
    	return params;
    }
    
	protected synchronized void login(IHTTPSession session, Ack ack) throws IOException, ResponseException{

    	Map parms = getParams(session);

    	String sid=parms.get(DTALK_SESSION);
    	if(sid ==null){
    		sid=session.getCookies().read(DTALK_SESSION);
    	}
    	
    	if (dtalkSession == null || sid == null ){
    		if(!validate(parms.get("password"), 
					Boolean.valueOf(MoreObjects.firstNonNull(parms.get("isMd5"), "true")))){
    			throw new ResponseException(Status.FORBIDDEN, 
    					ackError(INVALID_PWD));
    		}
			sid = dtalkSession = Long.toHexString(System.nanoTime());
	    	session.getCookies().set(DTALK_SESSION, dtalkSession, 1);
	    	logger.info("session {} connected",dtalkSession);
    	}
    	if(!Objects.equal(dtalkSession, sid)){
    		throw new ResponseException(Status.FORBIDDEN, 
    				ackError(CLIENT_LOCKED));
    	}
        ack.setStatus(Ack.Status.OK).setStatusMessage(AUTH_OK);
       	engine.setLastHitTime(System.currentTimeMillis());

	}
	protected synchronized void logout(IHTTPSession session, Ack ack) throws IOException, ResponseException{
    	checkAuthorizationSession(session);
       	logger.info("session {} disconnected",dtalkSession);
    	resetSession();
       	ack.setStatus(Ack.Status.OK).setStatusMessage("logout OK");
    }

	/**
	 * @return engine
	 */
	public ItemEngineHttpImpl getItemAdapter() {
		return engine;
	}

	/**
	 * @param engine 要设置的 engine
	 * @return 当前对象
	 */
	public DtalkHttpServer setItemAdapter(ItemEngineHttpImpl engine) {
		this.engine = checkNotNull(engine,"engine is null").setSupplier(webSocketSupplier);
		return this;
	}
	
	/**
	 * @return 当前对象
	 * @see gu.dtalk.engine.BaseItemEngine#getRoot()
	 */
	public DtalkHttpServer getRoot() {
		engine.getRoot();
		return this;
	}
	/**
	 * @param root 根菜单对象
	 * @return 当前对象
	 * @see gu.dtalk.engine.BaseItemEngine#setRoot(gu.dtalk.MenuItem)
	 */
	public DtalkHttpServer setRoot(MenuItem root) {
		engine.setRoot(root);
		return this;
	}
	/**
	 * 设置定义检查连接的任务时间间隔(毫秒)
	 * @param timerPeriod 时间间隔(毫秒)
	 * @return 当前对象
	 */
	public DtalkHttpServer setTimerPeriod(long timerPeriod) {
		if(timerPeriod > 0){
			this.timerPeriod = timerPeriod;
		}
		return this;
	}
	@Override
	protected WebSocket openWebSocket(IHTTPSession handshake) {
		return new DtalkWebSocket(handshake);
	}
	private class DtalkWebSocket extends WebSocket {

		public DtalkWebSocket(IHTTPSession handshakeRequest) {
			super(handshakeRequest);
		}

		@Override
		protected void onOpen() {
			synchronized (wsReference) {
				wsReference.set(this);
			}			
		}

		@Override
		protected void onClose(CloseCode code, String reason, boolean initiatedByRemote) {
			if(debug){
	            logger.info("C [" + (initiatedByRemote ? "Remote" : "Self") + "] " + (code != null ? code : "UnknownCloseCode[" + code + "]")
	                    + (reason != null && !reason.isEmpty() ? ": " + reason : ""));
			}
		}

		@Override
		protected void onMessage(WebSocketFrame message) {
            try {
                if(debug){
                	String payload = message.getTextPayload();
                	if(payload!= null && !payload.startsWith("ack:")){
                		send("ack:" + message.getTextPayload());
                	}
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
		}

		@Override
		protected void debugFrameReceived(WebSocketFrame frame) {
			if(debug){
				logger.info("frame:{}",frame);
			}
		}

		@Override
		protected void onPong(WebSocketFrame pong) {
		}

		@Override
		protected void onException(IOException exception) {
			
			logger.info("{}:{}",exception.getClass().getName(),exception.getMessage());
		}
		
	}
	/**
	 * 设置 DEBUG 模式,默认false
	 * @param debug 要设置的 debug
	 * @return 当前对象
	 */
	public DtalkHttpServer setDebug(boolean debug) {
		this.debug = debug;
		return this;
	}
	/**
	 * 设置是否不验证session合法性,默认false
* 开发模式下可以设置为true,跳过安全验证 * @param noAuth 要设置的 noAuth * @return 当前对象 */ public DtalkHttpServer setNoAuth(boolean noAuth) { this.noAuth = noAuth; return this; } /** * 设置是否不支持跨域请求(CORS),默认false
* @param noCORS 要设置的 noCORS * @return 当前对象 */ public DtalkHttpServer setNoCORS(boolean noCORS) { this.noCORS = noCORS; return this; } /** * 设置http服务的首页文件,默认值为 {@link #DEFAULT_HOME_PAGE}
* 应用层可以用此方法替换默认的设备首页 * @param homePage 要设置的 homePage * @return 当前对象 * @throws IOException 从homePage中读取内容发生异常 */ public DtalkHttpServer setHomePage(URL homePage) throws IOException { String content = new String(BinaryUtils.getBytes(checkNotNull(homePage,"homePage is null")),"UTF-8"); return setHomePageContent(content); } /** * 以字符串形式设置http服务的首页内容,默认为'/web/index.html'的内容
* 应用层可以用此方法替换默认的设备首页 * @param homePageContent 要设置的 homePageContent * @return 当前对象 */ public DtalkHttpServer setHomePageContent(String homePageContent) { this.homePageContent = checkNotNull(Strings.emptyToNull(homePageContent),"homePageContent is null or empty"); return this; } private String getHomePageContent() { if(null != homePageContent){ this.homePageContent = homePageContent .replace("{VERSION}", VERSION) .replace("{MAC}", selfMac) .replace("{EXTSERVE}", Joiner.on(",").join(extServes.keySet())); } return homePageContent; } /** * 返回name指定扩展的http响应实例 * @return Function 实例,没找到返回{@code null} */ public Function getExtServe(String name) { return extServes.get(name); } /** * @return 返回所有的扩展的http响应实例 */ public Map> getExtServes(){ return Collections.unmodifiableMap(extServes); } /** * 添加扩展的http响应实例
* 应用层可以通过此方法添加多个扩展实例处理额外的http请求, * 如果指定路径前缀的实例已经存在则用新实例替换 * http响应实例接口(Function):
* INPUT (IHTTPSession) http请求
* OUTPU (Response) http响应
* @param pathPrefix http请求路径前缀 * @param extServe 要设置的 extServe * @return 当前对象 */ public DtalkHttpServer addExtServe(String pathPrefix,Function extServe) { checkArgument(!Strings.isNullOrEmpty(pathPrefix),"path is null"); checkArgument(null != extServe,"extServe is null"); this.extServes.put(pathPrefix,extServe); return this; } /** * 添加扩展的http响应实例
* 应用层可以通过此方法添加多个扩展实例处理额外的http请求, * 如果指定路径前缀的实例已经存在则用新实例替换 * @param resTfulServe * @return 当前对象 */ public DtalkHttpServer addExtServe(RESTfulServe resTfulServe) { checkArgument(null != resTfulServe,"resTfulServe is null"); return addExtServe(resTfulServe.getPathPrefix(),resTfulServe); } }