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}